QR code linking to samf.sh/talks/2026/07/14samf.sh/talks/2026/07/14

Pre-Training AuroraGPT at Scale on Aurora

Less a story of peak FLOP/s than of what works, what breaks, and how cheaply you recover.

 
 

Sam Foreman1, Nathan Nichols, Varuni Sastry, Samuel Wheeler, Khalid Hossain, Huihuo Zheng, Murali Emani, Filippo Simini, Marieme Ngom, Ethan Wong, Venkat Vishwanath

2026-07-14

Outline

  1. The Good:
    • AuroraGPT-2B: 4.67T tokens, done
      • 20B/512N now beats 2B/256N / token
      • mano: Muon quality @ AdamW speed
    • Software stack:
    • Post-training now live: CPT · SFT · GRPO
  1. The Bad:

    • Rapidly evolving software (and hardware!)
      • Fork tax (fast-moving upstream!)
    • At scale, failure is the default
    • 80B: one bf16 cliff, three dead optimizers
    • MoE on XPU: 4–11% MFU (compile hurts!)
  2. The Restarts:

    • Towards resilient training
    • 3 layers of recovery:
      • Job → Node → Process
    • Native --auto-retry

Motivation

  • How to do production training on a rapidly evolving software stack?

    • across {Intel, NVIDIA, AMD, …} hardware?
    • while also mitigating failures ?
      • {hardware, system, network, lustre, …}
  • Tension between:

    • long running pre-training jobs
    • typical facility scheduling policies
      • INCITE (>= 20% of machine, 2k nodes on Aurora)
        -> very large batches
        -> bad model, unstable training345

Since TPC’26: where production stands

RunNodesStepsTokensLossStatus
2B base25692,8594.674T (100%)2.65complete
2B CPT2565,960~300B pilot2.49loss ↓ but evals ↓ (no winner)
20B/512N5125,400543.6B (11.6%)2.47advancing (auto-retry)
20B/256N2563,100156.0B (3.3%)2.68advancing
80B51214(NaN’d)NaN❌ optimizer cliff
  • 20B/512N beats the 2B/256N baseline on every eval, per token (HellaSwag-norm 0.635 vs 0.555)
  • The wins and the wall are both new since June. This talk: what changed.

The stack

Current stack:

Old stack (reference):

Hardware agnostic
Same code, every vendor; no cuda in user code !

🍋 ezpz: write once, run anywhere

# train.py

import ezpz
# auto device + backend selection
rank = ezpz.setup_torch()
print(rank)
ezpz launch python3 train.py

Same code, every site. No per-cluster mpiexec / srun, CPU bindings, or tile-compact wrappers. → ezpz.cool

flowchart LR EZ(["<span style='font-weight:700'>ezpz launch</span>"]) EZ --> AURORA["<i>PBS · Intel</i><br/><span style='font-weight:700'>Aurora</span>"] EZ --> POLARIS["<i>PBS · NVIDIA</i><br/><span style='font-weight:700'>Polaris</span>"] EZ --> PM["<i>SLURM · NVIDIA</i><br/><span style='font-weight:700'>Perlmutter</span>"] EZ --> FRONTIER["<i>SLURM · AMD</i><br/><span style='font-weight:700'>Frontier</span>"] EZ --> MAC["<span style='font-weight:700'>Multi-CPU</span>"] classDef hub fill:#ee8f2408,stroke:#ee8f24,color:#ee8f24,stroke-width:1.5px classDef aurora fill:#3b82f608,stroke:#3b82f6,color:#3b82f6,stroke-width:1.5px classDef polaris fill:#10b98108,stroke:#10b981,color:#10b981,stroke-width:1.5px classDef perlmutter fill:#06b6d408,stroke:#06b6d4,color:#06b6d4,stroke-width:1.5px classDef frontier fill:#ef444408,stroke:#ef4444,color:#ef4444,stroke-width:1.5px classDef mac fill:#a78bfa08,stroke:#a78bfa,color:#a78bfa,stroke-width:1.5px class EZ hub class AURORA aurora class POLARIS polaris class PM perlmutter class FRONTIER frontier class MAC mac

AuroraGPT-2B: the reference run on Aurora

SpecValue
Architecture1.986B params, 12 layers, GQA (16h / 4 kv)
Hardware256 Aurora nodes × 12 Intel Max GPUs = 3,072 GPUs, BF16
FrameworkMegatron-DeepSpeed (ZeRO Stage 0)
OptimizerSophiaG7 (β=0.9/0.95, ρ=0.01, wd=0.1, LR=2.28e-5)
Training Config50M tok/batch (8192 ctx · LBS=2)
TokenizerSentencePiece, vocab=256K
Stages3 (pretrain · continued-pretrain · math+code)
Tokens~7.77T total

This is the pre-torchtitan reference. Everything that follows is the migration story: same scale, same data, what changed and what broke when we cut over.

Why MDS (Megatron-DeepSpeed) first: the only option at the time

When AuroraGPT kicked off, MDS was the only LLM pre-training framework that ran at scale and supported:

  • Intel XPU
  • Model, pipeline parallelism
  • DeepSpeed ZeRO Offloading

Supporting context:

  • PyTorch FSDP1 had Intel XPU gaps (collectives, AC patterns, optimizer-state sharding)
  • torchtitan existed as a research project (not tested)
  • MDS was the pragmatic choice

By early 2026, the calculus changed: torchtitan + DTensor + FSDP2 closed the gap and the MDS fork’s maintenance cost crossed over.

Why SophiaG: large-batch stability at 50M tok/batch (256N)

W&B Report: AuroraGPT-2B Pre-Training8

AuroraGPT-2B optimizer comparison: AdamW vs ipex.FusedLamb vs Muon vs MuonClip vs SophiaG at GBS=6,144

SophiaG is the only one that stays in the low-loss band with bounded grad norms.

LR-finder — exponential sweep, blow-up / 10

Classic exponential LR sweep910; pick the steepest descent (blow-up / 10 as a safe default). See also our cross-optimizer LR scaling work11.

LR-vs-loss sweeps for AdamW, Muon, SophiaG across 2B and 20B configs on Aurora

Cross-optimizer sweep on Aurora. Full report: docs/experiments/lr-finder/README.md

Why we moved to torchtitan

MDSTT
Actively maintained❌️
Declarative parallelism (DTensor, FSDP2)❌️
FSDP+TP / EP / CP without plumbing❌️
MoE support❌️
Easy to extend, debug, maintain❌️

The trade-off we accepted: living on a fast-moving upstream  pytorch/torchtitan@main; the “fork tax”

All production runs: training loss

AuroraGPT all production training-loss curves: 2B and 20B v2 runs on one axis

Every current production run on one axis. 2B v2 256N: the full 4.674T-token run (step-92,859), final loss 2.65. 20B v2 512N (sync): step-5,400 / 543.6B tok (11.6%), loss 2.47 and still descending. Data: docs/production

Evals: 2B + 20B vs MDS, per token

AuroraGPT eval: 2B v2 (256N + 512N), 20B v2 512N, and the 2B-MDS reference on HellaSwag, ARC-Easy, ARC-Challenge, Winogrande vs tokens consumed

2B (4.67T tok): HellaSwag 0.561 · ARC-Easy 0.651, plateaus ~2T. 20B/512N is the most token-efficient run: at 442.9B tok already ARC-Easy 0.693 · HellaSwag 0.634, beating both 2B chains per token (~8×). Data: docs/evals

The fork tax: upstream-sync as a workflow

flowchart TB A["<i>start</i><br/><span style='font-weight:700'>upstream @ HEAD</span>"] --> B["<i>resync · 50 steps · deterministic</i><br/><span style='font-weight:700'>smoke test</span>"] B --> D{"bit-exact?"} D -->|✅| C["<i>diff = 0</i><br/><span style='font-weight:700'>✓ ship</span>"] D -->|❌️| F["<i>diff &ne; 0</i><br/><span style='font-weight:700'>bisect + fix</span>"] F -.->|retry| B classDef source fill:#7c4ed508,stroke:#7c4ed5,color:#7c4ed5,stroke-width:1.5px classDef stage fill:#118cc208,stroke:#118cc2,color:#118cc2,stroke-width:1.5px classDef ship fill:#1da81108,stroke:#1da811,color:#1da811,stroke-width:1.5px classDef gate fill:#ee8f2410,stroke:#ee8f24,color:#c9750f,stroke-width:1.5px classDef bad fill:#e0556008,stroke:#e05560,color:#e05560,stroke-width:1.5px class A source class B stage class D gate class C ship class F bad

MoE on Intel XPU: where we are (honestly)

DeepSeek-style MLA + MoE, 500M → 10B. FSDP-only today (TP=1, no-compile, AC=none, seq_len=4096).

RealityDetail
MFU4–11% (best ~7.4%); scaling degrades past 8N
torch.compilehurts MoE (−35% on 500M)
Activation checkpointing (7B+)CheckpointError (routing non-determinism)
Expert parallelism (EP)--parallelism.expert_parallel_degree

Open knobs we’re sweeping: EP 12 · TP 4 · context-parallel · per-block compile · DeepEP / HybridEP backends · Float8. This is exactly where cross-vendor MoE experience would help (see the ask).

mano: Muon quality at AdamW speed

mano12 normalizes updates on a rotating Oblique manifold with O(dim)O(\text{dim}) vector-norm ops (no Newton-Schulz iterations). So it matches Muon’s loss without Muon’s throughput tax.

OptimizerLossTPS
Muon3.5574,556
mano3.6317,048
AdamW3.8017,245
SophiaG4.7197,208
  • Muon and mano tie on loss (~3.6); mano runs at AdamW speed (~7,000 vs ~4,600 TPS) → wins on wall-clock.
  • Caveat: at large batch (GBS=384) AdamW still wins (2.71 vs 2.88); mano’s LR was tuned at GBS=48 and needs re-tuning.
Training loss vs step for Muon, mano, AdamW, SophiaG at 2B / GBS=48 over 1000 steps: mano and Muon converge lowest and nearly overlap, AdamW above, SophiaG highest

80B: one bf16 cliff, three dead optimizers

At 80B (dim=9216), every constant-finder LR NaN-ed in the first ~dozen steps: grad_norm → inf one step before the loss, while loss was still flat (~12.9):

OptimizerDied atSignature
manostep 5grad NaN (diverged first)
AdamWstep 9grad NaN
SophiaGstep 14Hessian-term overflow, ~6,100 node-h
  • mano (our 2B speed win) died first (step 5), despite its 80B LR-finder band looking the safest of the three. Early-step ranking does not predict sustained stability.
  • Shared failure across 3 optimizers ⇒ a corner-level instability (bf16 at dim=9216), not tuning. Two grad-path triggers: LBS>1 and large dp_degree.
  • Fix in flight: long warmup (≥200 steps) + grad clipping, possibly an fp32 grad path. Stable corner today: TP=4 · LBS=1 · GBS=372, validated 100/100 steps NaN-free (loss 12.93 → 7.72) at ~9.8% MFU.

80B cliff: the GBS ladder + why LR-scaling backfires

The stable corner (TP=4 · LBS=1 · bf16 · GBS=372) held NaN-free under batch ramp, until it didn’t:

BatchGBSResult
372✓ 100/100 steps (12.93→7.72)
1,488✓ 46/46 steps
2,976✓ 34/34 steps
16×5,952❌ NaN at step 29
16× + LR-scaled5,952❌ NaN at step 7 (worse!)
  • Do not linearly scale LR with batch here: it moves the cliff earlier.
  • dp_degree bisect: dp=192 and dp=264 both ran 30 steps clean, so the old “dp>186 unsafe” warning was over-conservative.
  • 2048N (dp=6138) SIGSEGVed in set_determinism (seed broadcast faulted at 24,864 ranks); 512N proven, 1024N is the missing measurement.
80B LR-finder at GBS=6144: mano, SophiaG, AdamW, muon loss-vs-LR. AdamW and muon cliff straight to NaN with no minimum; mano and SophiaG reach a real U-minimum first

80B LR-finder at the production batch (GBS=6,144, four sweeps). AdamW and muon cliff straight to NaN with no usable minimum (× marks the diverged LRs); mano and SophiaG reach a real minimum first (circled, ~11.9 / ~11.8) but SophiaG blows up just past it. Data: docs/experiments/lr-finder/agpt/80b

Beyond pretraining: CPT · SFT · RL

The base run is done, so the pipeline now extends past pretraining:

flowchart LR BASE(["<span style='font-weight:700'>2B base</span><br/><i>pretrain · 4.67T</i>"]) BASE --> CPT["<span style='font-weight:700'>CPT</span><br/><i>stages 2-3 · +3.1T</i>"] CPT --> SFT["<span style='font-weight:700'>SFT</span><br/><i>tulu math mix</i>"] SFT --> GRPO["<span style='font-weight:700'>GRPO</span><br/><i>arithmetic RL</i>"] classDef base fill:#118cc208,stroke:#118cc2,color:#118cc2,stroke-width:1.5px classDef cpt fill:#ee8f2408,stroke:#ee8f24,color:#ee8f24,stroke-width:1.5px classDef sft fill:#1da81108,stroke:#1da811,color:#1da811,stroke-width:1.5px classDef rl fill:#7c4ed508,stroke:#7c4ed5,color:#7c4ed5,stroke-width:1.5px class BASE base class CPT cpt class SFT sft class GRPO rl
flowchart TB BASE(["<span style='font-weight:700'>2B base</span><br/><i>pretrain · 4.67T</i>"]) BASE --> CPT["<span style='font-weight:700'>CPT</span><br/><i>stages 2-3 · +3.1T</i>"] CPT --> SFT["<span style='font-weight:700'>SFT</span><br/><i>tulu math mix</i>"] SFT --> GRPO["<span style='font-weight:700'>GRPO</span><br/><i>arithmetic RL</i>"] classDef base fill:#118cc208,stroke:#118cc2,color:#118cc2,stroke-width:1.5px classDef cpt fill:#ee8f2408,stroke:#ee8f24,color:#ee8f24,stroke-width:1.5px classDef sft fill:#1da81108,stroke:#1da811,color:#1da811,stroke-width:1.5px classDef rl fill:#7c4ed508,stroke:#7c4ed5,color:#7c4ed5,stroke-width:1.5px class BASE base class CPT cpt class SFT sft class GRPO rl
  • CPT: stages 2-3 of the SophiaG reference (dolmino, then math+code) → 7.77T tokens
  • SFT: checkpoint-729-hf (on that CPT’d base) is now a first-class production asset
  • GRPO: on-policy RL (TRL + vLLM-XPU) on the SFT checkpoint, verified multi-node

CPT: lower loss ≠ better model

Two ~300B-token pilots (256N, GBS=6,144) vs the olmo-100 base plateau:

2B CPT training/validation loss per data mix: dolmino-100 vs olmo50-dolmino50 vs olmo-100 base

dolmino-100 hit the lowest val loss 2.492 (−0.31 vs base ~2.80); olmo50-dolmino50 2.601.

⚠ but downstream evals reversed it
ARC-Easy: base 0.651 → dolmino-100 0.547 (−10.4pp) vs olmo50-dolmino50 0.610 (holds). HellaSwag fell for both. No mixing ratio declared a winner. Hypothesis: re-warm to peak LR caused forgetting; gentle-LR retry pending.

SFT: tulu_math_uc_mix on the CPT’d 2B

AuroraGPT-2B SFT loss curves on tulu_math_uc_mix

Run 1 complete: final loss 0.77 over 4.5B tokens (3 epochs, 32N, GBS=6,144).

  • checkpoint-729-hf is a first-class production asset: it feeds every downstream GRPO experiment.
  • Survived 3 SIGABRTs across 4 mpiexec auto-retry attempts.
  • Run 3 in progress on the full ~54B-token mix (~93.1M rows); the v2-base variant is blocked by a scale-only oneCCL fault at 32N.

GRPO: arithmetic RL on Intel XPU

On-policy RL via TRL GRPOTrainer + vLLM-XPU, on the SFT’d 2B (sum_digits task, 1,000 steps, 8N × 12 = 96 ranks, LR 1e-6):

Arithmetic GRPO reward and accuracy curves over 1000 steps on the SFT'd AuroraGPT-2B

Accuracy reward climbs ~0.4 → ~0.9 (from ~0 cold-start).

  • Multi-node GRPO now works (2026-07-06): the apparent “desync” was two ordinary bugs (a oneCCL MPI-transport SIGSEGV + an AVG→SUM reduction), not an XPU pathology.
  • Still experimental (verified on XPU, not production-ready).

The Restarts: At Scale, Failure is the Default

  • Llama 3 405B — 16K H100s · 54 days · 419 failures (≈ 1 every 3h); 99% recovered via automation13
  • OPT-175B — 35 manual restarts + 100+ cycled hosts in 2 mo on ~1K A100s14
  • BLOOM-176B — frequent loss spikes; embedding-norm + checkpoint cadence on 384 A100s × 3.5 mo15
  • GLM-130B — loss spikes “increasingly frequent”; some recover, others go to NaN16

“The Restarts”: three layers of recovery

Bad-node failover, hang-watchdog, and PBS resubmit each operate at a different scope.

flowchart TB subgraph job["📋 JOB scope — PBS · hours"] JOB_TXT["<i>crash / walltime</i> →<br/><span style='font-weight:700'>chained resubmit</span>"] subgraph node["🖥 NODE scope — failover wrapper · minutes"] NODE_TXT["<i>bad host detected</i> →<br/><span style='font-weight:700'>swap from spare pool</span>"] subgraph proc["⏱ PROCESS scope — ezpz launch · seconds"] PROC_TXT["<i>stdout idle ≥ N s</i> →<br/><span style='font-weight:700'>kill + backoff</span>"] end end end classDef jobC fill:#118cc208,stroke:#118cc2,color:#118cc2,stroke-width:1.5px classDef nodeC fill:#ee8f2408,stroke:#ee8f24,color:#ee8f24,stroke-width:1.5px classDef procC fill:#1da81108,stroke:#1da811,color:#1da811,stroke-width:1.5px classDef jobTxt fill:transparent,stroke:none,color:#118cc2 classDef nodeTxt fill:transparent,stroke:none,color:#ee8f24 classDef procTxt fill:transparent,stroke:none,color:#1da811 class JOB_TXT jobTxt class NODE_TXT nodeTxt class PROC_TXT procTxt class job jobC class node nodeC class proc procC
flowchart TB subgraph proc["⏱ PROCESS · seconds"] PROC_TXT["<i>stdout idle ≥ N s</i> →<br/><span style='font-weight:700'>kill + backoff</span>"] end subgraph node["🖥 NODE · minutes"] NODE_TXT["<i>bad host detected</i> →<br/><span style='font-weight:700'>swap from spare pool</span>"] end subgraph job["📋 JOB · hours"] JOB_TXT["<i>crash / walltime</i> →<br/><span style='font-weight:700'>chained resubmit</span>"] end proc -- "process exit" --> node node -- "node exhaustion" --> job classDef jobC fill:#118cc208,stroke:#118cc2,color:#118cc2,stroke-width:1.5px classDef nodeC fill:#ee8f2408,stroke:#ee8f24,color:#ee8f24,stroke-width:1.5px classDef procC fill:#1da81108,stroke:#1da811,color:#1da811,stroke-width:1.5px classDef jobTxt fill:transparent,stroke:none,color:#118cc2 classDef nodeTxt fill:transparent,stroke:none,color:#ee8f24 classDef procTxt fill:transparent,stroke:none,color:#1da811 class JOB_TXT jobTxt class NODE_TXT nodeTxt class PROC_TXT procTxt class job jobC class node nodeC class proc procC

Inner loops catch most failures; outer loops catch the rest.

Restarts, since TPC’26: native --auto-retry

The failover cascade moved out of bash and into ezpz:

ezpz launch --auto-retry --np 512 -- python -m torchtitan.train
  • Classifies each attempt → success / walltime / bad-node / stuck-pre-training; swaps a spare, re-execs. Ships in saforem2/ezpz#144.
  • Broke the 20B/512N stall: the sync chain sat walltime-blocked for weeks (a PBS mid-save kill left a stale step-4500/ placeholder); auto-retry relaunch drove 4,400 → 5,400 cleanly.

⚠ async checkpointing kills the cluster
At 20B / 512N+, —checkpoint.async-mode=async (a 244 GB background write) takes the job down. Workaround today: CHECKPOINT_ASYNC_MODE=disabled.

What generalizes, what doesn’t

Generalizes across vendors / sites / models

  • Bit-exact deterministic smoke gate after every upstream sync
  • lm-eval as the ground truth for “is it actually learning?”
  • Spare-node failover wrapper — same idea on Slurm
  • Launcher / env autodetect — push every vendor-shaped assumption out of training code

Doesn’t generalize (needs per-(config, hardware, version) tuning)

  • torch.compile decisions (and it hurts MoE on XPU)
  • AC boundaries (MoE + AC + compile = grief)
  • EP↔FSDP frontier
  • Collective tuning (XCCL vs gloo fallbacks, NCCL env)
  • Optimizer stability at the bf16 corner (LR-finder ranking ≠ sustained stability)

What it took to train AERIS

On the same machine (Aurora), but with its own separate stack, AERIS17 is a pixel-level Swin diffusion transformer for generative weather & climate prediction, the first diffusion model taken to this scale, a useful contrast point for what full-machine training demands.

ModelSwin diffusion transformer, 1.3B → 80B params
DataERA5 reanalysis at 0.25°, 1×1 patches
MachineAurora, up to 10,080 nodes
ParallelismSWiPe: window ∥ + sequence ∥ + pipeline ∥ (no added comm / no larger global batch)
Sustained10.21 EFLOP/s (mixed precision); 11.21 peak
Scaling95.5% weak, 81.6% strong
Skillbeats IFS ENS; stable to 90-day seasonal rollouts
  • A separate codebase, but the same operational realities we hit: bf16 stability, full-machine scaling, and failure tolerance at 10k nodes.
  • Its hard new problem was scaling diffusion stably at high resolution, which SWiPe’s window-parallelism addresses without inflating the global batch.
QR code linking to samf.sh/talks/2026/07/14samf.sh/talks/2026/07/14

Thanks

AuroraGPT team: Venkat Vishwanath, the AI/ML Group at ALCF, collaborators across ANL.

Argonne Leadership Computing Facility: Aurora time, Sunspot staging.

Intel: Intel Max 1550 XPU + oneAPI / XCCL / IPEX support throughout.

Code & docs

This research used resources of the Argonne Leadership Computing Facility, which is a DOE Office of Science User Facility supported under Contract DE-AC02-06CH11357.

Questions?

Appendix: backup slides

Material that didn’t make the main path but is here for Q&A.

  • Open questions
  • Silent-correctness bugs (bf16 RMSNorm freeze · TP loss reporting)
  • Post-training: CPT · SFT · GRPO
  • Failover engineering deep-dive
  • yeet-env tarball broadcast scaling

Open questions: the ask

  • A portable bit-exact regression suite across vendors — does anyone have one?
  • torch.compile at 1T scale — defensible decision tree?
  • Async-checkpoint Pareto frontier: recovery time × frequency × storage cost in production
  • Optimizer failure at 80B+: SophiaG (Hessian-diagonal estimate saturates) + Muon (Newton-Schulz iterations overflow bf16) both diverge — algorithmic limit or fp-precision artifact?
  • MoE EP↔FSDP scaling boundaries at 16B / 64B / 100B
  • Make xccl honor train_timeout_seconds so we don’t have to rely on an stdout-idle watchdog as the hang-detection ground truth

Operational reality — bad-node failover

5+ production jobs killed by bad-node failures in 2 weeks. Pattern: PBS gives us 256 / 512 nodes, one is bad, training crashes or hangs after N hours, walltime gone.

JobTrajectoryFailure
84598182B 256N v2shepherd died from signal 9 after step 2070
847010220B 256N v2gloo TCP Connection closed by peer after ~3h
847957920B 512N v2silent hang at step 803 (heartbeat continued)

Failover wrapper: request select=N+spare (~2%, min 4). Split into active + spare pool. On crash, scrape bad nodes from log, swap a spare in, retry.

qsub -q prod -l select=522 -v NHOSTS_TRAIN=512 \
    submit_agpt_2b_aurora_venv_failover.sh

Handles 6 recurring crash modes. Does not handle silent hangs — those still need a heartbeat watchdog.

Silent-hang detection: ezpz launch --timeout / --retries

The problem. xccl on XPU silently ignores train_timeout_seconds, so a torchtitan job stuck in a hung collective sits consuming the full PBS walltime instead of aborting. Every collective hang quiets stdout (every rank blocks in the same call, nothing reaches the log) — that’s the signal we can act on.

ezpz launch --timeout 600 --retries 3 \
    python -m torchtitan.train --config-file ./config.toml
  • --timeout SECONDS — kill the launched process if its stdout goes idle (not walltime) for this many consecutive seconds. Returns exit code 124 (matches GNU timeout(1)).
  • --retries N — re-execute on any non-zero exit (including the watchdog’s 124) up to N times. Exponential backoff: 5s → 10s → 20s → 40s → 60s (capped).

Scope caveat. Watches only the process ezpz launch spawns directly. If qsub runs a wrapper script that internally invokes python train.py, the watchdog needs to live inside that script (or you wrap the inner call with ezpz launch too).

ezpz launch --timeout: one hang/recover cycle

stateDiagram-v2 direction LR [*] --> Running Running --> Hung: no stdout for N seconds Hung --> Killed: SIGTERM → SIGKILL Killed --> Running: backoff, re-exec
stateDiagram-v2 direction TB [*] --> Running Running --> Hung: no stdout for N seconds Hung --> Killed: SIGTERM → SIGKILL Killed --> Running: backoff, re-exec

Every collective hang shows up as silence on stdout — the process is “alive” by kill -0 but nothing is happening. The watchdog fires on the absence of progress, not on a heartbeat ping.

--auto-retry: bad-node failover, on tap

Allocate spares up front, swap them in on failure:

# 522 nodes allocated, train on 512, keep 10 as spares.
# Loop until success, walltime, or spare exhaustion.
ezpz launch --auto-retry --np 512 -- python -m torchtitan.train
  • Classifies each attempt’s exit → success / walltime / bad-node / stuck-pre-training
  • On bad-node: scrapes the failing host from the log, swaps in a spare, re-execs
  • Guards against config bugs: 2 consecutive attempts with zero step= markers → stop (don’t burn the whole spare pool on a broken run)

Ships in saforem2/ezpz#144. Same scraper as the bash-lib path; pure-Python loop on top.

Failover wrapper: caught a real silent hang in production

Job 8505298, 2026-05-23. Attempt 1 trains cleanly steps 1→37, then log goes completely silent at step 37. No traceback, no MPI error, no rank dying. Just dead.

Time (CT)Event
21:06:41step 37 logged · loss 11.80 · tps 3,919
21:36:4130 min dead air · ezpz launch --timeout=1800 SIGTERMs
21:36:43wrapper classifies exit 124 → silent-hang (not walltime)
21:36:43no traceback to scrape → blind swap of rank-0 host
21:36:45attempt 2 launches on swapped node set
21:57:49walltime hit · step 296 · loss 5.68 · ckpts persisted

Three new pieces had to fire in sequence on a real-world hang to prove production-readiness: --timeout=1800 watchdog · exit 124 classification distinct from PBS exit 143 · failover_swap_one_blind() when no specific bad node can be identified. They did.

Full writeup: docs/experiments/agpt/aurora/20260523-failover-silent-hang-recovery-8505298.md

Silent bug #1 — bf16 master ⇒ RMSNorm frozen

Symptom. Loss curves looked reasonable. lm-eval scores didn’t move — ARC-Easy stuck at ~0.27 (random baseline) for 17K+ steps.

Cause. training.dtype=bfloat16 → bf16 master copy. RMSNorm weights init at 1.0; bf16 ULP at scale 1.0 is ~7.8e-3. Per-step optimizer update is ~1.6e-5every update rounds to zero. All 25 RMSNorm tensors stayed at exactly 1.0 from step 100 → 17,400.

Why other params trained fine. Linear layers init at std≈0.02 → bf16 ULP at scale 0.02 is ~3.8e-5, same magnitude as the update. RMSNorm’s larger init scale = coarser ULP = updates lost in rounding.

Fix. Default training.dtype=float32, FSDP MixedPrecisionPolicy(param_dtype=bf16, reduce_dtype=fp32). Master is fp32; forward/backward stay bf16. Extra ~1 GB master at 2B, ~10 GB at 20B — under budget.

Silent bug #1 — the smoking gun

v1 (bf16-master) vs v2 (fp32-master) — 20B lm-eval

v1 (bf16 master, 256N): ARC-Easy 0.27 flat across 2,500 steps · v2 (fp32 master, 512N): ARC-Easy 0.27 → 0.44 by step 800 · HellaSwag breaking out.

Lesson: loss looks like training. lm-eval is the only ground truth for “is the model actually learning?” Add a periodic eval gate.

Silent bug #2 — TP loss reported / dp_world_size

  • Symptom: Step-1 loss for agpt_* (vocab=256128) should be ln(256128) ≈ 12.45 - On TP=1 we see 12.95 ✓ - On TP=2 after upstream commit 1786292d (2026-04-27): - step-1 reported as 1.07 ✗ — exactly 12.84 / 12 where dp_world_size = 12

  • Cause: _dist_reduce() short-circuits on DTensor with full_tensor()

    • But the loss is Replicated on the TP mesh, and the reduction was requested over batch_mesh (orthogonal)
    • The short-circuit silently drops the cross-batch sum
  • Why it survived review: Gradients + optimizer steps are correct

    • Only the loss: field that lands in stdout / W&B is wrong
    • Loss curves look “reasonable” — just 1/12 of the true value
    • Filed as pytorch/torchtitan#3204; our workaround calls loss.full_tensor() before dist_sum in ezpz/trainer.py:503-516
  • Lesson: Bit-exact smoke caught this immediately; the prior 2B TP=1 baseline gave us a number to disagree with

ezpz yeet: Efficiently Running 50k Python Processes

Nodesyeet (s)First-step (s)Per-node (ms)
869.729.38,712
1689.731.65,606
3289.220.92,788
6491.234.61,425
128110.430.5862
256132.937.6519
512174.544.5341
1024255.460.8249
2048421.494.8206
4096750.6194.0183
Total wall-clock for yeet broadcast vs node count, 8 to 4096 nodes

Two regimes. 8–64 nodes extract-bound (~70–91s flat, per-node cost falls 8.7s → 1.4s); ≥128 nodes broadcast-bound, each 2× in nodes adds ~1.5–1.8× wall-clock.

Full write-up: samf.sh/posts/2026/05/01

Footnotes

Footnotes

  1. Argonne National Laboratory

  2. And away from argonne-lcf/Megatron-DeepSpeed!

  3. An Empirical Model of Large-Batch Training

  4. How Does Critical Batch Size Scale in Pre-training?

  5. How to Set the Batch Size for Large-Scale Pre-training?

  6. Weighted blending across datasets.

  7. Sophia: A Scalable Stochastic Second-order Optimizer for Language Model Pre-training

  8. See the 📊 All Runs section.

  9. Cyclical Learning Rates for Training Neural Networks (Smith 2015)

  10. How do you find a good learning rate (Gugger 2017)

  11. Extending µP: Spectral Conditions for Feature Learning Across Optimizers (Gupta et al. 2026)

  12. mano: manifold-normalized optimization (2026). Implemented in our fork alongside SPAM (2501.06842).

  13. Llama 3 herd of models (Meta AI, 2024), §3.3.2 (Training reliability)

  14. OPT-175B chronicles + dev log (Zhang et al., 2022)

  15. BLOOM: A 176B-Parameter Open-Access Multilingual Language Model (BigScience, 2022)

  16. GLM-130B: An Open Bilingual Pre-Trained Model (Zeng et al., ICLR 2023)

  17. AERIS: Argonne Earth Systems Model for Reliable and Skillful Predictions (arXiv:2509.13523).

samf.sh/talks/2026/07/14/ 1
 Slides
Theme
 Font
 Table of Contents
    Keybinds
    j / →Next slide / fragment k / ←Previous slide / fragment SpaceAdvance (fragments first) g / GFirst / last slide Ctrl+.Open speaker notes window EscToggle overview / close dialog fFullscreen pPrint / export to PDF ?This dialog