27届大模型面试准备(七十八):大模型推理线上问题定位与根因分析工程------TTFT/TBT 抖动、OOM、吞吐跌零排查手册
引言
本文是系列第 78 篇。前两篇讲了高可用、容灾与多模态训练,这一篇聚焦一个所有上线模型都逃不掉的现实问题:线上推理出问题了,怎么在 10 分钟内定位根因。面试官非常喜欢拷问候选人的"故障排查肌肉记忆"------因为能背架构的人很多,能在凌晨三点对着监控曲线说出"这是 PD 分离后 KV 调度不均导致 TBT 长尾"的人很少。
一篇合格的推理工程简历,必须能讲清楚四类高频故障:TTFT(首 token 延迟)抖动、TBT(token 间延迟)长尾、OOM 显存溢出、吞吐跌零(throughput 突然掉到接近 0)。本篇给出可操作的排查链路与代码级诊断手段。
线上推理问题定位总览
===========================================
监控告警触发
│
┌───────────┼───────────────┐
▼ ▼ ▼
TTFT 抖动 TBT 长尾 吞吐跌零/OOM
│ │ │
▼ ▼ ▼
Prefill 侧 Decode 侧 资源/数据/调度
排队/算子 KV/调度/批 瓶颈定位
│ │ │
└───────────┼───────────────┘
▼
根因定位 → 止血 → 复盘
一、关键指标定义与采集
先把指标说清楚,否则排查无从谈起。
| 指标 | 含义 | 健康量级 | 用户感知 |
|---|---|---|---|
| TTFT | 首 token 延迟(请求到第一个 token) | < 500ms(小模型) | "响应快不快" |
| TBT | token 间延迟(解码步间隔) | < 50ms(实时) | "打字流不流畅" |
| TPOT | 每 output token 时间 | 同 TBT | 同 TBT |
| E2E 延迟 | 整句完成时间 | TTFT + n*TBT | 总耗时 |
| QPS/吞吐 | 每秒请求/生成 token 数 | 视资源 | 成本与容量 |
| 批大小 | 同批并发序列数 | 动态 | 显存占用 |
采集方式:在推理框架里对每个请求打点(request_id、enqueue_time、first_token_time、last_token_time),用 Prometheus 暴露 histogram,Grafana 看分位数(p50/p90/p99)。一定要看 p99 而不是均值------均值掩盖长尾。
import time, prometheus_client as pc
HIST_TTFT = pc.Histogram("llm_ttft_ms", "Time to first token", buckets=[100,300,500,1000,2000])
HIST_TBT = pc.Histogram("llm_tbt_ms", "Inter-token latency", buckets=[10,30,50,100,200])
class ReqTracer:
def __init__(self, req_id):
self.req_id = req_id
self.t_enq = time.time()
self.t_first = None
def on_first(self):
self.t_first = time.time()
HIST_TTFT.observe((self.t_first - self.t_enq) * 1000)
def on_step(self, t_prev, t_now):
HIST_TBT.observe((t_now - t_prev) * 1000)
二、TTFT 抖动排查
TTFT 高通常卡在 Prefill 阶段。根因分布:
TTFT 高
├─ 请求排队(并发>容量)
│ 看调度队列深度、排队时长直方图
├─ Prefill 算子慢(长 prompt、未切分)
│ 看 prompt token 数分布、attention 是否用 flash
├─ 冷启动 / 模型未预热
│ 看实例刚起/刚扩的时段
└─ 批调度策略激进导致大请求阻塞小请求
看 chunked prefill 是否开启
最容易被忽略的一点:长 prompt 的 Prefill 是计算密集且不可抢占的,一个 8k token 请求 Prefill 可能要几百毫秒,会直接拖高同窗口所有请求的 TTFT。解法:开启 chunked prefill(把 Prefill 切成小块与 Decode 交织),或用 PD 分离把 Prefill 和 Decode 放到不同实例池。
# vLLM 开启 chunked prefill(示意)
# 启动参数
--enable-chunked-prefill \
--max-num-batched-tokens 8192 \
--scheduler-policy fcfs
三、TBT 长尾排查
TBT 高卡在 Decode 阶段 。Decode 每步只算一个新 token,计算量小但受 KV Cache 带宽和批大小支配。根因:
| 根因 | 信号 | 解法 |
|---|---|---|
| 批内混入超长序列,KV 挤占带宽 | 同批 max_len 远高于均值 | 按长度分组调度 |
| KV Cache 碎片化 | 显存够但申请失败 | 换 paged/prefix cache |
| 调度不均(PD 分离后 KV 传输慢) | Decode 实例等 KV | 优化 KV 传输/本地化 |
| 某卡降频/掉速 | 单卡 TBT 明显偏高 | 查 NVML 温度/功耗 |
| 采样 top-p 高导致核函数慢 | 高 top-p 时变慢 | 降 top-p/用投机解码 |
# 用 pynvml 巡检单卡降频(TBT 长尾时逐个卡排查)
import pynvml
pynvml.nvmlInit()
for i in range(pynvml.nvmlDeviceGetCount()):
h = pynvml.nvmlDeviceGetHandleByIndex(i)
util = pynvml.nvmlDeviceGetUtilizationRates(h)
clk = pynvml.nvmlDeviceGetClockInfo(h, pynvml.NVML_CLOCK_SM)
pwr = pynvml.nvmlDeviceGetPowerUsage(h) / 1000.0
print(f"gpu{i} util={util.gpu}% sm_clk={clk}MHz pwr={pwr}W")
# 某卡 sm_clk 远低于其他卡 => 降频/掉速,是 TBT 长尾元凶之一
四、OOM 显存溢出排查
OOM 是上线最常见的"硬故障"。多模态/长上下文尤其容易炸。定位清单:
OOM 排查树
├─ 静态占用的 KV Cache 预留过大?
│ 算:kv_bytes = 2 * n_layers * n_kv_heads * head_dim * seq * batch * dtype
│ 下调 max_num_seqs / max_model_len
├─ 单请求超长(恶意/异常 prompt)?
│ 设 max_input_len 上限 + 拒绝/截断
├─ 激活峰值(Prefill 大 batch)?
│ 降 max_num_batched_tokens、开梯度无关的重算
├─ 碎片化导致"有总显存但无连续块"?
│ 用 paged KV(vLLM/TSR 默认)、prefix cache 复用
└─ 多模型共卡?
隔离部署或限制并发
def est_kv_bytes(n_layers, n_kv_heads, head_dim, seq, batch, dtype_bytes=2):
per_tok = 2 * n_layers * n_kv_heads * head_dim * dtype_bytes # K and V
return per_tok * seq * batch
# 例:7B 模型 32层 32头 dim128, seq=4096, batch=64
print(est_kv_bytes(32, 32, 128, 4096, 64) / 1e9, "GB")
# 据此倒推 max_num_seqs,避免预留超显存
五、吞吐跌零排查
吞吐突然掉到接近 0,通常是"软死锁"而非硬件坏:
| 现象 | 根因 | 快速止血 |
|---|---|---|
| 请求全部卡住 | 调度死锁/某请求异常占批 | 超时踢出 + 重启 worker |
| QPS 为 0 但进程在 | 上游断流/健康检查误杀 | 查网关与探活配置 |
| GPU 利用率 0 但显存满 | KV 泄漏(请求未释放) | 修请求生命周期,加 watchdog |
| 吞吐阶梯下跌 | 扩缩容抖动/权重加载阻塞 | 预热 + 灰度发布 |
关键工程实践:给每个请求设硬超时与最大生成长度,并在请求结束时强制释放 KV 块,用 watchdog 监控"显存占用 vs 活跃请求数"的偏离度,偏离即告警。
import asyncio
async def guarded_generate(req, max_tokens=2048, timeout=60):
try:
return await asyncio.wait_for(
engine.generate(req, max_tokens=max_tokens), timeout=timeout)
except asyncio.TimeoutError:
engine.abort(req.req_id) # 必须显式 abort 释放 KV
raise TimeoutError(f"req {req.req_id} aborted after {timeout}s")
六、定位方法论总结
把上面串成一张"止血---定位---复盘"闭环:
1. 看告警:哪个指标 p99 异常(TTFT/TBT/吞吐/OOM)
2. 切维度:按实例/卡/路由/用户维度下钻,找异常子集
3. 对变更:是否刚发布/扩容/改配置(时间相关性最强)
4. 抓现场:保留一个异常请求 trace + 对应卡监控快照
5. 止血:降级(限流/关特性/回滚)→ 根因 → 加防护(超时/配额)
面试速答
问:TTFT 高一般查什么?
答:先看是不是 Prefill 阶段排队(并发超容量)或长 prompt 未切分;再确认 chunked prefill / PD 分离是否开启;最后看实例冷启动。核心是 Prefill 计算密集且不可抢占,长请求会阻塞同窗口。
问:TBT 长尾但 GPU 利用率不低,为什么?
答:多半是批内混入超长序列挤占 KV 带宽,或某卡降频/掉速。用 pynvml 逐个卡看 SM 时钟和功耗,往往能抓到单卡异常。
问:OOM 但显存总量够,怎么回事?
答:KV Cache 碎片化------没有连续块可分配。用 paged KV(vLLM/TensorRT-LLM 默认)和 prefix cache 复用解决,并下调 max_num_seqs。
问:吞吐跌零怎么快速止血?
答:先确认是否请求级死锁/KV 泄漏,给请求加硬超时与 abort 释放;显存满但利用率 0 基本是 KV 泄漏,重启 worker 并修生命周期。
高频追问清单
- chunked prefill 和普通 prefill 在调度上有什么区别?对 TTFT/TBT 各有什么影响?
- PD 分离后 KV Cache 怎么跨节点传输?带宽瓶颈怎么估?
- paged KV 的碎片率怎么度量?什么负载下仍会碎片?
- 连续批处理(continuous batching)和静态批处理对长尾延迟影响?
- 投机解码引入后 TBT 抖动变大,可能是什么原因?
- 多租户下怎么给不同优先级请求做资源隔离与配额?
- 显存监控里"保留但无活跃请求"的 KV 怎么排查泄漏?
- 怎样设计请求级 trace,使一次慢请求可还原完整调度路径?
- 压测时怎么构造能暴露长尾的流量模型(泊松/突发)?
- 大促/峰值前怎么做容量评估与限流阈值设定?