训练把模型”炼”出来,推理把模型”用”出来。前者是离线任务、按天计量、看吞吐;后者是在线服务、按毫秒计量、看尾延迟。训练工程师可以容忍一次 checkpoint 恢复花半小时,推理工程师被 P99 延迟超了 50ms 就要被叫到会议室。
本篇是系列的推理篇开篇,目的是把”大模型推理”这件事从第一性原理讲清楚:为什么要有 KV Cache、为什么要区分 Prefill 和 Decode、为什么 vLLM 的 Continuous Batching 能把吞吐翻几倍、为什么 DeepSeek / Kimi 都要做 Prefill / Decode 分离。之后的第 12–16 篇会分别展开 PagedAttention、主流引擎对比、量化、推测解码、长上下文。
一、推理 vs 训练的工程差异
1.1 两种 workload 的本质区别
训练是一个离线、同步、吞吐导向的批处理任务。一次 step 处理一个固定 shape 的 batch,forward + backward + optimizer step 三段式,所有样本同生同死。你不关心单样本何时返回,只关心每秒能吃多少 token、每轮 epoch 多长、loss 收敛多快。
推理是在线、异步、延迟导向的服务。请求一个一个来,长度不等、到达时间不等、产出 token 数不等。你不能让后到的请求等前面的请求跑完,不能让短请求陪长请求一起排队。你需要在亚秒级给出首 token,在几十毫秒级给出每个后续 token,还要把 GPU 利用率拉起来。
| 维度 | 训练 | 推理 |
|---|---|---|
| 时效 | 离线,可以跑几周 | 在线,毫秒级 SLO |
| 输入 | 固定 shape 的 batch | 变长、动态到达 |
| 方向 | Forward + Backward | 只 Forward |
| 显存 | 参数 + 梯度 + optimizer state + 激活 | 参数 + KV Cache |
| 瓶颈 | 算力 / 通信 | 访存 / KV 空间 |
| 核心指标 | Tokens/sec、MFU | TTFT、TPOT、并发数 |
| 计量 | 卡时 | QPS、1K tokens 价格 |
| 失败处理 | Checkpoint 回滚 | 请求重试 / 降级 |
1.2 为什么推理有自己的一套系统
一台训推混用的方案看上去很美,实际几乎没有人这样做。原因是两边的优化方向几乎相反:
- 训练要把 batch 撑大,推理要让每个请求尽快出结果。
- 训练要保 gradient 精度,推理可以做 INT4 / FP8 激进量化。
- 训练要 checkpoint 持久化,推理要 KV Cache 这种临时内存结构。
- 训练的并行是 TP+PP+DP+SP+EP 五维,推理更多是 TP + 少量 PP,且 PP 对延迟不友好。
所以主流路线是:训练用 Megatron / DeepSpeed / TorchTitan 等;推理用 vLLM / SGLang / TensorRT-LLM / TGI 等专用引擎。模型从训练态导出(通常是 HF safetensors),推理态再做量化、图优化、权重切分。
1.3 推理的 SLO 模型
一个在线推理服务的 SLO 典型长这样:
- TTFT P95 < 500ms(聊天)/ < 2s(长文档问答)
- TPOT P95 < 50ms(对应 20 tok/s 起的流式体验)
- 可用性 99.9%
- 超时 30s
这套 SLO 直接决定了引擎怎么调参:batch 不能太大(大 batch 会拉长 TTFT 和 TPOT 尾延迟),也不能太小(小 batch 浪费算力)。所有”继续 batching vs 立刻 decode”的取舍都围绕这条线。
1.4 模型从训练态到推理态
模型权重的生命周期不止是”加载一下那么简单”:
- 训练结束 → HuggingFace safetensors / Megatron 分布式 checkpoint。
- 格式转换:把分布式 checkpoint 合并为单文件,重命名 key,切分到推理侧的 TP 拓扑。
- 量化:GPTQ / AWQ / FP8 校准,生成量化权重 + scale。
- 图编译(TRT-LLM 专属):用
trtllm-build生成 engine,针对 batch size 桶和 seq 长度做 kernel 自动调优。 - 部署打包:权重 + tokenizer + 配置上传对象存储(OSS/S3),节点拉取。
这条链路一旦稳定下来,就是每天跑几十次的”模型发布流水线”,和 DevOps 世界的 CI/CD 平行存在。
二、Transformer 推理流程回顾
2.1 自回归生成
把大模型当黑盒,推理接口就是:
输入: prompt = [t1, t2, ..., tn]
输出: [tn+1, tn+2, ..., tn+m] (直到 EOS 或 max_new_tokens)
每一步生成新 token 的伪代码是:
tokens = prompt
while len(tokens) - len(prompt) < max_new_tokens:
logits = model(tokens) # shape: [seq, vocab]
next_id = sample(logits[-1]) # 只取最后一个位置
tokens.append(next_id)
if next_id == EOS: break这份代码能跑,但极慢。问题在 model(tokens)
这一行:每生成一个 token 就把前缀重新 forward
了一遍,计算量随长度平方增长。
2.2 KV Cache 的由来
Transformer 的每一层 attention 计算是:
Q = x @ Wq # [seq, d]
K = x @ Wk # [seq, d]
V = x @ Wv # [seq, d]
out = softmax(Q @ K^T / sqrt(d)) @ V
自回归推理时,生成第 t 个 token 只需要新的 Q_t、但需要与所有历史位置的 K、V 做 attention。历史位置的 K、V 在上一步已经算过了,下一步不会变(因为是 causal mask)。
这就是 KV Cache 的核心:把每一层每个位置的 K、V 缓存下来,下一步只算新位置的 K、V,拼到缓存上,再做 attention。
# 带 KV cache 的 forward
def attn_step(x_new, kv_cache_k, kv_cache_v):
q = x_new @ Wq
k_new = x_new @ Wk
v_new = x_new @ Wv
k = concat(kv_cache_k, k_new, dim=seq)
v = concat(kv_cache_v, v_new, dim=seq)
out = softmax(q @ k.T / sqrt(d)) @ v
return out, k, v # 新的 k/v 回写 cache复杂度从每步 O(n²) 降到每步 O(n),总体从 O(n³) 降到 O(n²)。代价是显存:要存下所有层所有位置的 K 和 V。
三、Prefill 和 Decode 两阶段
3.1 阶段划分
KV Cache 的引入,天然把推理分成两个阶段:
- Prefill 阶段:把整个 prompt 一次性
forward,计算并填充所有位置的 KV Cache。这里有 seq
个位置同时算,是一个并行的
[bs, seq, d]的矩阵乘。 - Decode 阶段:逐 token 生成。每一步输入是
[bs, 1, d],只新增一个位置的 KV,再做 attention。
一个典型 4K prompt 生成 512 tokens 的请求,它的时间线是这样的:
Prefill (4096 tokens 一次性): ~200ms → 给出 TTFT
Decode (512 步,每步 ~30ms): ~15s → 每步给出一个 token
3.2 Arithmetic intensity 的巨大差异
这两个阶段的计算访存比完全不同,决定了它们的瓶颈也不同。
先复习 roofline:一个 kernel 的上限性能由 arithmetic intensity I(计算 FLOPs / 访存 bytes)决定。I 高就是 compute-bound,吃算力(Tensor Core);I 低就是 memory-bound,吃带宽(HBM)。
A100 SXM4 的 peak:FP16 算力 312 TFLOPS,HBM 带宽 2.0 TB/s,拐点 I ≈ 156 FLOPs/byte。
对于一个 Linear 层 y = x @ W,x 形状
[B, d_in],W 形状
[d_in, d_out]:
- FLOPs ≈ 2 · B · d_in · d_out
- 读 W: d_in · d_out · 2 bytes(FP16)
- I ≈ B(近似,忽略 x 和 y 的访存)
看到没,算术强度几乎就是 batch size。
Prefill:输入是整个 prompt,有效 batch 是
bs · seq。一个 4K prompt + bs=4 就是 16K
的”宽度”,I 远远超过拐点,compute-bound,吃满
Tensor Core。
Decode:每一步输入只有 1 个 token,有效 batch 就是 bs。bs=4、bs=16 的 I 都远低于 156,memory-bound,瓶颈在读权重和读 KV。
这条结论非常重要,后面几乎所有推理优化都围绕它展开:
- Decode 受限于 HBM 带宽 → 加大 batch(continuous batching)、压 KV(GQA/MLA、量化)、合并 kernel(FlashDecoding)
- Prefill 受限于算力 → 常规的 GEMM 优化、FlashAttention、chunked prefill 避免阻塞 decode
- Prefill 和 Decode 特性不同 → 物理上分离部署(PD-disaggregation)
3.3 Prefill / Decode 时序图
这张图说明了一切:单请求在 Decode 阶段把 GPU”空转”得很厉害。把多个请求的 Decode 合并成一个大 batch,就是 continuous batching 的动机。
四、KV Cache 深入
4.1 维度与大小估算
标准 Multi-Head Attention(MHA)的 KV Cache 形状:
kv_cache: [num_layers, 2, batch_size, num_heads, seq_len, head_dim]
^
K / V
单请求单层单 token 的 KV 字节数(FP16):
per_layer_per_token = 2 (K+V) * num_heads * head_dim * 2 bytes
对几个代表性模型算一下每 token 的 KV 开销:
| 模型 | 层数 | heads × head_dim(KV) | 单 token 单层 | 总 KV/token |
|---|---|---|---|---|
| LLaMA-2 7B (MHA) | 32 | 32 × 128 | 2 × 32 × 128 × 2 = 16 KB | 512 KB |
| LLaMA-2 70B (GQA, 8 kv_heads) | 80 | 8 × 128 | 4 KB | 320 KB |
| LLaMA-3 70B (GQA, 8) | 80 | 8 × 128 | 4 KB | 320 KB |
| DeepSeek-V2 (MLA) | 60 | latent 512 | ~1 KB 级别 | ~70 KB |
拿 LLaMA-2 70B 算 4K 上下文:
KV per seq = 320 KB/tok × 4096 tok ≈ 1.3 GB(单序列!)
考虑一些实现把 K/V 分开对齐、加 padding,实际逼近 2 GB/序列。这个数字解释了为什么长上下文 + 多并发推理的显存瓶颈不在参数,而在 KV Cache:H100 80GB 减去权重后剩 ~40GB,只能同时服务 ~20 个 4K 上下文,或者 ~5 个 16K 上下文。这就是 KV 是推理显存头号杀手 的由来。
4.2 GQA / MQA:砍 KV head 数
MQA(Multi-Query Attention,2019):所有 Q head 共享同一组 K、V。KV 大小除以 num_heads,极致压缩,但精度损失大,训练难。
GQA(Grouped-Query Attention,2023):折中方案。Q 分 num_heads 组,KV 分 num_kv_heads 组(通常 8),每组 KV 被多个 Q 共享。LLaMA-2 70B / LLaMA-3 / Mistral / Qwen2 / DeepSeek-V1 全部用 GQA。
标准 MHA: Q[H], K[H], V[H]
MQA: Q[H], K[1], V[1]
GQA(G=8): Q[H], K[8], V[8] —— H=64 时 K/V 内存是 MHA 的 1/8
GQA 带来 4-8× 的 KV 节省,对长上下文 / 高并发场景是决定性的。代价是微小的 PPL 上升,且要在训练阶段就选好组数(不能事后转换)。
4.3 MLA:DeepSeek 的 KV 压缩方案
DeepSeek-V2 / V3 引入了 Multi-head Latent Attention(MLA)。核心思想是把 K、V 投影到一个低维 latent 空间缓存,算 attention 前再”解压”回来。
简化表述:
# 传统 MHA:缓存 K, V ∈ R^{H·d_h} 一共 2 · H · d_h
# MLA: 缓存 c_kv ∈ R^{d_c} 一共 d_c(通常 d_c << 2·H·d_h)
c_kv = x @ W_kv_down # 压到 latent
# 推理时:
K = c_kv @ W_k_up
V = c_kv @ W_v_up
实际实现里用了”吸收”技巧:W_k_up 与
W_q 可以预先合并,使 decode 时不需要真的把 K
解压到高维,而是把 query 投到 latent 空间,直接和 c_kv 做
attention。这样计算量也没增加多少。
DeepSeek-V3 的 MLA 把 KV Cache 从 GQA 的 ~4KB/层/token 进一步压到约 1-1.5KB/层/token,是长上下文成本下降的关键之一。DeepSeek V3 能在低成本上跑 128K 上下文,MLA 功不可没。
4.4 KV Cache 结构图
4.5 KV Cache 的量化与驱逐
除了结构上减小 KV,还有两类正交手段:
- KV 量化:把 FP16 的 KV 压到 INT8 / INT4 / FP8。KV-Int8 几乎无损,KV-Int4 要校准。vLLM、TRT-LLM 都原生支持。
- KV 驱逐 / 稀疏:长上下文下,注意到最近 n 个 token 和开头 n 个 token 权重最大,中间可以抛弃或者压缩。StreamingLLM、H2O、SnapKV 是代表方案。这条路线在 128K+ 场景下被逐步应用。
4.6 KV Cache 的显存预算推导
在一台 H100 80GB 上部署 LLaMA-3-70B(FP16),实操预算大致如下:
权重: 70B × 2 bytes = 140 GB → 4 卡 TP=4,单卡占 35 GB
框架保留: 激活 / 临时 buffer ≈ 5 GB
可用 KV: 80 - 35 - 5 ≈ 40 GB / 卡 → 4 卡合计 ~160 GB KV
单 token KV: GQA(8 kv_heads) = 320 KB / tok
可服务的"活跃 token 总量":
160 GB / 320 KB ≈ 500K tokens
500K 活跃 token 听起来不少,换算到并发就缩水得很快:
- 4K 上下文:最多 ~125 个并发请求
- 16K 上下文:~30 个
- 128K 上下文:~3-4 个(而且这是”稳态”,不含 prefill 期)
再乘上 PagedAttention 的碎片率(5-10%)和系统保留,真实数字还要再打折。这个预算直接决定了集群规模和商业化价格模型。
4.7 KV 的数据生命周期
一个请求内 KV 的生命周期:
[admit] → [prefill 产生 KV] → [decode N 步,每步 append 1 行] → [done / timeout → 回收]
完成后 KV 的处理策略分三种:
- 立即释放:最简单,memory 碎片问题由 PagedAttention 解决。
- 保留做 prefix cache:热 system prompt 留住,冷的 LRU 踢出。
- 下沉到 CPU/NVMe:Mooncake 风格,给下一次会话做二级命中。
多轮对话里,活跃会话的 KV 往往整会话保留(避免每轮从头 prefill),这是”会话粘性”必须做的工程决策。
五、Batching 的三代演化
单请求推理下,GPU 永远吃不饱。要拉吞吐必须 batch。但 LLM 的变长 + 分阶段让 batching 远比视觉模型复杂。
5.1 第一代:Static Batching
把同时到达的 N 个请求 pad 到最长,一起 forward。伪代码:
batch = collect_requests_for(timeout=50ms, max=8)
outputs = model.generate(pad(batch))问题很明显:
- 请求必须等到 batch 凑齐或超时,TTFT 被拖长。
- 最短请求必须陪最长请求跑完 max_new_tokens,GPU 和用户都亏。
- 只要 batch 里有一个长请求,整体的”结束时间”就被拉长。
Triton、TorchServe 在 LLM 前时代都是这种模式。
5.2 第二代:Dynamic Batching
Triton Inference Server 提供的模式:请求到达队列,调度器在 max_queue_delay 内窥视,尽量凑够 preferred_batch_size,再一起发到模型。
对 CV 模型和 BERT 有效,对 LLM 依然不够:LLM 每一步输入/输出都在变,不是”一次 forward 出完整结果”,batch 的粒度不应该是”请求”,而应该是”每一步”。
5.3 第三代:Continuous Batching / In-flight Batching
这是 vLLM(Orca 论文思想 + PagedAttention 实现)和 TensorRT-LLM 的核心贡献。
关键观察:Decode 阶段每一步都是独立的
[bs, 1, d] forward,不同请求的 decode step
完全可以拼到一个 batch。请求的”寿命”由它们产出多少 token
决定,但每一步 batch
的组成可以动态变化。
新的调度循环(简化版):
running = [] # 正在 decode 的请求
waiting = [] # 排队的 prefill 请求
while True:
# 1. 有新请求且算力允许:做 prefill,加入 running
if waiting and can_admit():
req = waiting.pop(0)
prefill(req) # 算出它的首 token + KV cache
running.append(req)
# 2. 把 running 里所有请求的"下一步 decode"拼成大 batch
batch = [r.last_token for r in running]
logits = model.decode_step(batch, kv_caches=[r.kv for r in running])
# 3. 每个请求采样各自的 next token
for r, lg in zip(running, logits):
r.append(sample(lg))
if r.done(): running.remove(r); yield r.output好处:
- 请求到达即进入 batch,不用等对齐。TTFT 大幅下降。
- 早完成的请求立刻释放 slot,晚到的请求立刻顶上,GPU 永不空转。
- Decode step 的有效 batch 可以做到几十甚至上百,Decode 从严重 memory-bound 的 bs=1 拉到 bs=64 的高 arithmetic intensity。
坏处 / 挑战:
- 每步 batch 形状都在变,需要 kernel 支持变长 QKV 和变长 KV Cache。
- KV Cache 不能按”最大长度”预分配,否则浪费极大 → 这就是 PagedAttention 的由来(下一篇详讲)。
- Prefill 和 Decode 混在一个 step 里会互相拖累(prefill 慢、decode 快)→ 需要 chunked prefill 或 PD 分离。
现代引擎(vLLM、SGLang、TRT-LLM、TGI、Mindie 等)默认都开 continuous batching。
5.4 Chunked Prefill
一个棘手问题:某个请求的 4K prompt prefill 要 200ms,这 200ms 里所有 decode 请求都在干等,TPOT 尾延迟会爆炸。
解法:把长 prefill 切成小块,每次只做比如 512 个 token 的
prefill,和一批 decode 拼成一个”混合 batch”一起 forward。vLLM
从 0.4 开始的 --enable-chunked-prefill、SGLang
的默认调度、DeepSeek-V3 的分离式 serve
方案都使用了这个思想。
5.5 Continuous Batching 时序对比
六、推理关键指标
6.1 延迟指标
- TTFT(Time To First Token):请求到达到输出第一个 token 的时间。由队列等待 + prefill 时间决定。聊天类目标 < 500ms,长文档目标 < 2s。
- TPOT(Time Per Output Token)/ ITL(Inter-Token Latency):产出相邻两个 token 的间隔。决定”流式打字”的体感速度。20 tok/s 是下限,40+ tok/s 令人舒适。
- E2E Latency:总响应时间,= TTFT + output_len × TPOT。
- Queue Time:请求排队到被调度的时间。过载时这一项会爆炸。
6.2 吞吐指标
- Total Tokens/sec:输入+输出 token 总吞吐。
- Output Tokens/sec:只计输出,更贴近计费和用户价值。
- Requests/sec(QPS):请求吞吐,和 token 数强相关,单独看意义不大。
- Concurrency:同时处理的请求数,continuous batching 下这是核心调优旋钮。
6.3 SLO 与 P50/P95/P99
在线服务从不看平均值。P95/P99 才是真实用户体验。
P50 TTFT: 150ms —— 一般用户
P95 TTFT: 400ms —— 刚好满足
P99 TTFT: 1200ms —— 1% 用户等了 1.2 秒
推理引擎的尾延迟常常来自:
- 某个超长 prompt 的 prefill 阻塞一切(→ chunked prefill)
- KV Cache 满了新请求被 preempt(→ 容量规划)
- 某个慢请求被反复让位
- 跨节点 TP 的通信尖峰
生产上必须 per-model 维度监控 TTFT/TPOT/Queue 三条线的 P95/P99,超限直接告警或触发扩容。
6.4 输入 vs 输出 token 的瓶颈
不同业务的输入输出配比,决定了到底卡在 Prefill 还是 Decode。
| 场景 | 输入 token | 输出 token | 瓶颈 |
|---|---|---|---|
| 短对话 | 200 | 200 | 均衡偏 Decode |
| 角色扮演 / 情感陪伴 | 500 | 1000 | Decode |
| Code 补全 | 2K | 50 | Prefill |
| 文档摘要 / 长文翻译 | 10K | 500 | Prefill 主 |
| RAG 问答 | 5K | 200 | Prefill 主 |
| Agent 多轮工具调用 | 累计 20K | 300 × N 轮 | Prefill 持续主导 |
| 推理类(o1 / R1)长思考 | 500 | 10K+ | Decode |
不同配比下最佳配置天差地别:RAG 场景要把更多算力分给 Prefill(更大 TP、更多 Prefill-only 节点);陪伴类聊天要压 Decode 显存(MLA、KV 量化、超长 continuous batching)。
6.5 压测与 benchmark 工具
生产上看的几套工具链:
- vllm benchmark_serving.py:vLLM 自带,支持 ShareGPT 数据集模拟真实请求分布。
- llmperf(Anyscale):开源,按 TTFT / ITL / 吞吐做压力测试。
- genai-perf(NVIDIA):配套 TRT-LLM 和 Triton 的官方压测。
- guidellm(Neural Magic):按 SLO 找最大 QPS。
一个标准 benchmark 报告至少包含:
模型 · 硬件 · 引擎版本 · 量化 · 并行配置 · 请求到达分布 · 输入/输出长度分布
-----------------------------------------------------------------------
QPS TTFT P50/P95/P99 TPOT P50/P95/P99 Output tok/s GPU util KV util
没有到达分布(poisson 或真实 trace)和长度分布的 benchmark 基本可以忽略 —— 用固定长度 + 同步压力的数字严重偏离真实场景。
七、Decode kernel 层优化
Decode 是 memory-bound 的关键阶段,这个阶段每一毫秒都要精打细算。
7.1 FlashAttention 系列在 decode 中的变体
标准 FlashAttention(v1/v2/v3)优化的是 prefill 那种大 seq
× seq 的 attention,通过 tiling 把 softmax
融到寄存器里,避免物化 QK^T。
Decode 场景有两个特点:
- Q 只有 1 个位置(或 few token,推测解码时几个)
- KV 长度可能很长(128K)
针对性优化是 FlashDecoding(2023)和 FlashDecoding++(2024):把 KV 维度切成多个 chunk,每个 SM 处理一段,分别算出局部 softmax,最后用 log-sum-exp 合并。让长 context 下的 decode 也能打满 SM。
7.2 Paged Attention kernel
PagedAttention(vLLM)不只是调度机制,还是一个 kernel:它在 CUDA 里实现了按”页表”访问非连续 KV 块的 attention。代价是访存比连续布局略低一些,收益是 KV 内存碎片几乎消失、跨请求共享 KV 变得平凡。下一篇有详解。
7.3 CUDA Graph:消灭 launch 开销
Decode 每步 forward 只要几十毫秒,但一个 70B 模型一次 forward 里有几百上千个 kernel,每个 kernel launch 要 5-20μs,累计就是毫秒级开销,占 TPOT 的相当比例。
CUDA Graph 把一段 kernel 序列录制成图,后续只用一次提交就能执行整张图,launch 开销降到几乎为 0。对固定 shape 的 decode step 尤其合适:batch 大小和 seq 长度虽然每步不同,但只要做成”按 batch 分桶”的几张 graph 就够用。
vLLM、TRT-LLM 都默认启用 CUDA Graph。注意事项:
- Graph 只能录制静态 shape,变长 batch 要分多个 graph 桶
- Paged attention 里”页表”是 device 内存,可以被 graph 正常捕获
- Prefill 一般不用 graph(shape 变化太大,编译不划算)
7.4 权重加载与 kernel fusion
Decode 受限于读权重,那就少读一次。
- QKV fused:Q、K、V 三个投影合成一个 GEMM。
- Gate + Up fused(SwiGLU):MLP 的 gate 和 up 投影合成一个。
- Residual + RMSNorm 融合到下一个 GEMM 里:少读一次激活。
TRT-LLM 的 plugin 系统、vLLM 的自定义 kernel、SGLang 对 FlashInfer 的依赖都在做这件事。
7.5 Speculative 执行与多流
高级优化里还有两条线:
- 多 CUDA Stream:prefill 和 decode 放不同 stream,通信和计算 overlap。
- Persistent kernel:几个 kernel 合并成一个”长待机”kernel 持续跑,避免 launch 和同步开销。FlashInfer 的 persistent kernel、CUTLASS 3.x 的 grouped GEMM 都走这条路。
- Prefetch 权重:下一层的权重在当前层计算时就开始 DMA 到 SM 附近的 shared memory 或 L2(硬件支持有限,但 B200 / H200 有改善)。
7.6 一次 Decode step 的性能拆解(70B / H100 · 参考数值)
单 token 单请求(bs=1)total ≈ 30 ms,其中:
读权重 70B × 2B / (3TB/s HBM) ≈ 23 ms <—— 主导
读 KV 320KB × 4K tok × 1 / HBM ≈ 0.4 ms
计算 FLOPs 2 × 70B × 1 / 989 TFLOPS ≈ 0.14 ms
kernel launch × 1000 ≈ 5-10 ms <—— CUDA Graph 能砍掉
同步 / all-reduce(TP=4) ≈ 1-2 ms
看清楚了吗:绝大部分时间在读权重,其次是 kernel launch。所以:
- 降权重读取 → 量化(Int8/Int4/FP8)直接把”读权重”时间减半、减四。
- 降 launch 开销 → CUDA Graph。
- 增大 batch → 权重读一次,服务 N 个请求,摊薄到
23/Nms,这才是 continuous batching 能把吞吐做到几千 tok/s 的根因。
八、Prefill / Decode 分离
8.1 为什么要分离
前面讲过 Prefill 和 Decode 的特性截然不同:
| 维度 | Prefill | Decode |
|---|---|---|
| 计算特性 | compute-bound | memory-bound |
| 偏好硬件 | 算力强(H100、B200) | 带宽高(HBM3e、大 NVLink 域) |
| 最佳 TP | 中等(2-4) | 小(1-2)或极大(靠 NVLink 域) |
| 对延迟敏感 | TTFT | TPOT |
| 对 batch 敏感 | 拼不太动 | 越大越好 |
把两者混在一个 pod 里,必然互相妥协。分离后,每个 pod 可以按自己的负载独立选 TP、选型号、选副本数。
8.2 典型架构
Client
│
▼
Router ──► Prefill Pool(少量大 GPU · 高 TP · 侧重算力)
│
│ 通过 RDMA / NVLink 把 KV Cache 传输过去
▼
Decode Pool(多节点 · 小 TP · 侧重带宽 · 长期持有 KV)
│
▼
Streaming 输出给客户端
关键难点在”KV Cache 传输”:prefill 完一个 4K prompt,KV 可能是几 GB 级别,要通过 RDMA(IB / RoCE)快速打给 Decode 节点,传输时间必须远小于 decode 一整轮的时间。这是一个纯粹的网络+内存工程问题。
8.3 代表性系统
- DistServe(2024):UCSD / 北大的开源系统,论文里首次系统给出 PD 分离的调度模型,证明在同样 SLO 下吞吐可 2-4×。
- Splitwise(Microsoft):Azure 的生产方案,定义了 prompt phase / token phase。
- Mooncake(Moonshot AI / Kimi):国内代表。开源 mooncake transfer engine,用 KVCache Store 做集中式 KV 管理,RDMA 传输 KV,内存池做二级缓存,在 Kimi 的超长上下文服务上大规模落地。
- DeepSeek-V3 Serving:DeepSeek 官方披露的 serve 方案,H800 集群上做 PD 分离,prefill 节点 EP 更大(MoE 专家并行),decode 节点 MLA 压 KV,两类节点异构部署,成本做到业界惊人水平。
- vLLM / SGLang:近期版本也在路由层支持 PD
分离(
--disaggregated)。
分离方案目前仍在快速演进。对多数团队,先用合一部署 + continuous batching + chunked prefill 能满足大多数负载;当单副本 P95 打不住,或 MoE / 超长上下文场景下,再考虑上分离方案。
8.4 PD 分离的工程挑战
- KV 传输的时间预算:decode 每步 30ms,KV 传输必须在 1 个 decode step 内完成。4K 上下文 GQA 模型的 KV 大约 1-2 GB,200 Gbps RDMA 单向传输约 40-80ms,单向就已经打满预算。解法:传输与 decode 的首步并行、层粒度流式传输(第一层传好就开始算)、或干脆要求更高带宽(400G / NVLink 域内)。
- Router 的调度策略:要同时考虑 prefill 池队列长度、decode 池 KV 占用、跨池传输延迟。热点 prompt 还要尽量调度到同一 prefill 节点以命中 prefix cache。
- 故障域:prefill 节点挂掉,其上的请求可以重做;decode 节点挂掉,所有在途请求的 KV 都丢失,用户体验直接中断。decode 节点的可用性要求更高,KV 多副本是未来方向。
- 配比动态性:白天 RAG 多(prefill 重),夜里 Agent 多(decode 重),两池子大小不同。需要弹性伸缩策略和跨池动态转换能力。
8.5 何时”不”分离
PD 分离不是银弹。以下场景保持合一部署更划算:
- 单副本规模不大(< 8 卡),运维复杂度不值得。
- 输入输出较均衡,chunked prefill 已经够用。
- 跨节点带宽不够(< 100Gbps RDMA),KV 传输会成为瓶颈。
- 模型很小(7B 以下),单步 decode 本身就很快,收益边际递减。
九、KV Cache 复用
9.1 Prefix Caching
真实业务里同一个 system prompt 被成千上万次复用:“你是一个专业助手…(2000 字设定)…请回答用户问题”。没有 prefix caching 的引擎,每次都要从头 prefill 这 2000 token,纯浪费。
Prefix caching 的思想:把最近处理过的 prefix 的 KV Cache 保留在显存(或下沉到 CPU / NVMe),新请求来时查前缀命中,命中的段直接复用 KV,只 prefill 新增部分。
命中一段长度 L 的前缀,直接省掉 L 个 token 的 prefill 时间 —— RAG / 长 system prompt / 多轮对话场景下这是数量级的加速。
9.2 Radix Tree:SGLang 的贡献
SGLang 把 prefix 组织成 Radix Tree(前缀树):每个节点是一段 token,边是一段 KV。新请求来时自底向上匹配,命中多深就复用多深。
相比”哈希存整个 prefix”的平面方案,Radix Tree 天然支持多分叉复用:比如一个 system prompt 下衍生出 100 个不同用户的对话,共享前缀只存一份,每个对话的私有部分挂在前缀节点下。这对 Agent 场景(同一套工具定义 prompt + 千差万别的后续)特别友好。
vLLM 在 0.5+ 也支持了 prefix caching(非 radix,基于 block hash),TRT-LLM、TGI 都有各自实现。这已经是现代推理引擎的标配。
9.3 分布式 KV 存储
单节点显存装不下所有活跃 prefix,于是出现多级:GPU HBM → CPU DRAM → NVMe → 分布式 KV 服务。Mooncake 的 KVCache Store、阿里 PAI-EAS 的 KV 分层、字节的 ByteCache 都是这条路线。命中远端 KV 也比重新 prefill 快(因为长 prompt 的 prefill 本身就是几百 ms)。
9.4 共享前缀的 attention 正确性
有个坑:prefix cache 命中后,不同请求共享同一段 KV,attention 计算在”共享段”上要正确处理 causal mask 和位置编码。RoPE 这类相对位置编码天然兼容(同样的 token 在同样的位置),ALiBi 也类似。但如果位置编码依赖 batch 内全局坐标,就会出错。主流引擎(RoPE 为主)已经默认兼容。
9.5 命中率观测
生产上 prefix cache 是”看起来有用,实际看命中率”。必须暴露指标:
prefix_cache_hit_tokens_total / prefix_cache_miss_tokens_total
prefix_cache_hit_rate(tokens 维度,不是 requests 维度)
prefix_cache_size_bytes / prefix_cache_evictions_total
RAG 类业务命中率往往不高(每次 context 都不同);Agent / 系统 prompt 驱动的业务命中率能到 70%+,TTFT 直接折半。
十、采样策略
生成 token 的时候,最后一层 logits 要转成一个具体的 id。策略不只影响质量,也影响工程 —— 某些采样(比如 beam search)会把 batch 规模膨胀。
10.1 基础采样
- Greedy:永远取 argmax,确定性最强,容易复读。
- Temperature:
logits / T后 softmax。T<1 更锐利,T>1 更平坦。 - Top-k:只在概率最高的 k 个里归一化采样。
- Top-p(nucleus):累计概率超过 p 的最小集合里采样。
- Min-p:按最大概率的比例设阈值,动态过滤。
- Repetition penalty / Presence penalty / Frequency penalty:惩罚已出现的 token,抑制复读。
一组常用默认:temperature=0.7, top_p=0.9, presence_penalty=0.1。
10.2 结构化生成
不少业务要求输出是严格 JSON / SQL / XML。主流方案是 约束解码:在每一步采样前,按文法规则屏蔽掉非法 token 的 logits,只在合法集合里采。代表实现:Outlines、xgrammar、lm-format-enforcer,以及 SGLang、TRT-LLM、vLLM 的内置约束解码。
10.3 推测解码(下一篇详讲)
简单提一下:用一个小”草稿模型”一次吐 k 个 candidate token,大模型并行验证(一个 forward 验证所有 k 个,因为 causal 下可以同时算)。接受的那些直接采纳,不接受的从分歧点继续。在大模型上能带来 2-3× 的 decode 加速。第 15 篇专题讨论。
10.4 采样在 batch 下的工程细节
当上百个请求拼到同一个 decode step,每个请求的采样参数可能都不同(temperature、top_p、seed 都是请求级)。主流引擎会做”向量化采样”:
logits: [B, V] → per-request divide temperature
→ per-request top-k/top-p 掩码(gather + scatter)
→ per-request categorical sample(使用不同 seed 生成的 uniform noise)
这些操作 vLLM 放在一个叫 Sampler 的融合
kernel 里完成。对高并发 decode 来说,采样 kernel 本身也能占
1-3ms,不容忽视。
十一、多模态推理
视觉语言模型(VLM)、音频语言模型(ALM)、端到端多模态模型(Qwen2-VL / GPT-4o / Gemini / GLM-4V / MiniCPM-V / InternVL)的推理链路大致是:
图像 ──► ViT / Vision Encoder ──► 一串 image tokens ──┐
文本 ──► Tokenizer ───────────────► 一串 text tokens ──┴──► LLM ──► 文本输出
工程上的独特挑战:
- Vision Encoder 是额外的一次 forward,可能用不同的 kernel 栈(CNN / ViT),延迟要算在 TTFT 里。
- 图像 token 很多:一张 448×448 图像常产生 256-1024 个 image tokens,视频更夸张(每帧几百 + N 帧)。Prompt 实际有效长度暴涨。
- 变长 prefill:同一 batch 里,一个请求有 3 张图、一个请求纯文本,prefill 形状天差地别 → 必须 continuous batching。
- 缓存策略:image token 的 KV 也能 prefix cache,同一张图反复问答的场景可以直接复用。
- 输出多模态(图 / 语音)需要引入 diffusion / vocoder 模块,推理链路更长,不在本篇范围。
11.1 视觉 encoder 与 LLM 的分离
大厂的工程实践里,Vision Encoder 通常和 LLM 分开部署:
- Vision Encoder 相对小(几百 MB 到 几 GB),但输入分辨率可变、batch 也不好凑。
- LLM 部分沿用现有推理引擎。
- 两者之间通过 gRPC 或 shared memory 把 image embedding 传过去,接到 LLM 的 prefill 之前。
分开的好处是 Vision 和 LLM 可以独立弹性:长文本场景 Vision 少用,短图多问答场景 LLM decode 少用。
11.2 视频推理的特殊性
视频输入一秒就是几十帧,token 数会爆炸。工程上:
- 采样:不是全帧喂,而是关键帧采样或时序压缩(Qwen2-VL 的 M-RoPE、Gemini 的 chunk 处理)。
- 长上下文: 一段 3 分钟视频 token 化后可达 10 万级,对 KV 压力极大,MLA / KV 量化几乎是必需。
- 流式视频:在线场景一边 encode 一边 prefill,类似音视频 codec 的 pipeline 思想。
主流引擎对多模态支持:vLLM、SGLang 均已原生支持主流 VLM;TRT-LLM 需要针对性插件;国内推理平台(火山方舟、阿里 PAI-EAS、腾讯 TI、百度千帆)都把 VLM 作为一等公民。
十二、动手:最简 Decode 循环
把所有概念落到一段可跑的 PyTorch 代码里。这里用 HuggingFace 的一个小模型演示手写 decode + KV cache。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL = "Qwen/Qwen2.5-0.5B" # 任意小 decoder-only 模型都行
tok = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(
MODEL, torch_dtype=torch.float16
).cuda().eval()
@torch.inference_mode()
def generate(prompt: str, max_new=64, temperature=0.7, top_p=0.9):
# === Prefill ===
input_ids = tok(prompt, return_tensors="pt").input_ids.cuda()
out = model(input_ids, use_cache=True)
logits = out.logits[:, -1, :] # 只要最后一位
past_kv = out.past_key_values # [(K0,V0), (K1,V1), ...] per layer
generated = [input_ids]
# === Decode loop ===
for _ in range(max_new):
# 采样
if temperature == 0:
next_id = logits.argmax(-1, keepdim=True)
else:
probs = torch.softmax(logits / temperature, dim=-1)
# top-p
sorted_p, sorted_i = torch.sort(probs, descending=True)
cum = sorted_p.cumsum(-1)
mask = cum - sorted_p > top_p
sorted_p[mask] = 0
sorted_p /= sorted_p.sum(-1, keepdim=True)
pick = torch.multinomial(sorted_p, 1)
next_id = sorted_i.gather(-1, pick)
generated.append(next_id)
if next_id.item() == tok.eos_token_id:
break
# 只把新 token 送进模型,KV cache 继续增长
out = model(next_id, past_key_values=past_kv, use_cache=True)
logits = out.logits[:, -1, :]
past_kv = out.past_key_values
return tok.decode(torch.cat(generated, dim=1)[0], skip_special_tokens=True)
print(generate("解释一下什么是 KV Cache:", max_new=128))这段代码做了两件关键的事:
- 第一次
model(input_ids)是 Prefill,建立所有层的 KV Cache,返回past_key_values。 - 之后每一步只喂 1 个新
token,配合上一次的
past_key_values做 Decode,HF 内部会把新 K/V append 上去。
把 past_key_values=past_kv 这一行改成
None,强制每步重算整个前缀,你会立刻感受到几十倍的速度差
—— 这就是 KV Cache 的价值。
想看 KV Cache 的真实大小,加一行:
k0, v0 = past_kv[0]
print(k0.shape, k0.dtype) # e.g. [1, num_kv_heads, seq, head_dim] fp16
total = sum(k.numel()*k.element_size() + v.numel()*v.element_size()
for k, v in past_kv)
print(f"KV bytes: {total/1024/1024:.2f} MB")对 Qwen2.5-0.5B + 4096 上下文,这个值大约几十 MB;换成 LLaMA-2 70B,同样上下文就是上面算过的 ~1.3 GB。
十三、引擎选型思路预告
13.1 一个推理引擎的内部结构
站在这里回头看,一个现代推理引擎的骨架大致是:
┌──────────────────────────────────────────┐
HTTP │ API Server(OpenAI 兼容 / gRPC) │
───► │ · 鉴权、限流、日志 │
│ · 把请求放入 RequestQueue │
└────────────────┬─────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ Scheduler(continuous batching 核心) │
│ · admit / preempt / schedule │
│ · prefix cache 查表 │
│ · 混合 batch(prefill chunks + decodes) │
└────────────────┬─────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ Model Executor(可多 worker / TP / PP) │
│ · Paged KV Manager(block table) │
│ · CUDA Graph 池 │
│ · Flash / FlashDecoding / 量化 kernels │
│ · Sampler(fused) │
└──────────────────────────────────────────┘
每一层后面都会有专文。但凡一个 production-grade 引擎,少不了这四层。各家差别在于:调度策略谁更聪明、KV 管理谁更省、kernel 谁更快、API 谁更兼容。
13.2 最终决策
下一篇会详聊 PagedAttention 与 Continuous Batching 的实现细节,第 13 篇做 vLLM / SGLang / TRT-LLM / TGI / LMDeploy 的横向对比。这里先给一张决策概览:
| 场景 | 推荐起点 |
|---|---|
| 通用 HF 模型 / 快速上线 | vLLM(生态最广、默认 continuous batching + paged attention + prefix cache) |
| 极致 TTFT / 复杂 prompt 编排 / Agent | SGLang(RadixAttention、Python DSL) |
| NVIDIA 生产级 / 极致单卡吞吐 / 量化 | TensorRT-LLM(闭源友好、FP8 / INT4 最强) |
| HuggingFace 官方 serve | TGI |
| 国产卡 / 华为昇腾 | MindIE;寒武纪 MagicMind;海光 DTK;摩尔线程 / 沐曦自研 |
| 国产云托管 | 火山方舟、阿里 PAI-EAS、腾讯 TI-ONE、百度千帆 |
十四、小结与常见误区
14.1 核心要点回顾
- LLM 推理分 Prefill + Decode 两阶段,前者 compute-bound,后者 memory-bound。
- KV Cache 是推理第一优化对象;显存主要被它吃。GQA / MLA / KV 量化 / 驱逐是四条正交路线。
- Continuous Batching 是吞吐提升的关键;Chunked Prefill 解决 prefill 阻塞 decode。
- Prefill/Decode 分离 是大规模 serve 的终极形态,DeepSeek、Kimi、Azure 都在用。
- Prefix Caching / Radix Tree 让多用户共享 system prompt 成本骤降。
- FlashDecoding + CUDA Graph + kernel fusion 把 decode 每毫秒榨干。
- 核心指标 TTFT / TPOT / Throughput,必须按 P50/P95/P99 监控。
14.2 常见误区
- “推理就是 forward 一遍” —— 忽略了自回归 N 次 forward。
- “batch 越大吞吐越高” —— 超出显存就 OOM,P99 也会起飞。
- “TTFT 短就是好” —— 如果 prefill 过度抢占 decode,TPOT 会糟糕。
- “KV 就是小东西” —— 对 70B + 长上下文,KV 比权重还大。
- “量化一定损失小” —— KV-Int4、Weight-Int4 有坑,要校准。
- “多模态 = 图像编码器拼 LLM” —— 工程上 prefill 形状极度动态,对调度是噩梦。
14.3 给推理工程师的 checklist
上线一个模型之前,逐项确认:
十五、延伸阅读与下一步
推理是 LLM 基础设施里发展最快、开源最活跃的一块。本篇只是从工程第一性原理搭了个骨架,后续几篇会逐层填肉:
- 第 12 篇:PagedAttention 的数据结构、block 大小、CoW、分配回收算法;Continuous Batching 的调度策略(FIFO / priority / fairness)、抢占机制。
- 第 13 篇:vLLM / SGLang / TRT-LLM / TGI / LMDeploy / MindIE 横向对比:支持模型、量化、性能、API、运维成熟度、踩坑实录。
- 第 14 篇:量化工程:SmoothQuant / GPTQ / AWQ / FP8 / INT4 各自的精度损失、硬件适配、工具链。
- 第 15 篇:推测解码(Speculative Decoding)、Medusa、EAGLE、MTP(Multi-Token Prediction),以及 DeepSeek V3 MTP 的工程落地。
- 第 16 篇:长上下文工程:RoPE 外推、YaRN、Position Interpolation、StreamingLLM、KV Retrieval、分段注意力,以及 128K/1M 上下文的真实成本模型。
推荐在读完本篇后亲自操作一遍:
- 在单张 24GB 卡上用 vLLM 起一个 Qwen2.5-7B,用
benchmark_serving.py跑 ShareGPT,观察GPU KV cache usage和 TTFT/TPOT 随并发变化的曲线。 - 关掉
--enable-prefix-caching,重新跑同一组负载,对比 TTFT 差异。 - 切到
--enforce-eager(关掉 CUDA Graph),对比 TPOT 尾延迟。 - 切换
--max-num-seqs从 16 到 256,观察吞吐与 P99 的 trade-off。
手上有 4 张 H100 的读者,可以复现 DistServe 的 PD
分离实验,直观感受分离带来的 P95 TTFT
改善。没有硬件的读者,可以基于本篇第十二节的代码,把 KV cache
enabled/disabled 切换,观察单序列速度差 ——
这是所有推理优化的”起点”。
还想深挖的方向有:
- 推理经济学:一次 1K tokens 调用的真实成本拆解(GPU 秒 × 量化比 × 并发 × 电费),对照各家云 API 定价。第 24 篇会展开。
- 异构硬件:H100 vs H200 vs B200 vs MI300X vs 国产卡(昇腾 910B、寒武纪思元、海光 DCU)的推理实测差异。
- 在线学习/增量更新:推理引擎热更新权重的工程实践(LoRA 动态挂载、Prefix Tuning 切换)。
推理工程的魅力在于:模型一旦定型,剩下的每 1% 提升都来自工程。把上面的拼图一块一块拼齐,你就能把一个”能跑”的模型变成”能赚钱”的服务。
上一篇:【大模型基础设施工程】10:Checkpoint 与故障容忍 下一篇:【大模型基础设施工程】12:PagedAttention 与 Continuous Batching
参考资料
- Kwon et al. Efficient Memory Management for Large Language Model Serving with PagedAttention (vLLM), SOSP 2023.
- Yu et al. Orca: A Distributed Serving System for Transformer-Based Generative Models, OSDI 2022.
- Dao. FlashAttention-2 / FlashAttention-3.
- Hong et al. FlashDecoding++, 2024.
- Zhong et al. DistServe: Disaggregating Prefill and Decoding for Goodput-optimized LLM Serving, OSDI 2024.
- Patel et al. Splitwise: Efficient Generative LLM Inference Using Phase Splitting, Microsoft, ISCA 2024.
- Moonshot AI. Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving, 2024.
- DeepSeek-AI. DeepSeek-V2 / V3 Technical Report(MLA 章节).
- Ainslie et al. GQA: Training Generalized Multi-Query Transformer Models, 2023.
- Zheng et al. SGLang: Efficient Execution of Structured Language Model Programs, 2024.
- Xiao et al. StreamingLLM / H2O / SnapKV 系列 KV 驱逐论文.
- NVIDIA TensorRT-LLM、HuggingFace TGI、vLLM、SGLang、LMDeploy、MindIE 官方文档.
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【大模型基础设施工程】01:大模型基础设施全景 —— 训练、推理、RAG、Agent、观测
面向工程师的大模型基础设施开篇地图,覆盖 2022 到 2026 的工程分水岭、五层工程栈、训练与推理的工程差异、中国与全球行业版图以及成本曲线。
【大模型基础设施工程】12:PagedAttention 与 Continuous Batching
vLLM 的两大核心革新——Continuous Batching 让 GPU 打满、PagedAttention 让显存不再碎,推理吞吐量因此跃升一个数量级。本篇从操作系统类比到工程实操全盘拆解。
【大模型基础设施工程】16:长上下文工程
从 4K 到 1M+ 上下文的训练与推理工程——位置编码扩展、稀疏 attention、Ring Attention、KV 压缩与长上下文评测
大模型基础设施工程
面向中国工程团队的大模型基础设施系列。从 GPU/CUDA/互联、训练框架与 3D 并行、vLLM/SGLang 推理引擎、量化与推测解码、RAG/Agent 到服务化、网关、可观测性与安全合规,覆盖 LLMOps 全链路。