从算法层面到工程实现,全面剖析大语言模型推理加速的核心技术。涵盖 Continuous Batching、KV Cache 管理、量化策略、PagedAttention 原理以及 VLLM 整体架构设计,适合高级工程师和架构师参考。
为什么 LLM 推理如此昂贵?
大语言模型(LLM)推理与传统 ML 推理有本质区别。其核心挑战在于两个维度:计算密集度和内存带宽压力。以 GPT-3 175B 为例,单次前向传播需要约 350 TFLOPS 的计算量,而权重体积接近 350 GB,即使在 A100 80GB 卡上也需要多卡并行才能装载。
推理分为两个阶段:
Decode 阶段是瓶颈所在。每次生成一个 Token 都需要将完整权重矩阵与当前隐藏状态相乘,而自回归特性导致大量中间结果(KV Cache)被反复访问。未经优化时,Decode 的 Token 生成速度往往低于 10 tokens/s,无法满足在线服务 50+ tokens/s 的体验要求。
通过 KV Cache 复用已计算结果,避免重复计算;PagedAttention 分块管理缓存,提升内存利用率。
Continuous Batching 打破静态批次限制,提升 GPU 利用率;算子融合减少内存访问开销。
量化技术(W8A16、FP8 等)以精度换算力,大幅降低内存占用和计算延迟。
Disaggated Prefill-Decode、投机解码、KV Cache 分布式管理,适应生产环境复杂调度需求。
动态批处理:从静态到自适应的请求调度
传统 Batching 将多个请求打包成一个批次并行处理,理论上可充分利用率。但 LLM 自回归生成天然存在生成长度不一致的问题——批次中某个请求先行结束时,其他请求仍在运行,导致 GPU 资源长时间空转。
一个批次中,最长序列拖累了整个批次的完成时间,实际 GPU 利用率通常低于 30%。
— Orca: Fine-Grained Reactive Batching, 2022
Continuous Batching(又称 Iteration-Level Batching)的核心思想是:以 iteration(每次 Token 生成)为调度粒度,而非以请求为粒度。每个 iteration 结束后,立即释放已完成的序列所占槽位,并插入新的请求填补空缺。
Continuous Batching 需要解决三个关键问题:
# Simplified Continuous Batching loop (pseudocode)
def scheduler_step(running_seqs, pending_seqs, engine):
# 1. Collect finished sequences from last step
finished = [s for s in running_seqs if s.is_done()]
for seq in finished:
seq.free_slots() # Release memory
running_seqs.remove(seq)
# 2. Fill freed slots with pending requests
free_slots = get_free_slots(running_seqs)
while pending_seqs and free_slots:
new_seq = pending_seqs.pop(0)
new_seq.assign_slots(free_slots.pop())
running_seqs.append(new_seq)
# 3. Run one step for all running sequences
engine.step(running_seqs) # Single kernel launch
| 指标 | Static Batching | Continuous Batching | 提升 |
|---|---|---|---|
| GPU 利用率 | 20–40% | 60–85% | ~2x |
| Throughput (req/s) | Baseline | 3–5x ↑ | 3–5x |
| P99 Latency | 高(被长序列阻塞) | 显著降低 | 显著 |
| 内存利用率 | 低效碎片化 | 高效紧凑 | 高效 |
自回归模型的核心瓶颈——如何避免重复计算?
在 Transformer 的自注意力机制中,每个 Token 在生成下一个 Token 时,都需要 attend 到此前所有 Token 的 Key 和 Value 向量。标准实现会在每一步重复计算这些 K/V,完全浪费了已计算过的结果。
以 LLaMA-2 70B 为例:
KV Cache 的核心思想是:为每个序列维护一个缓存区,将各层 attention 计算出的 K/V 向量持久化存储,后续生成步骤直接查询缓存,无需重新计算。
KV Cache 按层分布存储于 GPU HBM 或 CPU DRAM 中。现代推理引擎(如 vLLM)通常将 KV Cache 划分为多个 Block,以 Block 为单位进行管理,支持不规则长度的序列并实现高效的内存分配。
# KV Cache logical layout in vLLM
# Each sequence occupies a list of KV Blocks (physical)
class KVCache:
# Logical view: seq -> [block_ids]
seq_blocks: Dict[int, List[int]]
# Physical view: block_id -> PagedAttention block
# Stored in pinned CPU memory or GPU HBM
blocks: List[Block]
# When generating token t+1 for sequence s:
# 1. Read K_t, V_t from block(s) — zero-copy via PagedAttention
# 2. Compute attention(Q_t, cache[K_0:t, V_0:t])
# 3. Write new K_{t+1}, V_{t+1} to next free position in block
| 模型 | 参数量 | FP16 KV Cache/Token | 1000 tokens 显存 | 最大并发(L40 48GB) |
|---|---|---|---|---|
| LLaMA-2 7B | 7B | ~0.5 MB | ~500 MB | ~80 concurrent |
| LLaMA-2 70B | 70B | ~4 MB | ~4 GB | ~10 concurrent |
| GPT-4 (approx) | ~1.8T MoE | variable | varies | very limited |
Multi-Query Attention (MQA) 和 Grouped Query Attention (GQA) 通过让多个 Query 头共享同一组 K/V 头,将 KV Cache 体积减少为原来的 1/N(对于 N 组 Query 头),是 KV Cache 管理的重要辅助手段。
精度与速度的权衡:从 FP32 到 FP8 的完整技术图谱
量化(Quantization)通过将 FP32/FP16 权重和激活值映射到低位宽整数来表示,在保持模型能力的同时大幅降低内存占用和计算成本。
核心公式:
Y_quant = round(Y_fp32 / s) + z
其中s为缩放因子(scale),z为零点(zero-point,用于对称/非对称量化)。
W8A16 = Weight 8-bit + Activation 16-bit。这是目前生产环境最主流的量化方案,因为:
FP8(Float 8)是 NVIDIA H100/H200 系列引入的新格式,在 Hopper 架构中提供原生硬件支持。相比 INT8,FP8 保留了浮点数的动态范围表示能力,对大语言模型这种对数值分布敏感的任务更友好。
| 格式 | 存储格式 | 动态范围 | 精度(尾数位) | 适用场景 |
|---|---|---|---|---|
| E4M3 (FP8) | 1 sign + 4 exp + 3 mant | 448 (max) | ~3 bits mantissa | Weights & Activations 前向传播 |
| E5M2 (FP8) | 1 sign + 5 exp + 2 mant | 57344 (max) | ~2 bits mantissa | Gradients 反向传播(需要更大动态范围) |
FP8-E4M3 的动态范围约为 BF16 的 1/16,但精度更高。在 LLaMA-2 70B 上,使用 FP8 推理相比 BF16 可获得 1.5–2x 的吞吐量提升,困惑度损失通常小于 0.5%。
— FP8 Formats for Deep Learning, NVIDIA, 2023
W8A8(Weight 8-bit + Activation 8-bit)是最激进的量化方案,需要在 INT8 精度下完成矩阵乘法。这要求硬件支持 INT8 Tensor Core(如 NVIDIA Ampere 及以上),并且需要在线量化(Per-Tensor 或 Per-Token Scale)而非静态离线标定。
主要挑战:
| 方案 | 精度损失 | 加速比 | 显存节省 | 生产可用性 |
|---|---|---|---|---|
| FP16 (Baseline) | — | 1x | — | ★★★★★ |
| W8A16 | <0.5% | 1.5–2x | ~50% | ★★★★★ |
| FP8 (E4M3) | 0.5–1.5% | 1.5–2x | ~50% | ★★★★ |
| W8A8 (INT8) | 1–3% | 2–4x | ~60% | ★★★ |
| W4A16 (INT4) | 2–5% | 3–6x | ~75% | ★★★ |
| W4A8 | 5–10%+ | 4–8x | ~80% | ★★ |
操作系统级内存管理思想在 LLM 推理中的应用
传统推理系统为每个请求预分配连续的 GPU 内存区域存储 KV Cache。然而:
PagedAttention 借鉴了操作系统虚拟内存的 分页(paging) 机制:将 KV Cache 表示为一系列固定大小的"物理块"(physical blocks),逻辑上连续的 token 序列可以跨越多个不连续的物理块存储。
类似于进程访问虚拟地址空间时无需关心物理页的实际物理位置,PagedAttention 让每个序列在逻辑上拥有连续完整的 KV 缓存空间,按需映射到分散的物理块。
PagedAttention 在执行 attention 时,通过 block_table 将逻辑 token 位置映射到物理块,GPU kernel 可以直接 gather 对应物理块中的 K/V 数据,无需内存拷贝。这使得:
在实际部署中,多个请求往往共享相同的 System Prompt 或 RAG 上下文。PagedAttention 的 Block Table 机制天然支持KV Cache 共享:相同前缀的 Block 可被多个序列引用,Prefix 部分无需重新计算。
// PagedAttention kernel pseudocode
// each thread block processes one sequence in the batch
__global__ void paged_attention_kernel(
float* out, // [num_seqs, num_heads, max_len, head_dim]
const float* q, // query
const float* k_cache, // physical block stored K
const float* v_cache, // physical block stored V
const int* block_table, // [num_seqs, num_blocks_per_seq]
int block_size
) {
// 1. Get which physical blocks hold K/V for this seq
int seq_len = ...;
int num_blocks = (seq_len + block_size - 1) / block_size;
// 2. Iterate over blocks, compute partial attention
for (int i = 0; i < num_blocks; i++) {
int physical_block_id = block_table[seq_idx * max_blocks + i];
// Zero-copy: fetch K/V directly from k_cache/v_cache
const float* k_block = get_block_base(k_cache, physical_block_id);
const float* v_block = get_block_base(v_cache, physical_block_id);
// Compute partial attention and accumulate...
attention_partial(out, q, k_block, v_block, block_size);
}
}
从请求接收到 Token 输出的完整系统设计
VLLM 是一个端到端的 LLM 推理引擎,其核心设计目标是在有限的 GPU 显存中最大化吞吐量和降低延迟。架构分层如下:
Scheduler 维护 Running Sequences 和 Waiting Sequences 两个队列。每次 iteration:
基于 Block Manager 实现,负责:
支持三种并行策略的组合:
生产级 VLLM 部署中,常将 Prefill 和 Decode 阶段拆分到不同批次甚至不同硬件:
原因:Prefill 阶段的计算特性和延迟目标与 Decode 截然不同,混合调度会导致互相干扰。Disaggregation 允许独立扩缩容,并可结合 Prefix Caching 进一步加速。
VLLM 还支持 Speculative Decoding(推测解码):使用一个小模型(Draft Model)批量猜测多个后续 Token,再用大模型并行验证。接受率通常 70–90%,可将 effective token/s 提升 2–4 倍。
各技术组合的实际性能收益
| 优化组合 | Throughput (tokens/s/GPU) | 显存占用 (70B) | P50 Latency |
|---|---|---|---|
| Naive (PyTorch, batch=1) | ~15 | ~150 GB | ~67 ms/token |
| + Continuous Batching | ~60 | ~150 GB | ~17 ms/token |
| + PagedAttention (KV Cache) | ~150 | ~80 GB | ~7 ms/token |
| + W8A16 量化 | ~300 | ~45 GB | ~3 ms/token |
| + FP8 + TP4 | ~600+ | ~40 GB | ~1.5 ms/token |
*参考数据:LLaMA-2 70B @ H100 80GB SXM,throughput 为单卡,数据来自公开 benchmark 及生产环境测量。
以 iteration 为粒度调度,消除 GPU 空转,提升利用率 2–3x,是所有在线推理系统的基础。
避免自回归步骤中重复计算,显存估算与 MQA/GQA 结合使用,是 Decode 阶段性能关键。
W8A16 是生产首选,FP8 是 H100 的下一代方向,W4A16 适合显存极度受限场景。
分页管理 KV Cache,显存利用率从 30% 提升到 90%+,支持动态长度和 Prefix Caching。
端到端优化,Scheduler + KV Cache Manager + Model Executor 三者协同是性能最大化的关键。
通过小模型猜测+大模型验证,在接受率高(>70%)的场景可实现 2–4x 有效吞吐量提升。