Skip to content
This repository was archived by the owner on Jul 17, 2026. It is now read-only.

feat(optim): Muon — config-gated optimizer for both groups, with fast stacked-NS impl#982

Open
claude-spd1 wants to merge 14 commits into
feature/jaxfrom
feature/muon
Open

feat(optim): Muon — config-gated optimizer for both groups, with fast stacked-NS impl#982
claude-spd1 wants to merge 14 commits into
feature/jaxfrom
feature/muon

Conversation

@claude-spd1

@claude-spd1 claude-spd1 commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

The combined muon PR (per Oli: one PR, one thing) — supersedes and replaces the draft stack #931#975#978, rebased onto current feature/jax (post-#939 schedule unification, post-#966 carve, post-#976 eps change).

What this adds

1. Config-gated Muon for both main optimizers (type: muon on components_optimizer / ci_fn_optimizer): optax.contrib.muon under the same cosine schedule and S19 clip chain; consistent_rms: 0.2 makes AdamW LRs transferable (Kimi recipe). Default type: adamw deserializes for every existing config — canonical trajectories bit-identical.

2. Correct muon leaf labeling per group (run_state.build_optimizers): V/U and the MLP CI-fns use optax's default 2D rule; the chunkwise CI-fn passes explicit MuonDimensionNumbers (3D chunk stacks = batched matrices, 2D bias stacks = Adam fallback) — optax's default rule labels that tree exactly backwards (it would NS-orthogonalize the bias stacks).

3. Fast stacked-NS impl (impl: stacked, param_decomp/muon_stacked.py): same-shape muon leaves batched into one NS with the stack axis sharded over (replicate, fsdp) — device-local orthogonalization, one reshard in/out. GSPMD otherwise lowers per-leaf NS on the ÷N masters into per-iteration full-Gram all-reduces with the largest matmul replicated on every device (verified by HLO probe; a measured 2.1× muon-ci step hit same-hardware). Knobs: ns_steps (default 5), ns_dtype (bfloat16 = NS-only bf16, masters/momentum stay fp32 per N1). Same MuonState pytree → checkpoints round-trip across impls; trajectories match impl: optax up to reassociation (D4 class).

SPEC S20 carries the three dated amendments. Experiment configs are NOT committed per the config policy (CONFIGS.md/#983) — provenance for every quoted run lives in its run-dir pinned launch_config.yaml, snapshot ref, and wandb; the configs remain on the bridge/task-* experiment branches.

Evidence (4L pile PPGD testbed)

  • Quality: muon Pareto-dominates adamw across three imp-min regimes — Lp @2e-4, Lp coeff sweep (3-point Pareto curves don't cross), smooth-L0 @100k (endpoint-readable). muon/muon best everywhere. Findings: lore 2026-07-11--muon-cifn-matrix-pile4l, 2026-07-11--muon-cifn-impmin-pareto, 2026-07-13--smooth-l0-muon-matrix-100k; views in the runs' groups.
  • Speed (steady-state s/step, all same-hardware H100 dp=8): old per-leaf impl 0.44 (2.1× adamw's 0.206) → stacked-bf16 0.23 (~1.13×); muon-on-UV-only ~1.17× old impl, free under stacked. Acceptance parity: every eval metric within <2% of the optax reference through the clean window (runs p-583d58d6, p-3ed7465e). NOTE: earlier drafts quoted 3.3×/0.122-baselines — those mixed a B200 baseline into an H100 comparison (lore 2026-07-13--finding--h100-steptime-regression-was-b200-baseline); all numbers above are same-silicon.
  • Production: the smooth-L0 100k matrix ran its 3 muon cells on stacked-bf16, incl. clean checkpoint-resume through 5 preemptions.

Validation

  • Muon unit tests: 2D orthogonalization + fallback, chunk-stacked dimension numbers, stacked-vs-optax update parity (rtol 1e-4), sharded-vs-unsharded parity at 4 sim devices
  • Checkpoint roundtrip + exact-resume with muon on both groups, both impls
  • All 25 configs/muon/*.yaml parse + pass the canonical assert on the merged schema
  • make check clean; full suite at both device counts running as a SLURM job (result will be commented here)

The suspected step-time regression was resolved as a hardware mismatch (July-3 baselines ran on B200s) — bridge task h100-steptime-regression-jul8, closed.

Crew-Address: slack/C08T7UV4449/1783730484.936959

🤖 Generated with Claude Code

ocg-goodfire and others added 13 commits July 2, 2026 11:02
… sharding

The prior device_put-onto-sharding was a confirmed no-op (StandardRestore already
honors the sharding spec — verified via an instrumented resume: restored CI-fn was
correctly ÷N). The real resume-OOM is a ÷1-scale ENTRY RELAYOUT on the first
resumed step: orbax-restored arrays carry a default memory layout that differs from
what the jitted step was compiled for, so the same executable that runs fresh at
~150GB materialized a ~103GB buffer on resume. The fresh-init reference is built by
the same XLA layout assignment as the step, so its `.format` (layout + sharding) IS
the step's expected input layout; coercing the restored tree onto it removes the
entry relayout. Unblocks durable auto-resume (transient crashes can now recover).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvFotbQNtDNsgXJQZuzghR
… inputs (S31) (#919)

Both hidden-acts eval steps computed leading = residual.shape[:-1] on what is
actually the model INPUTS (an LM's [batch, seq] token ids), yielding (batch,)
instead of (batch, seq). Every leading-shaped tensor (stochastic delta masks,
zero deltas, the clean forward's all-false routes) lost the sequence axis and
crashed site_out's route/delta broadcast the first time a run reached a slow
eval with the metrics enabled — the deterministic CI step too, via
route[..., None] in the clean forward, not just the stochastic delta path.

leading now comes off the CI output [*leading, C] (_waist_leading), matching
how lm.py::stochastic_site_masks already draws delta masks (ci.shape[:-1]) and
staying target-generic. The misnamed residual arg is renamed to inputs: Any per
the DecomposedModel protocol — the misnomer is what invited the bug; the
sibling attn_patterns_eval.py names the same thing tokens.

Regression test with batch != seq (fails pre-fix on both steps) pinning
per-site sums/counts and the token-weighted accumulation.

Claude-Session: https://claude.ai/code/session_016wiNqVkCMzQ8UQtkeEZ8eK

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit ca215e7)
- AnyOptimizerConfig: discriminated adamw|muon union; untyped configs
  default to adamw (canonical, bit-identical trajectories).
- MuonOptimizerConfig: optax.contrib.muon (NS-orthogonalized momentum on
  2D leaves, Adam fallback elsewhere), consistent_rms knob, same cosine
  schedule + S19 clip chain.
- Lab canonical assert admits muon (still no-weight-decay subspace).
- Tests: muon orthogonalization + adam fallback + discriminator
  (test_optim_torch_parity), muon checkpoint roundtrip + exact resume
  (test_checkpoint, S22).
- configs/muon/: pile 4L PPGD 40k A/B arms (control + muon 1x/4x lr,
  consistent_rms 0.2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bers over chunk stacks)

The chunkwise CI fn's trainable leaves are per-chunk stacks: 3D [n_chunks, d_in, d_out]
matrix stacks and 2D [n_chunks, d] bias stacks. optax's default muon rule (2D -> muon,
rest -> Adam) labels that tree exactly backwards, so build_optimizers now passes explicit
MuonDimensionNumbers for the ci-fn group when the arch is chunkwise: 3D leaves are
NS-orthogonalized over the trailing two axes (chunk axis batched), everything else takes
the Adam fallback. V/U and the MLP CI fns keep the default 2D rule (all-2D / plain-2D
trees, unchanged). SPEC S20 amendment extended (2026-07-11).

Two matrix arm configs added, byte-derived from the try-muon control / muon-1x stored
launch configs with only ci_fn_optimizer flipped to muon (lr 5e-5, consistent_rms 0.2):
the two missing cells of the {adamw, muon} x {UV, ci-fn} matrix.

Tests: muon 3D-stack orthogonalization + 2D-bias-stack Adam fallback
(test_optim_torch_parity), muon-both-groups checkpoint roundtrip + exact resume
(test_checkpoint, the scavenge-preemption path).

Crew-Address: slack/C08T7UV4449/1783730484.936959

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… for the matrix arms

The shared cache's xla_gpu_per_fusion_autotune_cache_dir was created group-unwritable
on 2026-07-08, so any non-owner run dies PERMISSION_DENIED at the first train step
(job 1052115). JAX_PERSISTENT_CACHE_ENABLE_XLA_CACHES=none skips only that side cache;
the main executable cache still applies. The 2026-07-03 sibling arms predate the dir,
so this also matches their autotune behavior.

Crew-Address: slack/C08T7UV4449/1783730484.936959

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 0.5x (4e-4, 1e-4)

Oli's follow-up on the muon pilot thread: the 4 matrix cells land at different
sparsity-recon tradeoff points, so compare optimizer setups as 3-point Pareto
curves (L0 vs adv/stoch recon at step 30k) instead of single runs. Everything
except run_name and the ImportanceMinimalityLoss coeff is byte-identical to the
parent cell's config; full 40k schedule so the 30k point is comparable to the
existing coeff-2e-4 runs.

Crew-Address: slack/C08T7UV4449/1783730484.936959

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…amma 1->0.01) at 100k steps

Oli's follow-up (task smooth-l0-muon-matrix-100k): swap stock Lp imp-min for
SmoothL0ImportanceMinimalityLoss in each matrix cell — coeff 2e-4 (the pile-4L
smooth-L0 sweep center, wandb groups smoothl0-impmin-sweep / smoothl0-steplen;
numerically same as our Lp cells), gamma 1.0 -> 0.01 linear over the full run
(flagship p-594db290 precedent), steps 40k -> 100k. Frequency sub-term kept so
the only imp-min delta is the penalty shape. Everything else byte-identical to
the parent cell configs.

Crew-Address: slack/C08T7UV4449/1783730484.936959

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sharded (fast muon)

GSPMD lowers per-leaf NS on the ÷N-sharded fp32 masters into per-iteration full-Gram
all-reduces with the largest matmul replicated on every device (~24.6 GB serialized
fp32 collectives per step for the 4L ci-fn group — the measured 3.3x muon-ci hit).
muon_stacked.py batches same-shape muon leaves into one NS with the stack axis
sharded over (replicate, fsdp): device-local orthogonalization, one reshard in/out,
zero per-iteration collectives (the Kimi parameter-partitioned recipe, GSPMD-native).

Config-gated on MuonOptimizerConfig: impl: optax|stacked (default optax = the
07-02/07-11 arms' exact semantics), ns_steps (default 5), ns_dtype (default float32;
bfloat16 halves NS compute+comm, stacked-only — masters/momentum stay fp32 per N1).
Same MuonState pytree, so checkpoints round-trip across impls. SPEC S20 amended
2026-07-12. build_optimizers now takes the mesh (None for toys/CPU).

Tests: stacked-vs-optax update parity on mixed 2D/3D/fallback trees (2 steps,
rtol 1e-4); sharded-vs-unsharded parity at 4 sim devices; checkpoint roundtrip +
exact-resume with stacked muon on both groups. make check clean.

Acceptance configs: both-muon 40k at impl=stacked (fp32) and +bf16-NS, group
fast-muon-accept — quality vs p-c01f5833, step time is the metric (0.44s today).

Crew-Address: slack/C08T7UV4449/1783730484.936959

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ingress reshard was moving fp32 bytes even with bf16 NS (cast sat after the
sharding constraint); casting first halves the ingress bytes for ns_dtype=bfloat16.
fp32 NS unchanged (cast is a no-op).

Crew-Address: slack/C08T7UV4449/1783730484.936959

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e muon cells

The original sl0-100k muon arms launched on the per-leaf optax impl (0.40-0.44 s/step
for the ci cells); fresh 100k relaunches on stacked-bf16 (~0.23 s/step) finish ~2-3h
sooner than the originals' remaining tail, and give all three muon cells identical NS
semantics (the flagship-intended mode). Gated on the fast-muon acceptance arms'
quality-parity check vs p-c01f5833. adam/adam has no muon group and rides on.

Crew-Address: slack/C08T7UV4449/1783730484.936959

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replicates the +0.08s/step regression seen in every post-07-11 launch: two adamw/adamw
arms identical except JAX_PERSISTENT_CACHE_ENABLE_XLA_CACHES=none. HLO dumps land in
<run_dir>/hlo for diffing.

Crew-Address: slack/C08T7UV4449/1783730484.936959

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on PR)

Combines the three stacked muon PRs (#931 gate, #975 ci-fn dimension numbers,
#978 stacked-NS fast impl) onto current feature/jax. Conflict resolutions:
- run_state: upstream's optax_schedule (S20/#939) drives both groups; muon dispatch
  (impl gate, chunk-stacked dim numbers, mesh-sharded stacked NS) reinstated on top.
- SPEC S20: upstream's schedule-unified row + the three muon amendments appended.
- slow_eval: upstream side (my branch only carried the pre-#919 cherry-pick).
- configs/muon/*.yaml migrated to the ScheduleConfig schema (pnorm/p_anneal_* and
  gamma/gamma_anneal_* -> nested schedules, semantics unchanged).

Crew-Address: slack/C08T7UV4449/1783730484.936959

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude-spd1

Copy link
Copy Markdown
Collaborator Author

Full suite green on the merged branch at both device counts (SLURM job 1077049): 535 passed at default devices, 301 passed at 4 sim devices (XLA_FLAGS=--xla_force_host_platform_device_count=4), 0 failures. (An earlier suite job showed one failure in test_pretrained_target_converts_with_wildcards — that was the test job losing DATA_MOUNT, not code; passes with env set.)

Ready for review. Step-time context for the perf claims in the description: the absolute July-3 baselines are affected by a platform-level H100 pool regression (~65%, survives fresh compile — bridge task h100-steptime-regression-jul8, under investigation by another agent); all muon-vs-adamw ratios were measured same-era and stand.

Crew-Address: slack/C08T7UV4449/1783730484.936959

@claude-spd1

Copy link
Copy Markdown
Collaborator Author

Per Oli on the bridge task consumer-config-schema-drift: please adjust this PR under the new config policy (#983, CONFIGS.md) — the repo keeps a registry of <10 canonical config seats and sweep one-offs are not committed (a launched run's provenance is its run-dir pin + refs/runs/snapshot/<id> + wandb).

Concretely for the 25 yamls under configs/muon/: drop impmin_sweep/, smooth_l0_100k/, the pile4l_ppgd_40k_* variants and the probe_autotune pair — the sweeps already ran and their findings live in lore (2026-07-11--muon-cifn-matrix-pile4l, 2026-07-11--muon-cifn-impmin-pareto, 2026-07-13--smooth-l0-muon-matrix-100k). Keep at most one canonical muon config, and only if muon is becoming a default recipe — in that case add it as a seat row in CONFIGS.md. The parse gate from #983 covers param_decomp/configs/** recursively, so whatever stays is CI-gated from then on (these already parse + pass the canonical assert, so no extra work).

Also pinged your thread on Slack. Crew-Address: task/consumer-config-schema-drift

🤖 Generated with Claude Code

….md / #983)

Sweep one-offs don't get committed — provenance for the launched runs lives in the
run-dir pinned launch_config.yaml, the refs/runs/snapshot/* refs, and wandb. No
canonical muon seat yet: muon stays config-gated experimental until it's promoted to
a default recipe, at which point a seat row lands in CONFIGS.md. The experiment
configs remain on the bridge/task-* experiment branches for history.

Crew-Address: slack/C08T7UV4449/1783730484.936959

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants