推理系统与容量规划¶
本章导读:推理岗的面试很少停留在“KV Cache 是什么”,而是直接给场景:“7B 模型、4K 上下文、20 并发、单卡 24GB,能不能跑?”“P99 延迟怎么压?”“为什么 GPU 利用率只有 5%?”本章从 roofline 出发,把显存、延迟、吞吐和成本串成一套可计算的框架。
阅读建议:先掌握第 1、2 节的“两阶段 + 算术强度”,这是解释一切推理优化的基础;再用第 7 节的算例练习容量规划。
核心要点¶
- 推理分 prefill 和 decode:prefill 并行处理整段 prompt,通常 compute-bound;decode 逐 token 生成,通常 memory-bound。
- Decode 的算术强度约为 1 FLOP/byte,远低于 GPU 的算力带宽比,因此 GPU 算力大量闲置,瓶颈在显存带宽。
- KV Cache 显存与层数、KV 头数、头维度、序列长度、并发数成正比;GQA/MLA/量化是主要压缩手段。
- PagedAttention 用分页块管理 KV,解决碎片与预留浪费,并支持前缀共享与 copy-on-write。
- FlashAttention 是 IO 感知的精确注意力,通过分块与在线 softmax 避免物化 \(n\times n\) 矩阵。
- Continuous batching 提升吞吐,chunked prefill 改善 decode 的 TPOT,PD 分离把两种负载放到不同资源池。
- 容量规划的顺序:权重显存 → KV 显存 → 并发上限 → 吞吐/延迟 → GPU 数与冗余。
- 指标要一起看:TTFT、TPOT/ITL、P99、吞吐、Goodput、错误率;只看 tokens/s 会误导。
1. 两阶段:Prefill 与 Decode¶
1.1 Prefill¶
输入整段 prompt,一次性计算所有 token 的 Q/K/V 与注意力:
- 矩阵乘规模大,GPU 利用率高;
- 计算量约 \(2N\cdot S_{\text{in}}\) FLOPs;
- 时间决定 TTFT(Time To First Token);
- 长 prompt 会阻塞其他请求,需要 chunked prefill 或 PD 分离。
1.2 Decode¶
每次生成一个 token,只计算当前 token 的 Q,读取历史 K/V:
- 矩阵乘退化为矩阵-向量,GPU 算力利用率低;
- 每步要读取全部权重与 KV Cache,受显存带宽限制;
- 时间决定 TPOT(Time Per Output Token) / ITL;
- 优化重点是减少访存:量化、GQA/MLA、KV 量化、投机解码、更大 batch。
1.3 端到端延迟¶
排队时间另算。线上通常看 P50/P95/P99,而不是平均值。
2. Roofline:为什么 decode 慢¶
2.1 算术强度¶
Decode 一个 token、batch=1 时:
- FLOPs ≈ \(2N\);
- 需读取权重约 \(2N\) bytes(BF16);
- 算术强度 ≈ 1 FLOP/byte。
H100 的算力/带宽比:
也就是说,decode 的算术强度比硬件平衡点低约 300 倍,算力大量闲置。
2.2 Decode 时间下限¶
7B BF16 约 14GB,H100 3.35TB/s,理论下限约 4.2ms/token;实际因 kernel、KV、调度通常 10–20ms。
2.3 Batch 如何提升利用率¶
Batch=\(B\) 时,权重被复用 \(B\) 次:
- B=1:AI≈1,严重 memory-bound;
- B=32:AI≈32,仍低于 295;
- B=256:AI≈256,接近 compute-bound;
- 但大 batch 会增加排队延迟与 KV 显存,需要权衡。
这解释了为什么 continuous batching 能大幅提升吞吐:不是让单请求更快,而是让权重读取被更多请求分摊。
3. KV Cache 深入¶
3.1 公式¶
其中:
- 2:K 和 V 各一份;
- \(L\):层数;
- \(n_{kv}\):KV 头数(GQA 后小于 Q 头数);
- \(d_{head}\):每头维度;
- \(S\):序列长度(含 prompt 与已生成);
- \(B\):并发请求数;
- bytes:FP16=2,FP8=1,INT4=0.5。
3.2 估算表¶
| 模型 | 层数 | KV 头 | \(d_h\) | 每 token KV | 4K 上下文 | 32K 上下文 |
|---|---|---|---|---|---|---|
| 7B(GQA) | 32 | 8 | 128 | 128KB | 0.5GB | 4GB |
| 70B(GQA) | 80 | 8 | 128 | 320KB | 1.25GB | 10GB |
| 70B(MHA) | 80 | 64 | 128 | 2.56MB | 10GB | 80GB |
结论:
- MHA 在长上下文下 KV 成本极高,GQA/MLA 是必需;
- 32K 上下文的 70B GQA,单请求就要 10GB,并发数直接被压缩;
- KV 量化到 FP8/INT8 可以减半,但要注意精度与 kernel 支持。
3.3 GQA / MQA / MLA¶
- MHA:每个 Q 头对应一组 K/V,KV 最大;
- MQA:所有 Q 头共享一组 K/V,KV 最小,质量损失明显;
- GQA:分组共享,组数 \(g\) 可调,Llama 2/3 常用 8 组;
- MLA:把 KV 压缩成低秩潜向量,推理缓存潜向量,DeepSeek 系列使用。
MLA 的 KV 近似为:
其中 \(d_c\) 是潜向量维度,\(d_R\) 是解耦 RoPE 维度;DeepSeek-V2 公开数据约 576 维/层/token,远小于 MHA。
3.4 为什么显存“一直占着”¶
- 框架使用缓存分配器(caching allocator),释放的显存留在池里复用,不还给 OS;
- vLLM 等会预分配 KV 池,按 block 管理;
- 这不一定是泄漏;判断泄漏要看长期增长趋势与
nvidia-smi/框架指标; - 真正泄漏常见于请求未释放、异常路径、引用环、KV block 未回收。
4. PagedAttention 与调度¶
4.1 PagedAttention¶
把 KV Cache 切成固定大小的 block(如 16 token),逻辑块通过 block table 映射到物理块:
- 按需分配,减少预留浪费;
- 支持非连续存储,降低碎片;
- 支持前缀共享(相同 system prompt 的 block 共享)与 copy-on-write(并行采样/beam search)。
公开数据:vLLM 通过 PagedAttention 将 KV 浪费从传统实现的 60%–80% 降到 4% 以下(论文口径,量级参考)。
4.2 Continuous Batching¶
传统静态 batch 要等最慢的请求结束;continuous batching 在 token 级调度:
- 完成的请求立即退出,新请求立即加入;
- 提升 GPU 利用率与吞吐;
- 需要处理不同序列长度、KV 块分配、公平性。
4.3 Chunked Prefill¶
把长 prompt 的 prefill 切成多个 chunk,与 decode 请求交错执行:
- 优点:降低长 prompt 对 TPOT 的阻塞,改善 P99;
- 缺点:TTFT 可能变长,调度复杂;
- 适合混合负载(长 prompt + 短生成)。
4.4 PD 分离¶
Prefill 是 compute-bound,decode 是 memory-bound;把两者放到不同 GPU 池:
- P 池负责长 prompt 预填充,D 池负责逐 token 生成;
- KV 通过 NVLink/RDMA 传输;
- 优点:资源利用率高、尾延迟低;缺点:架构复杂、KV 传输成本。
4.5 公平性与优先级¶
- 多租户需要配额与优先级,防止大请求饿死小请求;
- 可按 token 预算、队列长度、租户权重调度;
- 长请求可限制最大长度或降级到小模型。
5. FlashAttention:IO 视角¶
5.1 标准注意力的问题¶
标准实现需要把 \(S\times S\) 的注意力矩阵写入 HBM:
- 显存 \(O(S^2)\);
- HBM 读写量大,长序列下成为瓶颈;
- 即使算力足够,IO 也拖慢。
5.2 FlashAttention 的做法¶
- 把 Q/K/V 分块,块能放进 SRAM;
- 对每个 Q 块,遍历 K/V 块,计算局部注意力;
- 用 online softmax 维护 running max 与归一化因子,边算边合并;
- 不物化完整的 \(S\times S\) 矩阵,只保留输出与统计量;
- 反向时重算注意力,减少显存。
5.3 复杂度与误区¶
- 计算量仍是 \(O(S^2d)\),没有变成线性;
- HBM 访问从 \(O(S^2)\) 降到约 \(O(S^2d^2/M)\)(\(M\) 为 SRAM 大小);
- 显存从 \(O(S^2)\) 降到 \(O(S)\);
- 结果是精确的,不是近似;
- FlashAttention-2 优化并行与工作划分,FlashAttention-3 利用 Hopper 异步与 FP8。
面试陷阱:把 FlashAttention 说成“稀疏注意力”或“线性注意力”都是错的。
6. 量化、投机与蒸馏¶
6.1 量化¶
| 方案 | 权重 | 激活 | 收益 | 风险 |
|---|---|---|---|---|
| W4A16 | 4bit | 16bit | 权重显存 4× 降,decode 带宽降 | 精度损失、kernel 支持 |
| W8A8 | 8bit | 8bit | 算力/带宽兼得 | 激活离群值、校准 |
| FP8 | 8bit | 8bit | H100 原生支持、精度较好 | 需要缩放 |
| KV INT8/FP8 | - | - | KV 显存减半 | 长上下文精度 |
常见方法:
- GPTQ:逐层量化,用 Hessian 近似补偿误差;
- AWQ:保护激活显著的少数通道,做缩放;
- SmoothQuant:把激活离群值迁移到权重,实现 W8A8;
- GGUF/llama.cpp:面向 CPU/边缘的量化格式。
6.2 投机解码¶
用小模型/额外头生成 \(\gamma\) 个候选 token,大模型一次验证:
- 接受率 \(\alpha\) 越高,加速越明显;
- 期望接受 token 数约 \(\frac{1-\alpha^{\gamma+1}}{1-\alpha}\);
- 草稿成本要远小于目标模型,否则不划算;
- 适合输出可预测的任务(代码、摘要、固定格式)。
6.3 蒸馏¶
- 用大模型 logits/生成结果训练小模型;
- 推理成本下降,但能力上限受 teacher 与数据限制;
- 常与量化叠加:先蒸馏到小模型,再量化部署。
7. 容量规划算例¶
7.1 场景¶
- 模型:7B,GQA(32 层、8 KV 头、\(d_h=128\)),BF16;
- 上下文:4K;
- 目标并发:20;
- GPU:24GB(如 4090/L4);
- 输出:平均 256 token。
7.2 显存¶
- 权重:14GB;
- KV/请求:\(128\text{KB}\times4096=0.5\)GB;
- 20 并发:10GB;
- 权重 + KV = 24GB,已无空间给激活、临时 buffer、框架开销 → 跑不下。
调整方案:
- 量化权重到 INT4:3.5GB,KV 仍 10GB → 13.5GB,可跑;
- KV 量化到 FP8:权重 14GB + KV 5GB = 19GB,可跑;若权重也 INT4,则 3.5GB + 5GB = 8.5GB,更宽裕;
- 降低并发到 10:5GB KV → 19GB,勉强;
- 换 48GB/80GB 卡,或张量并行到 2 卡。
7.3 吞吐与延迟¶
- 单请求 decode 约 10–20ms/token(受带宽与 kernel 影响);
- 256 token 输出约 2.5–5s;
- 20 并发时,若 batch 调度良好,总吞吐约 \(20/5=4\) req/s 量级;
- 但 TTFT 会因排队上升,需要压测确认真实分布。
7.4 GPU 数¶
示例:峰值 10 QPS、平均 5s、单卡可承载 20 并发:
加 30% 冗余 → 4 卡。实际要按 P99 与故障域调整。
8. 框架选型¶
| 框架 | 核心优势 | 适用场景 | 注意点 |
|---|---|---|---|
| vLLM | PagedAttention、continuous batching、生态成熟 | 通用在线服务 | 版本迭代快,需锁版本 |
| SGLang | RadixAttention、前缀复用、结构化生成 | 多轮/Agent/高吞吐 | 生态相对新 |
| TensorRT-LLM | NVIDIA 深度优化、极致性能 | 固定模型、追求低延迟 | 编译复杂、灵活性低 |
| llama.cpp | GGUF、CPU/GPU 混合、边缘部署 | 本地/端侧 | 高并发不如服务端框架 |
| Ollama | 易用、模型管理 | 开发/演示 | 生产级能力有限 |
| TGI | HuggingFace 生态 | HF 模型快速部署 | 性能与特性视版本 |
选型维度:模型支持、吞吐/延迟、量化、结构化输出、多 LoRA、可观测性、社区、license。
9. 线上故障与排查¶
| 现象 | 可能原因 | 排查/处理 |
|---|---|---|
| P99 飙升 | 长请求阻塞、队列积压、KV 不足 | chunked prefill、限流、扩容 |
| OOM | 并发过高、长上下文、KV 碎片 | 降并发、PagedAttention、量化 |
| 吞吐低 | batch 小、GPU 利用率低、CPU 瓶颈 | 调 batch、检查数据预处理、用连续批处理 |
| 输出乱码/截断 | tokenizer/stop token 不匹配 | 对齐 chat template、检查 EOS |
| 量化后效果差 | 激活离群、校准集不匹配 | 换 AWQ/FP8、关键任务不量化 |
| 首 token 慢 | prefill 长、排队、冷启动 | 前缀缓存、预热、PD 分离 |
| 重复/复读 | 采样参数、惩罚、模型 | 调 temperature/top-p、加惩罚 |
| 请求超时 | 生成长度不可控 | 限制 max_tokens、超时降级 |
10. 面试高频问题¶
Q1:Prefill 和 decode 的区别?分别怎么优化?¶
- Prefill 并行、compute-bound;decode 逐 token、memory-bound。
- Prefill:FlashAttention、chunked prefill、PD 分离、前缀缓存。
- Decode:量化、GQA/MLA、KV 量化、continuous batching、投机解码。
Q2:KV Cache 怎么算?7B 32K 多大?¶
- 公式 \(2L n_{kv}d_h S B\) bytes。
- 7B(32 层、8 KV 头、128 维、FP16)每 token 128KB,32K 约 4GB/请求。
- 加权重 14GB,单卡 24GB 只能跑少量并发。
Q3:PagedAttention 解决什么问题?¶
- 传统 KV 预分配导致内部/外部碎片与浪费。
- 分页块按需分配、非连续存储、块表映射。
- 支持前缀共享与 copy-on-write,提升显存利用率与吞吐。
Q4:FlashAttention 是近似算法吗?¶
- 不是,是精确注意力。
- 通过分块与 online softmax 避免物化 \(S\times S\) 矩阵。
- 计算量不变,减少 HBM IO,显存从 \(O(S^2)\) 降到 \(O(S)\)。
Q5:Continuous batching 为什么能提升吞吐?¶
- 静态 batch 要等最长请求;连续批处理在 token 级动态进出。
- 权重读取被更多请求分摊,GPU 利用率提升。
- 代价是调度复杂、尾延迟与公平性问题。
Q6:投机解码的加速比怎么估?¶
- 期望接受 token 数 \(\frac{1-\alpha^{\gamma+1}}{1-\alpha}\)。
- 加速比 = 接受 token 数 /(草稿成本 + 验证成本)。
- 接受率低或草稿模型太慢时不划算。
Q7:量化为什么对 decode 提升大?¶
- Decode 是 memory-bound,权重字节数减半/四分之一,带宽压力直接下降。
- Prefill 更 compute-bound,量化收益取决于算力与 kernel。
- 量化可能损失精度,需按任务评估。
Q8:GPU 利用率只有 5%,正常吗?¶
- Decode 单请求时正常:算术强度约 1,远低于硬件平衡点。
- 提升:增大 batch、continuous batching、减少 KV 访存、量化。
- 若 prefill 也低,检查数据加载、CPU 预处理、kernel 启动开销。
Q9:如何做容量规划?¶
- 权重显存 + KV×并发 + 激活/临时 → 单卡能否放下。
- 吞吐/延迟压测 → 单卡可承载并发。
- 峰值 QPS × 占用时间 / 单卡并发 → GPU 数,加冗余。
- 用 P99 与故障域校验,不要只看平均值。
Q10:长上下文推理的瓶颈是什么?¶
- KV 显存与带宽、注意力计算、调度。
- 优化:GQA/MLA、KV 量化、PagedAttention、Ring Attention、PD 分离、稀疏/分块注意力。
- 还要看模型是否真的具备长上下文能力,不能只看窗口大小。
本文档为面试复习资料,公式与数字建议结合论文与框架文档复核(vLLM/PagedAttention、FlashAttention、Orca/continuous batching)。