bash
1. NVLink降宽就是比如 A100 原来是nv12,现在变成nv6. 降速用p2p就能测试出来
2. Level 3 包含了 Level 1 和 Level 2 的所有检查项,并在此基础上增加了更严苛、更深层的硬件压力测试。
反正都是要停机,我直接用 -r 3(~20-40分钟),mlxlink 测的是网卡到主板的带宽
bash
场景1:训练 loss 突然 spike / NaN,但所有监控指标正常
这是最经典的 "静默数据损坏"(Silent Data Corruption, SDC)
运维操作:
1. 把出问题的节点 drain
2. 跑 dcgmi diag -r 3
3. 输出:
+---------------------------+--------------------------------------------+
| Diagnostic (compute) | FAIL |
| | GPU 6: Matrix multiply result mismatch |
| | Expected: 1.23456789 |
| | Got: 1.23456287 |
| | Bit errors detected in SM computation |
+---------------------------+--------------------------------------------+
4. 确认 GPU 6 有静默计算错误
5. 这张卡算出来的结果是错的,但不会报任何 error
6. 只有 r3 的矩阵乘法验证能抓到
场景2:GPU 间歇性挂死,偶尔 Xid 79 (GPU 从总线掉线)
【对比 XID 48:Double Bit ECC Error (双位 ECC 错误)】
运维操作:
1. 跑 r1、r2 都 Pass(因为短时间内没触发)
2. 跑 r3(持续压力 30 分钟)
3. 输出:
+---------------------------+--------------------------------------------+
| Memory Stress | FAIL |
| | GPU 2: ECC DBE detected during stress |
| | at address 0x7F3A_2000 |
| | GPU requires replacement |
+---------------------------+--------------------------------------------+
4. 长时间压力下终于复现了 DBE(Double Bit Error)
5. 这张卡必须换
drain
bash
# 1. 选定要巡检的节点,从调度器摘出来
# A100 集群
kubectl cordon node-a100-{01..64}
kubectl drain node-a100-{01..64} --ignore-daemonsets --delete-emptydata
1. 每个节点上都有日志采集,监控和 nvidia 插件这些daemonset,你 drain走,它又生成,陷入死循环。
2. 不加 --delete-emptydata,kubectl 会为了保护你的数据不丢失,拒绝驱逐该 pod。emptyDir 是 pod 删除或调度到别的节点,
数据也没了。当 Pod 分配到节点时,K8s 会在节点上创建一个空目录。只要 Pod 还在这个节点上运行,这个目录就一直存在。
3. drain 的节点要注意有没有coredns和 Metrics Server 这些不在下面四大static pod 的基础组件,它们是deployment/pod 形式的,
如果没有副本会发生脑裂现象,造成短暂的服务停止。
Metrics Server 是 Kubernetes 内置的"性能监控聚合器".从每个节点上的 kubelet 收集基础的资源使用数据(CPU、内存),并提供给
Kubernetes 的其他组件使用。没有它就会
1)kubectl top 命令:你将无法通过 kubectl top nodes 或 kubectl top pods 查看实时资源占用。
2)HPA (Horizontal Pod Autoscaler):这是最致命的。如果你设置了"当 CPU 超过 80% 时自动扩容",HPA 必须通过 Metrics
Server 才知道现在的 CPU 是多少。
4. drain 会自动忽略 静态pod,也就是yaml文件在 /etc/kubernetes/manifests 里的 kube-apiserver、kube-controller-manager、
etcd 、Scheduler 等。
# 2. 确认 GPU 上没有进程
ansible ib_a100 -m shell -a "nvidia-smi --query-compute-apps=pid --format=csv,noheader | wc -l"
# 所有节点应该返回 0
二、Monitoring
bash
cgroup 数据源头,负责限制和记录容器使用资源情况,比如 cpu 内存和 IO
cAdvisor 内置在 kubelet 里。它直接读取 cgroup 的原始数据, 并通过 http 暴露给 Prometheus 和 metrics server.
metrics server 定期去 kubelet (cAdvisor)那拉取 cpu 和内存使用情况,缓存在内存,为了 给 kubectl top 使用。集群中一个pod
node-exporter 主机层指标
kube--state-metrics 直接监听 API Server 的元数据,也就是k8s资源的逻辑状态,不看 cgroup,它也是集群中一个pod。
它告诉你 资源是什么状态,它们应该是什么样。
监控防抖
同组告警
bash
group_by: ['node', 'alertname']
# 同一时刻 node01 上触发了3条 GPUTemperatureHigh 告警(8卡里3张过热)
# 不会发3条消息,合并成1条:
# "node01: GPUTemperatureHigh - GPU0/GPU3/GPU5 温度超过83°C"
bash
节点指标(GPU温度突然飙到85°C)
→ Prometheus 每15s采集一次
→ 告警规则里写持续时间:
alert: GPUTemperatureHigh
expr: nvidia_gpu_temperature > 83
for: 5m # 关键:持续5分钟才触发,这就是防抖
→ 5分钟内恢复了 → 不告警
→ 5分钟还没恢复 → 触发告警 → Alertmanager
→ Alertmanager 再做 group_wait/group_interval:
group_wait: 30s # 同组告警等30s聚合
group_interval: 5m # 同组告警最少间隔5m才发下一条
repeat_interval: 4h # 同一条告警4小时才重复通知
→ 最终发到飞书/钉钉
三层防抖:Prometheus for 过滤瞬时抖动 → Alertmanager group_wait 聚合 → repeat_interval 防刷屏。
过了实时监控(防抖)和日常巡检这两关,只能说明表面上硬件没有问题。
巡检只是那一瞬,深度是压测,时间长,能看出一段时间内有没有问题。
如果上层应用同事反馈性能不达标【nccl-test】,或者loss有尖刺,监控偶尔发现xid 79掉卡问题【-r 3查】
过两关后 nccl-test查出来的问题
- GDR问题
bash
事件:运维升级了内核 / OFED 驱动 / CUDA 版本
原因:
新内核没加载 nvidia-peermem 模块
GDR 路径断了,数据走 GPU → CPU内存 → 网卡,多了一次拷贝
硬件完全正常,纯软件问题
日常巡检能发现吗?不能
ibstat 看端口状态,看不到 GDR
mlxlink 看物理层,看不到 GDR
它们的视角只到网卡这一层,分不出来源头是 GPU 还是 CPU
而nccl属于 upper layer,它可以查出来。
怎么确认:
$ lsmod | grep nvidia_peermem
(空) ← 模块没加载
但你不跑 NCCL test,根本不知道要去查这个
- 环境变量中没选网卡
bash
事件:有人改了集群调度模板 / 容器镜像 / 启动脚本
之前配置:
NCCL_IB_HCA=mlx5_0,mlx5_1(用两张网卡)
NCCL_ALGO=Ring
NCCL_CROSS_NIC=0
被改成(或者变量丢了):
NCCL_IB_HCA 没设置 → NCCL 只自动选了一张网卡
结果:
所有硬件指标全绿
ib_write_bw 两张网卡分别测都是 196 Gbps ✓
但 NCCL busbw 掉了一半,因为只用了一张网卡
- NVLink 部分链路降速但未断开
bash
事件:NVLink 信号劣化,链路自动降速
nvidia-smi nvlink -s → 全 Active ✓
nvidia-smi nvlink -e → 错误计数 = 0 ✓(降速后不报错了)
nvidia-smi topo -m → 全 NV18 ✓(链路还在)
但 NVLink 从 25 GB/s/link 降到了 20 GB/s/link 单向
日常巡检读寄存器看不到速率变化(NVLink 没有像 IB 那样直观显示速率的命令)
只有 p2pBandwidthTest 或 NCCL test 灌流量才能发现带宽掉了
1. NCCL test 什么时候用 WARN 而不是 INFO
bash
用于训练的镜像中的nccl,在训练过程中如果NCCL_DEBUG=INFO,那么在pod的日志里只会打印拓扑和选路信息,不会打印 busbw
busbw 是 nccl-test 自己算的,不是 NCCL 库的功能
NCCL 的日志级别通过环境变量控制:
bash
export NCCL_DEBUG=INFO # 默认,输出通信路径、选路信息
export NCCL_DEBUG=WARN # 只输出警告和错误
例子 1:大规模生产训练
bash
# 512 卡训练,每张卡都打 INFO 日志
# INFO 会输出每张卡的:选了哪张网卡、走的什么算法、Ring 拓扑、Tree 拓扑...
# 512 张卡 × 每张几十行 = 上万行日志,淹没真正有用的信息
# 生产环境用 WARN
export NCCL_DEBUG=WARN
# 只有出问题时才会看到输出,比如:
# WARN: Timeout waiting for ring 0 on rank 47
Rank 47(第 48 个 GPU 进程)在执行通信环路同步时超时了
# WARN: NET/IB: Got completion(完工) with error 12 on mlx5_1
在名为 mlx5_1 的网卡上,RDMA 传输任务以"错误代码 12"结束。
例子 2:调试完成后的日常 NCCL test 巡检
NCCL test 的目的是模拟真实训练场景
bash
# 你已经确认过集群配置没问题,现在只是每周例行跑 NCCL test 验证性能
# 不需要看选路细节,只关心有没有异常
export NCCL_DEBUG=WARN
mpirun -np 512 ... all_reduce_perf -b 1G -e 1G -g 1 -n 50
# 正常时:干干净净只有 busbw 结果
# 异常时:WARN 会告诉你哪个 rank 出了什么错
NCCL_DEBUG=WARN(默认模式)只能告诉你"结果:病倒了",而 NCCL_DEBUG=INFO 才能告诉你"过程:是怎么病的"。
比如:
1)运维升级了内核,但忘了加载 nvidia-peermem。
WARN 模式表现:完全静默。因为从网络协议看,通讯是通的,只是变慢了。程序能跑完,但带宽从 200Gbps 掉到了 10Gbps。
你只会看到业务慢,但没有任何报错。
NFO 模式威力:
它会打印出每一条链路的初始化细节。你会看到:
NET/IB : Using [0]mlx5_1:1/RoCE ... (GDRv3) ← 正常
或者
NET/IB : Using [0]mlx5_1:1/RoCE ... (后面没有 GDR 字样) ← 抓到真凶
2)在 8 卡 SXM 架构中,如果 NVSwitch 固件或驱动配置出错,NCCL 可能会放弃 NVLink,转而走 PCIe。
WARN 模式表现:同样无报错。程序能跑,但 nccl-tests 测出来的带宽只有 20GB/s。
INFO 模式威力:
它会输出拓扑探测的结果(Topology Search)。你会看到:
NCCL INFO Channel 00 : 0 1 2 3 4 5 6 7 [NVL] ← 正常,NVL 代表 NVLink
或者
NCCL INFO Channel 00 : 0 1 2 3 4 5 6 7 [PBM] ← 异常,PBM 代表经过了 PCIe Bridge
2. dcgmi -r 3 怎么测 Tensor Core,输出什么信息
dcgmi diag -r 3 的 Diagnostic (compute) 测试项就是在测 Tensor Core。
/ˌdaɪəɡˈnɒstɪk/ 诊断
Monitoring
只需要记住 实时监控和两种巡检 巡检的内容 和 两种报告方式:ansible(收集和 fetch) + Prometheus
还有最下面的自动 cordon + drain
Prometheus(情报员):负责盯着数据。如果发现某个指标(比如 GPU 温度)超过了你设定的阈值,它就会产生一个告警。
Prom 连 Alertmanager
yaml
alerting:
alertmanagers:
- static_configs:
- targets:
- 'localhost:9093' # Alertmanager 的地址
在 alertmanager.yml 里,你需要定义一个 receiver(接收者)。因为 Alertmanager 原生不支持飞书,所以我们使用 webhook 模式。
yaml
receivers:
- name: 'feishu-robot'
webhook_configs:
- url: 'http://feishu-adapter:8080/webhook' # 翻译官的地址
send_resolved: true # 问题解决了也要通知一声
bash
实时监控(紧急硬错误) 日常巡检(是否正常) 深度体检(隐疾, 要在没有训练任务的时候,因为要独占)
下面这两条都说明在深度体检的时候会影响到正在训练的任务,所以巡检脚本中要有检查是否在训练,训练就不能深度体检
bash
dcgmi diag -r 3:
→ 独占 GPU 跑 30 分钟烤机压测
→ 和训练任务抢显存、抢算力
→ 两者同时跑 = 训练挂掉或体检结果不准
nccl-tests:是一套专门用来压测 GPU 间通信效率的工具集
→ 要占满所有 8 张 GPU 跑 AllReduce
→ 训练任务在跑的话显存不够,直接 OOM
实时监控(7×24 自动,抓紧急硬错误)
目标:已经坏了的东西,必须立刻知道
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ① ECC DBE(双比特不可纠正错误) │
│ XID 48 = DBE 双比特错误 │
│ 处置:立即 cordon + drain,换卡 │
│ XID 79 = GPU 掉卡(从 PCIe 总线消失) │
│ XID 79 检查主板/PCIe 插槽 │
│ │
│ ② IB/RoCE 端口 Down │
│ │
│ IB 节点:ibstat 的 State 字段(通过自定义 exporter) │
│ RoCE 节点:ethtool 的 Link detected 字段 │
│ 阈值:State ≠ Active 立即告警 │
│ 处置:检查线缆/光模块,换端口 │
│ │
└─────────────────────────────────────────────────────────────────┘
日常巡检(每天凌晨 4 点,确认是否正常)
不影响正在运行的任务
就是(读寄存器/计数器)
bash
dcgmi diag -r 2 ⚠️ 唯一有影响的项
→ 会跑 GPU 压测 2-3 分钟
→ 会占 GPU 算力和显存
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ① ECC SBE 增长趋势(单比特可纠正错误) │
│ │
│ 检查方式: │
│ 今天的 SBE 累计值 vs 昨天的值 │
│ increase(DCGM_FI_DEV_ECC_SBE_AGG_TOTAL[24h]) │
│ 阈值:24小时增长 > 10 → warning │
│ │
│ ② PCIe 宽度 + 纠正错误 │
│ │
│ 什么问题: │
│ PCIe x16 降到 x8 → 带宽减半,训练慢但不报错 │
│ PCIe Receiver Error 增长 → 金手指氧化/线缆松动 │
│ 检查方式: │
│ mlxlink -d <dev> --port_type PCIE → 看 Width │
│ mlxlink -d <dev> --port_type PCIE -ce → 看错误计数 │
│ 阈值:Width ≠ x16 → warning,Receiver Error > 0 → warning │
│ │
│ ③ GDR 模块 + NVLink 错误计数 │
│ │
│ 什么问题: │
│ nvidia-peermem 模块掉了 → GDR 失效 → 性能降 30-50% │
│ NVLink 错误计数 > 0 → 链路信号在恶化 │
│ 检查方式: │
│ lsmod | grep nvidia_peermem │
│ nvidia-smi nvlink -e │
│ │
└─────────────────────────────────────────────────────────────────┘
深度体检(每周日 / 新机验收 / 维修后)
目标:表面正常但实际有隐疾,只有压测才能暴露
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ① dcgmi diag -r 3 静默计算错误 │
│ │
│ 什么问题: │
│ GPU Tensor Core 偶发计算错误 │
│ 现实案例: │
│ 训练 loss 偶尔出尖刺,换节点就好 │
│ → 跑 -r 3 发现 GPU5 Targeted Stress Test FAIL │
│ → Tensor Core 物理损伤,但不触发 ECC(ECC 只管显存) │
│ │
│ ② nccl-tests 节点内 AllReduce busbw │
│ │
│ 什么问题: │
│ NVLink 链路没断(状态 Active,错误计数 0) │
│ 但实际传输带宽衰退了 30% │
│ 检查方式: │
│ all_reduce_perf -b 1G -e 1G -g 8 │
│ A100 期望 busbw > 350 GB/s │
│ 某台只有 260 GB/s → 进一步用 p2pBandwidthLatencyTest │
│ 定位到 GPU4↔GPU5 之间 NVLink 带宽只有正常的一半 │
│ │
│ ③ ib_write_bw 跨节点裸带宽 │
│ │
│ 什么问题: │
│ IB 端口 Active,速率显示 200Gb/s(ibstat 正常) │
│ 但实际跑数据只有 15 GB/s(期望 24 GB/s) │
│ 原因可能是:光模块衰减、交换机端口背板问题、线缆弯折 │
│ 检查方式: │
│ Server: ib_write_bw -d mlx5_0 --report_gbits │
│ Client: ib_write_bw -d mlx5_0 <server_ip> --report_gbits │
│ 期望 ≥ 190 Gbps(~24 GB/s) │
│ 低于 150 Gbps → 换线缆/光模块 │
│ │
└─────────────────────────────────────────────────────────────────┘
怎么深度检查两台机器之间的带宽
bash
策略1:相邻节点配对(最常用)
node-0 ↔ node-1
node-2 ↔ node-3
node-4 ↔ node-5
...
→ 56 组测试,覆盖每台机器至少一次
→ 20 分钟跑完
ansible实现链条
bash
┌──────────────────────────────────────────────────────────────┐
│ │
│ 第一步:生成配对清单 │
│ │
│ 管理节点上的 Python 脚本读取 inventory │
│ → 输出配对列表: │
│ pair_1: server=node-0 client=node-1 │
│ pair_2: server=node-2 client=node-3 │
│ ... │
│ │
│ 第二步:Ansible 并行启动所有 server 端 │
│ │
│ 对所有 server 节点并行执行: │
│ ib_write_bw -d mlx5_0 --report_gbits & │
│ server 进程后台等待 client 连入 │
│ │
│ 第三步:Ansible 并行启动所有 client 端 │
│ │
│ 等 server 全部就绪(sleep 5) │
│ 对所有 client 节点并行执行: │
│ ib_write_bw -d mlx5_0 <对应server的IP> --report_gbits │
│ client 跑完自动退出,server 也跟着退出 │
│ │
│ 第四步:收集结果 │
│ │
│ Ansible fetch 回所有 client 端的输出 │
│ 解析 BW 数值,和阈值比较 │
│ 低于 190 Gbps 的标记异常 │
│ │
│ 第五步:汇总报告 │
│ │
│ 正常:pair node-0↔node-1: 196 Gbps ✓ │
│ 异常:pair node-4↔node-5: 87 Gbps ✗ ← 线缆/光模块问题 │
│ → 发飞书 │
│ │
└──────────────────────────────────────────────────────────────┘
二、完整链路(每种巡检举两个指标示例)
当我通过PromQL自己算出来一些指标来放到grafana上展示的时候,这个实现可以grafana取的时候自己算一下,然后取出来,也可以Prometheus 存储的时候算一下,给这个指标一个新名字,然后grafana直接拿
grafana 实时算 随复杂度增加而变慢(临时调试) Prometheus预先算的话始终极快
日常巡检 vs 深度体检:带宽检查的本质区别
bash
日常巡检做的事:
mlxlink -d /dev/mst/mt4125_pciconf0 --port_type PCIE
它只是 读一个寄存器的值,就像你看一眼仪表盘
┌──────────────────────────────────────────────────────────────────┐
│ 日常巡检:读状态寄存器 │
│ │
│ 做了什么: 问 PCIe 控制器 "你现在协商的宽度是多少?" │
│ 类比: 看汽车仪表盘上的转速表 │
│ 耗时: < 0.1 秒 │
│ 对业务影响:零(只读操作) │
│ 能发现: 宽度从 x16 降到了 x8(硬件层面已经降了) │
│ │
│ 它读到的是一个 "既成事实" │
│ 不是它去测的,是 PCIe 链路在启动时就协商好了 │
│ 或者运行中因为硬件故障自动降级了 │
└──────────────────────────────────────────────────────────────────┘
只有深度体检才能抓到的问题
bash
┌────────────────────────┬──────────────────────────┬────────────────────────┐
│ 问题 │ 日常巡检读寄存器看到的 │ 深度体检实际跑出来的 │
├────────────────────────┼──────────────────────────┼────────────────────────┤
│ NVLink 性能衰退 │ Link Active ✓ 看着正常 │ P2P 带宽只有150/250GB │
│ GPU 静默计算错误 │ ECC计数=0 ✓ 看着正常 │ dcgmi -r 3 算出错结果 │
│ HBM 显存带宽劣化 │ 显存容量正常 ✓ │ bandwidthTest 实测偏低 │
│ PCIe 信号劣化(未降宽) │ Width=x16 ✓ 还没降 │ 眼图测试 眼高不足 │
│ NCCL 通信路径异常 │ 所有端口都Active ✓ │ AllReduce busbw 偏低 │
│ GPU 高负载降频 │ 当前空闲频率正常 ✓ │ 压力测试时频率掉了 │
└────────────────────────┴──────────────────────────┴────────────────────────┘
2.1 实时监控层(7×24 自动)
IB/RoCE 端口状态实时监控链条
bash
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 第一步:自定义 exporter 采集端口状态 │
│ │
│ 写一个 Python 脚本,每 30 秒执行 ibstat / ethtool │
│ 把结果转成 Prometheus 格式,暴露 HTTP 端口 :9401 │
│ │
│ 第二步:Prometheus 抓取 │
│ │
│ prometheus.yml 添加 scrape target :9401 │
│ 每 30 秒 pull 一次 │
│ │
│ 第三步:告警规则 │
│ │
│ ib_port_state != 1 → AlertManager → 飞书 │
│ │
│ 第四步:Grafana 展示 │
│ │
│ 状态表格:每个节点每个端口一行,红绿标记 │
│ │
└─────────────────────────────────────────────────────────────────┘
指标1: GPU 温度
指标2: ECC SBE 增长率
链路:
GPU 硬件 → DCGM API → dcgm-exporter(:9400) → Prometheus(每30s抓)
│
┌─────────┴──────────┐
↓ ↓
Grafana 面板 AlertManager
(运维看大屏) (飞书推送)
dcgm-exporter 部署(K8s DaemonSet,每个 GPU 节点跑一个):
yaml
# 精髓部分
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: dcgm-exporter
spec:
selector:
matchLabels:
app: dcgm-exporter
template:
spec:
containers:
- name: dcgm-exporter
image: nvcr.io/nvidia/k8s/dcgm-exporter:3.2.5
ports:
- containerPort: 9400
securityContext:
privileged: true # 必须,要访问 GPU 设备
volumeMounts:
- name: dev
mountPath: /dev
volumes:
- name: dev
hostPath:
path: /dev
Prometheus 告警规则:
DCGM_FI_DEV_GPU_TEMP 才是指标(Metric),它是由 NVIDIA DCGM Exporter 采集并直接暴露给 Prometheus 的。
你写的这段代码是告警规则,它的逻辑是:
expr (表达式): 这是关键。它告诉 Prometheus:"去查一下所有 GPU 的温度,看看有没有大于 85 的。"
for: 5m: 这是一个过滤器,表示温度必须持续 5 分钟超过 85 度才会触发告警(防止尖峰瞬时误报)。
yaml
# 温度
- alert: GPU_Temp_High
expr: DCGM_FI_DEV_GPU_TEMP > 85
for: 5m
labels:
severity: warning
# ECC 增长
- alert: GPU_ECC_SBE_Growing
expr: increase(DCGM_FI_DEV_ECC_SBE_AGG_TOTAL[1h]) > 5
for: 10m
labels:
severity: warning
Grafana 面板:
Panel 1 PromQL: DCGM_FI_DEV_GPU_TEMP
→ 折线图,按节点+GPU编号分组,一眼看谁温度高
Panel 2 PromQL: increase(DCGM_FI_DEV_ECC_SBE_AGG_TOTAL[1h])
→ 折线图,正常是0平线,异常的卡会爬升
2.2 日常巡检层(每天凌晨 4 点)
指标1: GDR 模块 + NVLink 错误计数
指标2: PCIe 宽度(是否 x16)
链路:
cron(管理节点,4:00) → Ansible SSH → 每台GPU节点执行脚本
│
脚本输出结果
│
Ansible fetch 回管理节点
│
Python 汇总脚本解析
│
┌──────┴──────┐
↓ ↓
飞书群消息 HTML 报告
(异常才发) (存档可查)
Ansible playbook 精髓:
copy 模块的 content 参数把前两步 register 捕获的 stdout 拼到一起,写入目标主机上的 /tmp/check_{{ inventory_hostname }}.txt。
yaml
# daily_check.yml
- hosts: ib_nodes # A100/H20 节点组
become: yes
tasks:
- name: 检查 IB 端口状态
shell: |
for dev in mlx5_0 mlx5_1; do
STATE=$(ibstat $dev | grep "State:" | awk '{print $2}')
RATE=$(ibstat $dev | grep "Rate:" | awk '{print $2}')
echo "${dev}: State=${STATE} Rate=${RATE}"
[ "$STATE" != "Active" ] && echo "[CRITICAL] ${dev} is DOWN"
done
register: ib_result
- name: 检查 PCIe 宽度
shell: |
for dev in /dev/mst/mt*_pciconf0; do
WIDTH=$(mlxlink -d $dev --port_type PCIE 2>/dev/null | grep "Width" | awk '{print $NF}')
echo "${dev}: Width=${WIDTH}"
[ "$WIDTH" != "x16" ] && echo "[WARNING] PCIe degraded to ${WIDTH}"
done
register: pcie_result
- name: 保存结果
copy:
content: |
=== {{ inventory_hostname }} ===
{{ ib_result.stdout }}
{{ pcie_result.stdout }}
dest: "/tmp/check_{{ inventory_hostname }}.txt"
- name: 回收
fetch:
src: "/tmp/check_{{ inventory_hostname }}.txt"
dest: "/data/daily_reports/"
flat: yes
- hosts: management
tasks:
- name: 汇总发飞书
shell: |
cd /data/daily_reports/
# 提取异常
ISSUES=$(grep -h "\[CRITICAL\]\|\[WARNING\]" *.txt)
if [ -n "$ISSUES" ]; then
python3 /opt/scripts/send_feishu.py "巡检异常:\n${ISSUES}"
else
python3 /opt/scripts/send_feishu.py "巡检通过 ✓ 共检查 $(ls *.txt|wc -l) 台"
fi
cron 配置:
bash
# 管理节点 crontab
0 4 * * * ansible-playbook /opt/playbooks/daily_check.yml >> /var/log/daily_check.log 2>&1
飞书推送脚本精髓:
python
# send_feishu.py
import sys, requests, json
WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxx"
msg = sys.argv[1]
requests.post(WEBHOOK, json={
"msg_type": "text",
"content": {"text": f"[GPU集群巡检] {msg}"}
})
运维看到的飞书消息:
正常时:
[GPU集群巡检] 巡检通过 ✓ 共检查 112 台
异常时:
[GPU集群巡检] 巡检异常:
[CRITICAL] node-15 mlx5_1: State=Down
[WARNING] node-32 PCIe degraded to x8
2.3 深度体检层(每周日 / 验收 / 维修后)
跑带宽测试,不如nccl 和 dcgmi -r 3
Ansible 深度体检 Playbook
yaml
# deep_check.yml
# 用法:ansible-playbook deep_check.yml --limit node-15,node-16
# 前提:目标节点已 cordon + drain,无训练任务
- hosts: all
become: yes
vars:
report_dir: "/tmp/deep_check"
nccl_test_bin: "/opt/nccl-tests/build/all_reduce_perf"
p2p_test_bin: "/opt/cuda-samples/bin/p2pBandwidthLatencyTest"
busbw_threshold_ib: 350 # 节点内 NVLink busbw 期望最低值
busbw_threshold_roce: 100 # V100/4090 节点内期望最低值
tasks:
# ════════════════════════════════════════
# 0. 准备
# ════════════════════════════════════════
- name: 创建报告目录
file:
path: "{{ report_dir }}"
state: directory
- name: 确认没有 GPU 进程在跑
shell: nvidia-smi --query-compute-apps=pid --format=csv,noheader | wc -l
register: gpu_procs
failed_when: gpu_procs.stdout | int > 0
# 如果有进程在跑,直接 fail,防止影响训练
# ════════════════════════════════════════
# 1. dcgmi diag -r 3(核心:抓静默计算错误)
# ════════════════════════════════════════
- name: 深度 GPU 诊断 (dcgmi diag -r 3, 约30分钟)
shell: |
dcgmi diag -r 3 2>&1 | tee {{ report_dir }}/dcgmi_r3.log
register: dcgmi_result
timeout: 2400 # 40分钟超时
- name: 检查 dcgmi 结果
shell: |
if grep -q "FAIL" {{ report_dir }}/dcgmi_r3.log; then
FAILED_GPUS=$(grep "FAIL" {{ report_dir }}/dcgmi_r3.log)
echo "[CRITICAL] dcgmi diag FAILED: ${FAILED_GPUS}"
exit 1
else
echo "dcgmi diag -r 3: ALL PASSED ✓"
fi
register: dcgmi_check
# ════════════════════════════════════════
# 2. NVLink P2P 带宽(核心:抓链路衰退)
# ════════════════════════════════════════
- name: P2P 带宽测试
shell: |
{{ p2p_test_bin }} 2>&1 | tee {{ report_dir }}/p2p.log
register: p2p_result
when: "'4090' not in ansible_facts['hostname']"
# 4090 没有 NVLink,跳过
- name: 检查 P2P 带宽
shell: |
# 提取单向带宽矩阵中非对角线的最小值
MIN_BW=$(grep -A 100 "Unidirectional" {{ report_dir }}/p2p.log \
| grep "^[[:space:]]*[0-7]" \
| awk '{for(i=2;i<=NF;i++){if($i+0>0 && $i+0<min+0 || min==""){min=$i}}}END{print min}')
echo "P2P 最低带宽: ${MIN_BW} GB/s"
# A100 NVLink 期望 >200 GB/s
if (( $(echo "$MIN_BW < 200" | bc -l) )); then
echo "[WARNING] P2P 带宽偏低,可能有 NVLink 衰退"
exit 1
else
echo "P2P bandwidth: OK ✓"
fi
register: p2p_check
when: "'4090' not in ansible_facts['hostname']"
ignore_errors: yes
# ════════════════════════════════════════
# 3. 节点内 NCCL AllReduce(综合验证)
# ════════════════════════════════════════
- name: NCCL 节点内 AllReduce 测试
shell: |
{{ nccl_test_bin }} \
-b 1G -e 1G -g 8 -n 50 -w 10 \
2>&1 | tee {{ report_dir }}/nccl_intra.log
register: nccl_result
- name: 检查 NCCL busbw
shell: |
BUSBW=$(grep "1073741824" {{ report_dir }}/nccl_intra.log \
| awk '{print $7}' | head -1)
echo "节点内 AllReduce busbw: ${BUSBW} GB/s"
if (( $(echo "$BUSBW < {{ busbw_threshold_ib }}" | bc -l) )); then
echo "[WARNING] busbw ${BUSBW} 低于阈值 {{ busbw_threshold_ib }}"
exit 1
else
echo "NCCL busbw: OK ✓"
fi
register: nccl_check
ignore_errors: yes
# ════════════════════════════════════════
# 4. 显存带宽测试(检测 HBM 衰退)
# ════════════════════════════════════════
- name: 显存带宽测试
shell: |
for i in $(seq 0 7); do
/opt/cuda-samples/bin/bandwidthTest --device=$i --dtod \
2>&1 | grep "Device to Device" | tail -1
done | tee {{ report_dir }}/membw.log
register: membw_result
# ════════════════════════════════════════
# 5. IB 网卡眼图(PCIe 信号质量)
# ════════════════════════════════════════
- name: PCIe 眼图测试
shell: |
for dev in /dev/mst/mt*_pciconf0; do
echo "=== $dev ==="
mlxlink -d $dev --port_type PCIE -e 2>&1
done | tee {{ report_dir }}/eye.log
register: eye_result
when: "'mlxlink' in ansible_facts.packages | default([])"
ignore_errors: yes
# ════════════════════════════════════════
# 6. 生成汇总报告
# ════════════════════════════════════════
- name: 生成节点报告
shell: |
cat > {{ report_dir }}/summary.txt << 'SUMMARY'
╔══════════════════════════════════════════╗
║ 深度体检报告: {{ inventory_hostname }}
║ 时间: $(date)
╠══════════════════════════════════════════╣
║
║ 1. dcgmi diag -r 3: {{ dcgmi_check.stdout_lines[-1] }}
║ 2. P2P 带宽: {{ p2p_check.stdout_lines[-1] | default('SKIPPED') }}
║ 3. NCCL busbw: {{ nccl_check.stdout_lines[-1] }}
║ 4. 显存带宽: 见 membw.log
║ 5. PCIe 眼图: 见 eye.log
║
╠══════════════════════════════════════════╣
║ 总结:
SUMMARY
FAIL_COUNT=0
[ "{{ dcgmi_check.rc }}" != "0" ] && FAIL_COUNT=$((FAIL_COUNT+1))
[ "{{ p2p_check.rc | default(0) }}" != "0" ] && FAIL_COUNT=$((FAIL_COUNT+1))
[ "{{ nccl_check.rc }}" != "0" ] && FAIL_COUNT=$((FAIL_COUNT+1))
if [ $FAIL_COUNT -eq 0 ]; then
echo "║ ✅ ALL PASSED" >> {{ report_dir }}/summary.txt
else
echo "║ ❌ $FAIL_COUNT 项异常" >> {{ report_dir }}/summary.txt
fi
echo "╚══════════════════════════════════════════╝" >> {{ report_dir }}/summary.txt
cat {{ report_dir }}/summary.txt
# ════════════════════════════════════════
# 7. 回收报告
# ════════════════════════════════════════
- name: 回收报告到管理节点
fetch:
src: "{{ report_dir }}/{{ item }}"
dest: "/data/deep_reports/{{ inventory_hostname }}/"
flat: yes
loop:
- summary.txt
- dcgmi_r3.log
- nccl_intra.log
- p2p.log
- membw.log
- eye.log
调用上面的 yaml
bash
#!/bin/bash
# run_deep_check.sh
# 滚动深度体检入口脚本
BATCH_SIZE=2
NODES=$(kubectl get nodes -l nvidia.com/gpu=true -o jsonpath='{.items[*].metadata.name}')
echo "开始滚动深度体检,共 $(echo $NODES | wc -w) 台节点,每批 $BATCH_SIZE 台"
for BATCH in $(echo $NODES | xargs -n $BATCH_SIZE); do
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "当前批次: $BATCH"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# 1. cordon(禁止新任务调度)
for N in $BATCH; do
kubectl cordon $N
echo " cordon $N ✓"
done
# 2. drain(驱逐现有 Pod)
for N in $BATCH; do
kubectl drain $N \
--ignore-daemonsets \
--delete-emptydir-data \
--timeout=600s \
--grace-period=120
echo " drain $N ✓"
done
# 3. Ansible 跑深度体检
LIMIT=$(echo $BATCH | tr ' ' ',')
ansible-playbook deep_check.yml --limit "$LIMIT"
# 4. 根据结果决定是否放回
for N in $BATCH; do
REPORT="/data/deep_reports/${N}/summary.txt"
if grep -q "ALL PASSED" "$REPORT"; then
kubectl uncordon $N
echo " ✅ $N 通过,已 uncordon"
else
echo " ❌ $N 未通过,保持 cordon,通知运维"
python3 /opt/scripts/send_feishu.py \
"⚠️ 深度体检未通过: $N\n$(cat $REPORT)"
fi
done
# 5. 间隔等集群稳定
sleep 30
done
echo ""
echo "全部检完,汇总报告:"
python3 /opt/scripts/generate_html_report.py /data/deep_reports/
执行时间线:
02:00 脚本启动
02:01 cordon node-0, node-1
02:05 drain 完成(Pod 迁走)
02:06 Ansible 开始体检
02:40 dcgmi diag -r 3 完成(30分钟)
02:45 nccl-tests + p2p 完成(5分钟)
02:46 结果判断,uncordon 或保持
02:47 开始下一批 node-2, node-3
...
周日下午 112 台全部检完
自动 cordon + drain
实时监控: ✅ 需要自动 cordon+drain
日常巡检: ❌ 不需要自动,人工判断后手动操作
因为日常巡检的任务还能用
→ 贸然自动 drain 可能打断一个跑了 3 天的训练任务
→ 损失比"慢一点"大得多
自动 Cordon+Drain 完整链路
bash
GPU 硬件故障
│
▼
DCGM API 检测到异常
│
▼
dcgm-exporter 暴露指标(:9400/metrics)
│
▼
Prometheus 抓取(每 30 秒)
│
▼
Alerting Rule 触发(PromQL 判断)
│
▼
AlertManager 收到告警
│
├──→ 飞书通知运维(通知人知道)
│
└──→ Webhook 调用自动修复服务(自动止损)
│
▼
auto-remediation 服务
│
├── kubectl cordon <node>(禁止新任务调度)
├── kubectl drain <node>(驱逐现有 Pod)
└── 打标签记录原因和时间
Auto-Remediation 服务:参数传递全链条
完整链条
Step 1: GPU 硬件故障发生
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
node-23 的 GPU5 发生 ECC DBE 错误
DCGM API 读到:
GPU index=5, DBE count 从 0 变成 1
│
│ dcgm-exporter 每 30 秒采集一次
▼
Step 2: dcgm-exporter 暴露指标
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
curl node-23:9400/metrics 返回:
DCGM_FI_DEV_ECC_DBE_VOL_TOTAL{
gpu="5", ← 哪张卡
UUID="GPU-abc123",
Hostname="node-23", ← 哪台机器(★ 后面一路传下去)
pod="llama3-worker-3",
namespace="team-nlp"
} 1
│
│ Prometheus 每 30 秒来 Pull 一次
▼
Step 3: Prometheus 抓取并执行告警规则
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
告警规则:
expr: increase(DCGM_FI_DEV_ECC_DBE_VOL_TOTAL[2m]) > 0
Prometheus 算了一下:
node-23 GPU5 的 DBE 在 2 分钟内从 0 增长到 1
increase = 1 > 0 → 命中!
生成一条 Alert 对象,里面的字段来自规则定义:
labels:
alertname: "GPU_ECC_DBE_AutoCordon"
severity: "critical"
auto_action: "cordon_drain" ← 规则里写死的标签
gpu: "5" ← 从指标 label 继承
Hostname: "node-23" ← 从指标 label 继承
annotations:
node: "node-23" ← 规则里用模板 {{ $labels.Hostname }} 填充
reason: "ECC DBE detected on GPU5" ← 规则里用模板拼的字符串
│
│ Prometheus 把 Alert 推给 AlertManager
▼
Step 4: AlertManager 路由分发
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AlertManager 收到 Alert,看 labels 做路由:
auto_action == "cordon_drain" → 匹配到 receiver: auto-remediation
severity == "critical" → 同时匹配到 receiver: urgent-feishu
(因为 continue: true,两个都触发)
分发动作 1:POST 到自动修复服务
URL: http://auto-remediation-svc:8080/alert
Body(AlertManager 标准格式):
{
"alerts": [
{
"status": "firing",
"labels": {
"alertname": "GPU_ECC_DBE_AutoCordon",
"auto_action": "cordon_drain",
"Hostname": "node-23",
"gpu": "5"
},
"annotations": {
"node": "node-23", ← ★ 节点名在这里
"reason": "ECC DBE detected on GPU5" ← ★ 原因在这里
}
}
]
}
分发动作 2:POST 到飞书 webhook
→ 运维群收到消息
│
│ HTTP POST
▼
Step 5: auto-remediation 服务处理
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Flask 服务收到 POST 请求
解析参数:
alert['status'] → "firing"(正在发生,不是 resolved)
alert['labels']['auto_action'] → "cordon_drain"(确认需要自动处置)
alert['annotations']['node'] → "node-23"(★ 拿到节点名)
alert['annotations']['reason'] → "ECC DBE detected on GPU5"(★ 拿到原因)
限流检查:
过去 1 小时已 cordon 几台?
< 3 台 → 继续
≥ 3 台 → 暂停,发紧急飞书通知人工介入
│
│ 通过限流检查
▼
Step 6: 执行 Cordon
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
调用 K8s API:
v1.patch_node("node-23", {
"spec": {"unschedulable": true}, ← 禁止新 Pod 调度到这台
"metadata": {
"labels": {
"gpu-health": "failed", ← ★ 审计标签1
"cordon-reason": "ECC DBE detected on GPU5", ← ★ 审计标签2
"cordon-time": "20250110-143052" ← ★ 审计标签3
}
}
})
效果:
node-23 被标记为 SchedulingDisabled
新的训练任务不会再被调度到这台机器
已在跑的 Pod 暂时还在(下一步驱逐)
│
▼
Step 7: 执行 Drain(驱逐 Pod)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
列出 node-23 上所有 Pod:
v1.list_namespaced_pod(field_selector="spec.nodeName=node-23")
逐个判断:
pod=dcgm-exporter owner=DaemonSet → 跳过(监控不能停)
pod=fluentd owner=DaemonSet → 跳过(日志采集不能停)
pod=kube-proxy namespace=kube-system → 跳过(系统组件)
pod=llama3-worker-3 owner=Job → 驱逐!
驱逐:
v1.create_namespaced_pod_eviction("llama3-worker-3", "team-nlp", {
grace_period_seconds: 120 ← 给 2 分钟保存 checkpoint
})
效果:
llama3-worker-3 收到 SIGTERM
训练框架捕获信号 → 保存紧急 checkpoint → 退出
K8s 调度器把这个 Pod 重新调度到其他健康节点
训练从 checkpoint 恢复继续跑
│
▼
Step 8: 完成,等待人工修复
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
node-23 当前状态:
Status: Ready,SchedulingDisabled
Labels:
gpu-health=failed
cordon-reason=ECC DBE detected on GPU5
cordon-time=20250110-143052
没有任何训练 Pod 在跑
只剩 DaemonSet 的监控/日志 Pod
等运维来修
审计日志:存在哪里,怎么看
存储位置有两层:
第一层:Node Label(最直观)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
查看所有被自动下线的节点:
kubectl get nodes -l gpu-health=failed
NAME STATUS LABELS
node-23 Ready,SchedulingDisabled gpu-health=failed
cordon-reason=ECC DBE detected on GPU5
cordon-time=20250110-143052
node-47 Ready,SchedulingDisabled gpu-health=failed
cordon-reason=XID 79 GPU fell off bus
cordon-time=20250108-092011
→ 一条命令看到:哪些节点坏了、什么原因、什么时候下线的
第二层:服务日志(详细过程)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
auto-remediation 服务的标准输出(被 K8s 采集):
kubectl logs deployment/auto-remediation -n monitoring
2025-01-10 14:30:52 INFO 收到自动处置告警: node=node-23 reason=ECC DBE detected on GPU5
2025-01-10 14:30:52 INFO 限流检查: 过去1小时已cordon 0 台,允许继续
2025-01-10 14:30:53 INFO cordon node-23 ✓
2025-01-10 14:30:53 INFO evicted team-nlp/llama3-worker-3
2025-01-10 14:30:53 INFO drain node-23 完成
这些日志通过 fluentd → Elasticsearch → Kibana 可长期查询
第三层:飞书消息(人人可见)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
运维群收到:
⚠️ [自动处置] node-23 已自动 cordon+drain
原因:ECC DBE detected on GPU5
时间:2025-01-10 14:30:52
影响 Pod:team-nlp/llama3-worker-3
请安排维修后手动 uncordon
修复后放回流程
运维修完后:
1. 确认修复
dcgmi diag -r 3 → PASS
nccl-tests busbw → 正常
2. 清除标签 + uncordon
kubectl label node node-23 gpu-health- cordon-reason- cordon-time-
kubectl uncordon node-23
3. 节点重新接受任务调度