AI vLLM:PagedAttention不够用了之后
先说背景:我们团队去年要做个企业级问答系统,模型用的 Qwen2-72B。这不是你硬件不行,是传统推理框架在 KV Cache 上不够聪明。
为什么需要 vLLM
先说背景:我们团队去年要做个企业级问答系统,模型用的 Qwen2-72B。刚开始用 HuggingFace Transformers 直接推理,问题马上就来了:
- 显存浪费严重:KV Cache 占用 50% 以上显存,但很多 prompt 完成长度后,cache 里存的 token 根本用不上
- 吞吐量低:batch size 一大就 OOM,batch size 一小又浪费计算资源
- 推理延迟高:用户问一个问题,系统响应时间动辄几秒,体验很差
这些问题的根源在 KV Cache 的内存管理方式。传统推理框架里,每个请求的 KV Cache 是连续分配的,而且得按最大序列长度预留。就像给每个会议室都按 100 人装修,结果大多数时候只来 10 个人。
PagedAttention 的思路
vLLM 的核心创新是 PagedAttention,这个名字灵感来自操作系统的虚拟内存分页机制。
传统 KV Cache 是这样分配的:
每个请求都得按最大序列长度预留空间,中间空的显存其他请求还用不了,只能浪费。
PagedAttention 的做法是把 KV Cache 切成固定大小的块(Block),像内存页一样管理:
每个请求按需分配 Block,不够了再申请,不用的可以还给池子。这样显存利用率能提升 2-4 倍。
但这只是内存管理,vLLM 真正厉害的是在 Attention 计算层面的优化。
PagedAttention 的计算优化
在 Attention 计算时,PagedAttention 可以利用 Block 的连续性做批量计算。一个请求的 KV Cache 虽然物理上不连续,但逻辑上是连续的,可以一起做矩阵乘法。
# 伪代码:PagedAttention 的核心思路
def paged_attention(q, k_blocks, v_blocks, block_tables):
"""
q: [batch_size, num_heads, head_dim]
k_blocks, v_blocks: [num_blocks, block_size, num_heads, head_dim]
block_tables: [batch_size, max_seq_len // block_size]
"""
attn_weights = []
for i in range(batch_size):
# 获取当前请求的所有 Block
blocks = block_tables[i]
# 从 Block Pool 里提取对应的 K 和 V
k_seq = gather_blocks(k_blocks, blocks)
v_seq = gather_blocks(v_blocks, blocks)
# 计算注意力分数
scores = torch.matmul(q[i].unsqueeze(0), k_seq.transpose(-2, -1))
scores = scores / math.sqrt(head_dim)
# 计算 Attention 输出
attn_output = torch.matmul(softmax(scores), v_seq)
attn_weights.append(attn_output)
return torch.cat(attn_weights, dim=0)
关键点在于 gather_blocks 操作,vLLM 把这个做成了 CUDA kernel,直接在 GPU 上按 Block Table 索引取数据,避免了 CPU-GPU 之间的大量小内存拷贝。
部署实践
基础部署
先从最简单的开始,用 vLLM 部署一个单卡推理服务:
pip install vllm
# 启动 API 服务
python -m vllm.entrypoints.api_server \
--model Qwen/Qwen2-72B-Instruct \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.95 \
--max-model-len 8192 \
--block-size 16 \
--host 0.0.0.0 \
--port 8000
几个关键参数:
tensor-parallel-size:张量并行度,单卡就是 1gpu-memory-utilization:显存使用率,0.95 是相对安全的值max-model-len:最大序列长度,影响 KV Cache 预分配block-size:每个 Block 的 token 数,16 是默认值,一般不用改
多卡部署
我们的环境是 4 张 A100 80GB,张量并行设置为 4:
python -m vllm.entrypoints.api_server \
--model Qwen/Qwen2-72B-Instruct \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.95 \
--max-model-len 16384 \
--block-size 16 \
--host 0.0.0.0 \
--port 8000
注意 max-model-len 提升到了 16384,因为企业应用里不少场景需要长上下文。
性能对比
部署完后,我做了个简单对比测试,使用相同测试集(100 个真实问答)。实测数据显示 vLLM 在吞吐量和延迟上的提升很明显,显存占用也显著降低。

这个数据看起来很美好,但实际生产环境里情况复杂得多。
踩坑记录
坑一:CUDA OOM 依然存在
理论上 vLLM 通过 PagedAttention 能缓解显存问题,但实际运行时还是遇到了 CUDA OOM。
问题出在 max-model-len 设置太大了。max-model-len 影响 KV Cache 的预分配,虽然 PagedAttention 是动态分配,但 vLLM 还是会为每个请求预留一些 Block。
解决方案是:
- 根据实际业务场景设置合理的
max-model-len,不要盲目设大 - 使用
--max-num-batched-tokens限制批次 token 总数 - 开启
--enforce-eager模式调试,看看到底卡在哪里
python -m vllm.entrypoints.api_server \
--model Qwen/Qwen2-72B-Instruct \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.9 \ # 降到 0.9,留点余量
--max-model-len 8192 \ # 降低最大序列长度
--max-num-batched-tokens 4096 \ # 限制批次 token 总数
--block-size 16 \
--host 0.0.0.0 \
--port 8000
坑二:长文本性能下降
对于超过 4096 token 的长文本,vLLM 的性能下降很明显。原因是长文本的 KV Cache 占用 Block 多,导致 Block Pool 碎片化严重。
vLLM 提供了 --enable-chunked-prefill 参数,可以把长文本拆成小块处理:
python -m vllm.entrypoints.api_server \
--model Qwen/Qwen2-72B-Instruct \
--tensor-parallel-size 4 \
--enable-chunked-prefill \ # 启用分块预填充
--max-num-batched-tokens 4096 \
--block-size 16 \
--host 0.0.0.0 \
--port 8000
启用后长文本性能有一定改善,但提升有限(约 15%),说明碎片化问题还是存在。
坑三:多并发时延迟抖动
生产环境里,请求并发不是均匀的,有时候会突然来一波高并发。vLLM 在这种情况下延迟抖动很明显。
原因是 vLLM 的调度算法基于连续批处理(Continuous Batching),在高并发场景下调度开销变大。可以通过调整 --max-num-seqs 参数来缓解:
python -m vllm.entrypoints.api_server \
--model Qwen/Qwen2-72B-Instruct \
--tensor-parallel-size 4 \
--max-num-seqs 64 \ # 限制最大并发请求数
--block-size 16 \
--host 0.0.0.0 \
--port 8000
这个参数需要根据实际业务调,设置太大容易 OOM,设置太小会拒绝请求。
坑四:量化模型兼容性
我们尝试过用 AWQ 量化 Qwen2-72B 到 4-bit,结果 vLLM 不支持。vLLM 目前只支持自己的一套量化方案,比如 GPTQ、AWQ 的支持有限。
后来用了 vLLM 自己的量化方案:
from vllm import LLM, SamplingParams
llm = LLM(
model="Qwen/Qwen2-72B-Instruct",
quantization="gptq", # 使用 GPTQ 量化
tensor_parallel_size=4,
gpu_memory_utilization=0.95,
)
量化后显存占用减少了约 40%,但推理速度下降了约 20%,这是典型的显存-速度权衡。
结果和反思
经过几个月的折腾,我们最终在生产环境跑了 vLLM,整体效果还是不错的:
- 吞吐量相比原始 Transformers 提升了 4-5 倍
- 显存占用减少了约 35%
- P99 延迟从 5 秒降到了 1 秒以内
但也发现了一些边界:
- 模型大小限制:vLLM 对超大模型(比如几百 B 参数)支持不够成熟
- 量化方案有限:如果想用特定的量化算法,可能还得回 HuggingFace
- 调试复杂度高:出问题时,PagedAttention 的调度机制让问题定位更难
vLLM 不是银弹,它解决的是特定场景(大模型推理)的特定问题(KV Cache 管理)。如果你的模型不大、并发不高,其实 Transformers 也够用。但如果你的场景跟我们类似(大模型、高并发、长上下文),vLLM 值得试一试。
技术选型从来不是追求最新最酷,而是找到最适合当前问题的方案。vLLM 对我们来说是合适的选择,但对你来说未必。
折腾完这堆东西,最大的感受是:理论基础很重要,但实践中的细节和边界条件更致命。PagedAttention 的思路很漂亮,真正用起来时,显存碎片、调度开销、量化兼容这些细节问题,才是决定成败的关键。
技术的进步从来不是靠一个漂亮的思路解决所有问题,而是在实践中不断踩坑、填坑的过程中往前走的。vLLM 如此,其他技术也如此。
版权声明: 本文首发于 指尖魔法屋-AI vLLM:PagedAttention不够用了之后(https://blog.thinkmoon.cn/post/420-ai-vllm-pagedattention-inference-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。