跳转至

一次 LLM 推理服务 OOM 的完整复盘

这一篇复盘「vLLM 推理服务反复重启,显存被撑爆触发 OOM」的典型故障。你的服务可能是别的推理引擎、可能是 CPU 部署,但诊断思路通用:先确认是显存 OOM 还是健康检查误杀,再查并发配置是否超配、前缀缓存是否在涨,最后别把「调大显存」当唯一解药。

故障概述

上线后几小时,vLLM 推理服务开始反复重启:进程起来、跑一阵、显存冲高、OOM 被杀,然后 restart 循环。用户请求要么超时、要么中途断流。nvidia-smi 一看——显存使用率在重启前冲到 98%+,vllm 日志明确报 CUDA out of memory。这是典型的显存超配 + 缓存增长撑爆。

影响

  • 推理请求在 OOM 窗口内全部失败或中断:超时、流式中断、返回 5xx。
  • 服务处于「半死」状态:进程活着但每次请求都可能触发 OOM,吞吐极不稳定。
  • 持续约 2 小时,从首次 OOM 到调低并发并留足显存余量。
  • 因为 GPU 是共享资源,其他并发任务(如批量推理)也受显存挤压,连带变慢。
  • 连带损失:OOM 窗口内新建立的连接全部中断,客户端重试又叠加负载,形成「OOM → 重试 → 更高并发 → 再 OOM」的恶性循环。

时间线

时间 事件
T+0 首个 OOM,服务重启
T+20 重启后短时间正常,随后再次 OOM
T+45 进入 restart 循环,用户请求开始大量失败
T+70 nvidia-smi 定位显存 98%,确认显存 OOM
T+100 调低 max_num_seqs、降 gpu_memory_utilization,重启
T+130 显存稳定,吞吐恢复,加显存/OOM 监控
T+150 复盘收尾,确认根因是并发超配 + 前缀缓存增长

根因

真正的触发点是并发超配 + 前缀缓存(KV cache)增长叠加:gpu_memory_utilization 设得偏高(0.9),max_num_seqs 也偏大,当长上下文请求增多、前缀缓存随请求模式爬升时,显存占用超过预留上限,触发 CUDA OOM。叠加两个放大器:

  1. 无显存监控:只监控「服务存活」,没监控「显存使用率」;显存爬到 90%+ 时没人知道,直到 OOM 才暴露。
  2. 无 OOM 自动恢复策略:服务被 OOM 杀掉后,orchestrator 立即重启,但启动探活(/health)在模型加载完成前就判死,导致重启风暴——还没加载完又被判失败。

关键判据(也是复盘里的分水岭):nvidia-smi 显示重启前显存 98%,vllm 日志明确 CUDA out of memory——这是显存撑爆,不是探活误杀。

诊断里最容易踩的几个坑

这次拖慢恢复的不是「查不到」,而是把「反复重启」一律当探活问题。几个真实教训:

  1. 先看日志,再改探活。服务反复重启的第一反应容易是「健康检查配错了」。先 nvidia-smi 看显存、journalctl 看 vllm 日志——有 CUDA OOM 就别去调探活,那是浪费时间。
  2. 显存要间隔采样。单次 nvidia-smi 只能看到当下,要用 watch -n 2 nvidia-smi 或间隔采样看趋势——是持续爬升(泄漏/缓存增长)还是到顶即杀(超配)。
  3. 重启风暴是「探活 + 启动时间」没对齐。vllm 加载模型要几十秒到几分钟,探活超时却设得很短,导致「还没起来就判死」。给探活加 startup grace period,和实际启动时间对齐。
  4. 别只调 gpu_memory_utilization。调小显存上限只是留余量,如果并发还大,KV cache 照样涨。要同时调 max_num_seqs,限制并发才治本。
  5. OOM 不等于显存泄漏。OOM 可能是「增长型」(并发超配/前缀缓存)也可能是「泄漏型」(长连接未释放)。看趋势区分:单调爬升不回落是泄漏,冲高即 OOM 是超配。

修复

一步步定位,从看显存开始:

# 1) 看显存使用率(实时 + 间隔采样看趋势)
nvidia-smi
watch -n 2 nvidia-smi          # 持续观察是否单调爬升/到顶

# 2) 看 vllm 日志,确认是不是 CUDA OOM
journalctl -u vllm --since "2h ago" | grep -iE "out of memory|no available memory|CUDA"
# 有 OOM → 显存撑爆;没有 → 查探活/看门狗

# 3) 看当前并发/显存配置是否超配
# 启动参数里查 gpu_memory_utilization 与 max_num_seqs
ps aux | grep vllm | grep -oE "gpu-memory-utilization[= ][0-9.]+|max-num-seqs[= ][0-9]+"

临时止血:调低并发 + 降显存上限 + 重启,先让服务稳定:

# 降并发(如 256→128),降显存余量预留(0.9→0.85),重启
# 示例启动参数调整:
# --gpu-memory-utilization 0.85 --max-num-seqs 128
systemctl restart vllm
# 观察显存是否稳定在目标区间
watch -n 5 nvidia-smi

彻底解决:对齐探活与启动时间 + 限流 + 自动恢复。给探活加启动宽限,OOM 后自动重启而不是反复失败:

# 探活配置示例(k8s readiness/liveness,对齐启动时间)
# readinessProbe: /health, initialDelaySeconds=120, periodSeconds=10
# livenessProbe:  /health, initialDelaySeconds=120, periodSeconds=30
# restartPolicy: Always
# 关键:initialDelaySeconds 必须 ≥ 模型实际加载时间,否则启动即判死

防再发

这次事故暴露的三件事,逐条上护栏:

  1. 显存使用率监控(第一护栏):不只监控「服务存活」,要监控显存使用率。memory.used / memory.total 接近上限(如 90%)告警,给 OOM 留出干预窗口。
  2. OOM 计数监控:监控 GPU/进程的 OOM 事件计数,作为「已经发生」的兜底信号;配合显存趋势,能区分「增长型」与「泄漏型」。
  3. 并发限流与配额:max_num_seqs 与 gpu_memory_utilization 一起调,给 KV cache 留 15%-20% 余量;请求队列深度(pending)超限即限流/排队,防突发并发把显存顶爆。
  4. 优雅重启与探活对齐:探活的 initialDelaySeconds ≥ 模型加载时间;OOM 后自动重启容器/进程,配启动宽限防重启风暴。
  5. 故障演练:定期模拟「高并发 + 长上下文」,验证显存监控能否在 OOM 前告警、限流是否生效。

复盘结论

推理服务反复重启先看显存——nvidia-smi 确认是不是显存 OOM,别一上来就调探活。诊断顺序永远是:看显存趋势 → 看 vllm 日志确认 OOM → 查并发/显存配置 → 对齐探活与启动时间。最容易被忽略的是 「重启风暴」:探活超时比模型加载还短,服务还没起来就被判死。

复盘到最后,最值钱的一句话是:监控的对象错了,反应就永远慢半拍。 我们以前「监控服务存活」,它只在 OOM 已经发生时才有信号;这次之后我们「监控显存使用率 + OOM 计数」,它能在显存爬到 90% 时就把问题暴露出来。把监控前移一层,往往比多写几段告警规则更能缩短 MTTR——这也是这次事故给我最大的收获:不是技术多难,而是把「该看的地方」看对。


关联阅读:[vLLM 生产调优../../ai/vllm-production-tuning.md) · [vLLM 服务部署../../ai/vllm-serving.md) · [LLM 监控../../ai/llm-monitoring.md)