Skip to content

test(nightly): distributed SFT functionality suite - #4077

Draft
garrett361 wants to merge 41 commits into
mainfrom
feat/nightly-distributed-tests
Draft

garrett361 wants to merge 41 commits into
mainfrom
feat/nightly-distributed-tests

Conversation

@garrett361

@garrett361 garrett361 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Adds a nightly suite that runs trainer-only SFT across every registered trainer architecture, two 4-GPU parallel layouts and a few memory features, to catch catastrophic trainer breakage before it reaches a training run. This PR is the infra. It already catches real bugs (below), which get fixed in follow-ups.

  • Each case trains a seeded tiny checkpoint with uv run sft for 5 steps on one 4-GPU node and checks for a clean exit and finite loss. qwen3_moe's fsdp4 base case instead trains to step 8 with a checkpoint at step 5, resumes from it in a fresh process, and requires both runs' losses at steps 6-8 to agree within 1e-3. It trains at lr 1e-4, since at the default 1e-6 a few steps barely move the weights. Calibrated on that case, correct resumes differ by at most 2.5e-5, and a resume without the optimizer or dataloader state by 1.5e-2 or more. Save and resume is mostly arch-independent code, so one case covers it.
  • Tiny models start from each arch's published HF config, cut to the fewest leading layers that cover every layer kind. MoE archs then get a smaller hidden size, smaller intermediate sizes and fewer experts. Layer kinds, vocab, head counts and dims, and the attention and indexer shapes stay published. At published widths the MoE checkpoints reached 26 GB, took up to 77 GB of host memory to build, and made GLM-5 and minimax_m2 launches about 80 s slower each. qwen4_exp's n-gram table is also shrunk from about 51B params. GLM-5 and DSv4 use index_topk 128 and 64 instead of 2048 and 512: the suite's documents are shorter than the published top-k, so the indexer kept every key. Now 84% and 68% of query tokens have more candidates than picks. 128 is the smallest value FlashMLA's SM90 sparse kernel accepts for GLM-5.
  • MoE archs run fsdp4_ep4 and cp4_ep4, and dense archs run the same layouts with ep=1. The full suite is 33 launches.
  • Every arch's cp4 base case also launches with TORCH_LOGS=graph_breaks,dynamo, and test_no_graph_breaks fails on any graph break in that launch, reported as its own test next to test_sft. fullgraph=True is not a substitute: it enables capture_dynamic_output_shape_ops and can hide a fullgraph=False break.
  • Feature rows on qwen3_moe each change one setting: selective AC, activation offloading off, optimizer offloading off, full offloading, DeepEP dispatch at EP=4 (deepep_full_ac), and a fullgraph=True row on cp4 that fails on any graph break. Every other case keeps the defaults: compiled, full AC and both fusions.
  • qwen3.5 and qwen4_exp are covered through their text configs only. llama uses the ungated unsloth/Llama-3.2-1B-Instruct tokenizer with an explicit llama-3 renderer, since SFT's llama key is the gated meta-llama repo. afmoe is left out, as a TODO in archs.py: SFT has no typed renderer for it.
  • tiny_models.py holds one TinyModel per arch (tiny config, tokenizer, renderer) and the checkpoint writer, with nothing SFT-specific, so other suites can reuse it. sft/archs.py defines the matrix: the parallel layouts, the features, and one Arch per arch pairing its TinyModel with how the suite runs it. sft/harness.py renders and checks cases, and sft/README.md gives a short overview.
  • --shard I/N splits the suite across 4-GPU runners. Each arch's base cases and its feature cases are separate units, packed longest first by estimated launch time, so a shard builds each checkpoint it needs once.
  • A failed launch reports crash or timeout, the run dir, and the tails of trainer.log and of the rank that torchrun names as the root cause.
  • Runs write under the shared output_dir (PYTEST_OUTPUT_DIR in CI), use HF_HUB_OFFLINE=1 after downloading each tokenizer once per session, and pass --no-monitors.prime, as the integration tests do: the suite only reads the file monitor's metrics.jsonl.
  • The suite lives in tests/regression/distributed/, and test_parallel_invariance.py moves there too, since it is also a multi-GPU trainer test. Its path in regression.yml and the docs follows.
  • scripts/ci/run_distributed_nightly.sh runs the suite. The GitHub workflow comes in a later PR, once the 4-GPU runner pool exists.

The suite caught the crash fixed in #4037. Case ids are as they were before #4134 removed ring CP, and the fp32-router FSDP unit the crash needed was later removed by #4177:

case 1e5318108 (before #4037) 4f4d7e440 (#4037)
qwen3_moe-cp2_ring_ep4-base aten.empty_like.default ... not found in storage pass
qwen3_moe-cp4_ulysses_ep8-base aten.empty.memory_format ... not found in storage pass

Known failures (follow-ups)

These are left failing on purpose, so the nightly shows them until they are fixed. A full run of the matrix on one node gave 80 passes and exactly the 5 graph-break failures below. The DeepEP row, added after that run, fails on its own run as listed. Every test_sft case passes: the AC failure that broke qwen3_5_moe and qwen4_exp and qwen3_5_dense's Ulysses CP failure, both seen on the earlier 60-case matrix, no longer reproduce on current main.

case failure
test_sft[qwen3_moe-fsdp4_ep4-deepep_full_ac] Autograd not support dtype: Byte in DeepEP combine (#3954)
test_no_graph_breaks[qwen3_5_dense-cp4_ep1-base] torch.compiler.disabled call (gated_delta_net.py:88)
test_no_graph_breaks[qwen3_5_moe-cp4_ep4-base] same gated_delta_net.py:88 break
test_no_graph_breaks[qwen4_exp-cp4_ep4-base] same gated_delta_net.py:88 break
test_no_graph_breaks[gpt_oss-cp4_ep4-base] dynamo skips FA4's interface (flash_attn/cute/testing.py:492)
test_no_graph_breaks[nemotron_h-cp4_ep4-base] dynamic-shape op in FLA (fla/ops/utils/index.py:128)
  • gpt_oss's first-use FA4 kernel compile once took over 600 s on a cold node, so each run gets 900 s. The slowest run in the full pass took 416 s (deepseek_v4 fsdp4).
  • _from_config leaves MoE expert weights uninitialized on qwen3_moe, glm4_moe, glm_moe_dsa, minimax_m2 and laguna, so the suite's tiny-checkpoint writer initializes them itself. A shared fix in model code must not re-initialize loaded weights on the from_pretrained path.

A full pass of the matrix (32 launches before the DeepEP row) takes about 2 runner-hours on our cluster, 140 to 300 s per run with cold compile caches and imports from /home, so 4 shards finish in about 30 min plus runner setup. Per-launch overhead, not training, dominates: a 5-step launch spends about 120 s before step 1 and 35 to 75 s compiling in step 1. Two production changes cut that and come as separate PRs: a light import path for the launcher's pre_download_* (about 44 s per launch) and removing a CPU-side sum in the EP token dispatcher that makes inductor build C++ kernels (11 to 15 s of step 1).

Running on SLURM

From the repo root, on a node with at least 4 GPUs. Extra args go to pytest. -k selects cases by arch, layout or feature name, and --shard I/N runs one shard.

srun -N1 --exclusive -p <partition> --time=02:00:00 scripts/ci/run_distributed_nightly.sh -k qwen3_moe

Python imports from a venv on /home dominate per-launch time on our cluster. A node-local venv (UV_PROJECT_ENVIRONMENT=/tmp/$USER/venv uv sync) cuts a launch from about 3.2 min to about 70 s. None of that is in the code.

🤖 Generated with Claude Code

@garrett361
garrett361 force-pushed the feat/nightly-distributed-tests branch 2 times, most recently from 3548bba to 741c0fd Compare October 22, 2026 20:33
@garrett361
garrett361 force-pushed the feat/nightly-distributed-tests branch from 3c93c9b to 1608da7 Compare October 28, 2026 15:06
garrett361 and others added 27 commits November 4, 2026 17:54
A shared base config pins the fp32 MoE router and the batch shape, and each
arch overlay sets its real tokenizer. train.jsonl holds 300 short chat samples
(200 to 1200 tokens) from the chat split of PrimeIntellect/INTELLECT-3-SFT-10K
(Apache-2.0), with a .gitignore exception for that one file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Runs trainer-only SFT on seeded tiny checkpoints of qwen3_moe, glm_moe_dsa and
deepseek_v4 across 8-GPU FSDP, HSDP and CP layouts with EP, plus memory and
compile feature rows. Each case checks a clean exit, finite loss and zero NaN
micro-steps, and base cases also check a checkpoint resume. A CPU test
validates every case's config and layout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Arch.layouts and the skip dict were two ways to drop a layout, and only skip
showed up in the report. Every arch now runs all layouts, and GLM's ulysses
case is a visible skip because its sparse MLA ignores cp_style.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The TileLang sparse MLA backward kernel needs warp_row_tiles >= 16, which 16
heads did not provide ("got 8"), so the first backward failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nothing consumes it yet, and it wrote a report file into the repo root on
every run. The future workflow can add it if it uploads one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The per-arch overlay TOMLs held only tokenizer.name, so the tokenizer ids now
live in tiny_models.py and each case passes --tokenizer.name directly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… SFT suite

Adds tiny configs and tokenizers for llama, qwen3, qwen3.5 (dense and MoE text
configs), glm4_moe, gpt_oss, laguna, minimax_m2, nemotron_h and qwen4_exp.
Dense archs run on a separate ep=1 layout family selected through
Arch.layouts. llama and afmoe are visible skips: llama's renderer key is a
gated repo we cannot fetch, and afmoe has no typed renderer for SFT.

The tiny-checkpoint writer now calls init_buffers_post_meta like the trainer,
initializes MoE layers itself because _from_config leaves expert weights as
uninitialized memory on several archs, and drops an HF-format assert that a
dense Qwen3.5 model cannot satisfy. The per-launch timeout drops to 600 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ring CP and model.cp_style were removed upstream (#4134), so every case
failed config validation. CP layouts now carry only a degree (cp2_ep4,
cp4_ep8, cp2_ep1, cp4_ep1), and the CP-support skips are gone: cp_blocker
accepts cp 2 and 4 for every tiny config, and DSv4 and GLM-5 keep their own
internal CP.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…est under tests/regression/distributed

The nightly test directory was removed upstream (#4122), and scheduled GPU
tests now live under tests/regression/. The SFT matrix, its tiny-model
helpers and test_parallel_invariance.py are all multi-GPU trainer tests, so
they share tests/regression/distributed/. The SFT configs and data move to
tests/regression/configs/distributed/sft/ next to the other suites' configs,
and the parallel job in regression.yml, the docs and the launcher point at
the new paths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CI runners will have 4 GPUs. Every layout now spans NUM_GPUS = 4, set once in
cases.py, and MoE layouts use ep=4 wherever ParallelDims allows it: fsdp4_ep4,
cp2_ep4, cp4_ep4, plus hsdp2x2_ep2, since a 2x2 HSDP mesh caps EP at 2.
Dense archs run the same shapes at ep=1. batch_size drops from 32 to 16 so
each rank still gets 4*cp micro-batches per step.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SFT's only llama renderer key is the gated meta-llama repo, which our HF token
cannot fetch. llama now uses unsloth/Llama-3.2-1B-Instruct with an explicit
llama-3 renderer, passed through a new Arch.overrides field for arch-wide
config. afmoe stays skipped: SFT has no typed renderer for it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tible shapes

Both archs now default to the cuDNN FlashMLA sparse-attention backend, whose
constructor check needs 64 or 128 query heads, and head_dim 512 for DSv4. The
tiny configs used 32 heads (and head_dim 128 for DSv4), so every case failed
at model construction. GLM-5 moves to 64 heads, DSv4 to 64 heads with head_dim
512 and a 0.125 rotary fraction (the real model's 64 rotary dims).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#4177 removed model.moe_router_dtype, which broke base.toml's fp32 pin and
the bf16_router feature row. The base now keeps the ModelConfig defaults, and
a fullgraph=True row on qwen3_moe's cp2_ep4 layout turns any graph break in
the EP all-to-all (#4086) or Ulysses CP (#4068) paths into a hard failure.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sume on qwen3_moe only

Each tiny config is now the arch's published config.json cut to the shortest
layer prefix that covers every layer kind, with per-layer lists cut to match
and quantization and MTP fields left out. Two diversions keep it runnable on
4 H200s and are documented in the module docstring: DeepSeek-V4 uses 32
experts instead of 256, and qwen4_exp shrinks its n-gram table base from 20M
to 200k.

Real-size checkpoints reach 60-160 GB, and save/resume is mostly
arch-independent, so only qwen3_moe (Arch.resume) still saves and resumes;
its four layouts keep the layout-dependent DCP coverage.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The published llama, qwen3 and qwen3.5 dense configs tie the LM head to the
token embedding, and safetensors refuses to save a shared tensor under two
names. Tied checkpoints now omit lm_head.weight, as HF checkpoints do and as
the trainer's loader expects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rom training

Blocks compile with fullgraph=False, where a graph break silently runs part of
a block eagerly, and fullgraph=True can hide such breaks because it enables
capture_dynamic_output_shape_ops. Each arch's cp2 base case (EP and CP) now
launches with TORCH_LOGS=graph_breaks,dynamo, and test_no_graph_breaks parses
that same launch's rank logs into one line per distinct break. A shared
launch fixture runs each case once, so training and graph-break results are
separate tests. The check skips if the launch never finished step 1, and
fails if any rank lacks dynamo's tracing messages, so it never passes on
missing evidence.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MoE archs now use a smaller hidden size, smaller intermediate sizes and
fewer experts. Layer kinds, vocab, head counts and dims, and the attention
and indexer shapes stay published. At published widths the MoE checkpoints
reached 26 GB and took up to 77 GB of host memory to build, and GLM-5 and
minimax_m2 launches took about 80 s longer each.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Every arch runs fsdp4 and cp4 only. The graph-break check and the
  fullgraph row move to cp4, and resume runs on qwen3_moe fsdp4_ep4 only.
  The suite drops from 64 launches to 35.
- `--shard I/N` splits the suite across 4-GPU runners. Each arch's base
  cases and its feature cases are separate units, packed longest first by
  estimated launch time.
- A failed launch reports crash or timeout, the run dir, and the tails of
  trainer.log and of the root-cause rank's logs. `ProcessResult` records
  whether `run_process` timed out, and assertions in cases.py are rewritten
  so metric mismatches show their values.
- Launches write under the shared output_dir and kill any process left
  behind, found by an env marker. A launch fails fast if a GPU already
  holds memory, and runs offline after a once-per-session tokenizer
  download, with `uv run --no-sync`. Two timeouts in a row end the session.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
None of these configurations is run in practice, so the three rows cost
three launches per suite without guarding anything users depend on. Every
case keeps the defaults: compiled, activation checkpointing on and both
fusions (gate_up, qkv) enabled. The suite now has 5 feature rows and 32
launches.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sume

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es from case ids

Each arch and feature is now written once, inside ARCHS and FEATURES, so
there is no second list to keep in sync. check_k_selectable checks the
parts that actually appear in case ids.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… tokenizer

An Arch now holds its name, tiny config and tokenizer next to how the suite
runs it, so TINY_CONFIGS, TOKENIZERS and the name-keyed lookups across files
are gone. tiny_models.py moves to sft/archs.py, which defines ARCHS and
CASES. afmoe, which had no tiny model and was always skipped, leaves the
matrix, and with it Arch.skip. test_configs.py is removed: a bad override
or layout already fails the launch loudly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A short overview of what the suite checks, how its files are organized,
why only qwen3_moe runs feature rows and resume, and how to run it.
archs.py gets a Notes section and TODOs for afmoe and for the two weight
fixes that belong in model code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
garrett361 and others added 10 commits November 4, 2026 17:54
Removes the busy-GPU check, the env-marker kill of leftover processes and
the abort after two timeouts in a row, to keep the harness simple.
run_process already kills a timed-out run's process tree, and a CI job
timeout bounds a hung suite. Each `uv run sft` process is now an SFTRun,
so the resume case reads as a train run plus a resume run rather than as
retried attempts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…GPU steps

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… in harness.py

The Layout, Feature, Arch and Case classes move next to ARCHS in archs.py,
so one module defines the matrix. cases.py, which is left with rendering
`uv run sft` arguments, sharding and parsing metrics and logs, becomes
harness.py. The README describes the new layout and spells out the
coverage: features on one representative arch, base cases on every other.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`-rs` replaced pytest's default `-rfE`, so the end-of-run summary listed
skips but no failures. The slowest single run in a full cold-cache pass
took 416 s (deepseek_v4 fsdp4), close to the old 600 s timeout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each arch's tiny config, tokenizer and renderer now live on a TinyModel in
tests/regression/distributed/tiny_models.py, with the checkpoint writer.
An SFT Arch pairs a TinyModel with how the suite runs it, so a future
suite can reuse the models without the SFT policy. llama's renderer moves
from Arch.overrides onto its TinyModel, and Arch.overrides is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`--clean` only deletes the run dir once the launcher reaches its setup, so
a run that failed earlier under a reused PYTEST_OUTPUT_DIR left the
previous session's logs/latest in place, and the failure message showed
those stale logs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
deepep_full_ac runs qwen3_moe at EP=4 with DeepEP dispatch on the
defaults (compile, full AC, offloading). On main it fails at the first
backward with `Autograd not support dtype: Byte` in DeepEP's combine,
the full-AC failure that #3954 fixes, so the nightly shows it until then.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The suite's documents (about 200 to 1200 tokens) are shorter than the
published index_topk, so the indexer kept every key and the selection
never dropped one. index_topk is now 128 for glm_moe_dsa and 64 for
deepseek_v4, so about 84% and 68% of query tokens have more candidates
than picks. 128 is the smallest value FlashMLA's SM90 sparse prefill
accepts for GLM-5 (64 fails `params.topk % (2*B_TOPK) == 0`). Both archs
pass on fsdp4 and cp4, graph-break checks included.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sses

The resume case used to pass if only the step counter survived. It now
trains to step 8 with a checkpoint at step 5, resumes from step 5 in a
fresh process, and requires the two runs' losses at steps 6-8 to agree
within 1e-3. It trains at lr 1e-4, since at the default 1e-6 a few steps
barely move the weights. Calibrated on qwen3_moe fsdp4_ep4: two correct
resumes differ by at most 2.5e-5, while skipping the optimizer state
(1.5e-2 from step 7) or the dataloader state (3.6e-2 from step 6) breaks
the tolerance. Skipping the scheduler goes unseen, since the default
schedule is constant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since #4228 every sft launch registers a run on the Prime platform and
streams its metrics there. The suite only reads metrics.jsonl from the
file monitor, so the Prime monitor would add a platform run per launch
with a key, and fail every launch at startup without one. Integration
tests and the parallel-invariance test already pass --no-monitors.prime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@garrett361
garrett361 force-pushed the feat/nightly-distributed-tests branch from 3dc2796 to 48fa885 Compare November 4, 2026 18:30
@wiz-prime-intellect

wiz-prime-intellect Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings -
Software Management Finding Software Management Findings -
Total -

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

The Wiz data scanner flags personal details in the suite's training data:
names, dates of birth, ages, gender, citizenship, religion, an address and
a criminal conviction, mostly from public bios and news stories, and one
sample that looks like a private person's chat with their street address.
This removes the 22 samples matching its findings, leaving 278.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Wiz data scanner still finds names in author and director credits,
a named person in a reading-comprehension passage and a nationality.
This removes the 19 samples with those, leaving 259.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Wiz data scanner's last Name finding points at a singer, a musician,
a novel's character and a credited author. This removes those 4 samples,
leaving 255.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chat split of PrimeIntellect/INTELLECT-3-SFT-10K carries real names,
bios and at least one private person's address, which the Wiz data
scanner keeps flagging. The suite now trains on 300 samples from the
dataset's math split: each keeps its problem and cuts its solution at a
word boundary so its token count matches one of the original chat
samples (median 817 vs 818 tokens, 68 vs 66 packed 4096-token rows).
Samples with a capitalized word mid-sentence, a rough proxy for a proper
name, are skipped. The qwen3_moe base cases pass on it, resume check
included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant