27届大模型面试准备(五十四):大模型推理成本与吞吐调优实战------从连续批处理到 PD 分离
引言:为什么"能跑"和"跑得划算"是两回事
A25 讲过推理服务化的基础组件(PagedAttention、连续批处理、投机解码),A30 讲过全栈性能优化。但面试官真正想听的不是名词罗列,而是"给定一块卡、一个 SLA、一个成本上限,你怎么把吞吐和延迟同时压到最优"。本篇是工程实战深化的第二讲,把推理成本拆成"算力、显存、带宽"三本账,给出可落地的调优杠杆,并重点讲清近年最关键的架构演进------Prefill 与 Decode 分离(PD 分离)。
读完本篇,你应当能对着白板画出"请求从进来到出答案"的成本曲线,并指出每一个拐点对应哪个调优开关。
单请求延迟构成(自回归解码)
Latency = Prefill_time + Decode_time
Prefill_time ≈ (prompt_len + img_tokens) × 算力 / 并行度
Decode_time ≈ output_len × (每步耗时)
每步耗时 ≈ max(算力瓶颈, 显存带宽瓶颈) # 解码是 memory-bound
一句话点题:prefill 是 compute-bound(吃算力),decode 是 memory-bound(吃显存带宽)。这两个阶段的天性不同,决定了它们不该被同一个调度器用同一种方式对待------这正是 PD 分离的根本动机。
第一节 三本账:算力、显存、带宽
1.1 算力账(FLOPs)
一次前向的算力消耗与 token 数、模型参数量近似成正比。prefill 阶段一次处理整个 prompt,算力集中爆发;decode 阶段每步只算一个新 token,但每个 token 都要读一遍全部权重,算力反而不是瓶颈。
1.2 显存账(KV Cache)
KV Cache 是显存的主要占用者。粗略公式:
KV_bytes ≈ batch × seq_len × 2(K,V) × n_layers × d_model × 2(bytes,FP16)
举例:13B 模型、d_model=5120、40 层、FP16,单条 4K 长度序列的 KV 约:
2 × 4096 × 2 × 40 × 5120 × 2 ≈ 5.4 GB。这意味着一块 24GB 的卡,光 KV 就吃掉近四分之一,剩下要装权重和激活。所以"能并发多少请求"几乎由 KV Cache 容量决定。
1.3 带宽账(Decode 的命门)
decode 每生成一个 token,都要把全部权重从显存读到计算单元一次。这个"权重大小 ÷ 显存带宽"决定了每步的理论下界耗时。因此:
-
量化(W8A8、W4)直接缩小权重体积 → 每步读的字节更少 → decode 更快;
-
更大的 batch 把"读权重"摊到更多 token 上 → 算力利用率更高(batch 内并行 decode)。
成本杠杆对照
账本 主要优化杠杆 作用阶段
算力 prefix cache 命中、投机解码 prefill / decode
显存 PagedAttention、KV 量化、prefix 复用 全程(决定并发数)
带宽 weight 量化、大 batch、PD 分离 decode(主)
第二节 连续批处理:把"等待"变成"吞吐"
2.1 静态批处理的浪费
早期推理把请求凑成一个固定 batch,等最慢的请求生成完才释放整批。结果:一个长请求拖住整批,短请求空等,GPU 利用率低得可怜。这就是"静态批处理"的 vacancy 问题。
2.2 连续批处理(continuous batching)
核心思想:不按"请求"批,而按"step 里的 token"批。每解码一步,调度器把所有还在生成的请求的当前 token 聚成一个动态 batch;有请求结束了,立刻塞进新请求,不留空位。这样 GPU 始终满载,吞吐可提升数倍。
静态批处理: [ReqA ReqB ReqC ReqD] 全部生成完才进下一批
ReqA 早结束 -> 空等 -> 利用率低
连续批处理: step1: [A_t B_t C_t D_t]
step2: [A_t B_t C_t D_t] (D 结束了, 但 slot 立刻被新请求 E 占)
step3: [A_t B_t C_t E_t]
实现关键在 PagedAttention(A25 详述):KV Cache 被切成固定大小的 block,像虚拟内存一样按需分配、可按请求拼接,从而支持"变长、可动态增删"的批。没有它,连续批处理的碎片问题会拖垮显存。
第三节 PD 分离:把 prefill 和 decode 拆成两个工厂
3.1 为什么要分离
prefill 是算力密集型、可大 batch 并行;decode 是带宽密集型、必须低延迟响应。把它们塞在同一块卡上,会出现"prefill 的大 batch 把 decode 的延迟抖上去"的互相干扰。PD 分离(Prefill-Decode Disaggregation)把两类实例分开部署:
-
P 实例(prefill):专吃算力,大 batch 处理 prompt + 图像 token,算完把 KV Cache 传给 D 实例;
-
D 实例(decode):专吃带宽,小 batch 低延迟自回归生成,从 P 实例接收已算好的 KV 起步。
传统合设(混部):
[Req] -> [同卡: prefill + decode 抢资源] -> 延迟互相干扰PD 分离:
[Req] -> [P 实例: 大batch prefill] --KV 传输--> [D 实例: 低延迟 decode] -> answer
3.2 KV 传输是分离的代价
PD 分离引入了"KV 在 P、D 之间传输"的新开销。若 P、D 不在同一节点,要走网络(NVLink/IB/RDMA),KV 体积可能高达数 GB(长上下文)。工程上常用"KV 传输压缩 + 流水线 overlap"来掩盖:P 实例算完一段 KV 就流式发给 D,D 边收边开始 decode,不等全量到齐。
3.3 分离带来的调度自由度
分离后,P 和 D 可以独立扩缩容:对话类流量 decode 占比高,就多扩 D;摘要类流量 prefill 占比高,就多扩 P。还能做"prefill 队列削峰"------把突发的大 prompt 在 P 集群排队消化,不影响在线 decode 的尾延迟。这正是大规模服务里 PD 分离越来越成为标配的原因。
| 维度 | 合设(混部) | PD 分离 |
|---|---|---|
| 资源干扰 | 强(prefill 抖 decode) | 弱(各管各的) |
| 扩缩容灵活性 | 低(绑定) | 高(P/D 独立) |
| 尾延迟稳定性 | 中 | 高 |
| 系统复杂度 | 低 | 中(多 KV 传输) |
| 适用规模 | 中小流量 | 大流量/高低峰差异大 |
第四节 量化对成本的杠杆:不止是"省显存"
4.1 W8A8 与 W4 的取舍
权重量化(A19 详述 GPTQ/AWQ)在 serving 里最直接的好处是"每步读的字节变少"。W8A8 几乎无损、decode 提速明显;W4(如 GPTQ/FP4)提速更猛但可能掉点,需要校准。对成本敏感、且任务容错度高的场景(摘要、分类、草稿生成),W4 很划算;对精度敏感的生成任务,优先 W8A8。
4.2 KV Cache 量化常被忽视
KV Cache 量化(如 INT8/FP8 存 KV)能直接放大"可并发序列数",因为它压的是显存账里最大的一块。很多团队只量化权重忘了量化 KV,等于只做了一半。在长上下文场景,KV 量化带来的并发提升往往比权重量化更显著。
# KV Cache 量化示意(存储时用低精度,计算时反量化)
class KVCacheQuant:
def __init__(self, bits=8):
self.scale = None
def quantize(self, kv_fp16):
# 按 token 维度做 per-token 对称量化
amax = kv_fp16.abs().amax(dim=-1, keepdim=True)
scale = amax / (2**(self.bits-1) - 1)
kv_q = (kv_fp16 / scale).round().clamp(-127, 127).to(torch.int8)
return kv_q, scale # 存 int8, 省一半显存
def dequantize(self, kv_q, scale):
return kv_q.float() * scale # 计算前还原
第五节 容量压测与 SLA 设计:调优要打到靶上
5.1 先定义"好"的标准
调优前必须先定 SLA:TTFT(首 token 延迟,对应 prefill)、TPOT(每输出 token 延迟,对应 decode)、端到端延迟、以及可接受的 P99。没有 SLA 的压测是盲目的。
5.2 阶梯加压找拐点
从单并发开始逐步加压,记录吞吐与 P99 延迟曲线,找到"延迟开始陡增"的并发数------那就是该配置的真实容量。超过拐点后,应当触发限流或排队,而不是硬扛(硬扛只会让所有请求都慢)。
| 加压阶段 | 观察到的现象 | 对应调优动作 |
|---|---|---|
| 低并发 | 延迟低、GPU 利用率不足 | 提高 max_batch,榨算力 |
| 中并发 | 吞吐上升、延迟平稳 | 健康区,维持 |
| 拐点附近 | P99 陡增 | 限流/排队,或加 D 实例 |
| 过载 | OOM / 超时雪崩 | KV 量化、PD 分离、降级 |
5.3 成本换算成钱
最终要把"每千次请求成本"算出来:显存占用决定需要的卡数,吞吐决定单位时间能服务的请求数,二者相除即单请求成本。面试官常问"怎么把推理成本降一半",标准答案不是某个银弹,而是"量化压权重+KV量化压显存+PD分离稳延迟+连续批处理提利用率"的组合拳,并量化每一步的收益。
第六节 一次真实调优复盘(把前面所有杠杆串起来)
光讲杠杆容易,面试里真正加分的是"给你一个现场,你怎么排"。下面用一段工程复盘把全篇串起来,你可以直接当作案例讲。
背景:某在线问答服务,单卡 A100 80G 部署 13B 模型,SLA 要求 TTFT<P2s、TPOT<P80ms,目标把单请求成本压到原来的 60%。初始配置是"合设 + FP16 + 静态批处理",实测单卡 QPS 仅 2.1,且 P99 TPOT 经常突破 200ms。
第一步,定位瓶颈。打 tracing 发现 decode 阶段每步耗时稳定在 ~70ms,且 GPU 显存带宽利用率接近打满、算力利用率仅 35%。结论:decode 是 memory-bound,且静态批处理让 GPU 大量时间空等------两本账同时出问题。
第二步,上连续批处理 + PagedAttention。把静态批改成连续批,max_batch 调到 32。结果 QPS 直接到 5.8,TPOT P99 降到 95ms。这一步几乎零精度代价,收益最大。
第三步,权重 W8A8 量化。decode 仍是带宽瓶颈,量化把每步读的权重减半,TPOT P99 降到 62ms,QPS 到 7.4。精度评测掉分在 0.5% 以内,业务可接受。
第四步,KV Cache INT8 量化。长上下文请求占比高,KV 是显存大头。量化 KV 后,单卡可并发序列数从 18 提到 34,QPS 到 9.1,且 OOM 不再发生。
第五步,PD 分离削峰。上线后观测到"摘要类长 prompt"会周期性把在线对话的 TTFT 抖高。把 prefill 拆到独立 P 集群、KV 走 NVLink 流式传输,在线对话 TTFT P99 从 1.8s 稳到 1.2s,且不再受批量摘要任务干扰。
最终:QPS 从 2.1 提升到 9.1(约 4.3 倍),单请求成本降到约 23%(远低于 60% 目标),全部 SLA 达标。复盘结论:调优不是"都上了就好",而是按"瓶颈账→杠杆→验证"的顺序逐层打,每一步都有可观测的收益归属。
调优路径(实际生效顺序)
静态批+FP16 ──①连续批+PagedAttention──> QPS 2.1→5.8, TPOT 200→95ms
──②W8A8 权重量化──────────> TPOT 95→62ms, QPS→7.4
──③KV INT8 量化───────────> 并发 18→34, QPS→9.1, 无OOM
──④PD 分离削峰───────────> TTFT P99 1.8→1.2s, 稳定
这个案例的面试价值在于:它展示了"先量后调、按账归因、逐步验证"的工程方法论,而不是堆名词。面试官听到"我先打 tracing 看是算力还是带宽瓶颈,再决定上哪招",印象会明显不同。
第七节 排障:延迟突增的三条主线
其一,TTFT 变长:先看 prefill 是否被大图/长 prompt 拖住,检查 prefix cache 命中率;若命中率低,多半是 system prompt 被改或缓存键漂移。
其二,TPOT 变长(生成变慢):decode 是带宽瓶颈,检查是否 batch 过大导致每步排队、或权重量化没生效;也可看是否误用了高精度。
其三,吞吐上不去但 GPU 不满:典型"等数据"------KV 传输(PD 分离)或 RPC 序列化成了瓶颈,或连续批处理的调度有锁竞争。定位要靠端到端 tracing,而不是猜。
第八节 五个常见调优误区
误区一:只量化权重忘了 KV。很多团队把 W8A8 当作"降本全部",但长上下文场景下 KV Cache 才是显存主占,不量化 KV 等于只做了一半,并发上不去、成本降不下来。
误区二:把连续批处理当成银弹。连续批能提吞吐,但它不解决"prefill 抖 decode"的延迟干扰,也不解决带宽瓶颈。重载场景里光有连续批、不配合 PD 分离和量化,尾延迟照样崩。
误区三:盲目堆 max_batch。batch 不是越大越好。超过拐点后,每步排队时间超过并行收益,TPOT 反而上升,且显存压力陡增易 OOM。正确做法是用阶梯压测找到拐点,再留 20% 余量。
误区四:用平均延迟代替 P99 定 SLA。平均好看、长尾崩溃是 serving 最常见的"假健康"。面试官最爱追问"你的 P99 是多少",务必把尾延迟当一等公民。
误区五:调优不量化收益。每上一招就该记录 QPS、TPOT P99、成本的变化,否则无法判断"这招值不值"。复盘里那种"逐层打靶点"的思路,比一次性全上更显专业。
面试速答(本篇可直接背的 3 句)
- prefill 是 compute-bound、decode 是 memory-bound,二者天性不同,这是 PD 分离和所有批处理优化的根本出发点。
- 连续批处理按"每步 token"而非"整个请求"组批,配合 PagedAttention 消除碎片,是吞吐翻倍的基石。
- 成本三本账:算力(prefix/投机)、显存(PagedAttention/KV量化/复用)、带宽(权重量化/大batch/PD分离);调优要按 SLA 的 TTFT/TPOT 拐点打靶,不是盲目堆配置。
高频追问清单
- 为什么 decode 阶段量化权重能加速,而 prefill 阶段主要看算力?请用带宽瓶颈解释。
- PD 分离后,KV 在 P、D 之间怎么传?同节点和跨节点分别用什么?
- 连续批处理下,一个超长请求会不会饿死短请求?怎么做公平调度?
- KV Cache 量化掉点比权重量化更敏感还是更不敏感?为什么?
- 如果 SLA 要求 P99 TPOT < 50ms,你会先调哪个参数?给出排查顺序。
- 混合精度(部分层 FP8、部分 FP16)在 serving 里怎么落地?风险在哪?