【硬核】啃透vLLM源码:从PagedAttention到连续批处理,大模型推理加速24倍的秘密

张开发
2026/6/19 17:48:24 15 分钟阅读
【硬核】啃透vLLM源码:从PagedAttention到连续批处理,大模型推理加速24倍的秘密
啃透vLLM源码从PagedAttention到连续批处理大模型推理加速24倍的秘密如果说大模型是AI的“大脑”那推理引擎就是让它“开口说话”的声带。vLLM是如何成为业界公认的推理加速之王的本文带你手撕源码揭开PagedAttention与连续批处理的神秘面纱。引言一个推理工程师的深夜崩溃凌晨两点钉钉群炸了“线上推理服务超时率飙升到30%”你打开监控发现8张A100的GPU利用率只有15%但延迟却高达5秒。你颤抖着点开nvidia-smi显存占用95%但实际处理请求数不到50个。这就是没有用vLLM之前的日常。一、先看疗效24倍吞吐量是怎么来的在正式开撕源码之前先看一张官方测试表数据基于Llama-7BA100 80GB框架吞吐量 (tokens/s)显存利用率最大并发HuggingFace Transformers11235%4vLLM2,68892%9624倍。这不是PPT是真实的生产数据。vLLM的秘密就藏在下图这个架构里文字描述[请求队列] → [调度器(Scheduler)] → [Worker(GPU执行)] ↑ ↓ [Block管理器] ← [PagedAttention]现在我们一层层扒开它的衣服。二、PagedAttention向操作系统“偷师”的内存管理2.1 传统KV Cache的“四合院”式浪费在原生Transformer推理中每个请求的KV Cache必须提前分配一整块连续内存大小等于max_seq_len × num_layers × 2 × head_dim × num_heads。假设max_seq_len2048这就像一个住户住进一栋四合院——房子大得离谱而且一旦住进去即使你只用了其中两间房剩下的房间也不能给别人用。2.2 vLLM的“现代公寓”革命vLLM把KV Cache切成固定大小的Block默认16个token通过逻辑块→物理块的映射让每个请求只需占用实际需要的块数且物理块可以任意分散。源码验证vllm/block.py中PhysicalTokenBlock和LogicalTokenBlock的实现classPhysicalTokenBlock:实际的GPU内存块def__init__(self,device:Device,block_number:int,block_size:int):self.devicedevice self.block_numberblock_number self.block_sizeblock_size self.ref_count0# 引用计数用于共享self.token_ids[None]*block_sizeclassLogicalTokenBlock:逻辑上的连续token序列def__init__(self,block_number:int,block_size:int):self.block_numberblock_number self.token_ids[_BLANK_TOKEN_ID]*block_size self.num_tokens0defappend_tokens(self,token_ids:List[int])-None:curr_idxself.num_tokens self.token_ids[curr_idx:curr_idxlen(token_ids)]token_ids self.num_tokenslen(token_ids)关键点PhysicalTokenBlock有ref_count——这意味着多个请求可以共享同一个物理块比如共享前缀的prompt这是传统缓存做不到的。2.3 Block大小的“黄金分割”Block太小如4管理开销大太大如256内部碎片多。vLLM默认16但可以通过环境变量VLLM_BLOCK_SIZE调整。我曾经在代码里看到过某公司为了节省显存强行改成32结果长文本生成时延迟飙升——因为block太大导致不能充分利用反而浪费。三、调度器连续批处理的“指挥家”传统批处理是等一车人坐满了才发车发车后即使有人下车车也不能停下来接新人。vLLM的连续批处理Continuous Batching则像地铁每站都有人上下车门永远不关。调度核心在vllm/core/scheduler.py的schedule()方法中defschedule(self)-SchedulerOutputs:# 1. 从等待队列中挑选可调度的请求scheduled_seq_groups[]forseq_groupinself.waiting:# 检查是否有足够的blockifself.block_manager.can_allocate(seq_group):self.block_manager.allocate(seq_group)scheduled_seq_groups.append(seq_group)else:break# 显存不足停止调度# 2. 将正在运行的请求加入本次迭代forseq_groupinself.running:# 如果请求已经完成释放其block并加入finished队列ifseq_group.is_finished():self.block_manager.free(seq_group)self.finished.append(seq_group)else:scheduled_seq_groups.append(seq_group)# 3. 生成SchedulerOutputs交给Worker执行returnSchedulerOutputs(scheduled_seq_groupsscheduled_seq_groups,blocks_to_swap_in...,blocks_to_swap_out...,...)注意这里的blocks_to_swap_out——当显存不够时vLLM会把部分block swap到CPU内存等需要时再换回来这就是vLLM支持超长上下文的关键虽然会慢一点但不会OOM。四、Worker执行CUDA Graph与Triton的加持当调度器决定好本次迭代要执行的请求后Worker会调用execute_model()。其中最关键的是CUDA Graph的运用# vllm/worker/model_runner.pyclassModelRunner:def__init__(self,...):self.cuda_graph_memory_poolNonedefcapture_model(self,kv_caches):# 预热for_inrange(2):self._run_model(...)# 捕获self.graphtorch.cuda.CUDAGraph()withtorch.cuda.graph(self.graph,poolself.cuda_graph_memory_pool):self._run_model(...)CUDA Graph把一系列GPU操作打包成一个“图形”一次性提交给GPU避免了每次迭代的kernel launch开销。对于小batch size场景这个优化能带来30%以上的性能提升。五、实战从部署到调优的“血泪史”5.1 最简单的部署命令python-mvllm.entrypoints.openai.api_server\--modelmeta-llama/Llama-2-7b-chat-hf\--tensor-parallel-size4\--dtypefloat16\--max-model-len4096\--gpu-memory-utilization0.95.2 踩坑1量化导致精度下降vLLM原生支持GPTQ和AWQ但如果你用的是FP16模型直接加--quantization gptq会报错。正确做法是先用AutoGPTQ量化模型再加载。5.3 踩坑2动态batching与流式输出很多同学用curl测试时发现vLLM没有流式输出以为卡住了。其实vLLM默认开启流式但curl不会实时打印。正确的测试姿势是fromvllmimportLLM,SamplingParams llmLLM(modelyour-model)paramsSamplingParams(max_tokens100)outputsllm.generate([Hello],params,use_tqdmFalse)foroutputinoutputs:fortokeninoutput.outputs[0].text:print(token,end,flushTrue)六、总结与展望vLLM的成功本质上是系统思维对算法思维的一次降维打击。它没有发明新的模型结构只是用更好的内存管理和调度把现有硬件的潜力榨干到极致。未来随着PagedAttention v2支持多轮对话的显存共享、异步调度等新特性的引入vLLM的统治地位还会持续很久。

更多文章