跳到正文

推理栈与性能模型 ​

一句话:LLM 推理不是一次前向,而是“一次性读 prompt 的 prefill + 逐 token 生成的 decode”两段不同负载;prefill 吃算力,decode 吃显存带宽,调优前必须先判断当前卡在哪一段。

为什么需要 ​

同一个模型,有人抱怨“首字要十秒”,有人抱怨“输出太慢”,有人抱怨“并发一高就排队”。这三种现象对应不同瓶颈:

  • 首字慢:prefill 太长或排队太久,TTFT 高。
  • 输出慢:decode 每 token 都要读一遍权重和 KV,TPOT 高。
  • 并发一高全慢:KV cache 不够,调度器频繁换出或拒绝。

如果不拆开看,调参会变成碰运气:加副本可能只是让更多请求同时挤进 GPU,调大 batch 可能让尾延迟更差。性能模型的作用是把“感觉慢”变成“哪一个指标、受什么约束”。

核心机制 ​

两段式推理 ​

Prefill 把整段 prompt 一次性送进模型,所有 token 的注意力可以并行算,矩阵乘的并行度高,主要受算力限制。Decode 每步只生成一个 token,但要把模型权重和全部历史 KV 读一遍,计算量小、访存量大,主要受显存带宽限制。两段的资源画像不同,优化手段也不同:prefill 用更大的 chunk 提高利用率,decode 用更大的 batch 摊薄权重读取。

四个核心指标 ​

指标含义受什么影响
TTFT首 token 延迟排队时间 + prefill 时长
TPOT每输出 token 时间decode 带宽、batch 大小、KV 长度
吞吐每秒总 token 数batch、并行度、显存容量
并发同时在跑的请求数KV cache 容量、调度策略

平均值会骗人:P50 看起来正常,P99 可能差十倍。在线服务用 P95 / P99 定 SLA,离线批处理才看平均吞吐。一个常见误区是“吞吐上去了,体验就好了”;吞吐和尾延迟经常反向,batch 越大,单个请求等待越久。

显存里到底有什么 ​

占用规模是否随请求增长
模型权重参数量 × 每参数字节否
KV cache序列长度 × 每 token KV × 并发数是
激活与临时张量与 batch、序列长度、chunk 有关是,prefill 峰值高
CUDA context / 通信 buffer固定开销 + 并行度相关基本固定,但会波动

估算顺序是:先算权重,再看剩余显存能给 KV 多少,最后用每 token KV 反推并发。只看“模型能不能装下”不够,装下权重但 KV 只剩几 GB,并发会低得超出预期。

每 token KV 的估算 ​

text
per_token_kv = 2 × layers × kv_heads × head_dim × dtype_bytes

同一个公式,换模型只是换参数。以 40 层、40 个 KV head、head_dim 128、FP16 的 13B 模型为例:2 × 40 × 40 × 128 × 2 = 819,200 B,约 800 KiB(按十进制写作 819 KB)。一条 4K 序列约 3.1 GiB,100 条并发就是 310 GiB 量级,单卡放不下。

GQA 模型改的只是 kv_heads 这一项,效果却很大:

模型layerskv_heads每 token KV4K 序列128K 序列
Llama-2-13B(MHA)4040819,200 B ≈ 800 KiB约 3.1 GiB约 100 GiB
Qwen2.5-14B(GQA)488196,608 B ≈ 192 KiB约 0.75 GiB约 24 GiB

Qwen2.5-14B 层数更多、参数更大,但每 token KV 只有前者的 24%:层数涨了约 20%,KV head 从 40 降到 8,净效果接近除以四。同样显存下并发大约翻四倍,128K 上下文才从“单请求就要 100 GiB”降到“约 24 GiB”,长上下文从不可行变成可讨论。GQA 的价值不在缩小权重,而在把容量约束从 KV 这一侧松开。

量化降的是 dtype_bytes 和权重体积,GQA 降的是 kv_heads,两者收益都落在“同样显存放得下更多并发”,而不只是“模型更小”。

推理栈的分层 ​

网关负责“谁来、去哪、用多少”,服务层负责副本发现与流量分发,引擎负责单卡内的请求调度,GPU 负责实际计算。排障要按层定位:网关 5xx 看路由和限流,服务层看就绪探针和连接分布,引擎看排队与抢占,GPU 看利用率和显存。

一个常见误判是把引擎内的排队当成网关问题,加副本后每个副本仍然排队,因为瓶颈是单卡的 KV 容量,不是副本数。

分层与优化顺序是两个视角 ​

分层图回答“一个请求经过哪些部件”,切的是空间。真正指导调优的是另一条因果链,切的是顺序:

顺序要回答的问题典型手段
装得下权重 + KV + 激活放得进显存吗量化、多卡、降 max_model_len
算得快prefill 算力、decode 带宽够不够分块 prefill、算子融合、并行
塞得满每一步的槽位填满了吗连续批处理、前缀缓存
起得来副本能稳定接流量吗探针、PDB、路由、扩缩容
看得见变慢了能否定位到段分段指标、分位告警

这两条轴不能合并成一张图:分层里的每一层都可能同时出现在五个优化步骤中,硬画在一起会得到一张既不是部署图也不是流程图的东西。排障时先用分层定位“坏在哪一层”,再用因果链判断“这一层该动哪个旋钮”。

吞吐与延迟的取舍 ​

手段吞吐尾延迟代价
增大 batch上升变差单请求等待更久
连续批处理明显上升小幅变差调度复杂,状态管理多
分块 prefill平稳改善prefill 总步数变多
前缀缓存上升改善内存换命中率
量化上升通常改善精度与兼容性风险
多卡并行单卡放不下时必需受通信影响通信开销与复杂度

没有“全都更好”的开关。生产配置是按 SLA 选一组折中:在线对话优先 TTFT 和 P99,离线批处理优先总吞吐,代码补全优先极低延迟,长文档摘要优先 prefill 吞吐。

实战场景 ​

场景:合同问答首字 3.9 s,扩到 6 副本仍无改善 ​

现象:合同问答服务(Qwen2.5-14B FP16,单卡 A100 80 GB,8 路常驻并发、max_num_seqs=32),召回条数从 4 调到 16 后 prompt 中位长度由 1.4K 涨到 7.8K token,TTFT P99 从 0.95 s 升到 3.9 s(经验值,需校准),TPOT P50 只从 21 ms 升到 23 ms。客诉集中在“点了发送要等四秒才出字”,坐席改用别处检索。值班按“模型太慢”把副本从 2 加到 6,TTFT 没有变化,单卡 SM 利用率反而降到 38%。

排查路径:先看已有指标能否拆段(零成本),再动副本(可回滚),检索参数影响召回,放到最后并用评测兜底。

排除对象判定证据结论
指标口径混算网关端到端 3.9 s、引擎 TTFT 3.7 s,差 0.2 s,trace 可逐请求对齐排除:口径差只有 0.2 s
副本数不足副本 2→6 后 P99 TTFT 3.9→3.8 s,单副本 SM 利用率 38%,waiting 队列恒为 0排除:算力有余量,扩副本无效
decode 带宽受限TPOT P50 21–23 ms,与 8 路并发时的访存下界(28 GB 权重 + 12 GB KV)/ 2 TB/s ≈ 20 ms 同量级排除:输出段正常
prefill 输入过长prefill 3.7 s、排队 0.2 s;prompt P50 1.4K→7.8K,prefill 时长比 3.7/0.7≈5.3 与 token 比 7.8/1.4≈5.6 吻合确认:召回放大 prefill

对症做法:检索侧按 rerank 分数截断到 3.2K token,8K 以上请求单独分池,不再与交互请求共享同一份 prefill 预算。

yaml
retrieval: {top_k: 16, rerank_top_n: 6, max_context_tokens: 3200}
pools:
  interactive: {max_model_len: 4096, max_num_seqs: 32}
  long_ctx:    {max_model_len: 16384, max_num_seqs: 8}
slo:
  ttft_p99_ms: 1800      # prefill 1.5 s + 排队 0.2 s + 尾部
  tpot_p99_ms: 40        # 访存下界 17 ms 的 2.4 倍

prefill 在算力受限时与 prompt token 数近似线性,7.8K 降到 3.2K 是 0.41 倍,3.7 s 对应 1.5 s,实测 P99 回到 1.7 s;TPOT 只由每步读取的权重与 KV 决定,与 prompt 长度无关,砍 prompt 不会伤输出速度。

复盘要点:判据是“TTFT 高而 TPOT 平稳就去 prefill 与排队里找,TPOT 高而 TTFT 平稳就去 decode 带宽与 batch 里找”,两段必须分开打点。更早的信号是 prompt token P50:调 top-k 当天它就破了 4K,比 TTFT 告警早约 6 小时(经验值,需校准),把 prompt P50 超过 4K 设成告警可以在客诉之前拦住。

场景:80 GB 卡装得下 13B,并发只能到 9 路 ​

现象:文档批处理服务用 13B MHA 模型(40 层、40 个 KV head、head_dim 128、FP16),模板里照抄了 max_num_seqs=64 与 gpu_memory_utilization=0.90。压测到 11 路时 KV 使用率 92%,12 路起每 30 秒出现抢占,P99 TTFT 从 1.2 s 跳到 8.9 s(经验值,需校准),吞吐从 1,850 tok/s 掉到 620 tok/s;15 路时 Pod 被 OOMKilled,一小时内重启 4 次,40 万份合同解析只完成 8%。

排查路径:先按模型 config 算权重与固定开销(零成本),再用每 token KV 反推并发(纯算术),最后才动 max_num_seqs(需重启)。

排除对象判定证据结论
权重或固定开销超出预期权重 26 GB、CUDA context 与激活峰值 6 GB,与 nvidia-smi 实测逐项吻合排除:四类占用里的权重层没问题
每 token KV 算错按 2×40×40×128×2 得 819,200 B,与引擎启动日志里块大小乘 token 数反推的值一致排除:公式参数没取错
显存泄漏每次抢占后 free blocks 立即回升约 250 块,与一个 4K 请求的 256 块吻合,used 与活跃序列数线性对应排除:没有泄漏,是容量边界
并发配置64 × (4096 × 819,200 B) = 214 GB,超出 40 GB 可用 KV 五倍;11 路时 used blocks 已到 92%确认:max_num_seqs 超配

对症做法:把每 token KV 写进启动自检,用可用 KV 反推 max_num_seqs,留 20% 余量给激活峰值与块表。

python
per_token_kv = 2 * 40 * 40 * 128 * 2        # 819,200 B,只由层数、kv_heads、head_dim 决定
kv_per_req = 4096 * per_token_kv            # 3.36 GB
avail_kv = 80 * 0.90 - 26 - 6               # 40 GB,扣权重与固定开销
print(avail_kv / kv_per_req)                # 11.9,理论并发
# 留 20% 余量给激活峰值与块表波动:max_num_seqs=9
# max_model_len=4096  gpu_memory_utilization=0.90  swap_space=4

权重装得下只过了“装得下”的第一关;每 token KV 与 batch 无关,并发上限 = 可用 KV / (序列长度 × 每 token KV) = 40 / 3.36 ≈ 11,取 9 是留余量,max_num_seqs 填多大都不会让 KV 变多。

复盘要点:判据是“权重占显存不到一半,不等于并发够用”,容量必须按每 token KV 算一遍再定 max_num_seqs。启动日志里那条理论并发本该在上线前就打印出来:模板给 64、公式给 11,差 5.8 倍(经验值,需校准),这个差值比任何压测曲线都便宜。

场景:batch 从 32 提到 128,吞吐翻倍但 9% 会话超时 ​

现象:在线客服池(同一 14B GQA 模型)为压住晚高峰,把 max_num_seqs 从 32 提到 128、max_num_batched_tokens 从 8192 提到 32768。吞吐从 1,650 升到 3,400 tok/s,P50 TPOT 从 27 ms 升到 41 ms,P99 TPOT 从 52 ms 涨到 240 ms(经验值,需校准),P99 TTFT 从 0.95 s 到 1.4 s,5 分钟窗口内超时会话占比从 0.3% 升到 9%,抢占从 0 变成 9 次/分钟。

排查路径:先看分位而不是均值(零成本),再用每步访存量判断 batch 是否越界,最后回滚配置(重启 40 s)。

排除对象判定证据结论
网关或下游变慢网关自身耗时 P99 18 ms 稳定,超时会话里引擎段占 96%排除:慢在引擎
单请求 decode 退化batch 1 下重测 TPOT 21 ms,与上周同一镜像一致排除:模型与算子没变
batch 放大后的 KV 压力128 × 4096 token × 192 KiB = 103 GB,超出 38 GB 可用 KV,4K 请求开始被抢占确认:配置按均值定,边界被 P99 长度捅破
prefill 抢步TPOT 尖峰与抢占同时出现在 8K prompt 到达的步,32K 预算下一步可吃进 4 条 8K prompt确认:两个因素叠加放大尾巴

对症做法:按 P99 长度而不是平均长度定并发,把 batch 收回到 SLA 拐点之前,并给长 prompt 单独设预算。

yaml
scheduler:
  max_num_seqs: 40            # 38 GB / (4096 × 192 KiB) ≈ 47,留 15%
  max_num_batched_tokens: 8192
  enable_chunked_prefill: true
  long_prefill_token_threshold: 2048
slo:
  tpot_p99_ms: 90             # P50 的 4 倍以内,按下单峰值校准

每步访存量是权重 + 并发 × 平均 KV:batch 32→128 把 KV 需求从 25.8 GB 抬到 103 GB,超出 38 GB 可用池后实际并发被压在约 47 路;每步访存从 54 GB 抬到 66 GB,TPOT 下界从 27 ms 抬到 33 ms,实测 P50 27→41 ms,多出的部分是 prefill 抢步与抢占换出。按访存模型吞吐上界约 2.6 倍,实测 2.06 倍,差额被注意力计算与 prefill 抢步吃掉,而 TPOT 随并发上涨这条代价躲不掉。

复盘要点:判据是“先用 P99 TPOT 上限反推 max_num_seqs,再谈吞吐”,顺序反过来就会变成这次事故。更早的信号是抢占次数:它从 0 变成非 0 时 P99 TPOT 还在 90 ms 以内,把 preemption 连续 1 分钟大于 0 设成告警,比超时率告警早约 12 分钟(经验值,需校准)。

最小上手 ​

先测单请求基线,再测并发曲线,不要直接上生产流量:

bash
# 1. 单请求:记录 TTFT、TPOT、输出 token 数
# 2. 固定并发 1 / 4 / 16 / 64,各跑 5 分钟
# 3. 记录 P50 / P95 / P99、吞吐、显存峰值、排队长度

判据:并发上升时,吞吐先升后平,TTFT 开始明显上升的位置就是当前配置的拐点;拐点之前加副本有意义,拐点之后加副本只是增加排队。

生产动作 ​

  • 按段监控:分别记录排队时间、prefill 时间、decode 时间,不要只报端到端。
  • 按分位报警:P99 TTFT 和 P99 TPOT 各自设阈值,平均值只用于趋势。
  • 容量按 KV 算:用“剩余显存 / 每请求 KV”估算并发,再用实测修正。
  • 压测用真实分布:prompt 长度、输出长度、并发模式都要贴近生产,短请求压不出长上下文问题。
  • 保留基线:每次改模型、量化、引擎版本都重跑同一组负载,比较四个指标。
  • 区分在线与离线:两类流量最好分池,否则离线大 batch 会把在线尾延迟拖垮。

边界 ​

  • 性能模型是量级估算:算子融合、缓存、通信都会让实测偏离公式,公式用于定位方向,不用于承诺数字。
  • 单卡基线不能直接外推多卡:TP 有通信开销,PP 有流水线气泡,副本扩展还受网关和存储限制。
  • 吞吐不是唯一目标:交互式场景的体验由尾延迟决定,盲目追吞吐会伤害留存。
  • 长上下文有双重成本:显存占用和注意力计算都随长度增长,不能只看“支持 128K”的标称。
  • 不同引擎默认值差异大:参数名可能相同,语义和默认值不同,迁移时以目标版本文档为准。

讲清楚 ​

LLM 推理分两段:prefill 并行处理 prompt,吃算力,决定 TTFT;decode 逐 token 生成,吃显存带宽,决定 TPOT。显存由权重、KV、激活和固定开销四部分组成,真正限制并发的是 KV 容量,用“每 token KV × 序列长度 × 并发”估算;GQA 把每 token KV 降到约四分之一,长上下文才谈得上可行。推理栈分网关、服务、引擎、GPU 四层,那是部署视角;调优另有五层因果链——装得下、算得快、塞得满、起得来、看得见,两者不能混成一张图。吞吐和尾延迟经常反向,生产配置是按 SLA 选折中:在线看 P99,离线看总吞吐,先测单请求基线和并发拐点,再决定加副本还是调引擎。

适用:2026 年主流 LLM 推理引擎 | 初稿 2026-10

把 AI 工程讲到既懂也会用