背景:LLM 推理服务最怕”半死不活”的 GPU
在普通 Web 服务中,进程挂了重启通常就能恢复。但 LLM 推理服务完全不同——一次请求可能占用大量显存,推理进程还持有模型权重、KV Cache、CUDA Context、NCCL 通信状态和运行时调度队列。
GPU 出问题时,服务未必立刻崩溃,常见表现反而更隐蔽:
- 请求突然变慢、首 token 延迟拉高
- 流式输出中断
- 某张卡显存无法释放
- 容器还活着但 CUDA 调用失败
- 监控指标间歇性缺失
这类问题如果只靠”进程是否存活”来判断,极易误判——Pod 还在 Running,/healthz 还能返回 200,但新请求已无法稳定执行。更糟的是,某次 CUDA OOM 被简单重启后,节点反复加入服务、反复失败,进一步放大排队和重试风暴。
生产级 GPU 自愈的目标不是”所有错误都自动重启”,而是建立清晰的闭环:
发现异常 → 停止接收新请求 → 排空进行中请求 → 隔离可疑节点 → 重建推理进程 → 验证恢复结果 → 保留故障证据
这条链路缺一环,故障就会从单卡问题扩散为集群级抖动。
核心原理:把健康状态拆成四层
第一层:应用健康
应用健康回答的是”推理服务进程是否还能正常接收请求”。典型信号包括:
- HTTP health endpoint 是否可达
- 模型是否完成加载
- tokenizer、scheduler、worker 是否初始化完成
- 队列是否已进入过载保护
- 最近一段时间的请求错误率、超时率、取消率是否异常
这一层适合接入 Kubernetes readiness probe。readiness probe 的作用不是重启容器,而是决定 Pod 是否应继续接收 Service 流量。对 LLM Serving 而言,readiness 的语义应更严格:只要模型未完成加载、GPU worker 不可用、队列进入保护水位、节点正在排空,就应返回不 ready。
第二层:GPU 设备健康
GPU 设备健康回答的是”这张卡是否还能可信地执行计算”。不能只看利用率和显存占用,NVIDIA DCGM 的健康检查覆盖以下维度:
| 维度 | 检查内容 |
|---|---|
| PCIe | 带宽、重放计数、链路状态 |
| Memory | ECC 错误、pending page retirement、faulty memory |
| Thermal | 温度、降频事件 |
| Power | 功耗异常、供电不足 |
| NVLink | 链路错误、带宽下降 |
| Driver | 驱动版本、设备可见性 |
DCGM 文档将以下情况列为故障条件:GPU fallen off bus、严重 XID、需要 reset、需要 reboot、pending page retirement、faulty memory。应用层在消费这些信号时应分级处理:
- warning → 降权、观察、触发迁移
- error → 排空、隔离
- reset / reboot 类信号 → 不仅重启容器,还要把节点从调度池中摘出
第三层:Kubernetes 调度健康
调度健康回答的是”这个节点是否还应接收新的 GPU Pod”。NVIDIA Kubernetes device plugin 可以暴露 GPU 数量、跟踪健康状态,但官方文档明确指出:device plugin 本身不应被理解为完整的 GPU 故障自愈系统。
这意味着生产环境通常需要额外的 GPU node controller,它读取 DCGM、device plugin、Prometheus、Kubernetes event 和推理服务指标,在必要时对节点执行:
cordon—— 标记节点不可调度taint—— 增加驱逐性污点- 摘除 endpoints
- 触发 Pod 重建或通知人工介入
第四层:请求生命周期健康
请求生命周期健康回答的是”故障发生时,正在执行的请求如何收尾”。LLM 请求与普通 REST 调用不同,可能正在流式输出、已消耗大量 token、或挂在工具调用与长上下文 decode 阶段。
自愈逻辑必须区分三类请求:
- 已完成的请求:正常返回,记录节点处于可疑状态即可
- 正在输出的请求:优先完成或明确中断,不让连接长期悬挂
- 尚未开始执行的排队请求:尽快迁移或返回可重试错误
因此自愈流程必须包含 drain mode。实例进入 drain mode 后不再接收新请求,只处理已有请求,超过最大排空时间后强制关闭。
工程落地:一条可执行的 GPU 自愈链路
1. 定义健康状态机
不要只用 healthy=true/false。推荐将实例状态拆为以下几个阶段:
STARTING → WARMING → READY → DEGRADED → DRAINING → QUARANTINED → RECOVERING → READY
↘ FAILED
| 状态 | 含义 | 动作 |
|---|---|---|
STARTING | 容器启动中,模型未加载 | startup probe 等待 |
WARMING | 模型已加载,正在 warmup | 探针推理,不接生产流量 |
READY | 可接收生产流量 | readiness=true,正常调度 |
DEGRADED | 出现 warning 信号 | 降权、限制新长请求、告警观察 |
DRAINING | 停止接收新请求,排空中 | readiness=false,负载均衡摘流量 |
QUARANTINED | 节点或实例被隔离 | cordon/taint,不参与调度 |
RECOVERING | 重启 worker 或重建 Pod 后验证 | 探针推理 + 灰度恢复 |
FAILED | 自动恢复失败 | 人工介入,保留故障证据 |
该状态机应同时驱动 readiness probe、负载均衡权重、告警级别和运维动作。
2. 把 readiness 与 liveness 分开
很多团队把 readiness 和 liveness 都指向同一个 /healthz,这是常见错误。LLM 推理服务应至少提供三个端点:
| 端点 | 语义 | 失败效果 |
|---|---|---|
/livez | 进程是否还活着 | 重启容器 |
/readyz | 是否可以接收新请求 | 摘流量 |
/gpuz | GPU worker、CUDA context、DCGM 信号、显存状态是否可信 | 触发 DRAINING |
Kubernetes 配置示例:
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 5
failureThreshold: 2
livenessProbe:
httpGet:
path: /livez
port: 8080
initialDelaySeconds: 120
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 10
failureThreshold: 60
关键点:模型加载和 warmup 可能很慢,需要 startupProbe 避免 kubelet 在模型未加载完成时误杀容器。readiness 用于流量摘除,不能等到进程崩溃才触发。
3. 用 DCGM 信号触发节点隔离
DCGM 的健康结果分三类处理:
groups:
- name: llm-gpu-health
rules:
- alert: LlmGpuMetricsMissing
expr: absent_over_time(DCGM_FI_DEV_GPU_UTIL[3m])
for: 2m
labels:
severity: warning
annotations:
summary: "GPU metrics disappeared for a serving node"
- alert: LlmGpuXidError
expr: increase(DCGM_FI_DEV_XID_ERRORS[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "GPU reported XID errors; drain and quarantine the node"
重点不是照抄规则,而是把握两个思路:一是监控明确硬件错误(XID、ECC、NVLink),二是监控结构性异常——例如指标突然消失、scrape 延迟、设备数量变化。
4. 让请求排空成为一等能力
GPU 故障自愈最容易做错的就是直接重启,这会带来三类副作用:
- 已经开始输出的流式请求被突然断开
- 客户端重试将流量打到其他实例,造成二次峰值
- 故障现场被清理,后续无法定位根因
更稳妥的排空流程:
Controller 标记实例 DRAINING
→ readiness probe 返回失败,负载均衡摘除新流量
→ Gateway 停止分配新请求到该实例
→ 实例继续处理已进入 decode 的请求
→ 排队但未执行的请求迁移或返回可重试错误
→ 超过 max_drain_seconds 后强制终止
→ 保存最后错误、GPU 指标窗口、请求样本和容器事件
→ 重建 worker 或隔离节点
Pod 配置方向:
terminationGracePeriodSeconds: 180
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "curl -X POST localhost:8080/admin/drain && sleep 30"
terminationGracePeriodSeconds 必须大于最大可接受排空时间,否则 kubelet 会在请求未收尾时强杀进程。生产环境还需让 Gateway 和 Controller 感知实例进入 drain mode,而非仅在容器内 sleep。
5. 区分 CUDA OOM 的根因
CUDA OOM 不等于 GPU 坏了,至少要分四种情况:
| OOM 类型 | 典型触发 | 推荐动作 |
|---|---|---|
| 单请求超限 | prompt 或 max_tokens 过大 | 拒绝请求或降低 max_tokens |
| 批量过载 | scheduler 同时放入太多 token | 降低并发、降低 batch token 上限 |
| 显存碎片化 | 长时间运行后可用显存不连续 | drain 后重建 worker |
| 驱动/设备异常 | OOM 伴随 XID、CUDA context 错误 | 隔离节点并做 GPU 健康检查 |
如果把所有 OOM 都交给 liveness probe 重启,会掩盖真正问题。正确做法是记录每次 OOM 的上下文——输入 token、输出 token、batch size、KV cache 占用、GPU ID、模型版本、adapter 版本和最近 DCGM 信号——再做分类处理。
6. 设计节点隔离和恢复策略
节点隔离不应只依赖删除 Pod,建议分三层:
| 层级 | 范围 | 适用场景 |
|---|---|---|
| 实例隔离 | 单个推理进程进入 DRAINING | 单请求异常、队列过载 |
| Pod 隔离 | Pod 重建,节点仍可调度其他 GPU Pod | 显存碎片化、进程泄漏、worker 死锁 |
| 节点隔离 | cordon + taint,禁止新 GPU 工作负载 | fallen off bus、严重 XID、NVLink/驱动级故障 |
恢复时不要立即全量放回。应采用灰度恢复:先做探针推理 → 接收少量短请求 → 观察错误率、TTFT、TPOT、显存释放和 DCGM 信号 → 满足稳定窗口后恢复完整权重。
适用场景
这套方法适合以下场景:
- 自建 vLLM、TensorRT-LLM、TGI、SGLang 或 Ray Serve LLM 推理集群
- Kubernetes 上运行多副本 GPU 推理服务
- 存在长输出、流式输出、多租户请求或高并发队列
- GPU 节点数量较少,单节点故障会明显影响可用性
- 需要对推理中断、超时、重试风暴和节点雪崩做工程治理
如果是开发测试环境或单机离线推理,可简化为:健康检查、OOM 日志、手动重启和基础告警。但只要服务面向用户,就应至少实现 readiness、drain 和故障证据保留。
常见误区
误区一:只看 GPU 利用率
GPU 利用率正常不代表节点健康。驱动错误、设备丢失、指标缺口、PCIe/NVLink 错误、显存不可释放,都可能先于或独立于利用率变化出现。生产系统需要同时监控数值指标、结构性指标和请求结果。
误区二:所有错误都自动重启
自动重启适合进程级故障,不适合所有 GPU 故障。需要 reset 或 reboot 的 GPU 问题,容器重启通常不够;如果继续调度新 Pod 到该节点,只会反复失败。
误区三:排空只靠 Kubernetes 删除 Pod
Kubernetes 的 graceful termination 提供基础终止流程,但并不知道 LLM 请求是否仍在 decode、是否正在流式输出、是否可以迁移。排空必须由 Gateway、Serving Runtime 和 Controller 协同完成。
误区四:故障恢复后立即满流量
GPU 故障恢复后应先做 canary。尤其是驱动重置、节点重启、模型重新加载后,应先验证探针推理、短请求、长请求和并发请求,而不是直接恢复所有流量。
上线检查清单
健康探针
/livez、/readyz、/gpuz是否语义分离startupProbe是否覆盖模型加载和 warmup 时间readinessProbe是否会在 drain、过载、GPU worker 异常时失败livenessProbe是否避免误杀长时间 warmup 的实例
GPU 监控
- 是否接入 DCGM 或等价 GPU 健康指标
- 是否监控 XID、fallen off bus、reset required、pending page retirement
- 是否监控指标缺失、scrape 延迟、GPU 设备数量变化
- 是否把 warning、error、fatal 映射为不同动作
请求排空
- Gateway 是否支持实例级摘流量
- 排队请求是否可迁移或快速失败
- 流式请求是否能明确结束或返回可解释错误
terminationGracePeriodSeconds是否大于最大排空窗口
节点隔离
- 是否支持 cordon/taint 可疑 GPU 节点
- 是否区分 Pod 重建、GPU reset、节点 reboot、人工维修
- 是否有恢复前探针推理和恢复后稳定窗口
- 是否保留最后一段指标、日志、Kubernetes event 和请求样本