vLLM推理加速:PagedAttention与连续批处理部署实战

vLLM推理加速:PagedAttention与连续批处理部署实战 我们先把一个真实场景摆出来你在本地用 Python 部署了一个开源大模型比如 Qwen 或 Llama 系列单卡 A100 或 4090项目跑了几天发现响应越来越慢GPU 利用率却不到 30%。你开始怀疑是不是显存不够是不是显卡太旧甚至考虑换 A800。但换过 GPU 的同学都知道真正的瓶颈往往不在硬件而在推理引擎和调度策略。vLLM 之所以在 2024 到 2025 年成为大模型推理部署的事实标准之一不是因为它的名字好听而是因为它同时解决了两个非常核心的问题第一显存浪费严重导致并发上不去第二批量处理效率低GPU 计算单元大量空转。这两个问题叠加会让同样一张显卡的吞吐能力相差数倍。这篇文章的目标很明确从原理到部署帮你把 vLLM 的 PagedAttention、连续批处理Continuous Batching、OpenAI 兼容 API 这三件事讲透并用 Python 实际跑通一个可用的推理服务。你会看到为什么它能快也会看到实际部署中最容易踩的坑。1. 这篇文章真正要解决的问题很多同学第一次接触 vLLM其实是带着问题来的“为什么我用 Transformers 的 pipeline 推理那么慢为什么部署成 API 之后并发一高就超时”这些问题的根因并不是模型本身跑得慢而是推理服务的工程化程度不够。传统 Transformers 库的 generate 流程一次请求占用一整块显存而且请求之间是串行处理的。假设你的模型是 7B 参数FP16 精度光权重就占 14GB 显存。再加上每个请求的 KV Cache一个 8GB 显存的请求可能直接占掉 20GB 以上的空间。于是你发现一张 24GB 的 4090只能同时处理两三个请求还容易 OOM。vLLM 的贡献在于它把推理引擎从“模型权重中心”重构为“显存管理 调度器中心”。从这个角度看vLLM 不只是一个推理加速工具更像是一个为大模型量身定做的操作系统管理显存、调度请求、复用缓存、批处理任务。读完这篇文章你能收获什么理解 PagedAttention 为什么能提升显存利用率以及它和操作系统虚拟内存的关系。理解连续批处理为什么能提升吞吐以及它和静态批处理的本质区别。学会用 vLLM 部署一个兼容 OpenAI 接口的本地服务并用 Python 调用。掌握常见部署报错的排查思路和关键参数调优建议。适合的读者用 Python 做 LLM 应用开发、正在做本地模型服务化、或者准备在生产环境部署推理服务的工程师。不适合的读者完全没跑过任何大模型、只想看概念不打算动手的同学。2. 核心概念KV Cache、PagedAttention 与连续批处理2.1 为什么生成长文本时显存会爆炸先看一次普通的自回归生成过程。模型生成一个 Token需要依赖前面所有 Token 的键值特征。为了避免每个 Token 都重新计算前面全部 Token 的 Key 和 Value推理框架会把计算过的 Key 和 Value 缓存起来这个缓存就是 KV Cache。问题在于KV Cache 的大小和序列长度成正比而序列长度是动态的。传统框架在请求开始时就为最大可能长度预分配显存。比如你设置 max_length 为 2048但实际只生成到 200 个 Token中间就有大量显存被预占了却根本没用到。更糟糕的是不同请求的序列长度差异很大。一个请求刚进来就生成完了另一个请求却要生成几千个 Token。如果不能动态调配显存就会出现“一个长请求把显存挤爆其他短请求全部排队”的局面。2.2 PagedAttention像虚拟内存一样管理显存PagedAttention 的思路借鉴了操作系统中的分页内存管理。传统方案为每个请求分配连续显存PagedAttention 则把 KV Cache 切分成固定大小的块Block每个块可以放在显存中任意位置块之间通过索引表连接。这个设计带来的直接好处有四个显存碎片化减少块大小固定不需要为大请求预留一整块连续空间。按需分配请求只占用实际需要的块而不是预分配最大长度。共享前缀多个请求如果共享相同的系统提示词或对话历史块可以被复用省去重复计算。更高并发同样的显存下可以容纳更多请求。从工程角度看PagedAttention 并没有改变模型计算方式它改变的是显存的数据结构。正是这种结构变化让 GPU 利用率显著提升。2.3 连续批处理告别“等满一车再发车”什么是静态批处理就是把请求收集到一定数量后统一做前向传播。问题是每个请求生成速度不同有的 10 步就结束了有的要 100 步。如果所有人必须一起结束才能“下车”那些短请求就只能空等。连续批处理Continuous Batching则是在迭代级别调度。每 forward 一步处理完的请求立刻离开新的请求马上补进来。相当于公交车不再等满一车才发车而是每到一个站点下完客就立即上客出发。这个机制配合 PagedAttention让 vLLM 能在单个请求级别做资源调度。结果就是吞吐量上去了首 Token 延迟反而可能更低因为新请求不用等当前批次全部完成才开始推理。2.4 三者如何配合可以这样理解PagedAttention 负责让显存使用更高效连续批处理负责让 GPU 计算单元始终有活干。两者结合vLLM 可以在不降低单请求延迟的前提下大幅提高单位时间完成的请求数。根据官方文档和社区常见测试数据在相同硬件条件下vLLM 的吞吐量相比原生 Transformers 实现可以达到数倍甚至一个数量级的提升。这也是标题里“快 8 倍”说法的来源之一。具体数字会因模型、显存、请求长度和并发数不同而浮动但方向是确定的。3. 环境准备与依赖安装在写代码之前先把环境准备好。vLLM 官方目前主要在 Linux 和部分 Windows 环境下支持GPU 驱动和 CUDA 版本是关键。本文的示例环境如下但版本可结合实际项目调整操作系统Ubuntu 20.04 / 22.04Windows 也可通过 WSL2 尝试但生产环境建议 LinuxGPUNVIDIA 显卡建议显存 16GB 以上Python3.8 及以上版本CUDA11.8 或 12.1 均可以 vLLM 官方 wheel 支持的版本为准推荐使用 Conda 或 venv 创建独立虚拟环境安装 vLLM 最简单的方式是 pip# 创建虚拟环境推荐 python -m venv vllm-env source vllm-env/bin/activate # 安装 vLLM pip install vllm如果网络环境较慢可以使用国内 PyPI 镜像pip install vllm -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后验证版本python -c import vllm; print(vllm.__version__)需要说明的是vLLM 版本迭代很快具体 API 可能随版本微调。本文以通用稳定的用法为准如果你安装的版本较新遇到接口变动优先查看官方 Release Notes。3.1 模型下载与选择vLLM 本身不包含模型需要从 Hugging Face 或 ModelScope 下载权重。以 Qwen 系列为例常用模型可以是 Qwen2.5-7B-Instruct、Qwen3-8B 等。如果你在国内网络环境从 ModelScope 下载往往更稳定pip install modelscope使用 modelscope 的 Python SDK 下载模型from modelscope import snapshot_download # 下载到本地目录 model_dir snapshot_download(Qwen/Qwen2.5-7B-Instruct, cache_dir./models) print(model_dir)下载完成后记下本地模型路径vLLM 加载时会用到。4. vLLM 部署核心流程拆解vLLM 的使用方式总体上分为两种一种是在 Python 代码里直接调用离线推理另一种是启动一个常驻 API 服务对外提供 OpenAI 兼容接口。实际项目中第二种更常见因为服务化了才能被上层应用调用。4.1 离线推理快速验证模型是否可用离线推理适合写脚本做测试、跑批量评测、调试 Prompt。它的优点是简单直接不需要启动服务。# 文件路径offline_inference.py from vllm import LLM, SamplingParams # 加载模型 llm LLM(modelQwen/Qwen2.5-7B-Instruct, gpu_memory_utilization0.9) # 定义采样参数 sampling_params SamplingParams( temperature0.7, top_p0.8, max_tokens512, ) # 输入提示词 prompts [ 请用三句话介绍 vLLM 是什么。, 写一个 Python 函数判断一个字符串是否是回文。, ] # 执行推理 outputs llm.generate(prompts, sampling_params) # 输出结果 for output in outputs: prompt output.prompt generated_text output.outputs[0].text print(fPrompt: {prompt!r}) print(fGenerated text: {generated_text!r}) print( * 50)关键参数解释gpu_memory_utilization0.9允许 vLLM 使用 90% 的 GPU 显存。剩余部分留给 CUDA context 和其他开销。对于显存较小的显卡可以调低到 0.8 或 0.7。max_tokens512单次请求最大生成 Token 数。temperature和top_p控制随机性值越小越保守。运行脚本python offline_inference.py如果一切正常你会看到 vLLM 先加载权重打印模型信息然后逐个输出生成结果。4.2 启动 OpenAI 兼容 API 服务离线推理跑通后就可以把模型服务化。vLLM 内置了 OpenAI 兼容的 API Server启动命令如下python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --served-model-name qwen2.5-7b \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.9 \ --max-model-len 4096 \ --enforce-eager参数说明--model模型名称或本地模型路径。如果已经用 modelscope 下载到本地可以直接写本地路径避免重复下载。--served-model-name对外暴露的模型名称。客户端请求时model 字段要和这个值一致。--host 0.0.0.0监听所有网卡允许其他机器访问。本地测试也可以只监听 127.0.0.1。--port 8000服务监听端口。--gpu-memory-utilization控制 GPU 显存使用比例。--max-model-len限制最大上下文长度。如果模型本身是 32K 上下文但显存不够可以调低这个值。--enforce-eager强制使用 Eager 模式不启用 CUDA Graph。显存不够或启动报错时可以先加这个参数。启动后终端会打印类似这样的日志INFO: Started server process [12345] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8000到这里一个本地大模型 API 服务就已经跑起来了。4.3 用 curl 验证服务是否可用服务启动后先用 curl 做一次快速验证curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5-7b, messages: [ {role: user, content: 你好请介绍一下你自己} ], max_tokens: 256, temperature: 0.7 }正常的返回是 JSON 格式包含 choices、usage 等字段。如果返回 404可能是--served-model-name和你请求里的 model 不一致。如果返回 400通常是请求格式问题检查 messages 是否合法。4.4 用 Python 客户端调用 APIvLLM 兼容 OpenAI SDK所以可以直接用openai包来调用本地服务。pip install openaiPython 调用示例# 文件路径client.py from openai import OpenAI # 注意 base_url 指向本地 vLLM 服务 client OpenAI( base_urlhttp://localhost:8000/v1, api_keyEMPTY, # vLLM 本地服务默认不校验 key ) response client.chat.completions.create( modelqwen2.5-7b, messages[ {role: system, content: 你是一个乐于助人的助手。}, {role: user, content: 用 Python 写一个快速排序算法。}, ], max_tokens512, temperature0.7, ) print(response.choices[0].message.content)这里有个容易忽略的细节base_url必须以/v1结尾否则请求路径会变成/chat/completions导致 404。这是很多新手第一次接入时的常见报错。5. 完整示例压测脚本与性能对比为了直观感受 vLLM 的性能优势我们写一个简单的并发压测脚本向本地 API 连续发送多个请求统计总耗时和吞吐量。# 文件路径benchmark_client.py import time import threading from openai import OpenAI BASE_URL http://localhost:8000/v1 MODEL_NAME qwen2.5-7b def send_request(prompt, results, index): client OpenAI(base_urlBASE_URL, api_keyEMPTY) start time.time() try: response client.chat.completions.create( modelMODEL_NAME, messages[{role: user, content: prompt}], max_tokens256, temperature0.7, ) elapsed time.time() - start content response.choices[0].message.content results[index] {ok: True, elapsed: elapsed, len: len(content)} except Exception as e: results[index] {ok: False, error: str(e)} if __name__ __main__: # 并发请求数 concurrency 10 prompts [ 解释一下什么是 KV Cache以及为什么它对大模型推理很重要。, 写一个 Python 装饰器用于统计函数执行时间。, 列举三种提高大模型推理性能的方法并简要说明原理。, 什么是 PagedAttention它解决了什么问题, 用 Python 写一个简单的 HTTP 服务器。, 解释什么是连续批处理为什么它比静态批处理效率更高。, 请写一首关于秋天的五言诗。, 什么是 OpenAI 兼容 API为什么很多推理框架都支持它, 写一个二分查找算法并解释时间复杂度。, 在本地部署大模型时最需要注意哪些问题, ] * concurrency results [None] * len(prompts) threads [] total_start time.time() for i, prompt in enumerate(prompts): t threading.Thread(targetsend_request, args(prompt, results, i)) threads.append(t) t.start() for t in threads: t.join() total_elapsed time.time() - total_start # 统计结果 success [r for r in results if r and r[ok]] failed [r for r in results if r and not r[ok]] total_tokens sum(r[len] for r in success) print(f总请求数: {len(prompts)}) print(f成功请求数: {len(success)}) print(f失败请求数: {len(failed)}) print(f总耗时: {total_elapsed:.2f} 秒) print(f吞吐量: {len(success) / total_elapsed:.2f} requests/s) print(f平均响应时间: {sum(r[elapsed] for r in success) / len(success):.2f} 秒)这个脚本会同时发起 100 个请求然后统计吞吐和平均响应时间。如果同样条件下你用 Transformers 原生实现也写一个类似的 API能直观感受到差距。6. 运行结果与效果验证启动服务后建议先按以下顺序验证健康检查访问http://localhost:8000/health返回{status: ok}说明服务存活。模型列表访问http://localhost:8000/v1/models确认模型名与会话中的--served-model-name一致。单请求跑通用上面的 curl 命令验证一次对话回复。并发压测运行 benchmark 脚本观察吞吐量和失败率。如果失败优先看服务端日志。常见情况是模型权重还在加载中、显存不足导致 OOM或者请求参数超过了--max-model-len限制。一个值得关注的性能指标是首 Token 延迟TTFT。vLLM 的 API 响应默认是流式返回如果你用普通方式请求需要等完整结果生成完才返回。想要更实时的体验可以在请求体中加stream: true这样客户端可以边生成边接收。Stream 调用示例stream client.chat.completions.create( modelqwen2.5-7b, messages[{role: user, content: 讲一个程序员的笑话}], max_tokens256, streamTrue, ) for chunk in stream: delta chunk.choices[0].delta.content if delta: print(delta, end)流式输出的好处是用户不需要等待全部生成完成第一句话很快就能出来这在对话类应用里体验提升非常明显。7. 常见问题与排查思路问题现象可能原因排查方式解决方案服务启动时报 CUDA out of memoryGPU 显存不足或权重过大查看 GPU 显存用量nvidia-smi调整--gpu-memory-utilization量化模型或换更大显存显卡请求返回 404 not found请求的 model 名称和--served-model-name不一致访问/v1/models查看可用模型名修改请求中的 model 字段请求返回 400 bad requestmessages 格式错误或超出 max_model_len检查请求体 JSON 格式查看服务端日志修正消息格式减小 max_tokens 或增大 max_model_len首次请求特别慢模型权重尚未完全加载或 CUDA Graph 正在捕获观察服务端日志第一次请求前可以先发一个 warmup 请求生成速度不稳定时快时慢连续批处理导致长短请求混跑查看服务端日志中的排队信息调整并发限制或对超长请求单独处理服务启动报 ValueError: Unknown model format模型路径错误或量化格式不兼容确认本地模型文件完整性检查模型目录是否包含 config.json、tokenizer.json 等文件调用 OpenAI SDK 时提示 Connection error服务未启动或端口错误curl 测试 /health 接口确认服务进程存活检查端口占用情况显存占用过高导致系统卡死gpu_memory_utilization设置过高观察 nvidia-smi降到 0.7 或 0.8并配合--enforce-eager降低显存需求7.1 关于量化模型的补充说明如果你在热词里看到 vLLM 与量化模型相关的内容这里补充一点。vLLM 对多种量化方式有支持例如 AWQ、GPTQ、FP8 等。量化能显著降低显存占用也可能带来轻微的精度损失。在显存较小或需要高吞吐的场景里量化往往是比换卡更现实的选择。量化模型的加载方式通常是在--model参数指向量化后的模型目录或在构建LLM对象时指定相关参数。具体参数名会随版本变化建议优先参考你安装版本的官方文档。8. 最佳实践与工程建议8.1 显存分配要有余量在实际项目中gpu_memory_utilization不建议设置到 0.99。因为 CUDA context、TensorFlow/PyTorch 的临时缓冲、显存碎片等都需要额外空间。从生产经验看0.85 到 0.9 是比较合理的区间。如果还同时跑数据预处理或 embedding 模型更要留足余量。8.2 合理设置 max-model-len很多模型支持超长上下文比如 32K 甚至 128K。但不代表你的 GPU 能扛得住。KV Cache 大小和上下文长度呈线性关系长上下文请求会占用大量显存。在线上服务里建议根据业务需求设置一个合理的--max-model-len而不是无脑拉满。比如一般对话场景 8K 已经足够摘要场景 16K 也够了。8.3 启用 Prefix Caching 提升多轮对话性能vLLM 支持 Prefix Caching可以缓存相同前缀的 KV Cache。对于多轮对话、批量处理同模板请求的场景能显著减少重复计算。启动时加上参数--enable-prefix-caching不过要注意如果业务请求几乎都是毫不相关的新 prompt前缀缓存带来的收益就有限。8.4 用 docker-compose 管理生产环境服务化部署时建议使用 Docker 固定环境避免宿主机 Python 环境变化导致服务无法启动。vLLM 官方提供了镜像也可以基于官方镜像自定义。以下是一个 docker-compose 示例# 文件路径docker-compose.yml version: 3.8 services: vllm: image: vllm/vllm-openai:latest container_name: vllm-qwen command: - --model - /models/Qwen/Qwen2.5-7B-Instruct - --served-model-name - qwen2.5-7b - --host - 0.0.0.0 - --port - 8000 - --gpu-memory-utilization - 0.9 - --max-model-len - 8192 - --enable-prefix-caching ports: - 8000:8000 volumes: - ./models:/models deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] restart: unless-stopped启动命令docker-compose up -d生产环境建议把模型权重放到宿主机目录通过 volume 挂载进容器避免每次启动都重新下载或复制模型。8.5 接口安全与调用限制vLLM 默认不校验 API Key本地开发没问题但如果服务暴露到公网任何人都可以调用你的 GPU 资源。更稳妥的做法是在 vLLM 前面加一层反向代理比如 Nginx做访问控制和鉴权。只监听内网 IP不暴露到公网。在应用层做限流防止单个用户的请求打满整个服务。如果要暴露公网至少加一层 Token 校验或 IP 白名单。8.6 监控与日志服务上线后建议关注以下指标GPU 利用率nvidia-smi或 Prometheus DCGM Exporter。请求排队长度vLLM 的日志里可以看到 Running / Waiting 数量。平均首 Token 延迟和 Token 生成速率。错误率包括超时、OOM、非法请求。vLLM 启动时带有 metrics 相关功能可以接入 Prometheus 做监控。生产环境里只看“服务没挂”远远不够要看服务“卡不卡”“吞吐是否稳定”。8.7 模型多副本与水平扩展如果单卡性能不够或者需要更高可用性可以横向部署多个 vLLM 实例前面加负载均衡。每个实例加载同一个模型的副本互不干扰。在 K8s 环境下还可以结合 HPA 根据 GPU 利用率和请求量自动扩缩容。不过要注意vLLM 的显存管理是按单实例来的水平扩展之后KV Cache 的复用就不可能跨实例了。某些场景下比如长上下文多轮对话切流会导致缓存失效。更复杂的场景可以考虑 Prefix Cache 的分布式方案或者在应用层做会话亲和。9. 总结与后续学习方向到这里整篇文章的核心内容已经讲完了。我们从“本地部署大模型为什么慢”这个问题出发解释了 PagedAttention 如何通过分页管理显存来提升显存利用率连续批处理如何通过迭代级调度来提升吞吐然后一步步完成了 vLLM 的离线推理和 OpenAI 兼容 API 部署。如果你想继续深入有四个方向值得研究第一是 vLLM 的调度器实现。连续批处理说起来简单但调度策略里的细节非常多比如优先级、抢占、交换这些直接决定了服务在极端负载下的表现。第二是量化推理。同样的模型FP16 和 AWQ 量化部署后显存占用和吞吐可能差一倍以上但精度损失需要实际测试验证。第三是分布式推理。当单卡装不下模型时vLLM 的 Tensor Parallel 和 Pipeline Parallel 是绕不开的课题涉及通信开销和显存均衡。第四是所有推理框架都通用的性能调优方法论从监控指标出发定位瓶颈再针对性调整参数而不是盲目换卡。最后给一个实用建议不要一开始就追求最大并发和最高吞吐。先在低并发下跑通流程确认服务稳定、输出正确再逐步加压。压测过程中重点观察显存变化和首 Token 延迟这两个指标能帮你快速判断瓶颈在哪里。收藏这篇文章等到真正部署的时候翻出来对照应该能帮你少踩不少坑。