Kubernetes GPU 100% 利用率但显存 0、无进程:NVIDIA Xid 13/31/43 排查与恢复
这篇文章记录一次 Kubernetes GPU 节点的异常:容器内 nvidia-smi 显示一张卡利用率 100%,但显存为 0、进程表为空。继续回查宿主机后,发现这不是当前任务在正常计算,而是此前 Python/vLLM CUDA 任务触发 Xid 13、31、43,驱动没有完整清理 GPU context,导致 GPU 进入“无进程但保持高负载”的异常状态。
文章中的节点名、Pod 名、IP、GPU UUID 和业务名称均已替换为占位符。命令需要在你自己的 Kubernetes 工具机和目标节点上执行。


环境与问题表现
本次环境的关键版本如下:
| 项目 | 值 |
|---|---|
| GPU | NVIDIA GeForce RTX 5090 D |
| 驱动 | 590.44.01 |
| CUDA | 13.1 |
| 节点系统 | Ubuntu 22.04 系列 |
| 工作负载 | Kubernetes 中的 Python/vLLM 服务 |
容器内第一次看到的输出类似这样:
GPU 0: NVIDIA GeForce RTX 5090 D
GPU 1: NVIDIA GeForce RTX 5090 D
GPU 0: 100% 0 MiB / 32607 MiB
GPU 1: 0% 0 MiB / 32607 MiB
Processes:
No running processes found
这个组合不能直接解释成“容器里有进程把 GPU 跑满了”。至少要同时确认四件事:容器看到的 GPU UUID、宿主机对应的 GPU、compute-apps/pmon 是否能列出进程,以及宿主机内核是否记录了 Xid。
一、先确认容器内的现象是否稳定
非交互脚本不要依赖 ktt、kap 这类 shell 别名,直接使用明确的 kubeconfig:
K="kubectl --kubeconfig=/path/to/cluster-config"
NS=test
POD=<pod-name>
$K exec -n "$NS" "$POD" -- \
nvidia-smi --query-gpu=timestamp,index,name,uuid,utilization.gpu,memory.used,power.draw \
--format=csv -l 1
$K exec -n "$NS" "$POD" -- nvidia-smi pmon -c 5
如果多次采样都得到 100% + 0 MiB,并且 pmon 没有 PID,继续到宿主机核对。单次 nvidia-smi 读数可能只是瞬时采样,不能单独作为根因。
二、把容器 GPU 映射到宿主机 PCI 设备
先在容器内记录 GPU UUID:
$K exec -n "$NS" "$POD" -- \
nvidia-smi --query-gpu=index,uuid,name --format=csv,noheader
然后在节点上查询完整 GPU 列表:
ssh root@<node> \
'nvidia-smi --query-gpu=index,uuid,pci.bus_id,name,utilization.gpu,memory.used \
--format=csv,noheader'
不要只按容器内的 GPU 0、GPU 1 判断。容器经过 NVIDIA device plugin 隔离后,索引可能已经重新编号;UUID 和 PCI Bus ID 才是可靠的关联键。
三、区分“真正的计算进程”和“仅持有设备句柄的进程”
在宿主机执行:
nvidia-smi --query-compute-apps=gpu_uuid,pid,process_name,used_memory \
--format=csv,noheader
nvidia-smi pmon -c 5
fuser -v /dev/nvidia* 2>&1 || true
三类输出含义不同:
| 检查 | 能证明什么 |
|---|---|
compute-apps |
NVIDIA 当前登记的 CUDA 计算进程 |
pmon |
采样窗口中能关联到 GPU engine 的进程 |
fuser /dev/nvidia* |
进程打开过 NVIDIA 设备文件 |
fuser 中出现 Python、DCGM、gpud 或 device-plugin,并不等于这些进程正在提交 GPU kernel。只有 compute-apps 和 pmon 能把进程和计算活动直接关联起来。
本次异常中,GPU 1 的 compute-apps 为空,pmon 也为空;但利用率保持 100%、功耗约 110W、SM 和显存时钟保持高档。这说明当前没有可见的正常用户态计算进程。
四、用内核日志确认 Xid 事件
在宿主机查询最近的 NVIDIA 内核日志:
journalctl -k --since "48 hours ago" --no-pager \
| grep -Ei 'NVRM|Xid|nvidia|GPU' \
| tail -200
节点如果重启过,旧日志可能在轮转文件中:
zgrep -iE 'NVRM|Xid' /var/log/kern.log* 2>/dev/null | tail -200
本次节点的关键记录是:
Xid 13: Graphics SM Warp Exception ... Out Of Range Address
Xid 31: MMU Fault ... ACCESS_TYPE_VIRT_WRITE
Xid 43: channel exception
GPU1 refcntRequestReference_IMPL: Failed to enter state 1
这几类日志把“100% 利用率”与此前的 CUDA fault 连接起来了:
Python/vLLM CUDA kernel 非法地址访问
-> Xid 13 / Xid 31
-> channel 异常 Xid 43
-> 驱动引用计数/恢复失败
-> 原进程消失,但 GPU engine/context 没有被清理
-> nvidia-smi 继续显示 SM 100%,却找不到 compute process
这里的“驱动没有清理干净”是基于 Xid 43、refcntRequestReference_IMPL 和持续采样结果的诊断结论;它不是根据单条利用率指标猜出来的。
五、gpud 和 DCGM 应该怎样看
gpud 可以帮助确认当前硬件健康状态,但不能把所有应用类 Xid 都直接判成硬件故障。建议同时读取 gpud 的状态接口和日志:
kubectl -n gpud get pod -o wide
kubectl -n gpud logs <gpud-pod> --since=2h --all-containers \
| grep -Ei 'xid|ecc|slowdown|unhealthy|error|fail' | tail -100
kubectl -n gpud exec <gpud-pod> -c gpud -- \
sh -c 'curl -sk --max-time 5 https://127.0.0.1:15132/v1/states'
本次 gpud 仍报告 ECC、SXID 和硬件降速 Healthy。这并不推翻 Xid 证据:应用类 CUDA fault 可能不会立即表现为 ECC 或硬件降速故障,但仍会把某张卡留在不可用状态。
六、官方资料和相似案例
NVIDIA 官方 Xid 文档:
公开的 Kubernetes 案例:
该 issue 记录了 Xid 43 之后多个 Triton/PyTorch 推理 Pod 持续失败、同一 GPU 后续任务继续失败,最后通过 drain 并删除节点恢复。它还讨论了为什么 Xid 13、31、43 不能简单全部纳入默认自动修复:有些是单个输入或应用 kernel 触发的应用类错误,但如果 GPU 已经 wedged,就需要节点级处置。
为什么不能把 Xid 13、31、43 全部设成“看到就自动修复”
自动修复的目标是恢复节点健康,但 GPU Xid 的语义并不等于“整张卡已经永久损坏”。把这几个码无条件绑定到 drain、重启或替换节点,会有三类风险:
- 单个任务可能是错误源,而不是整张卡。 Xid 13、31 常见于 kernel 越界访问、非法地址或页表 fault。一个坏输入、一个自定义 CUDA 扩展或一次版本兼容问题,都可能只打坏当前进程。此时重启整台节点会影响同节点上其他正常 GPU 工作负载。
- 同一错误码的恢复结果可能不同。 有的任务退出后 GPU 能继续服务,有的任务会留下无法清理的 channel/context。只看 Xid 数字,无法区分“进程级失败”和“GPU 已经 wedged”。
- 共享节点会放大误伤。 一台节点可能承载多张 GPU 和多个租户。一次应用级 Xid 如果直接触发节点替换,会让原本健康的 Pod 一起迁移,造成不必要的容量抖动。
因此更稳妥的默认策略是:
单次 Xid 13/31
-> 记录事件,保留 PID、GPU UUID、Pod UID 和 traceback
-> 优先让业务进程自行退出或重建
短窗口内重复 Xid,或多个不同进程命中同一 GPU
-> 提升为 GPU 级异常
-> 暂停向该 GPU 调度新任务
Xid 43 + compute-apps 为空 + SM 持续 100% + 后续任务无法运行
-> 视为 GPU context/驱动状态卡住
-> 进入节点维护、GPU reset 或节点替换流程
生产告警可以把“事件告警”和“自动处置条件”分开:所有 Xid 都记录,但只有满足重复次数、时间窗口、多个 PID、当前 GPU 健康状态和业务失败等组合条件时,才触发自动隔离或节点修复。这样既不会漏掉本次这种 GPU 已经卡住的情况,也不会因为一次用户代码错误重启整台共享节点。
七、正确的恢复顺序
不要直接对持有 /dev/nvidia1 的 Python 进程执行 kill -9。正确的处理顺序是:
1. 先确认影响范围
kubectl get pods -A --field-selector=spec.nodeName=<node> -o wide
kubectl describe node <node> | grep -A 12 -B 3 'nvidia.com/gpu\|Allocated resources'
2. 停止继续调度并迁移 GPU 业务
在确认节点上的 GPU Pod 清单、PDB、替代节点容量和业务影响后,再执行集群标准的 cordon/drain 流程。生产环境必须先做变更确认和回滚准备。
3. 尝试单卡 reset
确认该 GPU 已没有业务进程且节点维护窗口成立后,才尝试:
nvidia-smi --gpu-reset -i <gpu-index>
不同 GPU 型号、驱动和进程占用条件下,reset 可能被拒绝。不要绕过 busy 检查强行卸载 NVIDIA 内核模块。
4. reset 失败时重启或替换节点
如果 GPU reset 失败、nvidia-smi 仍然卡住,或者 Xid 持续出现,节点重启/替换通常比反复杀进程更可靠。重启后重新检查:
nvidia-smi
nvidia-smi pmon -c 5
journalctl -k --since boot --no-pager | grep -Ei 'NVRM|Xid' || true
5. 恢复业务前做小流量验证
确认 GPU 利用率、显存、功耗和进程列表恢复正常,再逐步恢复工作负载。不要在刚恢复的节点上直接一次性放回全部 GPU 服务。
八、如何避免再次发生
恢复节点只解决当前状态,不能替代根因治理。建议保留以下证据:
- 记录镜像中的 Python、PyTorch、CUDA、vLLM 版本和 GPU 驱动版本。
- 对出现 Xid 13/31 的服务保留完整 CUDA traceback 和请求样本摘要。
- 测试环境可使用
CUDA_LAUNCH_BLOCKING=1缩短异步 kernel 错误与业务调用之间的距离。 - 排查自定义 CUDA 扩展、Tensor Parallel、NCCL 和非法显存访问。
- 对连续出现 Xid 13/31/43 的节点设置“节点级异常”告警,不要只看 Pod 是否 Ready。
- 把 Xid 事件、GPU UUID、PCI Bus ID、Pod UID 和节点维护动作放在同一条时间线上。
需要区分两层问题:
- 本次节点的直接原因:历史 CUDA fault 触发 Xid 13/31/43,GPU1 的驱动状态未恢复。
- 最初非法访问的代码原因:仅靠 Xid 日志不能判定是 vLLM、PyTorch、CUDA 扩展还是驱动兼容性问题,需要结合对应工作负载版本和可复现 traceback 继续定位。
快速参考
看到 100% + 0 MiB + 无进程 时
nvidia-smi --query-compute-apps=gpu_uuid,pid,process_name,used_memory --format=csv
nvidia-smi pmon -c 5
nvidia-smi dmon -s pucm -c 5
journalctl -k --since "48 hours ago" --no-pager | grep -Ei 'NVRM|Xid'
结论口径
compute-apps有进程:先按业务任务排查。compute-apps为空、pmon为空、Xid 13/31/43 存在:优先按 GPU context/驱动状态异常处置。- 只有
fuser有 Python:只能说明打开设备文件,不能证明它正在计算。 - reset 前先迁移业务;reset 失败再重启或替换节点。
这类故障最容易误判的地方,是把“GPU 利用率 100%”直接等同于“现在有一个进程在正常跑满 GPU”。真正可靠的结论必须同时对齐 GPU UUID、compute process、pmon、内核 Xid 和恢复结果。

浙公网安备 33010602011771号