vLLM架构解析:整体架构图、请求处理流程、KV Cache管理机制、调度器工作原理
说实话,vLLM这个项目我第一次接触时,就被它的架构设计惊艳到了。它不是那种「为了快而快」的暴力优化,而是把大模型推理的每个环节都拆解得明明白白。今天我就带大家把它的骨架摸清楚。
一、整体架构图:vLLM的「三驾马车」
先看一张我手绘的架构图。vLLM的核心可以拆成三个模块:调度器(Scheduler)、KV Cache管理器(Block Manager)、执行引擎(Worker)。它们各司其职,又紧密配合。
这张图我画了好几次才满意。你看,客户端请求先到调度器,调度器决定「谁先跑、跑多少」,然后KV Cache管理器负责「显存怎么分、怎么回收」,最后执行引擎真正干活。三个模块像流水线一样,环环相扣。
二、请求处理流程:一个请求的「一生」
一个请求进来,到底经历了什么?我拆成5步来讲。
- 请求入队:客户端发来一个prompt,调度器把它放进等待队列。这里有个细节——vLLM会先做一次
prefill,把prompt的KV Cache算出来。 - 调度决策:调度器每轮迭代(iteration)会从队列里挑一批请求。挑谁?看优先级、看显存够不够。我习惯把这一步叫「抢车位」——谁先抢到显存块谁先跑。
- KV Cache分配:Block Manager给每个请求分配物理块。注意,vLLM用的是逻辑块到物理块的映射,不是直接分配物理地址。这个设计很巧妙,后面细说。
- 执行推理:Worker拿到调度指令,在GPU上跑Transformer的前向计算。每次只生成一个token,然后更新KV Cache。
- 结果返回:生成完整个序列后,结果返回给客户端。如果请求还没结束(比如还在流式输出),调度器会把它放回「运行中队列」,下一轮继续。
核心要点:vLLM的请求处理是迭代级(iteration-level)的调度,不是请求级。这意味着每生成一个token,调度器都可能重新做决策。吞吐量就是这么提上来的。
三、KV Cache管理机制:PagedAttention的精髓
说到KV Cache管理,就不得不提vLLM的成名绝技——PagedAttention。这个名字起得好,说白了就是「分页式注意力」。我当年第一次看到这个设计时,拍了一下大腿:这不就是操作系统的虚拟内存吗?
传统做法是给每个请求预分配一块连续显存,但问题是——你根本不知道这个请求最终会生成多长的序列。预分配少了会OOM,预分配多了浪费显存。vLLM的做法是:
- 逻辑块:每个请求有一个逻辑地址空间,按块(block)分配。块大小通常是16或32个token。
- 物理块:GPU显存里维护一个物理块池。逻辑块和物理块之间通过块表(Block Table)映射。
- 按需分配:生成一个token,就分配一个物理块。不够了再申请,绝不浪费。
避坑指南:我曾经在生产环境遇到过一个问题——块大小设成了64,结果短请求(比如只有10个token的回复)也占了一个完整的块,显存利用率反而下降了。后来我改成动态块大小,短请求用小块,长请求用大块。嗯,效果好了不少。
另外,vLLM还支持Copy-on-Write。什么意思?多个请求如果共享同一个前缀(比如系统提示词),它们的KV Cache可以共用物理块,直到某个请求要修改它。这招在对话场景下特别管用。
四、调度器工作原理:谁先跑?跑多久?
调度器是vLLM的大脑。它的核心逻辑在schedule()方法里,我简化一下关键步骤:
def schedule(self):
# 1. 检查运行中的请求,哪些可以继续
running = self.running_queue
for req in running:
if req.is_finished():
self.free_blocks(req)
running.remove(req)
# 2. 从等待队列里挑新请求
waiting = self.waiting_queue
while waiting and self.has_free_blocks():
req = waiting.pop(0)
# 检查显存是否够做prefill
if self.can_prefill(req):
running.append(req)
else:
# 不够?放回队列,等下一轮
waiting.insert(0, req)
break
# 3. 生成调度指令
return self.build_schedule(running)
你看,逻辑其实不复杂。但有几个关键点:
- 抢占(Preemption):如果显存不够了,调度器会暂停一些低优先级请求,把它们的KV Cache换出到CPU内存。等显存释放了再换回来。我遇到过最极端的情况——一个请求被换入换出了十几次,延迟直接爆炸。后来我调大了块大小,情况好多了。
- 批处理(Batching):调度器会把多个请求拼成一个batch,一次性喂给GPU。批大小越大,吞吐量越高。但也不能太大,否则单个请求的延迟会变高。这是个trade-off。
- 优先级:vLLM支持按请求的到达时间、TTFT(首token延迟)等指标排序。我个人习惯给交互式请求(比如聊天)高优先级,给离线批处理任务低优先级。
注意:调度器的决策频率是每生成一个token就做一次。如果请求数量很大(比如上千个并发),调度本身也会成为瓶颈。我建议在调度器前面加一层请求聚合器,把短时间内的请求合并后再交给调度器。
五、总结:架构设计的「取舍」
vLLM的架构设计,说白了就是在显存效率和调度灵活性之间找平衡。PagedAttention让显存利用率接近100%,迭代级调度让吞吐量翻了几倍。但代价是什么?
- 调度开销变大(每token都要做决策)
- 实现复杂度变高(块表、换入换出、Copy-on-Write)
- 对长序列的支持不够好(块太多时,块表本身也占显存)
不过话说回来,没有完美的架构,只有适合场景的设计。vLLM在在线推理场景下,确实做到了「又快又省」。如果你也在做类似的项目,我建议你先把调度器和KV Cache管理器这两个模块吃透——它们是整个系统的灵魂。
公众号:蓝海资料掘金营,微信deep3321