Repository navigation
test(nightly): distributed SFT functionality suite - #4077
Draft
garrett361 wants to merge 41 commits into
Draft
garrett361 wants to merge 41 commits into
garrett361 wants to merge 41 commits into
Conversation
garrett361
force-pushed
the
feat/nightly-distributed-tests
branch
2 times, most recently
from
October 22, 2026 20:33
3548bba to
741c0fd
Compare
garrett361
force-pushed
the
feat/nightly-distributed-tests
branch
from
October 28, 2026 15:06
3c93c9b to
1608da7
Compare
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>
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
force-pushed
the
feat/nightly-distributed-tests
branch
from
November 4, 2026 18:30
3dc2796 to
48fa885
Compare
Wiz Scan Summary
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
uv run sftfor 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.index_topk128 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.fsdp4_ep4andcp4_ep4, and dense archs run the same layouts with ep=1. The full suite is 33 launches.TORCH_LOGS=graph_breaks,dynamo, andtest_no_graph_breaksfails on any graph break in that launch, reported as its own test next totest_sft.fullgraph=Trueis not a substitute: it enablescapture_dynamic_output_shape_opsand can hide afullgraph=Falsebreak.deepep_full_ac), and afullgraph=Truerow on cp4 that fails on any graph break. Every other case keeps the defaults: compiled, full AC and both fusions.unsloth/Llama-3.2-1B-Instructtokenizer with an explicitllama-3renderer, since SFT's llama key is the gated meta-llama repo. afmoe is left out, as a TODO inarchs.py: SFT has no typed renderer for it.tiny_models.pyholds oneTinyModelper arch (tiny config, tokenizer, renderer) and the checkpoint writer, with nothing SFT-specific, so other suites can reuse it.sft/archs.pydefines the matrix: the parallel layouts, the features, and oneArchper arch pairing itsTinyModelwith how the suite runs it.sft/harness.pyrenders and checks cases, andsft/README.mdgives a short overview.--shard I/Nsplits 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.trainer.logand of the rank that torchrun names as the root cause.output_dir(PYTEST_OUTPUT_DIRin CI), useHF_HUB_OFFLINE=1after downloading each tokenizer once per session, and pass--no-monitors.prime, as the integration tests do: the suite only reads the file monitor'smetrics.jsonl.tests/regression/distributed/, andtest_parallel_invariance.pymoves there too, since it is also a multi-GPU trainer test. Its path inregression.ymland the docs follows.scripts/ci/run_distributed_nightly.shruns 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:
1e5318108(before #4037)4f4d7e440(#4037)qwen3_moe-cp2_ring_ep4-baseaten.empty_like.default ... not found in storageqwen3_moe-cp4_ulysses_ep8-baseaten.empty.memory_format ... not found in storageKnown 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_sftcase 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.test_sft[qwen3_moe-fsdp4_ep4-deepep_full_ac]Autograd not support dtype: Bytein DeepEPcombine(#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]gated_delta_net.py:88breaktest_no_graph_breaks[qwen4_exp-cp4_ep4-base]gated_delta_net.py:88breaktest_no_graph_breaks[gpt_oss-cp4_ep4-base]flash_attn/cute/testing.py:492)test_no_graph_breaks[nemotron_h-cp4_ep4-base]fla/ops/utils/index.py:128)_from_configleaves 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 thefrom_pretrainedpath.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'spre_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.
-kselects cases by arch, layout or feature name, and--shard I/Nruns one shard.Python imports from a venv on
/homedominate 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