GPU node health check runbook
Daily, weekly, and pre-deploy health checks for GPU nodes — quick (10 min), deep (1 hour), and bring-up (full burn-in). Includes a cron-able bash script for the quick check that catches drift before customers do.
help for the full list, or solutions for copy-paste fix recipes.A healthy GPU cluster looks healthy because someone proactively keeps it healthy. The choice is between finding a bad node during a planned health check and finding a bad node when a customer's training job dies at step 47000 of 50000. The first costs you a 10-minute cordon; the second costs them a checkpoint restart and you a postmortem.
This page is the canonical schedule of checks. Three tiers — quick, deep, and pre-deploy — by frequency. Each tier has a defined scope, a runtime budget, and an expected outcome. The quick check is meant to be cron'd; the deep check is a planned weekly or monthly job; the pre-deploy is a one-shot before a node enters the production pool.
Schedule overview
| Tier | Frequency | Runtime | Disruption | What it catches |
|---|---|---|---|---|
| Quick | Daily (cron, every node) | ~10 min | None (read-only) | Driver / module / link state drift, dmesg errors, ECC creep, disk fill |
| Deep | Weekly (rolling, drained nodes) | ~1 hour | Drains the node | NVLink degradation, NCCL bandwidth, perftest BW, fio storage health |
| Pre-deploy | Before adding to prod pool | ~4-8 hours | Full burn-in | Hardware burn-in, network end-to-end, K8s + Slurm sanity |
The quick check should be running on every node every day. Drift is the most common cause of "the cluster used to work and now doesn't" — a kernel module that didn't auto-load after a reboot, a disk that filled up overnight, an ECC counter that crept up. Catching drift daily prevents incidents.
Quick check (~10 min, read-only)
Cron'd, runs on every node. Output goes to a log file and (ideally) to a metrics system that pages on regression.
What's in it
nvidia-smi— driver alive, all GPUs visible, ECC clean, persistence on.dcgmi diag -r 1— quick health (~30 s; see DCGM).ibstat— all HCA ports Active at expected rate.lsmod | grep nvidia_peermem— peermem loaded.kubectl get node $(hostname)— Ready, no taints we don't expect.- Disk space on
/,/var/lib/containerd,/var/lib/kubelet— under 80%. dmesg --since '24 hour ago' | grep -iE 'error|xid|nvlink|fault'— no recent kernel errors.systemctl --failed— no failed units.
The script
Save as /usr/local/sbin/gpu_quickcheck.sh. Add a cron entry: 0 */6 * * * /usr/local/sbin/gpu_quickcheck.sh.
#!/usr/bin/env bash
# gpu_quickcheck.sh — daily/6-hourly health check on a GPU node.
# Exits 0 on healthy, 1 on warning, 2 on critical.
# Logs to /var/log/gpu_quickcheck.log; ship to your monitoring backend.
set -uo pipefail
LOG=/var/log/gpu_quickcheck.log
NODE=$(hostname)
EXPECTED_GPUS=${EXPECTED_GPUS:-8}
EXPECTED_HCAS=${EXPECTED_HCAS:-8}
EXPECTED_RATE=${EXPECTED_RATE:-400} # Gb/s
DISK_THRESHOLD=${DISK_THRESHOLD:-80} # %
EXIT=0
warn() { echo "$(date -Is) WARN $NODE $*" | tee -a "$LOG"; [[ $EXIT -lt 1 ]] && EXIT=1; }
fail() { echo "$(date -Is) CRIT $NODE $*" | tee -a "$LOG"; EXIT=2; }
info() { echo "$(date -Is) INFO $NODE $*" | tee -a "$LOG"; }
info "=== quickcheck start ==="
# ─── 1. nvidia-smi ────────────────────────────────────────────────────────
if ! command -v nvidia-smi >/dev/null; then
fail "nvidia-smi not found"
elif ! nvidia-smi >/dev/null 2>&1; then
fail "nvidia-smi failed (driver/library mismatch?)"
else
GPU_COUNT=$(nvidia-smi -L | wc -l)
[[ $GPU_COUNT -eq $EXPECTED_GPUS ]] || fail "expected $EXPECTED_GPUS GPUs, got $GPU_COUNT"
# ECC: any uncorrectable aggregate errors?
UNCORR=$(nvidia-smi --query-gpu=ecc.errors.uncorrected.aggregate.total \
--format=csv,noheader,nounits 2>/dev/null | awk '$1 > 0' | wc -l)
[[ $UNCORR -eq 0 ]] || fail "uncorrectable ECC on $UNCORR GPU(s)"
# Persistence mode
PM=$(nvidia-smi --query-gpu=persistence_mode --format=csv,noheader \
| sort -u | tr -d '\n')
[[ "$PM" == "Enabled" ]] || warn "persistence mode not enabled ($PM)"
# Temperature
HOT=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits \
| awk '$1 > 85' | wc -l)
[[ $HOT -eq 0 ]] || warn "$HOT GPU(s) above 85°C"
fi
# ─── 2. dcgmi diag -r 1 ───────────────────────────────────────────────────
if command -v dcgmi >/dev/null; then
if dcgmi diag -r 1 2>&1 | tee /tmp/dcgmi.r1.$$ | grep -qE '^\s*Fail' ; then
fail "dcgmi diag -r 1 reported failures"
grep -E 'Fail' /tmp/dcgmi.r1.$$ | head -5 >> "$LOG"
fi
rm -f /tmp/dcgmi.r1.$$
else
warn "dcgmi not installed"
fi
# ─── 3. ibstat — all HCA ports Active ─────────────────────────────────────
if command -v ibstat >/dev/null; then
ACTIVE=$(ibstat | awk '/State:/ && $2 == "Active"' | wc -l)
[[ $ACTIVE -ge $EXPECTED_HCAS ]] || fail "only $ACTIVE HCA ports Active (expected $EXPECTED_HCAS)"
# rate check
SLOW=$(ibstat | awk -v r=$EXPECTED_RATE '/Rate:/ && $2 < r' | wc -l)
[[ $SLOW -eq 0 ]] || warn "$SLOW HCA(s) below expected rate $EXPECTED_RATE Gb/s"
else
warn "ibstat not installed"
fi
# ─── 4. peermem loaded ────────────────────────────────────────────────────
lsmod | grep -q '^nvidia_peermem' || fail "nvidia_peermem not loaded"
# ─── 5. node Ready in K8s ─────────────────────────────────────────────────
if command -v kubectl >/dev/null; then
READY=$(kubectl get node "$NODE" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null)
[[ "$READY" == "True" ]] || warn "kubectl reports node not Ready: $READY"
fi
# ─── 6. disk space ────────────────────────────────────────────────────────
for mount in / /var/lib/containerd /var/lib/kubelet; do
[[ -d $mount ]] || continue
USE=$(df -P "$mount" | awk 'NR==2 {sub("%",""); print $5}')
if [[ $USE -ge 95 ]]; then fail "$mount $USE% full"
elif [[ $USE -ge $DISK_THRESHOLD ]]; then warn "$mount $USE% full"
fi
done
# ─── 7. recent kernel errors ──────────────────────────────────────────────
RECENT_ERR=$(dmesg --since '24 hour ago' 2>/dev/null \
| grep -iE 'xid|nvlink|fatal|machine check|fabric manager' | wc -l)
[[ $RECENT_ERR -eq 0 ]] || warn "$RECENT_ERR recent kernel errors in dmesg"
# ─── 8. failed systemd units ──────────────────────────────────────────────
FAILED=$(systemctl --failed --no-legend | wc -l)
[[ $FAILED -eq 0 ]] || warn "$FAILED failed systemd units"
info "=== quickcheck end (exit $EXIT) ==="
exit $EXIT
For Prometheus integration, wrap this in a textfile collector or a node-exporter custom check.
What to alert on
| Quickcheck output | Alert? |
|---|---|
| Exit code 2 (CRIT) | Page on-call |
| Exit code 1 (WARN) repeated 3 times in a row | Investigate during business hours |
| GPU count short | Page (someone yanked a GPU or one died) |
| Uncorrectable ECC | Page; cordon node, schedule RMA |
| HCA port not Active | Page; check fabric |
nvidia_peermem not loaded | Page; GDR is dead |
| Disk > 95% | Page; will start OOM-killing soon |
| Disk > 80% | Ticket; clean up images |
Deep check (~1 hour, drains the node)
Weekly or monthly, on a rolling drain. Costs you GPU-hours, but it's the only way to catch latent issues like a slow-growing transceiver eye-degradation or a flaky NVLink that only fails under sustained load.
What's in it
dcgmi diag -r 3— full memory + bandwidth + NVLink test (~30 min). See DCGM.- nccl-tests
all_reduce_perf2-node with this and a known-good neighbor. ib_send_bw --use_cudaacross all 8 HCAs paired against a known-good node.fioagainst local NVMe and against shared FS (Weka, Lustre, etc.).- GPU thermal sweep — sustained 100% utilization for 30 min, watch temps.
Runbook
#!/usr/bin/env bash
# gpu_deepcheck.sh — weekly deep check, run on a drained node.
# Requires: kubectl drain has already been run; there's a peer node available.
# Usage: gpu_deepcheck.sh <peer-hostname>
set -euo pipefail
NODE=$(hostname)
PEER=${1:?usage: $0 <peer-hostname>}
WORK=/var/tmp/deepcheck.$NODE.$(date +%Y%m%d-%H%M%S)
mkdir -p $WORK
cd $WORK
echo "=== deepcheck start: $NODE vs $PEER ==="
echo "logs in: $WORK"
# 1. dcgmi diag -r 3
echo "[1/5] dcgmi diag -r 3"
dcgmi diag -r 3 -j > dcgmi.r3.json
grep -q '"status":\s*"Fail"' dcgmi.r3.json && { echo "FAIL: dcgmi -r 3 failed"; exit 1; }
# 2. nccl-tests 2-node
echo "[2/5] nccl-tests all_reduce_perf 2-node"
mpirun -np 16 -H "$NODE:8,$PEER:8" -bind-to none -map-by slot \
-x NCCL_DEBUG=WARN \
-x NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_2,mlx5_3,mlx5_8,mlx5_9,mlx5_10,mlx5_11 \
-x NCCL_SOCKET_IFNAME=eth0 \
/opt/nccl-tests/build/all_reduce_perf -b 1G -e 4G -f 2 -g 1 \
> nccl.out 2>&1
busbw=$(awk '/^[ ]+1073741824/ { print $11; exit }' nccl.out)
awk -v bw=$busbw 'BEGIN { if (bw < 320) { print "FAIL: 2-node busbw too low: " bw; exit 1 } }'
echo " -> 2-node busbw: $busbw GB/s"
# 3. ib_send_bw across all 8 HCAs
echo "[3/5] ib_send_bw across all 8 HCAs"
for hca_idx in 0 1 2 3 8 9 10 11; do
hca="mlx5_${hca_idx}"
cuda_dev=$([[ $hca_idx -lt 4 ]] && echo $hca_idx || echo $((hca_idx - 4)))
# peer-side launch first (assumes ssh + already-installed perftest):
ssh "$PEER" "ib_send_bw -d $hca --use_cuda=$cuda_dev -F -R --report_gbits" > /dev/null &
sleep 1
ib_send_bw -d "$hca" --use_cuda="$cuda_dev" -F -R --report_gbits "$PEER" \
> "ib_send_bw.$hca.out" 2>&1 || { echo "FAIL: ib_send_bw on $hca"; exit 1; }
wait
bw=$(awk '/^[ ]+8388608/ { print $4; exit }' "ib_send_bw.$hca.out")
echo " -> $hca: $bw Gb/s"
awk -v bw=$bw 'BEGIN { if (bw < 350) { print "FAIL: $hca BW too low: " bw; exit 1 } }'
done
# 4. fio
echo "[4/5] fio against local NVMe"
fio --name=randread --filename=/var/lib/fio.test --size=10G \
--rw=randread --bs=4k --iodepth=32 --runtime=60 --direct=1 \
--output-format=json > fio.local.json 2>&1
rm -f /var/lib/fio.test
# Optional: against shared FS, e.g. /mnt/weka
[[ -d /mnt/weka ]] && fio --name=randread --filename=/mnt/weka/fio.test.$NODE \
--size=10G --rw=randread --bs=4k --iodepth=32 --runtime=60 --direct=1 \
--output-format=json > fio.weka.json 2>&1 && rm -f /mnt/weka/fio.test.$NODE
# 5. thermal sweep — sustained 100% util
echo "[5/5] thermal sweep (30 min)"
nvidia-smi -q -d POWER,TEMPERATURE > thermal.before.txt
# kick off compute load — gpu-burn or any sustained kernel
for i in 0 1 2 3 4 5 6 7; do
CUDA_VISIBLE_DEVICES=$i /opt/gpu-burn/gpu_burn 1800 > /tmp/burn.$i.log 2>&1 &
done
sleep 1700 # let it run for ~28 min
nvidia-smi -q -d POWER,TEMPERATURE > thermal.during.txt
wait
# Check that no GPU exceeded 90°C during the run
HOT=$(grep "GPU Current Temp" thermal.during.txt | awk '$5 > 90' | wc -l)
[[ $HOT -eq 0 ]] || { echo "WARN: $HOT GPU(s) exceeded 90°C during sweep"; }
echo "=== deepcheck PASS ==="
Expected outputs (sanitized)
[1/5] dcgmi diag -r 3
-> all subsystems Pass
[2/5] nccl-tests all_reduce_perf 2-node
-> 2-node busbw: 363.1 GB/s
[3/5] ib_send_bw across all 8 HCAs
-> mlx5_0: 388.4 Gb/s
-> mlx5_1: 387.9 Gb/s
-> mlx5_2: 388.2 Gb/s
-> mlx5_3: 387.5 Gb/s
-> mlx5_8: 388.1 Gb/s
-> mlx5_9: 387.8 Gb/s
-> mlx5_10: 388.0 Gb/s
-> mlx5_11: 387.6 Gb/s
[4/5] fio against local NVMe
-> 1.2M IOPS @ 4k randread
[5/5] thermal sweep (30 min)
-> max GPU temp: 78°C (within budget)
=== deepcheck PASS ===
If any HCA shows < 350 Gb/s on a 400-link, dig into RDMA debugging. Mlxlink eye-opening is the next read.
Pre-deploy check (~4-8 hours, full burn-in)
Run before adding a node to the production pool. The cost of catching a problem here is one engineer-day; the cost of catching it after the node has run customer workloads is much higher.
Checklist
| Phase | Test | Pass criteria |
|---|---|---|
| Hardware | nvidia-bug-report.sh baseline | No new XIDs |
| Hardware | dcgmi diag -r 4 (extended, 1-2 hours) | All Pass |
| Hardware | gpu-burn 4-hour sustained | No GPU drops, no thermal throttling |
| Hardware | NVMe burn-in (fio 1-hour mix) | Stable IOPS, temps in budget |
| Hardware | Memory burn-in (memtester 1 GB × 4 hours) | No errors |
| Network | All 8 HCAs ib_send_bw against multiple peers | All > 95% line rate |
| Network | mlxlink -m -e -c per HCA | No physical errors, eye openings ≥ 20 mV |
| Network | nccl-tests 2-node, 4-node, 8-node | busbw within 5% of cluster baseline |
| K8s | Cordon test | Drains cleanly, pods reschedule |
| K8s | nvidia.com/gpu resource advertised | kubectl describe node shows 8 |
| K8s | rdma/hca resource advertised (if using IB device-plugin) | Resource present |
| K8s | Test pod requests 8 GPUs, runs nvidia-smi | All 8 visible inside pod |
| K8s | Test pod with /dev/infiniband access runs ibv_devinfo | All HCAs visible |
| Slurm | Submit a partition test job | Lands on node, runs, exits 0 |
| Slurm | NCCL training job under SLURM with this node | busbw matches expectations |
| Operational | Logs shipping to central collector | Logs visible in dashboard |
| Operational | Metrics shipping (DCGM, node-exporter) | Metrics visible in Prometheus |
| Operational | gpu_quickcheck.sh exits 0 | "I'd page on this" check passes |
Anything failing the pre-deploy check → fix or RMA before the node leaves the staging pool. Don't put a node into production "to see if it works" — the production cluster is not your staging environment.
Sample pre-deploy validation script (sketch)
#!/usr/bin/env bash
# gpu_predeploy.sh — full burn-in. Run on a drained, cordoned node not yet in production pool.
# Expected runtime: 4-8 hours.
set -euo pipefail
NODE=$(hostname)
WORK=/var/tmp/predeploy.$NODE.$(date +%Y%m%d-%H%M%S)
mkdir -p $WORK; cd $WORK
# Capture baseline
nvidia-bug-report.sh
# 1. dcgmi diag -r 4 (extended, ~1-2h)
dcgmi diag -r 4 -j > dcgmi.r4.json
# 2. gpu-burn 4 hours
for i in 0 1 2 3 4 5 6 7; do
CUDA_VISIBLE_DEVICES=$i /opt/gpu-burn/gpu_burn 14400 > burn.$i.log 2>&1 &
done
wait
# 3. fio NVMe burn-in
fio --name=mix --filename=/var/lib/fio.predeploy --size=100G \
--rw=randrw --rwmixread=70 --bs=4k --iodepth=64 --runtime=3600 --direct=1 \
--output-format=json > fio.local.json
rm -f /var/lib/fio.predeploy
# 4. memtester 4h on a chunk of host RAM
memtester 1G 4 > memtester.log 2>&1
# 5. ib_send_bw against multiple peers (delegated to deepcheck)
for peer in $PEER_NODES; do
/usr/local/sbin/gpu_deepcheck.sh "$peer" || exit 1
done
# 6. K8s test pod
cat > test-pod.yaml <<EOF
apiVersion: v1
kind: Pod
metadata: {name: predeploy-test-$NODE, namespace: validation}
spec:
nodeSelector: {kubernetes.io/hostname: $NODE}
restartPolicy: Never
containers:
- name: t
image: nvcr.io/nvidia/cuda:12.4.0-devel-ubuntu22.04
command: ["bash", "-c", "nvidia-smi && ibv_devinfo"]
resources: {limits: {nvidia.com/gpu: 8}}
EOF
kubectl apply -f test-pod.yaml
kubectl wait pod -n validation predeploy-test-$NODE --for=condition=Ready --timeout=120s
kubectl logs -n validation predeploy-test-$NODE
kubectl delete -f test-pod.yaml
# 7. quickcheck (must pass)
/usr/local/sbin/gpu_quickcheck.sh
echo "=== PASS: $NODE ready for production pool ==="
Specific GPU thermal sweep notes
Sustained 100% util for 30+ min is the standard thermal stress. What you're looking for:
| GPU | Idle temp | Loaded temp budget | Hot but OK | Bad |
|---|---|---|---|---|
| A100 SXM | 30-40 °C | < 80 °C | 80-90 °C | > 90 °C |
| H100 SXM | 30-45 °C | < 80 °C | 80-87 °C | > 87 °C |
| H200 SXM | 30-45 °C | < 80 °C | 80-87 °C | > 87 °C |
| B200 SXM | 35-50 °C | < 85 °C | 85-90 °C | > 90 °C |
What "bad" really means: thermal throttling kicked in, sustained throughput is silently lower than rated. The job runs but the busbw / FLOPS will be 10-20% below baseline. Detect with:
# during the sweep:
nvidia-smi --query-gpu=temperature.gpu,clocks.current.sm,clocks.max.sm \
--format=csv,noheader,nounits -lms 1000 \
| awk -F',' '$2 < $3 - 100 { print "throttled" }'
If clocks.current.sm is sustained below clocks.max.sm while the sweep is at 100% util, the GPU is throttling. Cooling problem (fan curve, dust, ambient temp) or PSU droop.
Pre-cron checklist
Before turning any of this loose:
- The script is checked into the cluster's automation repo, not just
/usr/local/sbin/on every node. - Every node has a recent (< 30 day) successful pre-deploy log archived.
- Quickcheck output goes to a central log + metrics system.
- Alerts are wired to your paging system, not just email.
- The on-call engineer can find the script and the alerts within 60 seconds.
See also
- Triage decision tree — when a check fails
- Incident response — when a failure becomes an incident
- NCCL multi-node failures — deep-check NCCL component
- RDMA debugging — deep-check fabric component
- DCGM —
dcgmi diagreference - K8s GPU pod failures — pre-deploy K8s sanity
External:
- gpu-burn: github.com/wilicc/gpu-burn
- nccl-tests: github.com/NVIDIA/nccl-tests
- perftest: github.com/linux-rdma/perftest
- nvidia-bug-report.sh: ships with the NVIDIA driver