【大模型部署02】—— vLLM 部署与推理优化实战:从启动参数到显存/KV 调优
vLLM 部署与推理优化实战:从启动参数到显存/KV 调优
搞大模型部署的人基本都踩过同一个坑:模型好不容易加载进去了,一压并发吞吐就掉,显存要么爆、要么大片闲置。说到底,LLM 在线服务的瓶颈不在算力,在显存管理和调度。vLLM 就是冲着这个瓶颈来的——它把显存当操作系统内存一样分页管理,硬生生把吞吐量拉到了 HuggingFace Transformers 的 24 倍。
这篇文章解决一件事:让你在生产环境(4×A100 80G)里把 vLLM 用明白。覆盖五个点——vLLM 是什么、启动参数怎么配、显存怎么省、KV Cache 怎么调、PagedAttention 到底是什么原理。基准环境以 vLLM 0.27.x(2026 年 8 月,V1 引擎为默认)为准,具体参数默认值以官方文档为准。
一、先厘清:vLLM 是什么,推理到底慢在哪
vLLM 是 UC Berkeley Sky Computing Lab 开源的 LLM 推理与服务引擎,论文发在 SOSP 2023,核心贡献是 PagedAttention。它目前由 2000+ 贡献者维护,支持 200 多种模型架构,NVIDIA/AMD/Intel/昇腾/TPU 都跑得动,还提供 OpenAI 兼容 API。
要理解它为什么快,得先搞懂自回归解码的 KV Cache 这个东西——它既是推理的核心机制,也是后面「KV 调优」和「PagedAttention」两章的地基,这里先讲透。
1.1 自回归生成,为什么必须缓存 K 和 V
LLM 生成 token 是逐字蹦的:给定前文,预测下一个 token,然后把它拼回去,再预测再下一个。每一步都做一次完整的 Transformer 前向。
注意力机制里,每个 token 会算出三样东西:Query(Q)、Key(K)、Value(V)。算下一个 token 时,当前 token 要和之前所有 token 做注意力。关键点来了——之前那些 token 的 K 和 V 在上一步已经算过了,只要上下文没变,结果就不会变。如果每一步都把整段序列重新算一遍 K/V,那生成第 N 个 token 时就要重复计算前 N-1 个 token 的 K/V,纯属浪费。
所以推理引擎会把历史 token 的 K 和 V 张量缓存在显存里,下一步直接复用,只算新 token 的那一份。这些缓存的键值张量就叫 KV Cache。
1.2 KV Cache 的两个麻烦
KV Cache 有两个让传统引擎头疼的特性:
- 大:官方博客给过一个数,LLaMA-13B 单个序列的 KV Cache 最高能到 1.7GB。
- 动态且不可预测:它的大小取决于序列长度,而序列长度在请求来之前根本不知道。
传统引擎(HF Transformers、早期 TGI)的做法是给每个请求预先分配一块连续显存,大小按最大可能长度预留。结果就是:请求实际只用了一半,另一半预留着浪费。vLLM 团队测下来,这种碎片化和过度预留会让系统浪费掉 60%–80% 的显存。
浪费显存意味着同样一张卡能同时服务的请求更少,GPU 利用率上不去,吞吐自然低。vLLM 的答案就是 PagedAttention——第七章细讲,先记住结论:它把浪费压到了 4% 以下。
一句话总结:推理慢的根因是显存管理——KV Cache 又大又动态,传统预分配浪费 60–80% 显存,vLLM 用分页管理解决了它。
二、环境说明与前置条件
本文所有命令和参数以这套环境为准:
| 组件 | 版本/规格 | 用途 |
|---|---|---|
| GPU | 4× NVIDIA A100 80GB(NVLink 互联) | 单节点多卡推理 |
| vLLM | 0.27.x(V1 引擎默认) | 推理引擎 |
| CUDA | 12.x | 驱动与内核 |
| Python | 3.10+ | 运行环境 |
| 示例模型 | Qwen2.5-72B-Instruct / Llama-3.1-70B | 70B 级权重 |
安装两种方式,生产环境更推荐 Docker(环境一致、好迁移):
# 方式一:pip(开发/单机调试)
pip install vllm
# 方式二:官方镜像(生产推荐)
docker run --runtime nvidia --gpus all \
-v ~/.cache/huggingface:/root/.cache/huggingface \
-p 8000:8000 --ipc=host \
vllm/vllm-openai:latest \
--model Qwen/Qwen2.5-72B-Instruct
--ipc=host 别漏,共享内存不够会让多卡推理直接崩,这是个高频坑。
三、部署:4×A100 多卡张量并行
3.1 单卡还是多卡
官方给出的选型规则很直白:
| 场景 | 方案 | 参数 |
|---|---|---|
| 模型单卡装得下 | 单卡推理,不折腾分布式 | 默认 |
| 单卡装不下、单节点装得下 | 张量并行(TP) | --tensor-parallel-size 4 |
| 单节点装不下 | TP + 流水线并行(PP) | --tensor-parallel-size 8 --pipeline-parallel-size 2 |
70B 级模型 FP16 权重约 140GB,单张 80G 卡放不下,4 张加起来 320GB,绰绰有余。所以走 TP=4,把每一层的权重矩阵按列切分到 4 张卡上,每张卡只持有四分之一权重,层内通过 NVLink 通信。
单节点内 vLLM 默认用 Python multiprocessing 做分布式后端,不用额外装 Ray;只有跨节点才需要 Ray。
3.2 起服务(可照抄)
export CUDA_VISIBLE_DEVICES=0,1,2,3
vllm serve Qwen/Qwen2.5-72B-Instruct \
--host 0.0.0.0 \
--port 8000 \
--tensor-parallel-size 4 \
--dtype auto \
--max-model-len 32768 \
--gpu-memory-utilization 0.90
vllm serve 是 V1 起 OpenAI 兼容服务的统一入口(老命令 python -m vllm.entrypoints.openai.api_server 仍可用但已不建议)。
3.3 验证:看日志里的两个数
启动时盯两行日志:
INFO ... [kv_cache_utils.py] GPU KV cache size: 643,232 tokens
INFO ... [kv_cache_utils.py] Maximum concurrency for 40,960 tokens per request: 15.70x
INFO ... model loaded to GPU in 34.2 seconds
GPU KV cache size是当前配置下 KV Cache 能塞的总 token 数。Maximum concurrency ... 15.70x是估算的并发倍数:每个请求按 40960 token 算,能同时服务约 15 个请求。- 看到
model loaded to GPU且没有 OOM,说明权重落卡成功。
✅ 成功标志:日志出现 model loaded to GPU,且 /metrics(Prometheus)能拉到指标,curl http://localhost:8000/v1/models 返回模型列表。
四、启动参数详解
参数背后都是钱——显存和吞吐。这里按「管什么」分组讲,最后给一张速查表。
4.1 模型加载类
--model:HuggingFace 模型名或本地路径,唯一必填。--dtype:权重和激活的数据类型,auto/half(FP16) /bfloat16/float32。默认auto会自动选——FP32/FP16 模型走 FP16,BF16 模型走 BF16。AWQ 量化时官方建议配half。--max-model-len:上下文最大长度。不指定则从模型 config 自动推导。这是省显存的第一大旋钮(见第五章)。--trust-remote-code:允许执行模型仓库里的自定义代码。私有/魔改模型常需要,生产环境要评估安全风险再开。--load-format:权重加载格式,auto优先 safetensors。冷启动慢可试npcache(numpy 缓存加速二次加载)。
4.2 显存与并发类(核心调优区)
--gpu-memory-utilization:给模型执行器用的显存比例,范围 0~1,默认 0.9。这是每个实例独立的上限——同一张卡跑两个实例,各设 0.5 就互不干扰。KV Cache 的大小就是由它决定的。--max-num-seqs:单次迭代能同时处理的最大序列数,直接决定并发上限和显存占用。⚠️ V1 默认值是 1024(V0 是 256),升级后如果不显式调低,可能突然 OOM——很多从旧版迁过来的人栽在这。--max-num-batched-tokens:单次迭代最多处理的 token 数,默认 8192(在线服务)。值越大 prefill 吞吐越高但单请求延迟越差;值越小交互响应越跟手。是「吞吐 vs 延迟」的核心旋钮。--swap-space:每张卡的 CPU 交换空间,默认 4 GiB。显存不够时把 KV Cache 块换到内存里兜底,代价是慢。--cpu-offload-gb:每张卡把多少 GiB 权重 offload 到 CPU,默认 0。可以当「虚拟加显存」用,但要求 CPU-GPU 带宽高,否则每个 forward 都卡在搬运上。
4.3 并行类
--tensor-parallel-size/-tp:张量并行副本数,多卡推理核心参数,默认 1。--pipeline-parallel-size/-pp:流水线阶段数,跨节点或卡数不整除模型时用。--distributed-executor-backend:mp(单机多进程,默认)/ray(跨节点)。
4.4 量化与缓存类
--quantization/-q:权重量化方法,awq/gptq/fp8等,配合预量化模型使用。--kv-cache-dtype:KV Cache 存储精度,auto/fp8/fp8_e4m3/fp8_e5m2。设fp8直接省一半 KV 显存(第五章细讲)。--block-size:每个 KV Cache 块的 token 数,CUDA 上最大 32。调小块能减少尾部浪费,但块表更大、调度开销略增。--enable-prefix-caching:前缀缓存。V1 默认已开启——多个请求共享同一段系统提示词/RAG 文档时,前缀 KV 只算一次,RAG 场景通常带来 30–50% 吞吐提升,零代码改动。
4.5 参数速查表
| 参数 | 默认值 | 作用 | 典型调优方向 |
|---|---|---|---|
--gpu-memory-utilization | 0.9 | 显存占用上限,决定 KV Cache 容量 | 单实例压满 0.90~0.95;多实例调低 |
--max-num-seqs | 1024 (V1) | 并发序列数上限 | 大模型/长上下文降到 64~256 防 OOM |
--max-num-batched-tokens | 8192 | 单迭代 token 上限 | 交互场景调低保 TTFT |
--max-model-len | 模型默认 | 上下文长度 | 按业务实际需求调低,省显存 |
--tensor-parallel-size | 1 | 张量并行卡数 | 单节点卡数填满 |
--kv-cache-dtype | auto | KV 存储精度 | 设 fp8 省一半 |
--quantization | 无 | 权重量化 | AWQ/FP8 省权重显存 |
--block-size | 平台默认 | 分页块大小 | 长上下文可调大 |
--swap-space | 4 | CPU 交换空间 GiB | OOM 兜底 |
五、显存优化:先搞懂显存都花在哪
优化显存,前提是知道显存被谁吃了。推理时一张卡的显存大致分成四块:
| 构成 | 说明 | 能否优化 |
|---|---|---|
| 模型权重 | 参数本身,FP16 下 70B ≈ 140GB | ✅ 量化 |
| KV Cache | 动态增长,随并发和上下文膨胀 | ✅ 精度/上限 |
| 激活值 | 前向计算的中间结果,短生命周期 | ⚠️ 空间有限 |
| 系统预留 | CUDA context、驱动、碎片 | ❌ 基本动不了 |
优化手段按「哪个构成」对号入座:
5.1 权重:量化
权重是最大头。AWQ 4-bit 能把权重从 FP16 压缩到约四分之一(官方文档口径:文件体积减少约 70%),代价是极小精度损失。用现成预量化模型即可:
vllm serve hugging-quants/Meta-Llama-3.1-70B-Instruct-AWQ-INT4 \
--quantization awq \
--dtype auto \
--tensor-parallel-size 4
FP8 权重在 Hopper 卡上有硬件加速,70B 级常见做法是直接上 FP8 量化模型(--quantization fp8),省一半权重显存还能吃到算力红利。
5.2 KV Cache:降精度
KV Cache 是第二大头,且随并发线性膨胀。--kv-cache-dtype fp8 能把 KV 存储精度从 FP16 压到 FP8,省约 50% 显存,换来更长上下文或更高并发:
vllm serve Qwen/Qwen2.5-72B-Instruct \
--tensor-parallel-size 4 \
--kv-cache-dtype fp8
不校准直接用也行(scale 默认 1.0),但官方推荐开 --calculate-kv-scales 让它在 warmup 时用一批随机 token 自动估缩放因子,精度更稳;追求极致精度才用 llm-compressor 拿校准数据集离线标定。
5.3 上限控制:让 KV Cache 别乱涨
KV Cache 总容量 = 显存预留 × 单 token KV 大小,而并发需求 = max-num-seqs × max-model-len。这两个参数直接框定了 KV Cache 的峰值:
--max-model-len别无脑拉满 128K。业务里 90% 请求可能就 8K,设 32768 已经富余,省下的显存全给了并发。--max-num-seqs按真实 QPS 收敛。V1 默认 1024,低并发场景降到 128 甚至 64,能释放大量预留给并发的显存。
5.4 兜底:offload 与 swap
显存实在不够,两个兜底手段:
--swap-space 8:把 KV Cache 块换到 CPU 内存,极端长请求能撑住,代价是延迟。--cpu-offload-gb 10:把部分权重放 CPU,相当于「虚拟扩卡」。但 CPU-GPU 带宽是硬伤,吞吐会明显掉,只适合非实时场景。
一句话总结:权重靠量化,KV 靠降精度(fp8),峰值靠 max-model-len + max-num-seqs 双上限,实在不够再 offload。
六、KV Cache 调优:先搞懂再动手
KV Cache 在 1.1 节已经讲过「为什么存在」,这里补上「怎么算」和「怎么调」。
6.1 一个序列的 KV Cache 有多大
公式:
每 token KV Cache = 2 × 层数 × KV头数 × 头维度 × 单元素字节数
「2」是因为 K 和 V 各一份。以 Llama-3.1-70B 量级(约 80 层、8 个 KV 头、头维度 128、FP16)估算,单 token 约 320KB。乘上序列长度和并发数就是总量:
KV 总显存 ≈ 每 token KV 大小 × 平均序列长度 × 并发请求数
这就是为什么「并发上不去」往往不是卡不够,而是 KV Cache 塞满了。具体数值以模型 config.json 为准,这里只给量级。
6.2 调优手段优先级
| 手段 | 效果 | 代价 | 建议 |
|---|---|---|---|
--kv-cache-dtype fp8 | KV 显存减半 | 极小精度损失 | 🔴 首选,几乎白捡 |
调低 --max-model-len | 直接砍峰值 | 截断超长请求 | 按业务 99 分位长度设 |
调低 --max-num-seqs | 降低并发预留 | 吞吐上限下降 | 按真实 QPS 收敛 |
--block-size 调优 | 减少尾部浪费 | 块表开销 | 长上下文场景试 32 |
--enable-prefix-caching | 共享前缀只算一次 | 哈希开销 | V1 默认已开,别关 |
6.3 吞吐 vs 延迟:两个方向的配法
KV 调优没有「最优」,只有「你要什么」。两套典型配置:
# 高吞吐:压满显存、拉高并发(离线批处理/高 QPS API)
vllm serve meta-llama/Meta-Llama-3.1-70B \
--tensor-parallel-size 4 \
--max-num-seqs 512 \
--max-model-len 8192 \
--quantization fp8 \
--gpu-memory-utilization 0.90
# 低延迟:控制并发、保首 token 延迟(交互/语音类)
vllm serve meta-llama/Meta-Llama-3.1-70B \
--tensor-parallel-size 4 \
--max-num-seqs 64 \
--max-model-len 4096 \
--gpu-memory-utilization 0.85 \
--block-size 16
前者要的是 tokens/秒,后者要的是首 token 时间(TTFT)和字间延迟(ITL)稳住。⚠️ 没有一套参数通吃,压测后用真实流量调。
7、PagedAttention:像操作系统一样管显存
前面反复提到「传统引擎浪费 60–80% 显存」,这里把原理讲透。
7.1 传统 KV Cache 的问题:碎片化和过度预留
传统做法给每个请求预分配一整块连续显存,大小按最大可能生成长度算。两个后果:
- 内部碎片:请求提前结束,剩下的预留空间没人用,空着。
- 外部碎片:不同请求长度不一,连续块之间留下无法利用的碎隙。
叠加起来,浪费高达 60–80%。这就是 PagedAttention 要解决的。
7.2 分页思想
PagedAttention 的灵感直接来自操作系统的虚拟内存分页。核心就一句话:KV Cache 不要求连续存储。
具体做法:把每个序列的 KV Cache 切成固定大小的块(block),每块装固定数量 token 的 K/V。块之间在物理内存里不需要挨着。做注意力计算时,内核通过一张**块表(block table)**按需定位、取用这些块。
映射关系记牢这三组类比:
| 操作系统 | PagedAttention |
|---|---|
| 页(page) | 块(block) |
| 字节(byte) | token |
| 进程(process) | 序列 |
序列的连续逻辑块通过块表映射到不连续的物理块,物理块随新 token 生成按需分配。这样一来:
- 不用提前预分配,浪费只发生在每个序列的最后一个块里。
- 实测内存浪费压到 4% 以下,接近最优。
显存利用率上去了,同样一张卡能同时塞更多序列,GPU 利用率跟着涨,吞吐自然起飞——这就是 vLLM 比 HF 快 24 倍的根本原因。
7.3 顺带的红利:内存共享
分页还带来了一个额外好处——跨序列共享。并行采样(一个 prompt 出多个结果)或 beam search 里,多个输出序列共享同一个 prompt 的 KV。通过块表,不同序列的逻辑块可以映射到同一个物理块,prompt 的 KV 只存一份。
共享的安全性靠引用计数 + 写时复制(Copy-on-Write)保证:某序列要往共享块里写新 token 时,先复制一份再写,互不污染。官方数据显示,这套机制让并行采样/beam search 的内存开销降低最高 55%,转化为 2.2 倍的吞吐提升——没有 PagedAttention,这些高级采样在生产环境根本玩不转。
一句话总结:PagedAttention 把 KV Cache 切成不连续的分页块,按需分配 + 引用计数共享,把显存浪费从 60–80% 压到 4% 以下。
八、总结:你真正需要记住的 N 件事
- vLLM 快的根因是显存管理,不是魔法:PagedAttention 把 KV Cache 分页管理,浪费从 60–80% 压到 4% 以下。
- 单节点多卡走张量并行:
--tensor-parallel-size填满节点卡数,4×A100 就填 4,默认 mp 后端,跨节点才上 Ray。 - 省显存三件套:权重量化(AWQ/FP8)、KV 降精度(
--kv-cache-dtype fp8省一半)、双上限收敛(--max-model-len+--max-num-seqs)。 - V1 的默认
max-num-seqs是 1024(V0 是 256),升级后别忘显式调低,否则突然 OOM。 - 没有通吃参数:高吞吐和低延迟是两套配法,用真实流量压测后定。
- 前缀缓存 V1 默认开,RAG/多轮对话别手贱关掉,白拿 30–50% 吞吐。
验证清单
-
vllm serve启动后,日志出现model loaded to GPU,无 OOM 报错 - 启动日志里
GPU KV cache size与Maximum concurrency符合预期 -
curl http://localhost:8000/v1/models返回模型列表 - Prometheus
/metrics能拉到吞吐、延迟、KV Cache 利用率指标 - 用真实流量压测,确认
--max-num-seqs与--max-model-len不触发 OOM - 开启
--kv-cache-dtype fp8后,做一轮精度回归(对比关键样例输出)
参考资源
- vLLM 官方文档(引擎参数):https://docs.vllm.ai/en/latest/serving/engine_args.html
- vLLM 并行与扩展:https://docs.vllm.ai/en/latest/serving/parallelism_scaling.html
- vLLM 官方博客(PagedAttention 发布):https://blog.vllm.ai/2023/06/20/vllm.html
- PagedAttention 论文(SOSP 2023):https://arxiv.org/abs/2309.06180
- vLLM V1 用户指南:https://docs.vllm.ai/en/latest/getting-started/v1-user-guide.html
- 量化 KV Cache 文档:https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache.html
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)