资讯详情

资讯详情

VLLM 0.7.3源码深度拆解:从PagedAttention到连续批处理的推理优化实践

简介VLLM-0.7.3推理引擎完整源码包面向从事大模型推理优化、部署与性能调优的算法工程师和系统开发者。该版本聚焦超大规模语言模型在推理阶段的高速令牌生成与高效内存管理可帮助读者理解并定制PagedAttention、连续批处理等核心机制适用于阅读推理引擎源码、二次开发或构建企业级推理服务。包内共1896个文件压缩包约46.33MB类型覆盖1186个Python源码、47个CUDA源文件、55个CUDA头文件及13个C源文件另含178个JSON配置、132个Markdown文档、45个YAML与40个Shell脚本及多个Dockerfile完整呈现Python控制层、C/CUDA算子层和容器化部署脚本结构清晰便于按需查阅。目前已有340人学习下载。通过这份源码读者能对照实际工程实现梳理VLLM的调度流程、显存管理及多模态支持借鉴模型并行与量化方案同时借助附带编译配置和Dockerfile快速搭建开发调试环境为研究或生产落地提供可靠参考。 从跑通到敢改代码中间隔着一层源码。我最初用VLLM 0.7.3纯粹是因为它“快”OpenAI 接口兼容一行命令能把 Qwen、Llama 这些大模型拉起来做推理服务。真正让我开始读它源码的契机是线上服务遇到一个诡异问题同样的并发量别人的实例能扛住我的实例却在跑到第 30 个请求左右开始变慢首 token 延迟飙升最后直接EngineDeadError。翻文档、改参数都没解决最后只能老老实实把vllm源码拉下来一点一点看。如果你也是做模型服务端、推理优化或者单纯想搞清楚“VLLM凭啥比原生 Transformers 快这么多”这篇内容应该能帮你少走不少弯路。这篇文章不是源码逐行注释而是我梳理出的阅读主线和关键模块的拆解附带我从源码构建到上线部署的完整实操记录。1. 源码没白啃先搞明白 VLLM 0.7.3 到底解决什么问题读源码最忌讳一上来就一头扎进.py文件里乱翻。我建议先想清楚一个问题VLLM 存在的意义是什么它的核心卖点不是简单的“加速推理”而是三大能力显存管理、连续批处理、前缀缓存。这三个点正好对应我在业务里遇到的三个痛点理解了它们后面看代码才有方向。1.1 我在服务端真正碰到的三个痛点第一个痛点是显存浪费。之前用transformers做批量推理时每条请求的 KV Cache 都是独立申请一整块连续显存长度不定就会出现大量碎片。请求一多显存明明还有空闲却因为不连续而申请失败表现就是 OOM。第二个痛点是长请求阻塞。传统静态批处理static batching必须等一个批次里所有序列都跑完才释放显存接下一批。我的场景里经常有人问一个很长的代码问题一条 3000 token 的请求能卡住后面几十条短请求体验非常差。第三个痛点是重复计算。多轮对话里system prompt 和前面的历史消息每次都要从头算一遍注意力造成大量无效计算。特别是智能客服场景用户问的问题千变万化前面的系统提示词却一模一样等于每轮都在反复烧钱。VLLM 0.7.3 对这三个痛点都有对应的源码实现PagedAttention解决显存碎片Scheduler实现连续批处理Prefix Caching复用前缀计算。所以理清楚这三条主线源码也就读懂了七成。1.2 框架选型时我为什么最终选了 VLLM现在开源推理框架不止一个同时期我对比过 Hugging Face TGI、NVIDIA Triton 推理后端还有 SGLang。这里有个结论可以先给出来没有绝对最好的框架只有当前最适合自己场景的框架。对比项VLLMTGISGLang接口兼容性OpenAI 格式原生支持OpenAI 格式支持OpenAI 格式支持调度粒度请求级连续调度请求级连续调度更细粒度的 radix 调度Prefix Cache支持基于 block hash支持支持但实现更激进社区生态最活跃模型适配快偏轻量较活跃分布式扩展完善一般加强中我最后选 VLLM第一理由是生态。新模型发布后 VLLM 通常几天内就会出适配版本vllm/entrypoints/openai/api_server.py直接提供了生产可用的 HTTP 服务省去自己搭 service 的时间。第二理由是代码结构相对清晰模块边界比较分明后期如果要改调度逻辑、加自定义算子不至于无从下手。SGLang 在调度上的激进设计确实让人眼馋但综合团队维护成本和踩坑风险VLLM 更稳。2. 从 API 请求到 CUDA kernel一条主线追完关键路径读 VLLM 源码时我最大的感悟是不要按目录结构顺序读而要按“一个请求从进来到出去”的完整生命周期去追。把这根主线追通了你自然就知道哪些目录是干嘛的、哪些类在什么阶段被调用。2.1 请求的完整生命周期我自己总结的主线路径如下HTTP 请求 - vllm/entrypoints/openai/api_server.py接收请求转成 OpenAI 协议格式 - AsyncLLMEngine.add_request()把请求转成内部 SequenceGroup - vllm/core/scheduler.py调度器决定这个请求什么时候被执行 - vllm/worker/model_runner.py加载模型执行 forward - vllm/attention/backends/paged_attn.py调用 PagedAttention 算子 - csrc/attention/attention_kernels.cu真正的 CUDA kernel对新手来说建议从api_server.py开始调试打印每个阶段的耗时。我当初做性能分析时就靠在这些关键位置加日志定位到瓶颈其实不在模型计算上而在调度器的等待队列里。2.2 我优先读的三个源码模块顺着主线我真正精读的不超过十个文件其中三个是重点中的重点第一个是vllm/core/scheduler.py。这个文件是调度器的核心里面Scheduler.schedule()方法输入当前所有等待请求和 GPU 资源状态输出本 step 可以执行的请求列表。读这里的重点是理解三类序列RUNNING正在计算、SWAPPED被换出到 CPU、WAITING排队中。VLLM 的调度策略简单说就是只要 GPU 资源够尽量把WAITING拉进来跑如果显存紧张优先把低优先级的RUNNING请求的 KV Cache 换出腾空间给新请求。第二个是vllm/attention/backends/paged_attn.py。这是 PagedAttention 的接口层里面定义了PagedAttention.forward()核心参数是k_cache、v_cache和block_table。k_cache的形状是[num_blocks, block_size, num_kv_heads, head_dim]看到这个形状你就知道 KV Cache 是按“块”存的了。而block_table是每个序列的逻辑块到物理块的映射表相当于一个翻译官把逻辑上连续的第 0 块、第 1 块翻译成物理显存上零散分布的块号。第三个是vllm/core/block_manager.py。这个文件负责物理块的分配和释放尤其是PrefixCachingBlockAllocator它实现了前缀缓存的核心逻辑。这个类里维护了一张block_hash到物理块的映射表如果两个请求的前缀 block hash 一致就直接复用同一个物理块不再重复申请显存。这个机制回头我会在第 4 节单独展开。3. PagedAttention内存分页这套思路是怎么落到代码里的PagedAttention 是 VLLM 的立身之本。它的灵感来自操作系统里的虚拟内存分页把逻辑上连续的 KV Cache 拆成固定大小的块物理上可以放在任意不连续的显存地址。对应到我们服务端场景就相当于把一张大桌子连续显存换成了一排小格子固定块谁要用就给谁分配几个格子用完再收回来灵活性提升了一大截。3.1 为什么 KV Cache 是大头显存账怎么算在 LLM 推理时权重只占显存的一部分真正随并发上涨的是 KV Cache。要优化显存必须先会算这笔账。我以 Qwen2.5-7B-Instructbf16为例层数 L 28KV heads 4head_dim 128dtype 2 bytes每个 token 每层的 KV Cache 2 * num_kv_heads * head_dim * dtype2 * 4 * 128 * 2 2048 bytes 2KB28 层累计28 * 2KB 56KB / token也就是说一个 token 的 KV Cache 要占 56KB。如果max_model_len 8192一条序列的完整 KV Cache 大约是 56KB * 8192 ≈ 448MB。听着还行对吧但并发 50 条时光 KV Cache 就要 21.8GB这还没算 14GB 的模型权重。所以 KV Cache 的管理效率直接决定你一台机能扛多少并发。而 VLLM 默认block_size是 16。意味着一个物理块的大小是16 * 56KB 896KB约 0.9MB这是 7B 模型一层的块大小精确讲这个值对应的是整 28 层中所有 layer 的子块合计但你可以粗略理解成一个块大概是 0.9MB。分配的最小单位从“整条序列的 448MB”变成了“0.9MB 的块”碎片化程度直接降低两个数量级以上。这就是 PagedAttention 最直接的收益。3.2 逻辑块到物理块的映射源码里最常见的参数是block_table我第一次看的时候一直没绕明白。现在用生活化的例子解释你点了一份套餐序列套餐本身是按固定菜量token计的但餐厅厨房里没有连续的大灶台连续显存只有从小到大乱七八糟的小灶物理块。block_table就是一张点菜清单第一块逻辑区对应 3 号灶台第二块逻辑区对应 17 号灶台第三块对应 8 号灶台。注意力计算时GPU 只需要拿着这张清单去对应灶台取菜读 KV不需要管它们在物理上是不是挨着。在paged_attn.py里最终会调用paged_attention_v1或paged_attention_v2这两个 CUDA kernel。这两个 kernel 的区别在于并行策略v2将block_table和max_num_partitions等参数传入对长序列的数值稳定性更好。实际部署我用 vLLM 默认选择稳定性和性能都在可接受范围。3.3 前缀缓存到底怎么提升命中率再回来说 Prefix Caching。它解决的问题是当两个请求的前缀完全一致时这些前缀的 KV Cache 不需要重复计算。典型场景就是带固定 system prompt 的多轮对话或者是给所有请求加了统一人设的客服机器人。源码实现的核心在block_manager.py的PrefixCachingBlockAllocator。每个物理块被分配时会计算它对应的 token 序列的 hash存在content_hash_to_block字典里。新请求到达时先用同样的规则计算前缀块的 hash如果字典里能查到相同的块就直接把物理块“共用”出来不重新申请显存、也不重新计算。这个设计精妙归精妙实际使用有三个必须注意的点前缀必须逐 token 完全一致多一个空格少一个标点都会导致 hash 变命中率骤降共享块不能随意释放。源码中每一块都有引用计数只有引用计数归零时才能回收开启--enable-prefix-caching并不会在所有场景都有收益如果你的每个请求前缀都不同这个功能只是白白增加 hash 计算的额外开销我在客服场景里实测固定 system prompt 为 500 token开启 Prefix Caching 后首 token 延迟从平均 1.2 秒降到 0.6 秒吞吐提升也接近 40%。这笔账非常划算。4. 调度器与连续批处理并发扛不扛得住全看这里性能优化做到后半段我才明白一个道理VLLM 里真正决定服务吞吐的不是模型计算多快而是调度器会不会“精打细算”。模型计算是死功夫调度策略是巧劲。4.1 调度器的核心逻辑长什么样scheduler.py里最核心的类是Scheduler它的入口是schedule()方法。我简化一下它做的事分三步显式判断当前可用的显存块数量也就是 KV Cache 还剩多少从WAITING队列里挑请求为它们分配块如果能分配就直接进入RUNNING如果显存不够把部分RUNNING请求的块换出到 CPUswap腾出空间给优先级更高的请求这里最反直觉的一点是VLLM 默认不做公平调度而是先到先服务。当请求并发高时先来的长请求会持续占用显存和计算资源后来的短请求可能一直排不上号。这也是为什么我强烈建议线上要按照自己的场景去调参而不是照搬默认配置。4.2 chunked prefill 的取舍在 0.7.x 版本里chunked prefill分块预填充已经是很重要的默认策略。它的思路是一个特别长的 prefill 阶段不一口气算完而是拆成多个 chunk穿插在 decode 请求之间执行。这样做的收益很明显长提示词的请求不会再独占 GPU短请求的 decode 延迟不会因为一个长 prefill 而飙升。但代价是调度复杂度提高并且 chunk 之间的中间状态需要维护。实操中我的建议是如果场景里既有长文档处理又有短问答开启 chunked prefill 是正确的如果你只做短文本、高并发小请求可以把它关掉或者调大--max-num-batched-tokens让每个 step 塞进更多的 token吞吐反而更高。4.3 影响并发上限的三个参数读源码时注意看这些参数怎么影响调度器的判断参数作用我的建议--max-num-seqs一个 step 最多执行的序列数调度器不会超过这个值显存够时调大默认 256小显存调小到 32~64--max-num-batched-tokens一个 step 最多处理的 token 总数短文本场景可以调大长文本场景保持适中--gpu-memory-utilizationKV Cache 可用的显存比例上限一般 0.85~0.95别设 1.0要给运行时和临时张量留余量这几个参数是线上服务的“黄金三角”我在调优时一般先固定前两个只动--gpu-memory-utilization和--max-model-len因为在源码里这两个参数直接影响调度器可用的 block 数量改动效果立竿见影。5. 实操从源码构建到上线一个 Qwen 服务前面讲了原理和代码这一节记录我实际部署 VLLM 0.7.3 的完整过程。注意我推荐用源码方式安装不只是为了能改代码还因为源码安装后你能直接看到vllm目录下的所有源码文件排查问题时方便得多。5.1 环境版本的选择VLLM 对环境的版本要求比较敏感0.7.3 我建议的环境组合如下组件版本备注操作系统Ubuntu 20.04 / 22.04其他 Linux 发行版也能跑但编译坑多Python3.10 / 3.113.12 也可以但部分依赖需要更新CUDA12.1 / 12.4需与 PyTorch 版本匹配PyTorch2.4.0 及以上建议 2.4NCCL跟随 PyTorch 自动安装多卡部署时注意版本踩过最大的坑就是 Python 3.12 配旧版 PyTorch 导致的编译失败。建议新建虚拟环境先装 PyTorch 再装 VLLM顺序不能反。5.2 源码编译安装的完整命令我实际执行的命令如下git clone --branch v0.7.3 https://github.com/vllm-project/vllm.git cd vllm python3.10 -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install torch2.4.0 --index-url https://download.pytorch.org/whl/cu121 pip install -e . --no-build-isolation -v这里有两个关键点第一pip install -e .会触发 C/CUDA kernel 的编译默认可能是多线程并行编译内存占用很大。如果物理内存不够建议在编译前设置export MAX_JOBS4控制并行度。我一开始没设置编译到一半系统直接卡死换成MAX_JOBS4后稳稳通过。第二第一次编译通常需要 30~60 分钟这很正常。VLLM 需要编译csrc目录下的多个 CUDA kernel包括cache_kernels.cu、attention_kernels.cu等。如果不想等可以直接装官方发布的 wheel 包但那就不方便改源码了。5.3 启动服务的完整参数与验证部署 Qwen2.5-7B-Instruct 时我的启动命令是python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.90 \ --max-model-len 8192 \ --max-num-seqs 16 \ --enable-prefix-caching \ --port 8000启动后用 curl 做一次最基础的验证curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: Qwen/Qwen2.5-7B-Instruct, messages: [{role: user, content: 用一句话解释什么是PagedAttention}], temperature: 0.7 }正常会返回带usage信息的 JSON。这里要特别说下--tensor-parallel-size单卡就是 1如果换成多卡参数改成--tensor-parallel-size 4VLLM 会自动把模型权重切分到 4 张卡上。但要注意多卡部署时 NCCL 通信很容易成为瓶颈实测下来 8 卡比 4 卡提升明显4 卡比 2 卡提升取决于模型大小不是线性的。6. 我在这套系统上踩过的坑和排查思路最后整理几个我在 VLLM 0.7.3 上用源码部署时真实踩过的坑每个都是可以复现的也附上排查方向。6.1 模型 max seq len 与 KV Cache 冲突启动时报错ValueError: The models max seq len (xxx) is larger than the maximum number of tokens that can be stored in KV cache根源是--max-model-len设太大导致按这个长度预留的 KV Cache 块数严重不足。解决办法两个一是调低--max-model-len比如从 16384 降到 8192二是调高--gpu-memory-utilization。但显存已经吃紧时优先调低--max-model-len更稳。这个坑本质是调度器没有足够的 block 给每条序列使用所以无论并发多低都会报错。6.2 并发一高就 EngineDeadError这是我最初遇到的顽疾。排查之后发现根因不是模型崩溃而是服务内部的队列堆积太多请求后调度器在等待锁或资源时超时导致 engine 被判定为死循环。解决思路是限制--max-num-seqs、--max-num-batched-tokens让同一时刻进入执行态的请求数可控。同时建议把日志级别打开观察是否出现Waiting for available KV cache blocks这类提示。出现这条日志就说明 block 分配严重不足要么扩容显存要么降并发。6.3 Prefix Caching 完全不生效开了--enable-prefix-caching但首 token 延迟没降检查下来发现是请求里的 system prompt 每次都多了一个时间戳变量导致前缀 hash 永远对不上。这不是 VLLM 的问题而是 OS 层的“纯前缀匹配”特性决定的。排查方法很简单用相同的两条请求日志对比确认除最后一轮用户消息外前面的内容完全一致。还有一个点Prefix Caching 在 0.7.x 版本里需要调度器开启enable_prefix_cachingTrue而部分自定义 Serving 层调用EngineArgs时没有透传这个参数导致实际没生效。我每次排查都会先确认启动日志里有没有这一行Enable prefix caching with a block size of 16。6.4 源码编译慢或编译 OOM编译 VLLM 源码最常见的问题是内存不足而不是显存不足。C 和 CUDA kernel 的编译需要大量内存建议MAX_JOBS4甚至MAX_JOBS2。如果编译中途报internal compiler error先检查内存而不是去找编译器版本问题。另外使用 VLLM 0.7.x 时建议把torch升级到 2.4 以上旧版torch与新版 VLLM 的算子注册逻辑不兼容经常报Operator was not registered这类错。6.5 VLLM 与 SGLang 的切换经验因为项目需要我中间对比过 SGLang。坦白讲 SGLang 在调度上更激进某些场景吞吐比 VLLM 高尤其长 prompt 场景的 prefix cache 命中率设计很亮眼。但它的社区生态和模型适配速度落后于 VLLM而且对自定义算子和自定义模型的支持成本更高。我的建议是如果你的场景比较标准两个框架都能胜任如果团队要频繁接入新模型VLLM 是更稳妥的选择。如果对极致性能有执念SGLang 值得单独做一轮压测再决定是否切换。最后再分享一个排查技巧在我读 VLLM 源码和做线上调优时最常用的一个方法是“跟随一次请求的日志”。先把日志等级调到DEBUG然后发起一个真实请求追踪这条请求从进入api_server到执行完的完整链路观察它何时被调度、何时被分配 block、KV Cache 具体用了多少块。把这些细节和scheduler.py的源码对照起来看比读十篇解析文章都管用。根据我个人实际维护这套系统的经验互联网上关于 VLLM 的参数调优帖很多但项目千差万别别人的“最优参数”未必适合你的场景。与其到处抄配置不如花一个下午把scheduler.py里关于 block 分配的那段逻辑读透自己算清楚显存账后面的优化都会顺很多。如果这篇文章能让你少走一点弯路我就觉得很值了。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →