ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

vLLM高吞吐推理:PagedAttention、连续批处理与部署调优实践

2026/8/28 19:41:40 拓冰建站 浏览量
vLLM高吞吐推理:PagedAttention、连续批处理与部署调优实践 vLLM 是目前部署大模型时最常见的高吞吐推理引擎之一。很多人第一次接触它是因为用原生 Transformers 跑生成时速度太慢、显存容易被并发请求占满或者部署在线服务时 QPS 上不去。这篇内容会把 vLLM 的内部机制、部署方式、参数调优和常见排查顺序完整拆一遍适合正在做模型选型、自己部署大模型服务或者已经被爆显存和未知报错折腾过的开发者。最值得关注的地方不是“它比 Transformers 快几倍”这种口号而是它为什么快、在什么条件下快、以及哪些场景不要硬上。1. 先把 vLLM 解决的问题说清楚1.1 大模型推理真正的瓶颈不是算力是显存和调度Transformer 自回归生成时每生成一个 token 都要读取整个模型权重和之前生成过的所有 token 的 KV 缓存。KV Cache 大小和序列长度成正比越长的对话、越大的并发显存占用越恐怖。如果每个请求都提前预留最大长度对应的缓存再乘以并发数显存很快被吃掉。更麻烦的是不同请求的输入长度、预期输出长度差异很大预留的空间要么不够要么大量浪费。传统 Transformers 的 naive 推理流程是“一个请求一个请求跑或者把一个 batch 整个跑完再接受下一批”。如果是同步 batch只要 batch 里有一个生成长文本的请求其他生成完的请求也必须等到最后才返回GPU 在等待期会出现空转。这个问题在高并发和长尾延迟下尤其明显。vLLM 的核心目标就是解决两个问题第一显存浪费第二批处理等待。它采用了自己设计的 PagedAttention 管理 KV Cache让显存像操作系统虚拟内存一样按页分配需要多少分配多少同时引入了 Continuous Batching 机制生成完一个请求就释放资源立刻插入下一个请求而不是等整个 batch 结束。这样做的效果是同样一块 GPU 上能够承载的并发序列数明显提升生成阶段不再被慢请求拖住GPU 利用率更高。这也是为什么很多部署教程默认推荐 vLLM——它不只是“加速单条输出”而是“提高系统整体吞吐”。1.2 先建立合适的预期vLLM 不是万能的。它主要优化的是解码阶段的吞吐如果你的任务全部是短输入短输出、并发又低vLLM 和原生 Transformers 的差距不会很大。如果任务以超长上下文为主或者单个请求独占整块 GPUvLLM 的收益更多体现在显存管理和长上下文处理上。更关键的是它需要 CUDA、合理显存和正确配置才能发挥优势否则启动阶段就可能被一堆 CUDA/依赖问题拦住。我建议先把“能不能启动、单条请求是否正常、批量并发是否稳定”这三个循序渐进的目标列出来再往下看部署方法。这样不会被各种性能优化参数带着跑也能更快定位问题。2. 拆开 vLLM高吞吐背后的几个关键机制2.1 PagedAttentionKV Cache 的动态分页管理PagedAttention 是 vLLM 最核心的原创设计。传统方式为每个序列分配连续显存长度不确定导致碎片化PagedAttention 将 KV Cache 切分为固定大小的 block每个 block 可以存储一定数量的 token 的 K 和 V。序列的逻辑地址是连续的但物理地址可以分散在不连续的显存块里由 block table 记录映射关系。这样做的好处很明显显存按需分配接近真实使用量而不是按最大长度预留。不同序列可以共享 KV 块比如 system prompt、few-shot 示例、多轮对话的前缀只要内容一致可以物理共享同一块显存再配合前缀缓存大幅降低重复计算。批处理时不用为每个序列预留空洞内存碎片减少。这也解释了为什么 vLLM 的 max-model-len 和 gpu-memory-utilization 设置会影响成功率如果你设置的总上下文长度太大KV Cache 预留空间就多能容纳的并发序列就少如果把显存利用率设得过高系统可能无法为 CUDA kernel 和激活值留下足够空间最终 OOM。2.2 Continuous Batching请求不是整批进、整批出自回归生成一次只能生成一个 token每个请求在不同时刻可能处于不同阶段。Continuous Batching连续批处理的思路是每一个 decoder step 都重新选择哪些序列进入 batch。简单理解一批请求进入后有的生成完了立刻把显存释放新的请求马上补进来没生成完的继续参与下一轮如果某个序列生成了 padding token 或达到 max tokens就退出。整个过程在 token 级别动态调度而不是在请求级别固定 batch。这样 GPU 几乎一直在算有效 token而不是等待最长请求。vLLM 默认会尽量把 batch 打满这也是它高吞吐的主要原因之一。调度策略涉及 preemption当请求数超过显存容量时把部分早期请求的 KV Cache 换出到 CPU等资源释放后再换回有点类似操作系统的 swap。这种行为会影响性能所以生产环境中最好通过参数限制并发而不是让系统频繁抢占。2.3 CUDA Graph、投机解码、量化这些优化在 vLLM 里扮演什么角色除了 PagedAttention 和 Continuous BatchingvLLM 还整合了不少推理优化手段CUDA Graph减少 kernel launch 的开销。PyTorch 执行很多小 kernel每个都有 CPU 启动成本CUDA Graph 把一串 kernel 捕获成图一次提交减少调用开销。代价是需要额外显存和预热。--enforce-eager就是关闭 CUDA Graph改用 eager 模式显存占用会下降但吞吐可能下降。投机解码Speculative Decoding用一个更小的 draft model 生成候选 token再用目标模型一次验证多个 token从而减少自回归步数。适合小模型带大模型的场景vLLM 支持多种算法。并不是所有环境都能获得明显收益需要正确选择草稿模型和采样参数。量化把权重从 FP16/BF16 压到 INT8、INT4 等格式减少显存占用和内存带宽。vLLM 支持 AWQ、GPTQ、FP8 等常见方式。量化不是免费午餐精度和兼容性要实际测试。这些优化叠加后vLLM 的吞吐优势才会明显。如果你只是默认参数跑起来没做任何配置也可能已经不错但系统调优还有很大空间。2.4 除了快vLLM 还解决了服务化接口问题对很多后端开发来说vLLM 的价值还在于它直接提供 OpenAI 兼容接口。你不需要自己写 HTTP 服务、请求队列、并发调度启动一个 API server就能统一对外提供/v1/chat/completions、/v1/completions和部分工具调用能力。这意味着现有代码可以无缝切换也可以用上层框架对接。这一点在 2025 年的部署环境里几乎是刚需。vLLM 已经从一个“显存优化库”演变成一个完整推理服务系统支持流式输出、结构化输出、多模态输入、离线批处理、分布式推理等。理解它不能只看显存优化还要把它当作一个生产组件来对待。3. 本地部署 vLLM从环境准备到单条请求跑通3.1 环境条件先确认一遍vLLM 官方主要支持 Linux NVIDIA GPU CUDA 环境。生产环境最稳妥的是用 Docker 镜像镜像里已经包含对应版本的 CUDA、PyTorch 和依赖能避开很多系统库问题。如果你在 Ubuntu 上手动安装要先确认GPU 驱动能通过nvidia-smi正常显示。PyTorch 版本、CUDA 版本和 vLLM 版本匹配。Python 版本在 vLLM 要求的范围内通常 3.9 到 3.12 左右具体以当前版本为准。能正常安装依赖包因为 vLLM 依赖较多可能需要编译磁盘空间和编译工具也要有。如果需要在 Windows 10/11 上跑建议直接用 WSL2 或基于 WSL 的 Docker原生 Windows 支持通常不推荐。Mac 用户如果只是学习推理也可以考虑 llama.cpp 这类更适合 Apple Silicon 的引擎vLLM 的 Apple 支持目前比较有限生产环境还是以 Linux NVIDIA 为主。注意不要一上来就装最新版或源码编译版。先确认你的 CUDA 驱动和 PyTorch 能跑通一个小模型再用 vLLM 减少变量。3.2 安装 vLLM最简单的方式是使用 pip 安装pip install vllm如果使用 Docker可以拉取官方镜像docker pull vllm/vllm-openai:latest然后挂载模型目录、映射端口启动容器。手动安装时如果遇到编译报错常见原因是 CUDA 路径、gcc 版本或 ninja 缓存有问题先把基础编译环境清理干净。安装完成后可以先用 Python 验证import vllm print(vllm.__version__)能打印版本号说明基本依赖没问题。如果这一步就报 CUDA 或符号错误说明 PyTorch/CUDA 版本和 vLLM 不匹配。3.3 启动一个本地大模型服务先用最小配置启动一个开源对话模型。以下面 Qwen 系列的 7B/8B 模型为例python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --served-model-name qwen-7b \ --dtype bfloat16 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --host 0.0.0.0 \ --port 8000如果你的机器显存只有 24GB 左右这个配置通常可以启动如果显存只有 12GB 或更小建议换 3B/4B 模型或者把--max-model-len降到 4096并考虑加--enforce-eager减少显存占用。启动日志里能看到配置信息、显存分配和模型加载进度。等到日志出现类似 Starting vLLM API server 之后再发起请求。3.4 用 curl 或 Python 验证请求裸启动 API 后先不要接网关用一条 curl 验证curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen-7b, messages: [{role: user, content: Hello!}], max_tokens: 64 }返回 JSON 中包含choices和生成内容说明服务正常。接着可以用 Python 写一个简单脚本连续发 5 条请求观察首 token 延迟、生成速度和显存变化。这里最容易踩的坑是model参数没传对要用--served-model-name指定的名字。max_tokens没设置或太短结果看起来像模型“没输出”。端口已经被占用需要换端口或停掉旧进程。4. 高吞吐场景下的参数调优和批量任务设计4.1 核心参数的含义和常见调整思路参数作用调优建议--gpu-memory-utilization允许 vLLM 使用多少显存比例默认 0.9如果还要跑其他服务降到 0.7-0.8如果显存很大且模型很小可以设到 0.92-0.95但要留激活值空间--max-model-len模型最大上下文长度输入输出不是越大越好。超长上下文会让 KV Cache 预留变大影响并发和启动成功率--max-num-seqs最大并发序列数默认值通常是 256实际受显存限制。并发高、输出短可以调大输出很长时不要盲目调大--enforce-eager是否关闭 CUDA Graph显存不足时先用它排查能启动但速度下降显存充足不用开--dtype精度float16、bfloat16、float32新卡优先bfloat16如果没有用float16float32占用翻倍一般不用--tensor-parallel-size张量并行卡数多卡吞吐不一定线性提升通信开销和模型切分方式都要评估这些参数不是独立起作用的。比如你把max-num-seqs调到很高但显存不足系统可能频繁抢占或直接 OOM你把gpu-memory-utilization设到 0.98CUDA 初始化可能都失败。4.2 在线服务的高吞吐压测思路先跑单条再跑并发。我一般会先在脚本里构造 10 个并发请求每个请求输出 128 tokens观察成功返回的请求数平均生成速率tokens/s是否存在超时显存是否稳定不持续增长。如果 10 个并发稳定再逐步加到 50、100。不要一开始就压满因为一旦触发 preemption日志会出现大量抢占信息延迟会恶化不好定位是调度问题还是显存问题。vLLM 还提供/metrics端点Prometheus 格式可以看vllm:num_requests_running、vllm:num_requests_waiting、vllm:num_preemptions等指标。如果 waiting 一直很高说明请求超过了处理能力如果 preemption 很多说明显存或 max-num-seqs 设置不合理。4.3 离线批处理用 Python 批量生成时怎么组织输入输出如果要做离线批量生成可以用 vLLM 的 Python API例如from vllm import LLM, SamplingParams llm LLM(modelQwen/Qwen2.5-7B-Instruct, tensor_parallel_size1) prompts [解释一下什么是 KV Cache, 写一首关于秋天的短诗, ...] params SamplingParams(max_tokens256, temperature0.7) outputs llm.generate(prompts, params) for out in outputs: print(out.outputs[0].text)批处理时要注意输入长度差异差异过大时长输入会让 batch 里短输入的 padding 变多推荐按输入长度分组分批。输出长度控制max_tokens如果统一设很大速度会慢如果业务上有些问题只需要短回答可以分开设置。输出文件命名和保存批量任务跑完前不要靠日志判断结果建议把 request_id、prompt、output 写入结构化文件方便排查乱序和缺失。4.4 多卡、量化与精度选择多卡并行时vLLM 支持--tensor-parallel-size。它的原理是把模型权重和 KV Cache 按层/张量切分到多张卡适合单卡放不下的模型。但跨卡通信开销客观存在不一定每加一张卡吞吐都翻倍。如果两张卡之间没有 NVLink或 PCIe 带宽低通信可能成为瓶颈。社区里有人问“L20 不能用双卡运行模型吗”。实际上问题不在这张卡能不能 TP而在于你的 CUDA、驱动、vLLM 版本、模型大小和通信链路是否满足。如果只是显存不够可以先压缩上下文长度或量化如果必须双卡建议先看nvidia-smi topo -m了解卡间拓扑再决定是否用 TP。没有高速互连时TP 的收益可能不如单卡 离线分片。精度方面社区搜索里经常出现fp16、fp32、bf16的选择问题。简单来说fp32精度高显存大速度通常慢一般只在调试精度问题时使用。fp16常见半精度多数模型能用但训练/推理中范围比 bf16 小。bf16动态范围和 fp32 接近适合大模型推理Ampere 及以上架构支持较好。如果模型权重本身就是bf16启动时指定--dtype bfloat16最直接。量化场景则根据模型和 task 实测不要只看显存下降还要看输出质量是否可接受。4.5 使用 --enforce-eager 的时机和影响很多人遇到 OOM 后第一反应是加--enforce-eager。这个参数会关闭 CUDA Graph 优化让 vLLM 像普通 PyTorch 一样逐 op 执行能明显减少预分配的显存因此小显存环境或排查 OOM 时很有用。代价是 kernel 启动开销变大生成吞吐可能下降越小的 batch 越在 CPU 启动上吃亏。如果你设置了--enforce-eager才能启动并不代表这是最佳生产配置。更合理的做法是先降低gpu-memory-utilization降低max-model-len合理设置max-num-seqs最后才考虑关闭 CUDA Graph。显存实在不够就用更小模型或量化。不要为了启动一个模型把所有性能优化都关掉。5. 常见报错、稳定性问题和排查链路5.1 启动失败先按日志顺序排查vLLM 启动失败时日志通常会告诉你哪一步出错。我建议按这个顺序排查看 CUDA 状态nvidia-smi是否正常驱动显存是否够用。看依赖版本pip list | grep vllm、torch、transformers版本是否兼容。看模型路径本地路径是否有权限模型仓库是否能访问路径写没写错。看端口和进程8000 端口是否被占用是否有残留 vLLM 进程。看显存和内存启动前显存是否被其他进程占用内存是不是不足。如果报错包含CUDA error: out of memory直接进显存排查如果报ImportError: ...先重装匹配版本如果报ValueError: The models max seq len检查--max-model-len是否小于模型要求。5.2 OOM 和爆显存怎么定位显存不足是 vLLM 最常见的问题。一次 OOM 可能发生在加载权重阶段、KV Cache 预分配阶段也可能发生在请求运行中途。不同位置处理方式不同。加载权重阶段 OOM模型权重就占掉大部分显存需要换更小模型、开启量化或换更大显存的机器。预分配阶段 OOMgpu-memory-utilization设置过高或max-model-len太大导致 KV Cache 预算过大。先降低这两项。请求运行中 OOM并发过高或某个请求生成了超长输出。可以降低max-num-seqs设置更合理的max_tokens或打开--enforce-eager临时判断。除了 OOM显存泄漏现象是显存持续增长且不回落。可以开启 vLLM 的日志观察已使用显存和缓存变化。如果长时间运行后显存不断上涨优先更新 vLLM 版本并排查是否使用了自定义模型或插件。5.3 输出为空、速度慢、并发上不去怎么判断输出为空或极短通常不是模型坏了而是max_tokens设置为 0 或太小请求中stop参数误伤输出一生成就被终止输入格式不对比如 chat 模型缺了messages或者用的不是对应的 chat 模板tokenizer 与模型不匹配导致生成乱码或空内容。速度慢要区分是单请求慢还是并发后变慢。单请求慢可能是因为使用了 CPU 推理或者 GPU 太老或enforce-eager关闭了 CUDA Graph。并发后变慢则可能是显存不足导致 preemption或者max-num-seqs设置过小等待队列堆积。并发上不去时我会先看running和waiting指标。如果waiting高但 GPU 利用率低可能是请求输入过长或输出过长导致 batch 很容易被撑满如果 GPU 利用率已经很高那就是真实瓶颈需要加节点或优化模型。5.4 关于 Embedding、Reranker 和特殊硬件的适配vLLM 不止能生成也支持 Embedding 模型比如启动时可以指定--task embed或使用/v1/embeddings接口。但对 Embedding/Reranker 的支持和生成模型不是完全一样的路径不是所有模型都能直接跑起来。有人问昇腾 910B 服务器能不能通过 vLLM 启动 embedding 和 reranker这里要区分两层问题vLLM 对推理后端有硬件算子要求非 NVIDIA 平台需要确认自定义算子、CUDA Graph 和量化是否兼容。Embedding/Reranker 是特殊任务类型除了模型架构外还要看版本是否实现了对应 task 的预处理和输出接口。如果遇到“不支持”的报错不要先怪模型应该看版本说明和模型配置。如果只是普通业务里需要向量化建议优先使用官方针对 embedding 的框架或原生的 transformers 管线不要在 vLLM 里一路硬啃到 reranker。6. 什么情况适合 vLLM什么情况要换方案6.1 vLLM 和 SGLang、Transformers、llama.cpp 怎么选很多人在选型时会纠结 vLLM 和 SGLang。两者都是高性能推理框架理念上有很多相似之处。vLLM 的生态更成熟OpenAI 兼容接口、社区文档、第三方工具适配都更丰富SGLang 在某些场景下的调度和 Auto Prefix 处理可能更激进部分测试里表现更好但迭代速度和稳定性需要自己评估。如果你只是需要把模型跑起来做前端 demo原生 Transformers 或 vLLM 都可以但 vLLM 更适合长时间服务。如果你在低资源边缘设备比如只有 CPU 或 Apple Siliconllama.cpp 的部署体验通常比 vLLM 顺畅得多。vLLM 的定位始终是“高性能 GPU 推理服务”不是全场景推理器。6.2 在线服务、离线批处理、Embedding 场景分别怎么规划在线对话服务优先 vLLM 的 OpenAI 兼容接口配合流式输出、前缀缓存对并发支持好。离线批量任务vLLM 的 PythonLLM类很好用但要注意输入分组、输出落盘、失败重试。如果任务量极大也可以写成脚本分片避免单进程内存过大。Embedding 和 Reranker先确认 vLLM 版本是否支持该模型类型。大多数常规嵌入模型可以跑但如果要求高并发和精确排序专门框架更稳。长上下文研究可以先试 vLLM 对长序列的支持但要关注 KV Cache 显存占用。如果上下文长度超过 128K需要仔细评估max-model-len和并行策略。6.3 一些值得提前确定的边界vLLM 的配置可以调得很激进但生产环境里我一般把“可观测性”放在第一位。启动参数里要能看到显存分配、KV Cache 池大小、模型并行方式运行中要有/metrics和结构化日志请求失败时能根据 request id 定位到输入和输出。这些比单纯追求一个更高的 tokens/s 重要得多。如果只是学习默认配置通常够用如果要长期使用就要把日志、输出目录和任务队列提前整理好。很多看起来像功能不支持的问题最后发现是输入格式、依赖版本或资源限制造成的。踩过几次之后你就会发现vLLM 这套系统真正难的不是跑起来而是跑起来之后还能稳定地高吞吐运行。