Skill: fault-injection-loop
Iterative closed-loop skill that runs a prioritized fault-injection experiment pool 2 jobs at a time, analyzes every artifact, scores attribution accuracy, aggregates gaps across the matrix, and proposes targeted improvements to attribution modules.
Overview
┌───────────────────────────────────────────────────────────────────────┐
│ 0. POOL → build ordered pool of (fault_type, rank, iter, nodes) │
│ GPU faults first, then crash, Python-hang, signal │
│ │
│ repeat until pool exhausted: │
│ 1. SUBMIT → sbatch 2 jobs from pool head │
│ 2. WAIT → poll until both jobs leave RUNNING/PENDING │
│ │
│ after all jobs done: │
│ 3. ANALYZE → watch_and_analyze.sh: /log-analysis + /fr-analysis │
│ per completed job, streaming as jobs finish │
│ 4. SCORE → compare attribution output vs injected ground truth │
│ 5. AGGREGATE→ build results table; identify systematic failure modes │
│ 6. IMPROVE → patch restart_agent/ policy or evidence extraction │
│ 7. LOOP → re-run same pool with updated attribution code │
└───────────────────────────────────────────────────────────────────────┘
Step 0 — Fault Pool Design
The pool is defined as an ordered list of FAULT_TYPE:RANK:ITER:NODES entries
inside scripts/prepare_node_alloc.sh. Default pool (34 experiments, 17 batches):
# GPU hangs — highest priority; full rank sweep across all node counts
GPU_SLEEP:1:5:2 GPU_SLEEP:0:5:2 # 2-node: rank-1, rank-0
GPU_SLEEP:4:5:2 GPU_SLEEP:7:5:2 # 2-node: mid-rank, last-rank
GPU_SLEEP:1:5:4 GPU_SLEEP:0:5:4 # 4-node: rank-1, rank-0
GPU_SLEEP:8:5:4 GPU_SLEEP:15:5:4 # 4-node: mid, last
GPU_SLEEP:1:5:8 GPU_SLEEP:0:5:8 # 8-node: rank-1, rank-0
GPU_SLEEP:16:5:8 GPU_SLEEP:31:5:8 # 8-node: mid, last
# GPU errors — high priority; rank-0 and rank-1 across all node counts
GPU_ERROR:1:5:2 GPU_ERROR:0:5:2
GPU_ERROR:1:5:4 GPU_ERROR:0:5:4
GPU_ERROR:1:5:8 GPU_ERROR:0:5:8
# Crash faults
SIGKILL:1:5:2 SIGKILL:0:5:2
SIGKILL:1:5:4 SIGKILL:1:5:8
SEGFAULT:1:5:2 SEGFAULT:0:5:2
SEGFAULT:1:5:4 OS_ABORT:1:5:2
# Python-level hangs
LOCK_GIL:1:5:2 LOCK_GIL:0:5:2
WORKLOAD_EXC:1:5:2 ASYNC_EXC:1:5:2
# Signals
SIGTERM:1:5:2 SIGINT:1:5:2
SIGSTOP:1:5:2 SIGNAL_EXC:1:5:2
Rank coverage per node count (4 GPUs/node):
| Nodes | Total ranks | rank-0 | rank-1 | mid | last |
|---|---|---|---|---|---|
| 2 | 8 | 0 | 1 | 4 | 7 |
| 4 | 16 | 0 | 1 | 8 | 15 |
| 8 | 32 | 0 | 1 | 16 | 31 |
To run a custom subset, override POOL before calling the script:
POOL="GPU_SLEEP:0:5:2 GPU_SLEEP:1:5:2" bash scripts/prepare_node_alloc.sh
Local User Config
Start from the tracked template:
cp scripts/user.env.example scripts/user.env
Then edit scripts/user.env with cluster-specific settings. This file is
sourced by run_session.sh, prepare_node_alloc.sh, watch_and_analyze.sh,
l4_gb200_reduced.sh, and n3_super_gb200_fi.sh. It is required for this skill
to run and is intended to stay local and untracked.
Recommended contents:
PARTITION=gb-nvl-134-135
BASE_EXPERIMENTS_DIR="${HOME}/nvrx-attr-experiments"
MEGATRON_REPO_HOST_PATH="${HOME}/megatron-lm-main"
SHARED_TMP_BASE_DIR="${HOME}/tmp"
WORKSPACE_HOST_PATH="${HOME}/tmp"
CONTAINER_IMAGE="nvcr.io/nvidia/nemo:26.04"
LLM_API_KEY_FILE="${HOME}/.llm_api_key"
JUDGE_API_KEY_FILE="${HOME}/.llm_api_key"
NVRX_LLM_MODEL="nvidia/nemotron-3-super-120b-a12b"
NVRX_LLM_BASE_URL="https://integrate.api.nvidia.com/v1"
# RESTART_AGENT_CONFIG="/path/to/restart_agent.json"
JUDGE_MODEL="qwen/qwen3.5-397b-a17b"
JUDGE_BASE_URL="https://integrate.api.nvidia.com/v1"
FR_SEGMENT_SIZE=32
Use user.env for stable site defaults such as partition, container image, and
host paths, plus local LLM credentials and endpoint settings for log-analysis
and the judge. Use per-run environment overrides for experiment-specific
controls such as POOL, WORKLOAD, BATCH_SIZE, FAULT_TYPE,
FAULT_AT_ITER, or FAULT_DELAY.
If you use local Triton/Inductor cache staging, set the cache variables in
scripts/user.env. See scripts/user.env.example for the supported
ENABLE_NFS_CACHE_STAGING, NFS_TRITON_CACHE, and NFS_INDUCTOR_CACHE
entries and workload-specific path examples.
Environment variables:
| Variable | Default | Description |
|---|---|---|
WORKLOAD |
llama4_scout |
Select a registered workload by name (see scripts/workloads.conf) |
ACCOUNT |
(cluster default or scripts/user.env) |
SLURM account |
PARTITION |
(cluster default or scripts/user.env) |
SLURM partition |
GPUS_PER_NODE |
4 |
GPUs per node |
TIME |
00:30:00 |
Per-job wall-clock limit |
BATCH_SIZE |
2 |
Jobs submitted per round |
POLL_INTERVAL |
30 |
Seconds between queue polls |
BASE_EXPERIMENTS_DIR |
${HOME}/nvrx-attr-experiments |
Root for all output |
MEGATRON_REPO_HOST_PATH |
${HOME}/megatron-lm-main |
Host path to the Megatron checkout mounted into the container |
SHARED_TMP_BASE_DIR |
${HOME}/tmp |
Shared filesystem path used for cross-step coordination |
WORKSPACE_HOST_PATH |
${HOME}/tmp |
Host path mounted at /workspace inside the container |
CONTAINER_IMAGE |
nvcr.io/nvidia/nemo:26.04 |
Container image used by the workload script |
LLM_API_KEY_FILE |
unset | File containing the log-analysis API key |
JUDGE_API_KEY_FILE |
unset | File containing the judge API key |
RESTART_AGENT_CONFIG |
unset | Optional restart-agent config JSON; when set, it owns model-route behavior |
NVRX_LLM_MODEL |
nvidia/nemotron-3-super-120b-a12b |
Model for log-analysis |
NVRX_LLM_BASE_URL |
https://integrate.api.nvidia.com/v1 |
Base URL for log-analysis |
JUDGE_MODEL |
qwen/qwen3.5-397b-a17b |
Model for judge scoring |
JUDGE_BASE_URL |
https://integrate.api.nvidia.com/v1 |
Base URL for judge scoring |
FR_SEGMENT_SIZE |
32 |
Ranks per segment for coarse FR scoring |
SBATCH_SCRIPT |
scripts/l4_gb200_reduced.sh |
Job script to submit |
POOL |
(default pool above) | Space-separated experiment triplets |
Registered workloads (scripts/workloads.conf)
| Name | Script | Base dir | Description |
|---|---|---|---|
llama4_scout |
l4_gb200_reduced.sh |
${HOME}/nvrx-attr-experiments |
Llama4-Scout (reduced layers) on GB200; minimum supported size is 2 nodes |
n3_super |
n3_super_gb200_fi.sh |
${HOME}/nvrx-attr-experiments |
Nemotron3-Super on GB200; minimum supported size is 8 nodes |
Workload note:
llama4_scoutrequires at least 2 nodes.n3_superrequires at least 8 nodes. Its default registered pool contains only 8-node experiments.
# Run the full pool against the validated example workload
bash scripts/prepare_node_alloc.sh
# Run a custom subset against llama4_scout
POOL="GPU_SLEEP:1:5:2 SIGKILL:1:5:2" WORKLOAD=llama4_scout bash scripts/prepare_node_alloc.sh
Step 1 & 2 — Batched Submission + Wait (automated)
bash scripts/prepare_node_alloc.sh
The script loops: submit 2 jobs → poll squeue every 30 s until both finish →
submit next 2. Progress is printed inline:
>>> Batch 1: experiments 1–2 of 34
submitted: GPU_SLEEP rank=1 iter=5 nodes=2 -> job=1850
submitted: GPU_SLEEP rank=0 iter=5 nodes=2 -> job=1851
waiting for GPU_SLEEP:1:5:2 GPU_SLEEP:0:5:2 (1850,1851) ... 30s 60s done.
>>> Batch 2: experiments 3–4 of 34
...
A session directory and TSV tracking file are created at launch time:
${BASE_EXPERIMENTS_DIR}/fault_injection/<YYYYMMDD_HHMMSS>/
experiments.tsv ← tracking file (all job IDs + paths)
n<N>_<FAULT>_r<R>_i<I>/ ← one subdir per experiment
logs/slurm/<JOB_ID>.launch.out
logs/slurm/<JOB_ID>.*.1.main_workload.log ← log-analysis input
checkpoints/ ← fr-analysis input (FR dumps)
tensorboard/
experiments_report.md ← generated by watch_and_analyze.sh
Tracking file columns: JOB_ID FAULT_TYPE RANK ITER NODES EXPERIMENT_DIR
Step 3 — Analyze All Experiments
Run the watcher/analyzer — it reads the tracking file and processes each experiment as its job state leaves RUNNING/PENDING (works whether jobs are still running or already done):
bash scripts/watch_and_analyze.sh \
${BASE_EXPERIMENTS_DIR}/fault_injection/<YYYYMMDD_HHMMSS>/experiments.tsv
The watcher:
- Reads each row from the tracking TSV
- Calls restart-agent as
python -m nvidia_resiliency_ext.attribution.restart_agent.cliand parses the JSONdecision - Calls FR analysis as
python -m nvidia_resiliency_ext.attribution.trace_analyzer.fr_attribution --fr-path "${EXPERIMENT_DIR}/checkpoints" -p "_dump_*"and passes the raw table output to the judge - Scores 7 dimensions (restart correctness, rank primary, rank any, category, type, FR rank)
- Appends a scored row to
<session>_report.mdas a markdown table row - Repeats until all experiments are analyzed
To also run the sub-skills interactively for a single experiment:
python -m nvidia_resiliency_ext.attribution.restart_agent.cli \
"${EXPERIMENT_DIR}/logs/slurm/${JOB_ID}.*.1.main_workload.log" --job-id "${JOB_ID}"
python -m nvidia_resiliency_ext.attribution.trace_analyzer.fr_attribution \
--fr-path "${EXPERIMENT_DIR}/checkpoints" -p "_dump_*"
Step 4 — Score Each Experiment
Scoring is performed by scripts/score_attribution.py, an LLM judge that
receives the ground truth, the filtered raw log, the restart-agent attribution output, and the FR
analysis output, then returns structured JSON scores with a reasoning note.
| Column | Values | Meaning |
|---|---|---|
| restart_correct | true / false / N/A |
Restart decision matches expected for this fault type |
| rank_primary | true / false / partial |
Injected rank is the primary root-cause in attribution |
| rank_any | true / false |
Injected rank mentioned anywhere in attribution |
| fault_described | true / false / partial |
Fault nature (hang/crash/signal/exception) correctly described |
| fr_rank_correct | rank / node / segment / false / no_dumps |
FR analysis narrows correctly to the injected rank, exactly one GPUS_PER_NODE rank block containing that rank, the configured FR_SEGMENT_SIZE rank block containing the injected rank, or fails to narrow usefully |
| judge_notes | string | One-sentence summary of the main gap or confirmation |
The judge is given:
- Ground truth:
fault_type,rank,iter,nodes - Expected restart decision + rationale (derived from
score_attribution.py:_RESTART_TABLE) - Filtered raw log (last 400 lines, with fault-injection markers removed)
- Raw restart-agent JSON output
- Raw FR analysis table output from
fr_attribution.py --fr-path ... -p "_dump_*" GPUS_PER_NODEandFR_SEGMENT_SIZEto map the injected rank to exact node-sized and segment-sized scopes for FR scoring
Default judge model: qwen/qwen3.5-397b-a17b. Override with --model in score_attribution.py.
Default segment size for FR scope scoring: 32 ranks. Override with FR_SEGMENT_SIZE.
Step 5 — Aggregate Results
The canonical output of the loop is the markdown table in <session>_report.md.
When summarizing results for users, prefer linking to that file and reproducing the
same table shape rather than flattening the results into plain prose.
The report markdown table from watch_and_analyze.sh gives a matrix view. Look for
patterns across rows:
| FAULT_TYPE | NODES | RANK | restart_correct | rank_primary | rank_any | fault_described | fr_rank_correct | judge_notes |
|------------|-------|------|-----------------|--------------|----------|-----------------|-----------------|-------------|
| GPU_SLEEP | 2 | 0 | true | false | true | true | true | rank-0 identified only in secondary issues |
| GPU_SLEEP | 2 | 1 | true | true | true | true | true | correct on all dimensions |
| GPU_ERROR | 2 | 1 | false | false | false | partial | true | LLM issued RESTART; rank not mentioned |
| SIGKILL | 2 | 0 | true | false | false | false | true | attribution describes timeout not kill signal |
Common failure mode patterns and their meaning:
| Pattern | Interpretation |
|---|---|
rank_primary=false, rank_any=true |
Rank detected but treated as collateral rather than the primary root cause |
rank_any=false for rank-0 |
Rank-0 hang silences watchdog on other ranks; log attribution lacks rank-0 signal |
fault_described=partial for crash types |
Crash keywords present but fault type not specifically named |
restart_correct=false for GPU_ERROR |
LLM conflating hardware error with recoverable hang |
fr_rank_correct=no_dumps |
NCCL watchdog did not fire before job ended — adjust TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC |
fr_rank_correct=node |
FR isolated exactly one GPUS_PER_NODE rank block containing the injected rank, but not the exact rank |
fr_rank_correct=segment |
FR isolated the configured FR_SEGMENT_SIZE rank block containing the injected rank, but not the exact node/rank |
Step 6 — Identify and Apply Improvements
FR analysis
Deterministic graph algorithm — do not modify automatically. Note misidentifications and escalate to the team.
Log analysis (safe to modify)
| Observation | Target location | Suggested fix |
|---|---|---|
| Wrong restart for hang | restart_agent/l0 or restart_agent/l4 |
Strengthen NCCL timeout → RESTART mapping |
| Missing rank in attr text | restart_agent/l0 evidence extraction or L1 prompt/tooling |
Extract rank from NCCL watchdog lines; add regex |
| Crash misclassified as hang | restart_agent/l0 evidence extraction or L1 prompt/tooling |
Add SIGKILL/SEGFAULT/GPU_ERROR keyword patterns |
Missing primary_failure when errors exist |
restart_agent/l0 evidence extraction or L1 tooling |
Loosen failure-candidate extraction/filtering |
| rank-0 not detected | restart_agent/l0 evidence extraction or L1 prompt/tooling |
Add explicit rank-0 hang heuristic (other ranks silent) |
| attr off by many iters | restart_agent/l0 progress/failure-position evidence or L1 prompt |
Increase weight of iteration-stamped log lines |
| LLM wrong on GPU_ERROR | restart_agent/l1 prompt or recovery assessment |
Distinguish cudaError → crash from NCCL timeout → hang |
Editable area: attribution/restart_agent/
After each patch, re-run the same pool subset that previously failed:
POOL="GPU_SLEEP:0:5:2 GPU_ERROR:1:5:2" bash scripts/prepare_node_alloc.sh
Step 7 — Loop
Increment experiment counter. Suggested sweep order across code-change iterations:
- Iteration 1: full default pool (34 experiments)
- Iteration 2: targeted re-run of all failing cells from iteration 1
- Iteration 3: expand iter dimension (FAULT_AT_ITER=2 and 10) for remaining gaps
- Iteration 4: add SEGFAULT and LOCK_GIL 4-node/8-node coverage
Stop condition: all cells pass all four scoring dimensions for two consecutive code-change iterations.
Adapting A SLURM Script For The Feedback Loop
The feedback loop is not tied to l4_gb200_reduced.sh, but your sbatch script must
match a small contract so the loop can submit, analyze, and score each run.
Required changes for a custom workload script:
- Accept these exported variables from
prepare_node_alloc.sh:FAULT_TYPE,FAULT_RANK,FAULT_AT_ITER,EXPERIMENT_DIR,BASE_EXPERIMENTS_DIR, andGPUS_PER_NODE. - Write the main training log to:
${EXPERIMENT_DIR}/logs/slurm/${SLURM_JOB_ID}.*.1.main_workload.logsowatch_and_analyze.shcan find it. - Write NCCL flight-recorder dumps under
${EXPERIMENT_DIR}/checkpoints/. - Emit a fault-injection marker when the fault is injected.
watch_and_analyze.shuses this to decide whether the run reached the injection point. - Preserve the per-experiment directory layout:
logs/slurm/,checkpoints/, andtensorboard/.
This has only been validated with Megatron-LM because the current run-valid check and
fault markers depend on Megatron's debug_fault_injection.py behavior. If you adapt the
loop to another framework, update both the sbatch script and watch_and_analyze.sh.
Appendix A: SBATCH_SCRIPT fault parameters
The example SBATCH_SCRIPT reads these env vars from prepare_node_alloc.sh via --export:
| Variable | Default | Description |
|---|---|---|
FAULT_AT_ITER |
5 |
Training iteration at which to inject |
FAULT_DELAY |
15 |
Delay in seconds before fault injection after the iteration anchor |
FAULT_RANK |
1 |
Global rank to inject [0, total_ranks) |
FAULT_TYPE |
GPU_SLEEP |
Megatron fault type enum name |
GPUS_PER_NODE |
4 |
GPUs per node (used to compute TOTAL_TASKS) |
EXPERIMENT_DIR |
${BASE_EXPERIMENTS_DIR}/fault_injection/n${SLURM_NNODES}_${FAULT_TYPE}_r${FAULT_RANK}_i${FAULT_AT_ITER} |
Per-experiment output root |
BASE_EXPERIMENTS_DIR |
${HOME}/nvrx-attr-experiments |
Shared root (datacache, triton/inductor caches) |
Valid FAULT_TYPE values:
GPU_ERROR, GPU_SLEEP, WORKLOAD_EXC, ASYNC_EXC, SIGNAL_EXC, OS_ABORT,
LOCK_GIL, SEGFAULT, SIGINT, SIGKILL, SIGTERM, SIGSTOP
Appendix B: Single-experiment manual run
# Manual runs land under fault_injection/manual/ by default (no session dir needed)
EXPERIMENT_DIR=${HOME}/nvrx-attr-experiments/fault_injection/manual/n2_GPU_SLEEP_r1_i5
mkdir -p ${EXPERIMENT_DIR}/logs/slurm ${EXPERIMENT_DIR}/checkpoints ${EXPERIMENT_DIR}/tensorboard
sbatch \
--nodes=2 \
--output=${EXPERIMENT_DIR}/logs/slurm/%j.launch.out \
--error=${EXPERIMENT_DIR}/logs/slurm/%j.launch.err \
--export=ALL,FAULT_TYPE=GPU_SLEEP,FAULT_RANK=1,FAULT_AT_ITER=5,FAULT_DELAY=15,GPUS_PER_NODE=4,EXPERIMENT_DIR=${EXPERIMENT_DIR} \
scripts/l4_gb200_reduced.sh
Optional site-specific cleanup:
export CONTAINER_CLEANUP_CMD='
ENROOT_DIR="/var/lib/enroot/data/$(id -u)"
rm -rf "${ENROOT_DIR:?}"/* 2>/dev/null || true
echo "$(hostname): / $(df -h / | tail -1 | awk "{print \$3\" used, \"\$4\" avail\"}")"
'