Node-smoke test instruction#
A lightweight, distributed-rendezvous-free preflight check that runs on every node in parallel under SLURM. It produces a single PASS/FAIL verdict per node plus SLURM-ready passing_nodes.txt / failing_nodes.txt you can pipe straight into srun --nodelist= / --exclude=.
Use it to screen a cluster fast and exclude bad nodes before launching a real training job. A bad GPU, NIC, wedged driver, or leaked process on any node surfaces as a node FAIL — without a single global rendezvous, so a stuck node can’t wedge its peers.
Recommended launcher:
runner/primus-cli slurm srun -- direct -- node_smoke ...(auto-resolves the distributed env, appliesslurm.*config defaults, same pattern astrain/benchmark). The shorterrunner/primus-cli direct -- node_smoke ...(baresrun+direct) is equivalent and handy for ad-hoc runs.Companion tool:
preflight— the heavier diagnostic with a global rendezvous and inter-node bandwidth tests. The recommended workflow is node-smoke first, preflight second (see §10).
1. What it does#
Node-smoke answers one question fast: “which nodes are healthy enough to run anything?” Because training jobs allocate whole nodes, a single degraded GPU (or NIC, or wedged driver) takes an entire node out of rotation. Node-smoke checks each node independently and emits a per-node verdict plus a ready-to-use exclude list, so you can prune broken nodes before committing a large job to a global rendezvous.
It deliberately does not measure cross-node bandwidth — that’s what preflight is for.
2. How it works#
Per-node and independent — every node runs the checks on its own. No
MASTER_ADDR, noMASTER_PORT, no globaltorch.distributedrendezvous, so a stuck node cannot wedge its peers.Per-GPU isolation — each GPU’s checks run in their own Python subprocess with a hard timeout (
--per-gpu-timeout-sec, default 15 s). A stucktorch.cuda.set_device()(whichsignal.alarmcannot interrupt because it sits inside a driver syscall) isSIGKILL’d from the parent without affecting the rest of the node’s checks.Local-only RCCL — the optional Tier 2 all-reduce uses
torch.multiprocessing.spawnovertcp://127.0.0.1. No cross-node communication.Rank-0 aggregation —
NODE_RANK==0polls for the expected number of per-node JSONs (with a timeout), computes cluster-wide drift, writes the Markdown report + pass/fail lists, and returns non-zero if any node FAILs or never reports.
3. Prerequisites#
Prerequisite |
How |
|---|---|
Python venv on a shared filesystem |
Same venv used by |
|
|
Inside an existing SLURM allocation |
One task per node. Recommended: |
No MASTER_ADDR, no MASTER_PORT, no global rendezvous required.
4. Quick start#
Git clone the Primus repository to a shared filesystem that all nodes can read.
git clone --recurse-submodules https://github.com/AMD-AGI/Primus.git
cd Primus
Note: remember to set up the Python virtual environment and NCCL / fabric environment variables as described in §3 Prerequisites.
⚠ Set the NCCL / RCCL environment first if you plan to run with
--tier2-perf(the local 8-GPU RCCL all-reduce). Even though the smoke test never opens a cross-node rendezvous, the Tier 2 RCCL step callsdist.init_process_group(backend="nccl", ...), and RCCL enumerates every transport at init (XGMI / PCIe P2P + IB + sockets). A misconfiguredNCCL_IB_HCA/NCCL_SOCKET_IFNAME/NCCL_IB_GID_INDEXcan stall init or make the all-reduce silently fall back to a slow path. The launcher’sbase_env.shauto-detects these, but auto-detect sometimes picks the wrong values inside a container (devices masked by the network namespace, frontend NICs picked up instead of fabric NICs, etc.), so check them and set them explicitly if auto-detection is wrong.Minimum-viable checklist before running with
--tier2-perf:# Pin the RDMA / RoCE training NICs the container can actually see. # On a bare-metal host the auto-detect in base_env.sh usually picks # the right set; inside a container or on a multi-role node, list # them explicitly. Use the same set you would pass to a training job. export NCCL_IB_HCA="rdma0,rdma1,rdma2,rdma3,rdma4,rdma5,rdma6,rdma7" # Pick the RoCE v2 GID index for your fabric: # - Mellanox / Broadcom: typically 3 (base_env.sh default). # - Pensando Pollara (AINIC): 1. export NCCL_IB_GID_INDEX=3 # The bootstrap socket interface. Auto-detect prefers the first # non-loopback interface from `hostname -I`; override when that # picks a frontend NIC instead of the data-plane interface. export NCCL_SOCKET_IFNAME=eno0 export GLOO_SOCKET_IFNAME=eno0See
preflight-without-container.md§4 Cluster-specific NCCL configuration for the canonical Broadcom / Pensando Pollara values (the sameNCCL_*set is used by both tools). If you skip--tier2-perf, the RCCL step is not executed and none of the above applies — Tier 1 (host limits, RDMA roll-call, leaked-process detection, etc.) does not depend on RCCL.Quick verification:
runner/primus-cli direct --dry-run -- node_smoke --tier2-perfprints the resolvedNCCL_*block under “Environment Variables” so you can confirm the values before launching for real.
Recommended — through the primus-cli slurm srun wrapper (auto-resolves MASTER_ADDR/NNODES/NODE_RANK, applies slurm.* config defaults):
export VENV_ACTIVATE=~/envs/preflight/.venv/bin/activate
# Basic Tier 1 check (~5 s/GPU, ~30 s total)
runner/primus-cli slurm srun -N "$SLURM_NNODES" --ntasks-per-node=1 \
-- direct -- node_smoke
# Tier 1 + Tier 2 perf sanity (GEMM TFLOPS, HBM GB/s, local 8-GPU RCCL)
runner/primus-cli slurm srun -N "$SLURM_NNODES" --ntasks-per-node=1 \
-- direct -- node_smoke --tier2-perf
# Then re-run training, excluding any node the smoke test failed:
srun --exclude=$(paste -sd, output/preflight/failing_nodes.txt) ... your-real-job
Equivalent with bare srun (works the same; useful when composing with custom srun flags):
srun -N "$SLURM_NNODES" --ntasks-per-node=1 \
runner/primus-cli direct -- node_smoke
srun -N "$SLURM_NNODES" --ntasks-per-node=1 \
runner/primus-cli direct -- node_smoke --tier2-perf
Single-node sanity check (no SLURM):
runner/primus-cli direct -- node_smoke
Both forms produce the same workload. The wrapper form is recommended because it resolves the distributed env once on the launching node and propagates it via
--env, and applies anyslurm.* config defaults (partition / time / etc.). Thedirectkeyword between the two--s is mandatory to take the no-container path — without it the wrapper routes through the container path. Seepreflight-without-container.md§ Wrapper vs. bare-srun for the precedence table.
5. What’s checked#
A level='fail' finding in any check FAILs the node. Everything else is reported as info / warn.
Tier 1 — always runs (~5 s/GPU)#
Per-GPU liveness (each GPU in its own subprocess with a hard timeout):
torch.cuda.set_device(i)— proves the device is bindable (a stale / wedged GPU often fails here).256 MB allocation.
Tiny 2048² bf16 GEMM with an
isfinite()check on the result.
Host / GPU / network inventory (no rendezvous):
dmesg recent-error scan — greps the last
--dmesg-minutes(default 15) ofdmesgfor known patterns (xid,gpu reset,hung_task,mce:,amdgpu.*error, …). Matches are surfaced in the report.A. Software-stack fingerprint — kernel / OS / Python, ROCm version, amdgpu kernel-module version, PyTorch /
torch.version.hip/ RCCL versions, and per-IB-device firmware + HCA model. Used for cluster drift detection.B. NIC / RDMA roll-call — per-port state read from
/sys/class/infiniband(works inside containers; noibv_devinfo/ibstatdependency). Many clusters expose more RDMA ports than the training job uses, so the hard-fail rules only run against the training-NIC subset, selected by this precedence:--rdma-nic-allowlist(NCCL_IB_HCAsyntax: comma-separateddevice[:port],^...denylist,=devexact-match).NCCL_IB_HCAenv (same syntax) — so the smoke test and the training launch agree by construction.Heuristic: auto-exclude any port whose
phys_stateisDisabledorSleep(admin-disabled).Fallback: every IB port must be ACTIVE / LinkUp.
Hard-fail rules (on the included set only): port not ACTIVE / not LinkUp; active port with zero RoCE v2 GIDs (RoCE) or zero valid GIDs (IB); included-NIC count ≠
--expected-rdma-nics N(when set). If every discovered port gets excluded, the node still fails — a node with zero training NICs cannot participate in inter-node training. Excluded ports stay visible in the report for diagnostics but don’t contribute to the FAIL signal.C. Host limits / system tunables —
RLIMIT_MEMLOCKbelow--ulimit-l-min-gb(default 32 GiB) → “RDMA pin will fail under load”;/dev/shmbelow--shm-min-gb(default 8 GiB) → “NCCL shared-mem may fail”. NUMA node count, CPU count, andcpu0scaling governor are collected for drift detection.Foreign / leaked process detection — foreign PIDs holding a GPU FAIL the node by default (the most common cause of “training fails to launch on a healthy-looking node”). Allowed by default:
gpuagent,rocm-smi-daemon,amd-smi,dcgm-exporter. See the container note in §6.5 — running inside a container almost always needs--allow-foreign-procs.rocm-smi self-latency — a
rocm-smi --versioncall slower than--rocm-smi-timeout-sec(default 5 s) is a node FAIL; a wedging amdgpu driver typically hangsrocm-smifor 30–60 s before the GPU itself stops responding.
Tier 2 — optional perf sanity (--tier2-perf)#
Per-GPU steady-state metrics, with iteration counts aligned to the preflight --quick preset so smoke and preflight numbers are directly comparable. It’s a single switch — you cannot enable just one half.
GEMM TFLOPS — 8192³ bf16
torch.matmul; FAIL below--gemm-tflops-min(default 600).HBM GB/s — 512 MB device-to-device
torch.Tensor.copy_(counts read + write); FAIL below--hbm-gbs-min(default 2000; a healthy MI300X is ≈ 4500–5000).Local 8-GPU RCCL all-reduce GB/s — algorithmic bandwidth
2·S·(P-1)/P / t / 1e9at--rccl-size-mb(default 64 MB); FAIL below--rccl-gbs-min(default 100). Local only, no cross-node traffic.
6. Examples (by configuration knob)#
Convention used below. The examples are written with bare
srunfor brevity. Anywhere you seesrun <flags> runner/primus-cli direct -- node_smoke ..., the equivalent wrapper form isrunner/primus-cli slurm srun <flags> -- direct -- node_smoke .... Pick whichever matches your habits; both target the same launcher.
6.1 Hard-fail on partial NIC enumeration#
Catches “7 of 8 RDMA NICs visible” — a common cause of crashes after RoCE init. The count is compared against the training-NIC set (after the selector chain), so frontend / storage RoCE NICs do not inflate it.
srun -N "$SLURM_NNODES" --ntasks-per-node=1 \
runner/primus-cli direct -- node_smoke --tier2-perf --expected-rdma-nics 8
6.2 Pin the training-NIC set explicitly#
When auto-detection picks the wrong ports, name the training NICs directly (otherwise NCCL_IB_HCA env is used; otherwise admin-disabled ports are auto-excluded):
srun -N "$SLURM_NNODES" --ntasks-per-node=1 \
runner/primus-cli direct -- node_smoke --tier2-perf \
--rdma-nic-allowlist 'rocep158s0:1,rocep190s0:1,rocep206s0:1,rocep222s0:1,rocep28s0:1,rocep62s0:1,rocep79s0:1,rocep96s0:1'
6.3 Tighten Tier 2 perf thresholds#
Reject GPUs that come in below your acceptance bar. Defaults: GEMM 600 TFLOPS, HBM 2000 GB/s, local RCCL 100 GB/s.
srun -N "$SLURM_NNODES" --ntasks-per-node=1 \
runner/primus-cli direct -- node_smoke --tier2-perf \
--gemm-tflops-min 700 --hbm-gbs-min 4500 --rccl-gbs-min 180
6.4 Tighten host limits#
Fail nodes whose RLIMIT_MEMLOCK or /dev/shm is too small for production training.
srun -N "$SLURM_NNODES" --ntasks-per-node=1 \
runner/primus-cli direct -- node_smoke \
--ulimit-l-min-gb 64 --shm-min-gb 16
6.5 Allow / extend the foreign-process whitelist#
By default, leaked / foreign processes holding a GPU FAIL the node. Allowed by default: gpuagent,rocm-smi-daemon,amd-smi,dcgm-exporter.
# Add a site-specific monitoring agent to the whitelist
srun ... runner/primus-cli direct -- node_smoke \
--allowed-procs gpuagent,rocm-smi-daemon,amd-smi,dcgm-exporter,my-monitor
# Don't fail at all on foreign processes (still reported in the markdown)
srun ... runner/primus-cli direct -- node_smoke --allow-foreign-procs
⚠ Running node_smoke inside a container almost always trips this check.
amd-smi process --jsonreportsname="N/A"for kernel/system PIDs likegpuagentwhose/proc/<host_pid>/commit cannot read, and the fallback name resolution insidenode_smokealso fails because the container’s/proctypically does not expose host PIDs (private PID namespace without--pid=host, or ahidepid=2mount). The unresolved name doesn’t match the allowlist, so the check fires and the node FAILs — even though the only “foreign” processes are well-known system daemons holding zero HBM.In the container path, pass
--allow-foreign-procs:srun ... runner/primus-cli direct -- node_smoke --tier2-perf --allow-foreign-procsThe processes are still listed in
smoke_report.mdunder “Busy GPUs / leaked processes”, so a real leak is still visible; only the FAIL verdict is downgraded.Narrower alternative — add the literal sentinel
N/Ato the allowlist so the check still catches leaks with resolvable names (e.g. a leftoverpythonrank):srun ... runner/primus-cli direct -- node_smoke --tier2-perf \ --allowed-procs gpuagent,rocm-smi-daemon,amd-smi,dcgm-exporter,N/AName resolution runs first, so whenever a real name can be resolved it overrides
N/Aand the normal allowlist applies — theN/Aentry only matches PIDs whose name genuinely could not be recovered.Root-cause fix (preferred long-term): grant the container access to host PIDs so names resolve and the report shows
gpuagentetc. instead ofN/A. Typical fixes: launch with--pid=host(Docker / Podman); mount/procwithouthidepid=2; or loosenptrace_scope/ grantCAP_SYS_PTRACE.
6.6 Require specific tools#
Make missing CLI tools a hard FAIL (default: warn-only).
srun ... runner/primus-cli direct -- node_smoke --require-tools amd-smi,rocm-smi,lsof
6.7 Skip dmesg scan (containers with no privileges)#
srun ... runner/primus-cli direct -- node_smoke --skip-dmesg
6.8 Custom dump path#
Keep one report per smoke run instead of overwriting the default location.
srun -N "$SLURM_NNODES" --ntasks-per-node=1 \
runner/primus-cli direct -- node_smoke --tier2-perf \
--dump-path /shared/smoke-archive/$(date +%Y%m%d-%H%M%S)
6.9 Re-aggregate from existing per-node JSONs (no re-run)#
The primus-cli wrapper always runs both phases (per-node run + rank-0 aggregate). To only re-render the report from JSONs collected earlier, use the standalone aggregate subcommand — it reads the existing <dump>/smoke/*.json without re-running the per-node step:
python -m primus.tools.preflight.node_smoke aggregate \
--dump-path output/preflight --expected-nodes 6 --wait-timeout-sec 5
6.10 Silent mode (for CI)#
Suppresses wrapper stdout, but the final report path is still printed and stderr / exit code are preserved.
srun ... runner/primus-cli direct --silent -- node_smoke --tier2-perf
6.11 Combined “production-ready screen”#
A representative one-shot for a production cluster screen:
srun -N "$SLURM_NNODES" --ntasks-per-node=1 \
runner/primus-cli direct --silent -- node_smoke --tier2-perf \
--expected-rdma-nics 8 \
--gemm-tflops-min 700 --hbm-gbs-min 4500 --rccl-gbs-min 180 \
--ulimit-l-min-gb 64 --shm-min-gb 16 \
--require-tools amd-smi,rocm-smi,lsof \
--dump-path /shared/smoke-archive/$(date +%Y%m%d-%H%M%S)
7. Outputs#
All written under --dump-path (default output/preflight/).
File |
Purpose |
|---|---|
|
Per-node verdict + every collected metric. One file per node. |
|
Human-readable cluster report (status table, drift sections, perf summary, failing-node detail). |
|
Newline-separated short hostnames. Pipe into |
|
Newline-separated short hostnames. Pipe into |
|
Auto-populated from |
Read the cluster verdict at a glance:
head -10 output/preflight/smoke_report.md
Feed bad nodes into a re-run:
srun --exclude=$(paste -sd, output/preflight/failing_nodes.txt) ... your-real-job
8. Understanding the report#
smoke_report.md renders in a stable order. Each section short-circuits to a placeholder (e.g. *All nodes match.*, *No NIC issues.*) on a healthy cluster, so a clean report stays short. In order:
Status table — one row per node:
node_rank, hostname, PASS/FAIL, duration, top fail reason.Stack drift across cluster — per fingerprint key, outliers vs the cluster majority (catches “1 of N nodes on a different RCCL build”).
NIC firmware drift across cluster — per-IB-device firmware drift.
NIC / RDMA roll-call issues — every offending node + port (included set only).
NIC port-count summary — cluster-majority training-NIC count and any node that disagrees (catches partial-NIC degradation even without
--expected-rdma-nics).NIC excluded ports (informational) — ports the selector chain dropped, grouped by source. Does not contribute to FAIL.
Host limits issues — per-node hard-limit violations.
GPU visibility issues — nodes where torch couldn’t see the GPUs, or amd-smi sees more GPUs than torch (stale ROCm / wedged driver).
GPU low-level outliers (PCIe link / HBM) — per-GPU outliers vs the cluster majority on PCIe width/speed and HBM total.
XGMI link issues — any non-XGMI GPU pair (intra-node collectives silently fall back to PCIe).
Cluster clock + time daemons — wall-clock spread plus per-node time-daemon health.
Tooling self-latency (
rocm-smi --version) — slow / timed-out tool calls (precursor to a wedged driver).Tooling availability — inventory of
amd-smi/rocm-smi/lsofper node, plus which Tier 1 checks have no working tool on each node.Busy GPUs / leaked processes — foreign PIDs holding GPUs at smoke start.
GPU pre-touch HBM usage outliers — GPUs with non-trivial HBM in use before smoke touched the device.
GPU compute-activity outliers — GPUs above
--gpu-activity-warn-pctat smoke start (warn-only).Tier 2 perf summary (only when at least one node ran Tier 2) — per-node GEMM TFLOPS / HBM GB/s as
min / median / max, plus local RCCL GB/s.Failing nodes — full reasons (only when there are failing nodes) — every fail reason, expanded per node.
9. Configuration reference#
9.1 Common knobs (cheat sheet)#
Flag |
Default |
When you’d change it |
|---|---|---|
|
off |
Always on for production screens — adds GEMM TFLOPS, HBM GB/s, local RCCL all-reduce. |
|
600 |
Site-specific acceptance bar. |
|
2000 |
Site-specific acceptance bar (MI300X healthy ≈ 4500–5000). |
|
100 |
Site-specific acceptance bar. |
|
unset |
Hard-fail on partial NIC enumeration. |
|
32 |
Raise for production training profiles. |
|
8 |
Raise for large-batch / many-rank profiles. |
|
off |
Co-tenant clusters, shared GPUs, or the container path (see §6.5). |
|
|
Add site-specific monitoring agents. |
|
|
Fail-fast if a CLI tool is missing in PATH. |
|
off |
Inside unprivileged containers. |
|
|
Archive each run separately. |
|
off |
CI / scripted runs. |
9.2 Full node_smoke (per-node) flags#
Authoritative source: python -m primus.tools.preflight.node_smoke run --help.
Flag |
Default |
Purpose |
|---|---|---|
|
|
Output directory. |
|
auto |
Override GPU count (auto-detected from |
|
15 |
Hard timeout per per-GPU subprocess. |
|
off |
Enable Tier 2 perf sanity (per-GPU GEMM TFLOPS + HBM GB/s + node-local RCCL all-reduce). Single switch. |
|
600 |
Tier 2 GEMM threshold. |
|
2000 |
Tier 2 HBM threshold. |
|
64 |
Local RCCL message size. |
|
100 |
Local RCCL bandwidth threshold. |
|
120 |
Hard timeout for the RCCL phase. |
|
off |
Skip dmesg scan (e.g. inside containers). |
|
15 |
dmesg |
|
auto (report-only) |
When set, a mismatch between the included (training-NIC) count and N becomes a node FAIL. |
|
unset |
Explicit training-NIC selector in |
|
32 |
|
|
8 |
|
|
5.0 |
Hard timeout for the |
|
2.0 |
FAIL if any GPU has ≥ this much HBM in use before smoke touches the device. Boundary inclusive. |
|
off |
Do NOT FAIL on foreign processes holding a GPU. They are still reported. |
|
|
Process names OK to find holding the GPU. Set to |
|
20.0 |
Warn (does NOT fail) if any GPU’s |
|
|
CLI tools that MUST be in PATH ( |
|
off |
Do NOT auto-wipe stale per-node JSONs / aggregator outputs from |
9.3 Standalone aggregate flags#
The primus-cli wrapper runs the aggregator automatically on rank 0 and fills --expected-nodes / --expected-nodelist-file from SLURM. These matter only when you invoke python -m primus.tools.preflight.node_smoke aggregate yourself (see §6.9).
Flag |
Default |
Purpose |
|---|---|---|
|
|
Same as |
|
none |
If fewer JSONs land within |
|
60 |
Polling timeout. |
|
1.0 |
Flag (warn-only) any node where |
|
30.0 |
Warn when wall-clock spread across nodes exceeds this many seconds (includes srun launch jitter). |
|
2.0 |
Mirrors the |
|
20.0 |
Mirrors the |
|
none |
One short hostname per line. Missing nodes get their real short hostname in the report and |
9.4 Launcher-level knobs (primus-cli direct)#
Consumed by primus-cli-direct.sh before the -- separator (not forwarded to the node_smoke Python tool):
Flag |
Purpose |
|---|---|
|
Redirect launcher + tool stdout to |
|
Verbose launcher logging. |
|
Print the resolved configuration and command without executing. |
|
Inject an env var into the Python process. |
Run vs. aggregate. The primus-cli
node_smokesubcommand always runs the per-node checks on every rank, then aggregates on rank 0 — which is what you want ~100% of the time, so there is no--aggregate-onlywrapper flag. For the rare single-phase cases, call the standalone CLI directly:python -m primus.tools.preflight.node_smoke run ...(per-node only, no report) or... aggregate ...(report only, from existing JSONs).
10. Comparison with the full preflight#
Aspect |
|
full |
|---|---|---|
Rendezvous |
None — every node independent |
Global |
Wall clock |
~30–60 s for 6 nodes (Tier 1+2) |
Minutes; scales with N for inter-node tests |
Granularity |
Per-node PASS/FAIL |
Per-rank measurements (no auto-fail by default) |
GEMM |
Hard threshold per GPU |
Reports per-GPU numbers, no auto-fail |
HBM bandwidth |
Yes (D2D |
Not measured |
Inter-node all-reduce / all-to-all |
Not tested (intentionally) |
Yes |
Drift detection |
Yes (versions, NIC firmware, port count) |
No |
Host limits / RDMA roll-call |
Yes (hard fail) |
Reported via |
Output format |
Per-node JSON + cluster md + SLURM-ready txt |
Markdown + PDF |
Use node_smoke to screen a cluster fast and exclude bad nodes. Use the full preflight when you want deep cross-node measurements (inter-node bandwidth matrix, ring-P2P, etc.). The recommended sequence is node-smoke first, then preflight --quick on the surviving nodes.
11. Troubleshooting#
Symptom |
Likely cause / fix |
|---|---|
|
Fix the path ( |
Every node FAILs with |
Container |
Some nodes never produce a JSON |
The aggregator names them in |
Tier 2 perf numbers below threshold on a known-good node |
Almost always insufficient CPU cores on |
Re-run on a smaller nodelist still shows the previously removed nodes as PASS |
Default behavior cleans stale JSONs on rank 0. If you passed |
12. See also#
preflight.md— the heavierpreflighttool with a global rendezvous and inter-node bandwidth tests.preflight-without-container.md— runningpreflightdirectly on the host (no container), including the shared venv + NCCL setup.primus/cli/subcommands/node_smoke.py— the primus-cli subcommand wiring (two-phase dispatch: per-rank run + rank-0 aggregate).primus/tools/preflight/node_smoke/cli.py— canonical flag definitions and per-node / aggregate phase bodies.