Kubernetes GPUAllocatableError 排查:Pod 内 nvidia-smi Unable to determine the device handle ... Unknown Error + 安全 drain 迁移(RTX 5090 / torch 2.8 / CUDA 12.8)

Kubernetes GPUAllocatableError 排查:Pod 内 nvidia-smi Unable to determine the device handle ... Unknown Error + 安全 drain 迁移(RTX 5090 / torch 2.8 / CUDA 12.8)

凌晨 05:12,Prometheus 一条 critical 告警:GPUAllocatableError——某节点有 GPU 无法被 device-plugin 识别。主机无法 SSH,告警备注里写着"如需重启且节点有业务,务必先沟通维护窗口"。这种场景下不能先动手,只能先回答两个问题:① 这台节点上的 Pod 现在到底有没有受影响?② 如果要维护,迁走它们是否安全?

本文记录一次只读排查 + 安全驱逐(cordon → drain --dry-run=server → drain)的完整路径,重点是讲清一个反直觉现象:Kubernetes 显示节点 Ready=Truenvidia.com/gpu allocatable 8/8,容器里 nvidia-smi 却报 Unknown Error,PyTorch cuda_available=False。如果只看 K8s 账面会漏判;只有进 Pod 内探测才暴露。

环境

集群类型 Kubernetes(多租户 vcluster 视图,kubectl 接的是 fake-node 层,底层物理集群不可见)
节点机型 8×GPU,15 个节点,集群 GPU 总量 120
GPU 型号 NVIDIA GeForce RTX 5090(每节点 8 张)
业务运行时 PyTorch 2.8.0+cu128(CUDA 12.8)
监控 Prometheus + DCGM exporter
告警源 GPUAllocatableError(custom rule,基于 DCGM 指标)
排查工具 kubectl(标准命令)
关键约束 主机无法 SSH,只能在 K8s 层和 Pod 内做只读排查

文中节点 IP 统一用占位 192.168.1.1,集群 ID 用 <CLUSTER_ID>,业务命名空间用 prod(通用名),Pod/Deployment 名做了脱敏。具体值替换成你自己的即可。

收到的原始告警

Prometheus 告警: GPUAllocatableError
集群:   <CLUSTER_ID>
级别:   critical
状态:   firing
节点:   my-gpu-node-01      (annotation 里挂的别名)
开始时间: 2026-05-28 05:12:21
描述:   节点 my-gpu-node-01 有 GPU 无法被 device-plugin 识别,
        请检查 GPU 及 device-plugin 状态。
风险提示: 如需重启且节点有业务运行,请先沟通维护窗口再重启!!!

关键不是节点 Ready 与否,而是单张 GPU 是否还能被 device-plugin 和业务容器识别——这两件事在生产里可以解耦。

步骤 1:节点上有哪些业务 Pod,K8s 怎么看节点

NODE=my-gpu-node-01
kubectl get pod -n prod -o wide --field-selector spec.nodeName=$NODE
NAME                              READY   STATUS    RESTARTS   AGE   IP              NODE
app-infer-edit-client-...-fc4df   1/1     Running   0          5d    192.168.1.10    my-gpu-node-01
app-infer-vocal-...-wh8k8         1/1     Running   0          5d    192.168.1.11    my-gpu-node-01
app-infer-edit-...-rst74          1/1     Running   0          5d    192.168.1.12    my-gpu-node-01
app-infer-v565-...-6wzfp          1/1     Running   0          5d    192.168.1.13    my-gpu-node-01
app-infer-v565-free12-...-s4pj6   1/1     Running   0          5d    192.168.1.14    my-gpu-node-01
app-img-encode-...-4d4q5          1/1     Running   0          5d    192.168.1.15    my-gpu-node-01
app-img-pipeline-...-86wtw        1/1     Running   0          5d    192.168.1.16    my-gpu-node-01
app-img-pipeline-...-dl76s        1/1     Running   0          5d    192.168.1.17    my-gpu-node-01

8 个 Pod 全部 1/1 Running、零重启。看节点本身:

kubectl describe node $NODE | grep -A2 -iE "Conditions|DiskPressure|MemoryPressure|Ready|Capacity|Allocatable|Allocated"

关键段(精简):

Conditions:
  MemoryPressure   False   KubeletHasSufficientMemory
  DiskPressure     False   KubeletHasNoDiskPressure
  PIDPressure      False   KubeletHasSufficientPID
  Ready            True    KubeletReady

Capacity:
  nvidia.com/gpu:  8
Allocatable:
  nvidia.com/gpu:  8

Allocated resources:
  Resource           Requests   Limits
  nvidia.com/gpu     8          8       <-- 8 张全部已分配给上面 8 个 Pod

Events:           <none>

K8s 账面看一切正常:节点 Ready、8 张 GPU 全部 allocatable、全部已分配、节点事件为空。

如果到这里就关单,会漏掉真实异常。继续往容器里探。

步骤 2:进 Pod 探,定位是哪张卡掉了

对节点上 8 个 Pod 逐个跑只读探测:

for p in $(kubectl get pod -n prod --field-selector spec.nodeName=$NODE --no-headers | awk '{print $1}'); do
  echo "===== $p ====="
  kubectl exec -n prod "$p" -- sh -lc '
    echo "CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES"
    echo "NVIDIA_VISIBLE_DEVICES=$NVIDIA_VISIBLE_DEVICES"
    nvidia-smi -L
  '
done

7 个 Pod 输出正常:

CUDA_VISIBLE_DEVICES=0
NVIDIA_VISIBLE_DEVICES=GPU-<uuid>
GPU 0: NVIDIA GeForce RTX 5090 (UUID: GPU-<uuid>)

第 8 个 Pod(app-infer-edit-...-rst74)输出异常:

CUDA_VISIBLE_DEVICES=0
NVIDIA_VISIBLE_DEVICES=GPU-1e4061a0-76ef-e74e-1799-e86971167897
Unable to determine the device handle for GPU0: 0000:16:00.0: Unknown Error
No devices were found

进一步看容器里 /dev/nvidia*/proc/driver/nvidia 是不是还在:

kubectl exec -n prod app-infer-edit-...-rst74 -- sh -lc '
  echo "--- /dev/nvidia*"; ls -l /dev/nvidia*
  echo "--- /proc/driver/nvidia/gpus"; ls /proc/driver/nvidia/gpus
'
--- /dev/nvidia*
crw-rw-rw- 1 root root 195,   0 ... /dev/nvidia0
crw-rw-rw- 1 root root 195, 254 ... /dev/nvidia-modeset
crw-rw-rw- 1 root root 195, 255 ... /dev/nvidiactl
crw-rw-rw- 1 root root 195, 254 ... /dev/nvidia-uvm
--- /proc/driver/nvidia/gpus
0000:16:00.0

设备节点和 sysfs 路径都在——也就是说,容器编排层没出错,问题在更底层的 NVML / driver runtime。再用 PyTorch 加一道验证:

kubectl exec -n prod app-infer-edit-...-rst74 -- python3 -c '
import torch
print("torch", torch.__version__)
print("cuda_available", torch.cuda.is_available())
print("device_count", torch.cuda.device_count())
'
torch 2.8.0+cu128
Can't initialize NVML
cuda_available False
device_count 0

再翻业务日志(最近 80 行):

kubectl logs -n prod app-infer-edit-...-rst74 --tail=80 \
  | egrep -i 'cuda|gpu|nvidia|nvml|error|exception|traceback'
CUDA error: unspecified launch failure
torch.AcceleratorError: CUDA error: unspecified launch failure
  Compile with `TORCH_USE_CUDA_DSA` to enable device-side assertions.

至此结论坐实:

  • 节点 Ready=True、K8s 显示 8/8 allocatable, PCI 0000:16:00.0(UUID GPU-1e4061a0-...)这张卡的 NVML 已经挂了。
  • 业务 Pod 进程还活着(restart=0),但 GPU 计算已不可用,日志里有持续 CUDA 错误。

为什么 K8s 显示 OK,容器里却掉卡?

这是这次排查最关键的 mental model。简化的关系图:

[NVIDIA driver] --(NVML)--> nvidia-smi / 业务进程 cuda runtime
       |
       v
[device-plugin DaemonSet] --gRPC--> [kubelet]
                                       |
                                       v
                                 Node.status.capacity / allocatable
                                 (一次性快照,Pod 启动时分配 device id)

device-plugin 只在启动 / 重启 / 周期上报时把"我有几张卡"同步给 kubelet;kubelet 把这个数字落到 Node 对象。单张 GPU 在驱动层挂掉(XID 错误、ECC、NVML 断连)不会自动回吐 allocatable——除非 device-plugin 主动 ListAndWatch 把这张卡剔除,或者 DaemonSet 重启重新枚举。

而已经分配给 Pod 的 GPU,容器里的 /dev/nvidiaN 设备节点是 kubelet 通过 cgroup device whitelist 在容器创建时挂进去的,容器生命周期内不动。所以坏卡也照样"挂"在容器里——直到业务真的去 NVML 调用,才会暴露。

这就解释了 split state:控制面账面正常,数据面 NVML 已经不可用。 想可靠检出,必须 Pod 内探测

一个容易踩坑的额外信号

集群在 vcluster / 多租户 fake-node 视图下,kubectl get ds -A 看不到 nvidia-device-plugin 这类 DaemonSet——这不是异常,它在底层物理集群,你的视图里本来就看不见。节点标签出现 vcluster.<vendor>/fake-node=true 就是这种场景。这时只能靠 Pod 内探测和 DCGM 指标定位。

步骤 3:相关 Deployment 副本数与"业务损失程度"

通过异常 Pod 反查 owner:

kubectl get pod -n prod app-infer-edit-...-rst74 \
  -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
# ReplicaSet/app-infer-edit-...-c598bdfbb
kubectl get rs -n prod app-infer-edit-...-c598bdfbb \
  -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
# Deployment/app-infer-edit

汇总该节点 8 个 Pod 涉及的所有 Deployment:

Deployment READY/REPLICAS 该节点占用副本 单 Pod GPU request
app-infer-edit-client-ww5090 3/3 1 1
app-infer-vocal-to-instrument 4/4 1 1
app-infer-edit 8/8 1(掉卡 Pod) 1
app-infer-v565 14/14 1 1
app-infer-v565-free12 12/12 1 1
app-img-encode-text 2/2 1 1
app-img-pipeline 15/15 2 1

业务面损失:app-infer-edit 这个 8 副本 Deployment 损失了 1/8 实例。其他 Deployment 都还有多副本兜底。

步骤 4:集群剩余 GPU 够不够把 8 个 Pod 接走?

# 总量与各节点 allocatable
kubectl get nodes -o custom-columns='NAME:.metadata.name,READY:.status.conditions[?(@.type=="Ready")].status,GPU_CAP:.status.capacity.nvidia\.com/gpu,GPU_ALLOC:.status.allocatable.nvidia\.com/gpu' --no-headers

# 各节点 Pod requests 实际占用合计
kubectl get pod -A -o jsonpath='{range .items[*]}{.spec.nodeName}{"\t"}{range .spec.containers[*]}{.resources.requests.nvidia\.com/gpu}{" "}{end}{"\n"}{end}' \
  | awk 'NF>=2{s=0;for(i=2;i<=NF;i++) if($i ~ /^[0-9]+$/) s+=$i; if(s>0) n[$1]+=s} END{for(k in n) print k, n[k]}' | sort

# 没有 Pending 的 GPU Pod
kubectl get pod -A --field-selector=status.phase=Pending \
  -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name} {range .spec.containers[*]}{.resources.requests.nvidia\.com/gpu}{" "}{end}{"\n"}{end}' \
  | awk '/[0-9]/{print}'

# 节点 taint(避免"有空卡但不可调度")
kubectl get nodes -o custom-columns='NAME:.metadata.name,TAINTS:.spec.taints' --no-headers

账面汇总:

指标
集群 GPU 总量 120
当前 Pod requests 合计 108
剩余可调度 GPU 12
Pending GPU Pod 0
节点 taint 全部 <none>
目标节点待迁移 Pod 总 GPU 8

再看相关 Deployment 的调度约束:

kubectl get deploy -n prod app-infer-edit \
  -o jsonpath='nodeSelector={.spec.template.spec.nodeSelector}{"\n"}affinity={.spec.template.spec.affinity}{"\n"}tolerations={.spec.template.spec.tolerations}{"\n"}'
nodeSelector={"node_env":"prod"}
affinity=<nil>
tolerations=<nil>

约束很宽,没有亲和/反亲和也没有 toleration。容量与可调度性都满足:8 个 Pod 迁出后,集群仍有 4 张空 GPU 冗余。

步骤 5:cordon + drain --dry-run=server(只校验不动 Pod)

不要直接 drain。先 cordon 阻止新 Pod 调度,再用 server 端 dry-run 校验:

kubectl cordon $NODE
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data --dry-run=server

--dry-run=server 让 apiserver 走完一遍真实校验逻辑(PDB、裸 Pod、emptyDir、DaemonSet 限制),但不真正驱逐 Pod。这一步真实输出:

node/my-gpu-node-01 already cordoned (server dry run)
Warning: ignoring DaemonSet-managed Pods:
  kube-system/loongcollector-ds-xxxxx,
  observability/promtail-xxxxx
evicting pod prod/app-img-pipeline-...-dl76s        (server dry run)
evicting pod prod/app-infer-edit-...-rst74          (server dry run)
evicting pod prod/app-infer-v565-...-6wzfp          (server dry run)
evicting pod prod/app-infer-vocal-...-wh8k8         (server dry run)
evicting pod prod/app-infer-v565-free12-...-s4pj6   (server dry run)
evicting pod prod/app-img-encode-...-4d4q5          (server dry run)
evicting pod prod/app-img-pipeline-...-86wtw        (server dry run)
evicting pod prod/app-infer-edit-client-...-fc4df   (server dry run)
node/my-gpu-node-01 drained (server dry run)

逐行确认:

  • DaemonSet Pod 被忽略(日志采集 / 监控 agent,留在节点上正常)。
  • 8 个业务 Pod 全部可驱逐,没有 PDB / 裸 Pod / emptyDir 阻塞
  • 校验通过。

--delete-emptydir-data 表示允许丢弃 Pod 的 emptyDir 临时数据。GPU 推理服务一般把模型放镜像或 PV 里,emptyDir 多是临时缓存,丢了无碍——但执行前一定要确认你的业务也是这样

步骤 6:正式 drain 后,只看 Running 不够,要等 READY 1/1

确认维护窗口后,正式执行:

kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data --timeout=20m

第一时间确认节点已空:

kubectl get pod -n prod -o wide --field-selector spec.nodeName=$NODE
# No resources found in prod namespace.

再看 Deployment:

kubectl get deploy -n prod | egrep 'app-infer|app-img'
NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
app-infer-edit                 7/8     8            7           120d   <-- 1 个还在重建
app-infer-v565                 13/14   14           13          120d
app-img-pipeline               13/15   15           13          120d
...

新 Pod 处于 0/1 Running、年龄约 100s——Pod phase 是 Running 不代表业务已恢复。GPU 推理服务有模型加载时间,要等 READY 1/1(readinessProbe 通过)才算真接管流量。

# 持续观察未 Ready 的相关 Pod
kubectl get pod -n prod -o wide \
  | awk '$2!="1/1" || $3!="Running" {print}'

等所有相关 Deployment 都回到 READY = REPLICAS = AVAILABLE,才算迁移收尾。

真正修这张坏卡(主机能登了之后)

业务迁走只是把伤口绕开,不等于卡修好了。 主机能 SSH 后(或机房 OPS 介入),按以下顺序定位:

# 1. 看内核日志里的 NVRM Xid 错误(最权威的硬件/驱动异常证据)
dmesg -T | grep -iE 'NVRM|nvidia|xid' | tail -50
# 关注 "NVRM: Xid (PCI:0000:16:00): 79" 这类——具体 Xid 码可对照 NVIDIA 文档定位
# (常见:13=Graphics Engine Exception, 31=GPU memory page fault,
#        43=GPU stopped processing, 48=Double Bit ECC, 79=GPU fallen off the bus)

# 2. 在物理机上跑 nvidia-smi(容器外)
nvidia-smi -q -i 0 | grep -iE 'xid|ecc|retired|persistence|fan|temperature'
nvidia-smi -q | grep -i 'ECC Error'

# 3. DCGM 诊断(更详细)
dcgmi diag -r 3 -i 0   # level 3 较完整;生产环境慎用 level 4(会跑负载)

# 4. 如果 NVML 完全挂了,尝试重置(单卡)
# 注意:reset 期间这张卡不可用,且通常需要先确保没有进程占用
sudo nvidia-smi -i 0 -r

# 5. 仍不行 → 联系机房/云厂商,提供:
#    - Xid 错误码 + 时间戳
#    - dmesg 完整片段
#    - nvidia-smi 报错原文
#    - 受影响的 GPU UUID 和 PCI 地址

修好之后:

kubectl uncordon $NODE       # 节点重新参与调度
# 业务 Pod 会逐步均衡回这台节点(下一次调度/扩缩容时)

步骤 7:告警恢复后的复查(等节点修好、uncordon 之后)

告警从 firingresolved 不等于事故收尾——Prometheus 看到的是指标侧恢复,业务侧是否真的"原样"了要回到 Kubernetes 侧自己验。这一节是节点修好后(uncordon 之后)必跑的复查清单,五个动作:

7.1 节点本身回到 schedulable + GPU 账面归零

kubectl describe node $NODE | grep -A2 -iE "Conditions|Unschedulable|Taints|Capacity|Allocatable|Allocated"

预期:

Ready              True
Unschedulable      false           <-- 已 uncordon,重新参与调度
Taints             <none>
MemoryPressure     False
DiskPressure       False
PIDPressure        False
NetworkUnavailable False

Capacity:
  nvidia.com/gpu:  8
Allocatable:
  nvidia.com/gpu:  8

Allocated resources:
  Resource           Requests   Limits
  nvidia.com/gpu     0          0       <-- 8 张全释放,等下次调度填回

Events:            <none>

两个关键信号:Unschedulable=false(没忘记 uncordon);nvidia.com/gpu 账面回到 8/8 且 Allocated 0/0(driver/device-plugin 重置成功)。如果还卡在 0/8 allocatable,说明 device-plugin 没正常重启,要去查 DaemonSet。

7.2 原节点上没有遗留业务 Pod

kubectl get pod -n prod -o wide --field-selector spec.nodeName=$NODE
# No resources found in prod namespace.

为空才正常。如果 uncordon 之前有 Pod 因为某些约束(如 affinity)调度回来了,要看看是不是过早。

7.3 受影响的 Deployment 全部满副本

kubectl get deploy -n prod \
  app-infer-edit app-infer-edit-client-ww5090 app-infer-vocal-to-instrument \
  app-infer-v565 app-infer-v565-free12 app-img-encode-text app-img-pipeline \
  -o custom-columns='NAME:.metadata.name,READY:.status.readyReplicas,REPLICAS:.spec.replicas,AVAILABLE:.status.availableReplicas,UPDATED:.status.updatedReplicas'
NAME                            READY  REPLICAS  AVAILABLE  UPDATED
app-img-pipeline                15     15        15         15
app-infer-edit                  8      8         8          8
app-infer-v565                  14     14        14         14
app-infer-vocal-to-instrument   4      4         4          4
app-infer-v565-free12           12     12        12         12
app-img-encode-text             2      2         2          2
app-infer-edit-client-ww5090    3      3         3          3

READY == REPLICAS == AVAILABLE == UPDATED 四列一致才是真恢复。任一列对不上,说明还有 Pod 在启动 / 拉镜像 / 模型加载,继续等。

7.4 集群层面没有新冒头的异常 Pod

# 相关 namespace 内非 1/1 Running 的 Pod
kubectl get pod -n prod -o wide | awk 'NR==1 || $2!="1/1" || $3!="Running"'

# 全集群异常状态兜底扫一眼
kubectl get pod -A -o wide \
  | egrep 'Pending|ContainerCreating|CrashLoopBackOff|Error|ImagePullBackOff|ErrImagePull' \
  || echo "all clean"

预期:第一条只剩表头;第二条输出 all clean

7.5 关键判断:要不要让业务"迁回"这台节点?

复查全绿 ≠ 这台机立刻满血复用。这台节点刚发生过 GPU 掉卡,必须先观察一段(数小时~一天)再让它正常承载业务,期间盯:

  • DCGM 指标:DCGM_FI_DEV_XID_ERRORS、ECC 计数有没有再次冒头;
  • 主机侧 dmesg | grep -iE 'NVRM|nvidia|xid' 不再出现新 Xid 事件;
  • 在这台节点上先跑一个小规模 canary(单 Pod、低 QPS)验证负载下稳定。

确认无反复后,不需要手动迁移——调度器会在下次扩缩容/重建时把业务自然填回来。如果观察期里 Xid / NVML 异常复发,就走「步骤 5/6」再 drain 一次,把卡转送修。

快速参考

一句话流程

GPU 告警 → 进 Pod 内跑 nvidia-smi(K8s 账面会骗人)→ 定位坏卡 → 评估剩余 GPU 容量 → cordon + drain --dry-run=server → 正式 drain → 等 READY 1/1 → 主机能登后查 Xid / DCGM 真正修卡。

命令速查

NODE=my-gpu-node-01

# 节点上的业务 Pod
kubectl get pod -n prod -o wide --field-selector spec.nodeName=$NODE

# 进每个 Pod 探 GPU(关键)
for p in $(kubectl get pod -n prod --field-selector spec.nodeName=$NODE --no-headers | awk '{print $1}'); do
  echo "=== $p ==="
  kubectl exec -n prod "$p" -- sh -lc 'nvidia-smi -L'
done

# 集群剩余 GPU node_env 是环境标签
kubectl get nodes,pods -A -o json | jq -r '(.items|map(select(.kind=="Node"))|map({name:.metadata.name,label:(.metadata.labels["node_env"]//"-"),alloc:(.status.allocatable["nvidia.com/gpu"]//"0"|tonumber)})) as
  $n|(.items|map(select(.kind=="Pod"))|map({node:.spec.nodeName,gpu:([.spec.containers[].resources.requests["nvidia.com/gpu"]//"0"|tonumber]|add)})|group_by(.node)|map({node:.[0].node,used:(map(.gpu)|add)})) as
  $u|$n[]|.name as $name|.label as $l|.alloc as $a|(($u[]|select(.node==$name).used)//0) as $used|"\($name)\t\($l)\t\($a)\t\($used)\t\($a-$used)"'

# 安全 drain 三连
kubectl cordon $NODE
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data --dry-run=server
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data --timeout=20m

# 修好后
kubectl uncordon $NODE

四条铁律

  1. K8s 显示 OK ≠ GPU 都正常:device-plugin 上报是快照,单卡 NVML 挂了不会自动回吐 allocatable。只有 Pod 内 nvidia-smi / torch.cuda 才是真相
  2. 不要直接重启生产 GPU 节点:先 cordon 阻止新调度,drain --dry-run=server 校验,确认有容量+无阻塞再正式 drain。
  3. Pod Running ≠ 业务恢复:GPU 推理服务有模型加载时间,认 READY 1/1 才算接管。
  4. 业务迁走只是把伤口绕开:真正修卡靠 dmesg 里的 Xid 错误码 + DCGM 诊断 + 机房/云厂商工单。
posted @ 2026-05-28 10:32  Hello_worlds  阅读(12)  评论(0)    收藏  举报