LLM 推理优化:从 vLLM 看高性能推理引擎的设计哲学
当你调用 OpenAI、Claude 或 DeepSeek 的 API 时,在用户友好的接口背后,是一个精密设计的推理引擎在默默工作。这些引擎决定了你的请求多快得到响应、服务能同时处理多少用户、以及每次调用的成本。今天,我们通过 vLLM——当前最流行的开源推理引擎之一,来深入理解高性能 LLM 推理系统的设计哲学。
为什么推理优化如此重要?
训练一个大模型可能花费数百万美元,但这是一次性成本。真正的长期支出在推理阶段——每次用户请求都要消耗 GPU 资源。当你的服务每天处理数百万次请求时,哪怕 10% 的性能提升都意味着巨大的成本节约。
更重要的是用户体验。一个优化良好的推理引擎能够:
- 降低延迟:用户不喜欢等待,尤其在对话场景
- 提高吞吐量:同样的硬件服务更多用户
- 减少成本:更高效的资源利用直接转化为更低的运营成本
推理的核心挑战
LLM 推理不同于传统深度学习推理,它有独特的挑战:
1. 内存墙问题
大模型的参数通常在数十到数千亿级别。以 70B 参数的模型为例,仅加载权重就需要 140GB 显存(FP16)。但这还不是全部——推理过程中还需要存储中间状态,特别是 KV Cache。
KV Cache 是什么?在 Transformer 架构中,每个 token 的 Key 和 Value 向量都需要被保存,供后续 token 的 attention 计算使用。对于一个 2048 token 的序列,这些缓存可能占用数 GB 的显存。传统方式下,变长序列的内存管理效率低下,导致大量碎片化。
2. Prefill vs Decode 的二重性
LLM 生成分为两个阶段:
- Prefill:处理输入 prompt,一次性计算所有输入 token 的表示。这是计算密集型操作。
- Decode:逐个生成输出 token,每次只生成一个。这是内存密集型操作。
这两个阶段的计算特性完全不同。Prefill 阶段可以高度并行化,能充分利用 GPU;Decode 阶段则受限于序列化生成,每一步都要等待前一步完成。优化推理引擎必须同时照顾这两种场景。
3. 吞吐量 vs 延迟的权衡
批处理(batching)是提高吞吐量的关键——把多个请求打包一起处理,分摊 GPU 的固定开销(如 kernel 启动、数据传输)。但批处理有代价:批次中最慢的请求决定了整体时间,导致单个请求的延迟增加。
如何在高吞吐和低延迟之间找到平衡点?这是推理引擎设计的核心问题之一。
vLLM 的关键创新
vLLM 通过几个巧妙的设计突破了这些瓶颈:
1. PagedAttention:内存管理的范式转变
vLLM 的核心创新是 PagedAttention——借鉴了操作系统的虚拟内存思想。
传统方式为每个序列分配连续的大块内存,这导致:
- 内存碎片化严重
- 必须预先分配最大长度的空间(浪费)
- 无法灵活共享
vLLM 的做法是将 KV Cache 切分成固定大小的 块(blocks),默认每块 256 个 token。一个 700 token 的序列会占用 3 个块:两个满块(512 tokens)+ 一个部分块(188 tokens,68 个槽位未使用)。
关键优势:
- 消除碎片:块是固定大小,易于管理和复用
- 按需分配:序列增长时动态分配新块,不浪费空间
- 支持共享:多个序列可以引用同一个块(见下文 Prefix Caching)
2. Prefix Caching:智能缓存复用
在实际应用中,很多请求有相同的前缀。比如:
- 聊天应用的系统 prompt(所有用户共享)
- Few-shot 示例(RAG 场景)
- 模板化的请求格式
vLLM 通过 哈希 + 引用计数 实现了自动的前缀缓存:
- 每个块的内容被哈希,建立 hash → block_id 的映射
- 新请求到来时,计算其块哈希并查表
- 如果块已存在,直接复用(增加引用计数),无需重新计算
这带来巨大的性能提升。对于有相同 system prompt 的聊天应用,第一个请求会计算并缓存 prompt 的表示,后续所有请求都直接复用,节省了大量计算。
重要细节:Block Manager 只在 CPU 内存维护元数据(block 分配、引用计数、哈希映射),真正的 KV Cache 数据存储在 GPU 显存。这种控制平面(CPU)与数据平面(GPU)的分离,让分配决策非常快速。
3. Continuous Batching:动态批处理
传统批处理是静态的——等所有请求都完成才开始下一批。这导致:
- 短序列等待长序列(队头阻塞)
- GPU 利用率波动大
vLLM 实现了 Continuous Batching:
- 序列完成后立即从批次移除
- 新序列可以随时加入正在运行的批次
- 批次大小动态调整
这让 GPU 保持持续高负载,同时减少了短序列的等待时间。
4. CUDA Graphs:减少 Kernel 启动开销
在 Decode 阶段,每次只生成一个 token,计算量小。此时 CUDA kernel 的启动开销相对于实际计算变得显著。
CUDA Graphs 允许将一系列 GPU 操作"录制"下来,然后用不同的输入参数"回放"。vLLM 针对常见的批次大小(1, 2, 4, 8, …, 512)预先录制 CUDA Graph,Decode 时直接回放,大幅降低了启动开销。
架构设计深度解析
vLLM 的架构采用经典的 生产者-消费者模式:
用户请求 → Tokenizer → Scheduler → Model Runner → Sampler → 输出
↓ ↓ ↓
Waiting Queue Running Queue GPU(s)
Scheduler:调度的艺术
Scheduler 维护两个队列:
- Waiting Queue:新提交但还未开始的序列
- Running Queue:正在处理中的序列(Prefill 或 Decode)
调度流程:
- 新序列进入 Waiting Queue
- Scheduler 向 Block Manager 请求资源分配
- 分配成功后,序列进入 Running Queue
- Scheduler 从 Running Queue 选择一批序列进行下一步计算
- 如果显存不足,**抢占(preempt)**正在运行的序列,移回 Waiting Queue 前端
抢占机制是关键创新:当 GPU 显存满了,不是拒绝新请求或者让系统崩溃,而是暂停一些进行中的序列,释放资源给其他请求。被抢占的序列会在资源释放后立即恢复,不会丢失进度。
Model Runner:并行执行引擎
当模型太大无法装入单个 GPU,vLLM 支持 Tensor Parallelism(张量并行)。
通信架构采用 Leader-Worker 模式:
- Rank 0(Leader):接收调度指令,协调所有 Worker
- Rank 1-N(Workers):轮询共享内存,执行相同操作的不同部分
Leader 将方法名和参数写入共享内存,所有 Worker 检测到后读取参数并执行。每个 Worker 知道自己的 rank,计算对应的数据分片。这种方式在单机多 GPU 场景下非常高效,避免了网络开销。
Sampling:从概率到 Token
模型输出的不是一个 token,而是整个词汇表上的 logits(未归一化的概率分布)。最终的 token 通过 **采样(sampling)**选出。
Temperature 参数控制随机性:
- 低温(接近 0):分布变尖锐,几乎总选最高概率的 token(确定性输出)
- 高温:分布变平坦,低概率 token 也有机会被选中(多样性输出)
这就是为什么相同的 prompt 可能产生不同结果——采样引入了受控的随机性。
性能优化技巧
基于 vLLM 的设计,我们可以总结一些实践建议:
1. 合理设置批次大小
- 高吞吐场景(离线批处理):增大
max_batch_size,充分利用 GPU - 低延迟场景(在线服务):减小批次大小,降低排队等待时间
- 监控 GPU 利用率和请求延迟分布,找到最佳平衡点
2. 利用 Prefix Caching
- 如果有共同的 system prompt,确保所有请求都使用完全相同的文本(哈希才能匹配)
- 对于 RAG 应用,考虑将常用的 few-shot 示例固定化
- 预热缓存:在服务启动时发送典型请求,让常用前缀进入缓存
3. 调整块大小
- 默认 256 tokens/block 适合大多数场景
- 短文本场景(如分类任务)可以减小块大小,减少内存浪费
- 长文本场景(如文档问答)可以增大块大小,减少块管理开销
4. 选择合适的并行策略
- Tensor Parallelism:单个请求需要多个 GPU(大模型)
- Pipeline Parallelism:不同层在不同 GPU(超大模型)
- Data Parallelism:多个请求分配到不同 GPU(高并发)
vLLM 主要使用 Tensor Parallelism。对于 70B+ 的模型,通常需要 TP=4 或 TP=8。
总结:设计哲学的启示
vLLM 的成功不仅在于具体的优化技巧,更在于其设计哲学:
- 借鉴经典思想:PagedAttention 来自操作系统,Prefix Caching 来自数据库
- 分离关注点:控制平面(调度、分配)与数据平面(计算)解耦
- 动态适应:Continuous Batching、抢占机制都体现了对动态负载的适应性
- 极致优化细节:从内存管理到 kernel 启动,每个环节都追求极致
当我们设计高性能系统时,这些原则同样适用。理解推理引擎的内部工作原理,不仅能帮助我们更好地使用这些工具,也能启发我们在其他领域的系统设计。
最后推荐一个学习资源:Nano-vLLM,一个仅 1200 行 Python 代码的 vLLM 精简实现,由 DeepSeek 的贡献者创建。它保留了所有核心特性,是学习推理引擎设计的绝佳入口。
推理优化是一个不断演进的领域。随着模型越来越大、应用场景越来越复杂,新的挑战和解决方案还在不断涌现。期待看到更多创新!🫧