VLLM 0.7.3源码精读:PagedAttention与连续批处理如何提升吞吐

发布时间:2026/10/14 14:58:18
VLLM 0.7.3源码精读:PagedAttention与连续批处理如何提升吞吐 简介VLLM-0.7.3源码完整工程包面向大模型推理引擎开发者、算法工程师与进阶研究者旨在解决超大规模语言模型推理阶段的高并发响应与显存瓶颈问题。资源共1896个文件压缩包约46.33MB以Python源码(1186个)、CUDA/C核心实现、JSON/YAML配置和Markdown文档为主另含构建脚本、Docker部署文件等从底层算子到上层接口均有覆盖目录结构清晰便于按模块阅读与二次编译。当前已有342人学习下载属于热度持续上升的优质源码资源。通过这份源码可系统查看VLLM的高速令牌生成、高效内存管理等关键实现理解从模型加载到推理输出的完整链路同时工程内附带的测试脚本、工具配置和示例文件也为后续性能调优、框架对比与二次开发提供了扎实基础。对想深入掌握大模型推理加速工程的读者来说是一份难得的完整参考。1. 为什么 0.7.3 值得逐行看VLLM 源码里藏着吞吐量差距的答案同一张卡上跑同一个 7B 模型有人能做到每秒四千个 token有人只能卡在一千出头。差距往往不在硬件而在推理框架怎么管显存和调度。VLLM 0.7.3 源码就是把这件事讲清楚的一条路径它用 PagedAttention 把 KV Cache 拆成物理块用连续批处理填满每一轮生成间隙再用张量并行把大模型拆到多卡上。这一代版本处于相对稳定的中期保留了早期架构的核心机制又还没背上太多实验性开关适合逐行精读。目标读者是做推理服务优化、想把手头的生成延迟和吞吐瓶颈真正解决的从业者。这篇文章从源码地图一路拆到调度器再给出复现和排错路径照着读就能少走弯路。2. 从入口类到 Worker 进程VLLM 0.7.3 的源码地图与启动链路2.1 源码目录怎么读先分清引擎层、执行层和模型层VLLM 0.7.3 的源码体积不大但分层非常规整。我第一次读的时候按模型推理的直觉去找 forward 函数结果被目录结构绕了一圈。后来摸出的顺序是从入口往内层走依次看引擎层、执行层、模型层。引擎层负责请求生命周期和调度执行层负责把任务分发到单卡或多卡模型层才真正管算子和权重布局。引擎层里对外接口和内部循环是分开实现的。对外接口提供同步的 LLM 类和异步的客户端封装内部核心则维护调度器、缓存配置和 Worker 池。执行层会区分单卡和多卡两套实现多卡时额外做通信组初始化和张量切分。模型层按模型类型和算子类型组织文件PagedAttention 的实现、量化实现都在这一层找算子时直接往这里走。我一般建议按问题驱动读代码你现在被什么问题卡住就定位到对应层。看不懂调度就只读调度器类不碰模型算子看不懂显存就只读缓存引擎和块管理器。这套读法比从头到尾通读省时间而且每层读完都能直接对应到一个线上问题。读的时候保持一个习惯把每个类里的init先读一遍它决定了这个类的全部状态从哪来。2.2 启动一条推理请求LLM 到 EngineCore 的完整调用链# vllm 0.7.3 客户端最小启动脚本 from vllm import LLM, SamplingParams llm LLM( model/data/models/qwen2.5-7b-instruct, # 换成你本地可访问的模型路径 tensor_parallel_size1, # 单卡时保持 1多卡时改成卡数 gpu_memory_utilization0.85, # 分配给 KV Cache 和中间张量的显存比例 max_model_len8192, # 与模型 config 里的最大位置编码对齐 ) params SamplingParams( temperature0.7, top_p0.9, max_tokens256, ) out llm.generate([请解释什么是 KV Cache], params) print(out[0].outputs[0].text)这段代码是 0.7.3 对外接口的标准用法。gpu_memory_utilization 不是越高越好我习惯留出 0.1 到 0.15 给驱动、通信库和临时张量否则跑长序列时容易直接 OOM。max_model_len 如果超过模型原生长度启动时日志会给出警告但不同版本处理方式不一样有的直接截断有的报错读源码时值得注意。从 generate 方法进去调用路径大致是LLM.generate 把请求交给引擎核心引擎核心先做 tokenizer 编码再进入调度循环。调度器根据显存余量决定哪些序列可以进入本次迭代然后交给 Worker 线程完成真正的 forward 计算。输出再沿着同一条路径返回每一层都有对应的日志关键字。把日志级别调到 DEBUG 再跑一次看到的日志顺序就是这条调用链的最直观证明。从接收到请求、分配 KV Cache 块、执行 prefill、逐个 decode到返回结果每一步都有打印点。以后排查性能问题先看日志里的调度阶段在哪一步耗时最长而不是直接怀疑算子。2.3 VLLM 0.7.3 里值得精读的 5 个核心类类名职责读源码时关注点LLM对外同步入口负责 tokenizer 和结果组装generate 方法如何拆解批请求EngineCore引擎核心持有调度器和缓存配置init里各配置如何固化Scheduler迭代级调度决定谁进本轮计算三个序列队列如何流转Worker实际执行 forward 的进程/线程如何加载权重和调用模型运行器ModelRunner把输入批次喂给模型的执行器如何构造输入张量、调用模型 forward这五个类正好对应五个线上问题。生成速度慢就查 Scheduler 和 EngineCore 的调度参数显存不足就查 EngineCore 里的缓存配置怎么传给 Worker多卡行为不一致就查 Worker 之间的通信初始化。我读的时候按 LLM 到 EngineCore 到 Scheduler 再到 Worker 的顺序ModelRunner 放在最后因为它依赖前面几个类传入的张量布局。把这五个类理清之后你会发现自己已经能看懂 0.7.3 大部分日志输出了。比如启动时打印的 KV Cache 分配大小来自缓存配置计算运行时打印的吞吐数据来自调度器的迭代统计。这些不是黑匣子只是以前没有按链路拆开看。3. PagedAttention 与显存调度0.7.3 的 KV Cache 管理是怎么做的3.1 从块表分配看 PagedAttention物理块不连续也不慌传统推理框架为每个请求预分配一整段连续显存请求多了就互相挤占碎片严重。PagedAttention 的思路来自操作系统的分页机制把 KV Cache 切成固定大小的块物理块散落在显存各个位置通过块表把逻辑视图映射到物理块。请求不再需要一整段连续空间显存利用率因此提升。VLLM 0.7.3 里块大小由 block_size 参数控制默认是 16也就是每个块能存放 16 个 token 位置的 KV 值。块表的实现逻辑可以用下面这个简化模型理解实际源码中还要处理空闲块池、引用计数和写时复制# 模拟 PagedAttention 的逻辑块表结构 class BlockTable: def __init__(self, block_size: int 16): self.block_size block_size self.logical_blocks [] # 逻辑块编号列表 self.block_cache {} # 逻辑块编号 - 物理块编号 def allocate(self, num_tokens: int): needed (num_tokens self.block_size - 1) // self.block_size for _ in range(needed): logical_id len(self.logical_blocks) physical_id allocator.free_pool.pop() # 从空闲物理块池取 self.logical_blocks.append(logical_id) self.block_cache[logical_id] physical_id return self.block_cache这段代码演示了块表的两级映射。allocate 方法先计算需要的块数再为每个逻辑块分配一个物理块。实际源码里不会直接操作 free_pool而是通过块分配器统一管理但核心思想一致。block_size 越大块表映射开销越小但长序列尾部浪费的空间也越大block_size 越小显存碎片越少但块表条目更多。注意修改 block_size 后必须重启进程引擎启动时缓存配置已经按旧值完成初始化运行期修改不生效。读源码时重点看块分配器如何回收块、如何判断块是否空闲。有个细节值得留意新请求接入时调度器会优先复用已有序列组释放的物理块而不是每次都向 CUDA 申请新显存这也是 0.7.3 在持续对话场景下延迟表现稳定的原因之一。3.2 连续批处理调度器不再等整批结束才释放显存没有连续批处理的框架里一批请求必须同时完成才能一起释放显存慢的请求会拖住快的请求。VLLM 0.7.3 的调度器在每个解码迭代结束后都重新做一次决策新请求可以插入、完成的请求立刻退出、超预算的请求被暂时换出。这个机制通常被称为 Continuous Batching是吞吐提升的关键。调度器内部维护三类序列集合等待运行的、正在运行的、被换出的。每次迭代开始时调度器优先把等待队列里的请求塞进运行集合但要满足显存预算上限。吞吐和延迟的平衡主要通过 max_num_seqs 和 max_num_batched_tokens 控制后者限制了单次迭代最多能处理的 token 总数。# 调度决策的简化逻辑 def schedule(self): # 1. 先处理上一轮完成的请求释放它们的物理块 for seq in self.running: if seq.is_finished(): self.running.remove(seq) self.block_manager.free(seq.block_table) # 2. 尝试从等待队列加入新请求 while self.waiting and len(self.running) self.max_num_seqs: new_seq self.waiting.pop(0) if self.block_manager.can_allocate(new_seq): self.running.append(new_seq) self.block_manager.allocate(new_seq) else: self.swapped.append(new_seq) # 显存不足就先放一边这段逻辑说明调度器不是简单地先来先服务。它先释放已完成请求的资源再评估等待队列能否接入接入条件是显存块够不够。注意 can_allocate 和 allocate 是两步先检查后分配避免分配失败留下脏状态。我把 max_num_seqs 调大后观察过等待队列很快排空但单次迭代计算时间也在上升因为更多序列挤在同一轮 forward 里。连续批处理的另一个关键是 prefill 和 decode 拆开。新请求需要先做 prefill 生成首 token已有请求继续做 decode。0.7.3 的调度器会把这两类请求合并到同一次迭代计算但内部的注意力掩码不同。这个设计让 GPU 在每一轮都尽量满负荷运转而不是等整批请求同步推进。3.3 这代显存分配参数怎么配四个数的搭配逻辑参数默认值调大效果调小效果gpu_memory_utilization0.9KV Cache 更多并发更高留给临时张量空间更足不易 OOMblock_size16块表更短映射开销低尾部浪费少碎片更少max_num_seqs256单轮并发高吞吐上升延迟更稳定显存压力小max_num_batched_tokens2048单次 forward 计算量增大调度更频繁时延更平滑这四个参数不是独立调优的。max_num_seqs 乘上序列平均长度决定了活跃 token 总量这个总量必须小于 KV Cache 能容纳的块数。如果 max_num_seqs 设得太大调度器会频繁把序列换出换入显存和计算都花在搬运上吞吐反而下降。我一般先用默认值跑通再按线上请求长度倒推这几个数。有一种流量场景值得单独说长上下文但并发低。此时 max_num_seqs 不需要大倒是 max_model_len 和 block_size 的组合更关键因为长序列会持续占用大量块。如果尾部 token 使用量不固定把 block_size 调小能减少浪费比如从 16 降到 8。反之短请求为主的场景就不要动 block_size默认值已经足够。4. 把源码跑出一条可观测路径单卡最小复现与双卡张量并行4.1 从源码构建可调试环境依赖组合与最小安装读源码最怕的就是边读边猜跑不起来就全白读。我建议先在本机构建一个可调试环境不用多卡集群一张卡足够跑通全链路。VLLM 0.7.3 对 Python 版本有要求我用 3.10 或 3.11 都跑过配合 CUDA 12.x 的 PyTorch 版本最省事。装完基础环境后从源码安装而不是直接装发布包这样能随时跳进源码加打印。# 从源码构建 vllm 0.7.3 的最小环境 conda create -n vllm-dev python3.11 -y conda activate vllm-dev # 安装 PyTorch这里用 cu124 作为示例实际按你驱动版本选 pip install torch --index-url https://download.pytorch.org/whl/cu124 # 克隆源码并进入目录安装可编辑模式 git clone 你的 vllm-0.7.3 源码仓库地址 cd vllm pip install -e . --no-build-isolation安装过程中最容易出问题的是编译算子核心是 flash-attention 或 xformers 与 CUDA 版本的匹配。如果编译报错先检查 nvcc 版本和 torch.version.cuda 是否一致不一致时以 PyTorch 自带的 CUDA runtime 为准。pip install -e . 会先把 Python 包装好再编译 C 和 CUDA 算子这个过程可能需要十几分钟属于正常现象。装完后验证环境是否就绪直接导入再跑一次帮助命令然后加载一个很小参数的模型先做 smoke test比如用一个几百 MB 的对话模型。这一步能过滤掉九成环境问题后面再调大模型就会顺畅很多。4.2 单卡跑通最小生成验证 KV Cache 确实被分配# 单卡最小生成脚本 from vllm import LLM, SamplingParams llm LLM( model/data/models/qwen2.5-7b-instruct, tensor_parallel_size1, gpu_memory_utilization0.8, max_model_len4096, enable_prefix_cachingTrue, # 开启前缀缓存方便观察块复用 ) params SamplingParams(max_tokens32) out llm.generate([给我写一句欢迎语], params) print(out[0].outputs[0].text)跑这段脚本时重点不是输出内容而是终端里的启动日志。VLLM 0.7.3 启动时会打印模型加载信息、张量并行配置和 KV Cache 的分配结果。你会在日志里看到类似“KV Cache size”的描述那串数字就是这一轮运行时实际分给缓存块的显存大小记下来继续读源码时能对上号。验证 KV Cache 是否生效的简单办法是对比加载模型前后的显存占用。先跑一次上面的脚本记录 nvidia-smi 显示的显存占用再退出进程重新加载模型但不跑 generate比较两次占用的差值。多出来的部分就是 PagedAttention 为 KV Cache 预留的块池。这个差值应该接近 gpu_memory_utilization 乘以总显存再减去模型权重占用。如果跑完发现显存占用远高于预期常见原因是 max_model_len 设得太大导致块池预分配过大。0.7.3 里 KV Cache 的容量跟 max_model_len 直接相关最大长度翻倍块池通常也会翻倍。读代码时注意看容量计算逻辑它把 max_model_len、max_num_seqs 和 block_size 三者乘在一起你就明白为什么改一个参数会牵动另外两个了。4.3 双卡张量并行观察 TP2 时显存与算子切分# 双卡张量并行启动脚本 from vllm import LLM, SamplingParams llm LLM( model/data/models/deepseek-vl-7b, # 用一个支持多卡的模型 tensor_parallel_size2, # 两块卡并行 gpu_memory_utilization0.85, max_model_len8192, ) params SamplingParams(max_tokens64) out llm.generate([用两句话介绍张量并行], params) print(out[0].outputs[0].text)tensor_parallel_size2 时模型权重会被按张量维度切分到两张卡上。以 QKV 投影矩阵为例常见切法是把输出维度按 head 数均分每张卡各持有半条权重前向计算时需要跨卡做一次通信同步结果。你现在可以在另一终端跑 nvidia-smi 观察两张卡的显存占用会发现两边几乎相等这是正常的不是加载错误。要确认两层 Worker 各拿到了半条权重可以看启动日志里的权重加载信息通常每个 worker 进程都会打印自己加载的模型分片信息。如果两边输入路径和 env 变量一致它们拿到的模型前缀就相同差异只在权重数值。继续读源码时重点看模型并行层如何把 QKV 权重切分后再喂给 PagedAttention 的算子。注意双卡并行跑通只是第一步真正的问题排查往往发生在通信环节。如果出现结果不一致或卡死先看 NCCL 初始化是否完成再看是否有自定义算子漏掉了通信调用。5. 读 VLLM 0.7.3 源码时最容易踩的 5 个坑5.1 改了调度参数不生效现象在脚本里修改 max_num_seqs 或 gpu_memory_utilization 后启动日志里打印的配置值没有变化吞吐表现也和默认值一致。原因引擎核心在初始化阶段就把这些配置固化成内部状态了。后面对配置对象做修改不会重新触发缓存池分配和调度器初始化。很多人以为改完参数再调用 generate 就能生效实际上日志里那行配置在进程启动瞬间就已经定死。解决改完任何调度或显存参数一定要重启进程。想确认参数是否生效直接看启动日志里引擎配置那一段不要只看代码里的传参。我习惯把关键配置打到启动日志里这样每次调参都能对照。# 启动时确认配置生效 llm LLM(model..., max_num_seqs128) print(llm.llm_engine.scheduler.max_num_seqs) # 通过调度器实例读取实际值5.2 KV Cache 显存碎片化导致 OOM现象nvidia-smi 显示显存只用了七成但继续发请求却报 CUDA OOM。接着看日志KV Cache 分配的盒子大小正常但新请求仍无法分配连续块。原因block_size 和 max_num_seqs 搭配不合理导致物理块池被切割成无法合并的小块。PagedAttention 的块表虽然允许物理块不连续但块分配器本身需要从空闲块里找到足够数量碎片化严重时单个请求申请多个块会失败。解决把 block_size 调小、把 max_num_seqs 调小。先用默认值跑通再逐步压缩。我踩过一次坑把 max_num_seqs 拉到 512结果短请求把块池拆得七零八落长请求全被拒绝。后来按长度聚类流量把并发控制在 128 以内就好了。5.3 KV Cache 精度下降导致输出退化现象同样 prompt 和采样参数长上下文对话越往后越重复甚至开始输出循环内容。对比 FP16 与 FP32 缓存的结果差异明显。原因KV Cache 默认按低精度存储来省显存。序列越长误差累积越明显。0.7.3 里缓存数据类型可以通过配置指定很多人没设置就沿用默认策略精度不符预期。解决在配置里显式设置 kv_cache_dtype比如用 FP8 存储 KV Cache同时保留输出端的高精度计算。改完对比同一批 prompt 的生成分布如果循环现象缓解就是精度问题。此类问题适合量化敏感的模型纯小模型影响不大。5.4 多卡并行时状态不同步现象tensor_parallel_size2 跑起来后生成结果时对时错。同一个 prompt 有时输出正常有时直接报错或出现乱码。原因自定义算子或者较新的模型结构在 forward 里漏掉了跨卡通信。张量并行要求每张卡的计算结果在进入下一层之前做一次全局规约漏掉这一步后续张量布局就对不齐。解决看报错堆栈是不是卡在模型算子和通信原语之间重点检查模型代码里是否有 all_reduce 之类的调用。我一般先把 tensor_parallel_size 改回 1 跑同一段代码确认单卡正常再逐步排查多卡。如果算子是自己写的先对比每个 worker 的中间张量形状是否一致。5.5 换小版本后算子行为漂移现象同一份代码从更早版本升到 0.7.3 后生成结果发生变化但配置完全没变。原因某些算子的默认实现被切换比如 attention 从高效注意力实现换到另一套算子计算顺序变化导致浮点结果有细微差异。这种差异通常在采样时被放大表现为输出文本不同。解决固定镜像版本记录算子派发日志。0.7.3 的算子选择逻辑里有 fallback 机制日志会打印实际用了哪个算子。对比两个版本各自的派发日志就能定位差异源头。别小看这个坑线上模型如果依赖稳定的采样结果版本升级前必须做输出一致性校验。6. 给 VLLM 0.7.3 加一条调度日志最小可验证的调试习惯6.1 找到调度器的决策点读了一个星期源码你可能已经知道调度器在每次迭代都会做决策但看不到它实际挑中了谁、跳过了谁。我习惯在调度方法入口挂一条打点用 monkey patch 的方式在导入后替换方法不用改动源码本体。# 在调用 vllm 之前挂载补丁 from vllm.core.scheduler import Scheduler _original_schedule Scheduler.schedule def patched_schedule(self, *args, **kwargs): result _original_schedule(self, *args, **kwargs) # result 里包含调度决策后的序列组列表 for seq_group in result: print( f[schedule] seq_id{seq_group.seq_id} fstate{seq_group.state} fblock_num{len(seq_group.block_table)} ) return result Scheduler.schedule patched_schedule补丁的原理很简单先调用原始调度方法再遍历调度结果把每个序列组的核心字段打印出来。state 字段能看到是运行中还是换出block_num 能看到当前占用的物理块数量。这条日志能直接回答两个问题请求是不是一直在等待队列里以及显存是不是实际分到了块。验证方法跑一个并发请求的脚本对比终端日志顺序和最终吞吐数据。如果某个请求一直打不到 staterunning说明调度策略把它卡住了如果 block_num 频繁变化说明块表在反复分配释放。这个办法帮我解决过一次高并发下的请求饥饿问题只靠源码阅读反而容易漏掉。我自己读源码的习惯是能加打印就不靠猜测过的路径才算数。远程环境改了源码不方便重编译monkey patch 是最后一道后悔药。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询