Skip to content

Add mesh rules that replicate dense weights across context (cp-as-ep-dense-no-cp, cp-as-ep-dense-fsdp) - #5783

Draft
parambole wants to merge 1 commit into
parambole/rl-trainer-4600from
parambole/ag-l2-dense-no-cp
Draft

parambole wants to merge 1 commit into
parambole/rl-trainer-4600from
parambole/ag-l2-dense-no-cp

Conversation

@parambole

Copy link
Copy Markdown
Collaborator

Draft until #5758 lands; lint at the CI pins has not run yet. Follow-up to #5758, stacked on its head parambole/rl-trainer-4600 (20c877aa1), so the diff shows only this commit, 20c93dc4f. Retarget it when #5758 lands.

Two custom mesh rules, both cp-as-ep with the dense weights taken off the context axis. Under cp-as-ep, the attention and shared-expert projections, the MoE router and the shared-expert gate are sharded over fsdp x context x expert. On the 128-chip v7x bench mesh (256 devices, fsdp 32 x context 4 x expert 2), every all-gather of them spans all 256 devices and also synchronizes the four context ranks. cp-as-ep-dense-no-cp shards them over fsdp x expert and replicates them over context, so those all-gathers run in four independent groups of 64 devices. cp-as-ep-dense-fsdp shards them over fsdp only (32-way). The GDN projections, routed experts, embedding, LM head and activations are sharded as in cp-as-ep.

On the RL trainer bench (#5701; Qwen3.5-397B-A17B, 128 TPU v7x), with #5758's code and the companion tunix recipe (google/tunix#2913), changing only custom_mesh_and_rule from cp-as-ep to cp-as-ep-dense-no-cp takes a step from 82.185 s (mean of four runs in three pods) to 81.253 s (mean of two runs in two pods, 81.261 and 81.245 s): 3,588 -> 3,630 tok/s/chip (+1.1%), with the bench's numerics checks passing in both pods. The regular micro-batch goes from 8.94-8.96 s to 8.838 s in both pods. The prediction written before the run said the opposite: 0 to +0.5 s per step, probably slower. Why it is faster is only partly established (see Results).

What moves

The decoder's weights on the bench mesh. Blue: moved by cp-as-ep-dense-no-cp. Gray: unchanged.

flowchart TB
  classDef pr fill:#dbeafe,stroke:#1d4ed8,stroke-width:2px,color:#0b1f4d
  classDef same fill:#f3f4f6,stroke:#9ca3af,color:#6b7280

  subgraph MOVED["moved: fsdp x context x expert (256-way) -> fsdp x expert (64-way), replicated over context"]
    direction LR
    ATT["full attention q, k, v, out<br/>15 layers, 1.57 B params"]:::pr
    SHX["shared expert wi_0, wi_1, wo<br/>and its gate, 60 layers, 0.75 B params"]:::pr
    RTR["MoE router<br/>60 layers, frozen in the RL recipe"]:::pr
  end
  subgraph KEPT["unchanged: as in cp-as-ep"]
    direction LR
    GDN["GDN in_proj_qkvz, in_proj_ba, out_proj<br/>45 layers, 5.31 B params, 256-way"]:::same
    EXP["routed experts<br/>experts over context x expert"]:::same
    VOC["embedding, LM head<br/>256-way"]:::same
  end
  GRAD["gradients of the moved weights:<br/>256-way reduce-scatter as in cp-as-ep,<br/>then a 4-way all-gather over context"]:::pr
  ATT --> GRAD
  SHX --> GRAD
  style MOVED fill:none,stroke:#1d4ed8
  style KEPT fill:none,stroke:#9ca3af
Loading

What changes

file change
src/maxtext/configs/custom_mesh_and_rule/cp-as-ep-dense-no-cp.yml (new) cp-as-ep.yml with embed, embed_attn, q_lora and kv_lora on ['fsdp', 'expert'], plus two new logical axes, embed_gdn and embed_grad, both on ['fsdp', 'context', 'expert']
src/maxtext/configs/custom_mesh_and_rule/cp-as-ep-dense-fsdp.yml (new) the same, with the four dense axes on ['fsdp']
src/maxtext/common/common_types.py CustomRule.CP_AS_EP_DENSE_NO_CP and CustomRule.CP_AS_EP_DENSE_FSDP
src/maxtext/layers/moe.py RoutedMoE keeps ("context", "expert") as its expert-parallel axes under both rules, as under cp-as-ep (lines 1179-1185); GateLogit routes the router kernel's gradient (line 891)
src/maxtext/layers/linears.py route_dense_weight_grad (lines 148-206), called by DenseGeneral after its two-stage all-gather (line 510)
src/maxtext/models/qwen3.py the GatedDeltaNet projections take their embed axis from embed_gdn when the rules map it, otherwise from embed_attn as before (line 700)
docs/guides/optimization/custom_mesh_and_rule.md a section on the two rules
tests/unit/cp_as_ep_dense_no_cp_test.py (new) CPU test, see Testing
  • RoutedMoE. The routed experts stay sharded over context x expert, so expert parallelism must keep running over both axes. Without this change it runs over expert alone. On CPU the loss is then off by 28.5 (a positive control, see Testing). The bench program still compiles, with no error, but its ring-of-experts collectives become 2-way instead of 8-way.
  • Gradient routing (route_dense_weight_grad). A weight's gradient is a partial sum over every mesh axis the tokens are sharded on: fsdp, context and expert. Once the weight is replicated over context, GSPMD reduces that partial sum with a full-size all-reduce over all 256 devices and keeps a slice of the result. In the compiled bench program, each backward scan iteration then has full-size all-reduces of the query gradient (bf16[4096,32,512], 128 MiB), the attention output gradient (bf16[8192,4096], 64 MiB) and one 104 MiB tuple of the 12 shared-expert gradients and the key and value gradients, where cp-as-ep has reduce-scatters: twice the bytes, and still a 256-way synchronization. When the rules map embed_grad, the backward of every kernel with an embed or embed_attn dimension first constrains the gradient to the kernel's axes with that dimension on embed_grad. That is one reduce-scatter over all 256 devices, as under cp-as-ep. It then constrains the result to the kernel's own sharding: a 4-way all-gather of the shard over context. The forward is unchanged. Under a rule set that does not map embed_grad, the function returns the kernel unchanged. It raises if the rules map embed_grad and no mesh is set with jax.set_mesh.
  • Why the optimization barrier. Without it, XLA:TPU folds the two constraints back into the same full-size all-reduce: the bench program compiled with the routing but no barrier has the same all-reduces as without the routing. With the barrier, each backward iteration has the 25 reduce-scatters over 256 devices that cp-as-ep has, plus 16 4-way all-gathers over context.
  • The GDN projections stay on fsdp x context x expert (embed_gdn). They hold 5.31 B parameters, 2.3x the moved attention and shared-expert weights. See Not in this PR.
  • Rule sets that map neither embed_grad nor embed_gdn are unaffected. At the bench cell with cp-as-ep, this commit compiles to the same program as Qwen3.5-397B RL trainer on 128 v7x: MoE and GDN speed options as config keys #5758, apart from source locations inside 7 Mosaic kernel bodies (see Testing).

Results

RL trainer bench (#5701): Qwen3.5-397B-A17B on 128 TPU v7x, mesh fsdp 32 x context 4 x expert 2, 9 micro-batches per optimizer step, 294,912 tokens per chip per step. Seconds per step: mean of steps 1-4 (step 0 compiles). Regular micro-batch: median of fwd_bwd dispatch entries 3-9 over steps 1-4. Every arm runs MaxText at #5758's head 20c877aa1 and the bench image's tunix with google/tunix#2913's changed files at bee63532f, with the same 90 MaxText keys apart from custom_mesh_and_rule. The arms of this PR also have this commit's six source files installed over that MaxText tree.

arm (pod) rule s per step tok/s/chip regular micro-batch tag
o275x-prf-l3-prof (bprf, profiled) cp-as-ep 82.200 3,588 8.949 s MEASURED
o275x-prf-l3-r (bprf) cp-as-ep 82.158 3,590 8.948 s MEASURED
o275x-prfc-l3 (bl3) cp-as-ep 82.111 3,592 8.942 s MEASURED
o275x-prf-roll4c-prof (bslow, rows rolled by 4, 4 chips profiled) cp-as-ep 82.269 3,585 8.959 s MEASURED
mean of the four cp-as-ep 82.185 3,588 8.942-8.959 s ARITH
o275x-l2-l3-prof (bl2, profiled) cp-as-ep-dense-no-cp 81.261 3,629 8.838 s MEASURED
o275x-l2-l3-prof replicate (bl2r, profiled) cp-as-ep-dense-no-cp 81.245 3,630 8.838 s MEASURED
mean of the two cp-as-ep-dense-no-cp 81.253 3,630 8.838 s ARITH
  • Difference: -0.931 s per step, the mean of the two against the mean of the four (-1.1%). Per pod: -0.923 and -0.939 s against the mean of the four, -0.939 and -0.955 s against the arm with the same profiler settings (o275x-prf-l3-prof), and -0.850 and -0.866 s against the fastest. Regular micro-batch: -0.104 to -0.121 s. The four cp-as-ep runs span 0.158 s per step across three pods and the two runs of this PR 0.016 s, so the difference is about six times the larger spread. None of this PR's 56 regular micro-batches (8.829-8.844 s) overlaps any of the 112 of the cp-as-ep runs (8.932-8.962 s) (ARITH). Every comparison is across pods; there is no same-pod control.
  • Steps 1-4 of this PR's arms: 81.258, 81.261, 81.266, 81.260 s (bl2) and 81.262, 81.231, 81.247, 81.241 s (bl2r).
  • Pre-registered in the job manifest before the run: "0 to +0.5 s/step, LIKELY SLOWER ... A WIN needs delta <= -0.3 s/step (2x the 0.15 s/step cross-pod noise)". The measurement contradicts the predicted direction. It meets the win criterion in both pods. The replicate's manifest, written before its run, set out to reproduce about -0.92 s per step in a second pod; it measured -0.939.
  • Numerics on TPU: the bench's six-check numerics A/B passes in both pods, and the loss equals that of Qwen3.5-397B RL trainer on 128 v7x: MoE and GDN speed options as config keys #5758's code in all 3 steps (see TPU validation status).

Why it is faster: partly established. What the compiled bench program shows (MEASURED, per scan iteration of 4 layers): the moved weights' all-gathers become 64-way, 20 in the forward and 26 in the backward. That leaves 9 and 18 256-way all-gathers, all of them the GDN projections'. The backward keeps its 25 reduce-scatters over 256 devices and adds 16 4-way all-gathers over context. The prediction expected the added replication and the extra all-gathers to cost more than taking the context ranks out of the gather groups saves.

What the TPU profiles show (MEASURED; bl2's profile against o275x-prf-l3-prof's, the same two devices of one chip, across pods):

  • In the executed program, the moved all-gathers run in 4 groups of 64 devices (fsdp x expert, one group per context rank); under cp-as-ep each runs in one group of 256. The GDN projections' all-gathers stay 256-way and the routed-expert collectives are unchanged.
  • The profiled chip holds context rank 0 of two short rows, so it is not the chip that sets the pace. On it, compute takes 12.5 ms more per micro-batch (attention unchanged), while waiting at collectives takes 108 ms less: busy time falls by 95 ms per micro-batch. With 12 ms less idle time, that is close to the run's -111 ms per regular micro-batch.
  • The 108 ms is spread over about 15 synchronization points; no single collective explains it. Waits at collectives over all 256 devices fall by 655 ms per micro-batch and waits at the new 64-device groups add 604 ms (net -51 ms). Waits at the 32-device fsdp collectives fall by 71 ms, and at the SparseCore gather offloads of the MoE backward by 40 ms. Waits at the ring-of-experts collectives rise by 30 ms.
  • The largest single wait did not move: the backward all-gather of the attention query kernel waits about 26 ms per attention layer in both programs.
  • On the SparseCore, the small moved all-gathers (4-8 MiB) run 1.6-1.9x faster in groups of 64 than in groups of 256; the two large ones, the attention output kernel (64 MiB) and the query kernel (128 MiB), run 5% and 11-13% slower. Net, about 30 ms less SparseCore time per micro-batch (ARITH).
  • Not in the prediction: the gradient routing puts a pairwise collective-permute between each 256-way reduce-scatter and its 4-way all-gather, 300 per micro-batch, and adds one small all-reduce over context. On the profiled chip they cost up to 37 ms and 23 ms per micro-batch.

INFERRED: the chip that sets the pace reaches these synchronization points sooner; the profiled chip only shows where it waits less. A profile of the pacing chip under this rule would separate the causes; none was taken.

Memory

Compiled for the bench cell (AOT on CPU for 128 TPU v7x chips, the bench mesh and compiler flags, including the bench's --xla_tpu_max_hbm_size_mib=84992, which is 83.0 GiB). Per device:

quantity cp-as-ep (#5758) cp-as-ep-dense-no-cp cp-as-ep-dense-fsdp
XLA memory-usage report of fwd_bwd, HBM total 78.28 GiB 81.05 GiB (+2.77) 78.39 GiB (+0.11)
fwd_bwd arguments / outputs / temporaries 2.92 / 5.78 / 89.98 GiB 2.98 / 5.89 / 94.29 GiB 3.05 / 6.03 / 90.62 GiB
train-kernel device peak: fwd_bwd's arguments, outputs and temporaries, plus the optimizer state and gradient accumulator not passed to it 104.46 GiB 109.04 GiB (+4.58) 105.73 GiB (+1.27)
on TPU runs ran without running out of memory (5 steps, in each of two pods) not run
  • State (weights, gradients, accumulator) grows by 0.28 GiB (no-cp) and 0.63 GiB (fsdp), matching the arithmetic written down before the compile (0.26 and 0.61): the moved parameters times 1/64 - 1/256 (or 1/32 - 1/256), at 10 bytes per trainable parameter (bf16 weight, float32 gradient, float32 accumulator) and 4 bytes per router parameter (float32, frozen). The optimizer state is on host.
  • The rest is temporaries: +4.31 GiB for cp-as-ep-dense-no-cp and +0.64 GiB for cp-as-ep-dense-fsdp. The rule that replicates more adds less, so the temporaries track the schedule, not the replication (INFERRED).

How to turn it on

custom_mesh_and_rule=cp-as-ep-dense-no-cp

The mesh axes (data, stage, fsdp, context, expert) and every other logical axis rule are cp-as-ep's. custom_mesh_and_rule=cp-as-ep-dense-fsdp is the 32-way variant. It is compiled and CPU-tested but has not run on TPU, and its value is unknown: the same reasoning that predicted cp-as-ep-dense-no-cp would be slower also predicted it would be slower.

Testing

  • Unit test (tests/unit/cp_as_ep_dense_no_cp_test.py, new, CPU): a tiny hybrid Qwen3.5 (GatedDeltaNet kernel path, gated full attention, routed and shared experts, ring of experts, sequence-sharded GDN context parallelism) on 16 CPU devices, fsdp 2 x context 4 x expert 2, under cp-as-ep and both rules, from the same parameter values. It runs in a child process. On the commit's own sources it passes all 9 checks, rc 0:
    • Shardings: 24 of 70 parameters change, each in exactly one dimension, from fsdp x context x expert to the rule's axes: attention q/k/v/out, shared-expert wi_0/wi_1/wo and gate, and the router. The GDN projections, routed experts and embedding keep their cp-as-ep sharding.
    • Collectives: all-gathers over all 16 devices drop from 70 to 22. All-gathers over 4 devices rise from 10 to 84 (cp-as-ep-dense-no-cp), and all-gathers over 2 devices from 24 to 74 (cp-as-ep-dense-fsdp).
    • Numerics: the loss is bit-identical under both rules. Every gradient leaf is within 2^-6 x max|reference|. The worst leaf is a shared-expert wo kernel at 6.02e-3 (relative L2 2.72e-3); 21 of 70 leaves are not bit-identical. The routed backward reduces the bfloat16 partial sums in a different order. A float32 variant (dense MLP in place of the MoE block, whose ragged sort is bfloat16-only) agrees to 2.07e-7, under its 1e-5 tolerance, so the differences are reduction-order rounding.
    • Positive controls, each of which must fail the numerics check: the rule without the RoutedMoE change (loss off by 28.5, gradients by 1.36 relative) and one attention query kernel scaled by 1 + 1e-3 (loss off by 0.226, gradients by 9.5e-2). Both fail it, as they must.
  • Bench-cell compile checks (the AOT setup of Memory):
    • cp-as-ep on this commit vs Qwen3.5-397B RL trainer on 128 v7x: MoE and GDN speed options as config keys #5758 (20c877aa1): the optimized fwd_bwd HLO is identical (55,912 lines) once op metadata, stack-frame tables and Mosaic kernel bodies are stripped. 7 of the 110 kernel bodies differ by a few bytes at equal length, in what reads as their source-location table (INFERRED: line numbers that moved). Memory is identical (104.46 / 78.28 GiB). The comparator gives the same verdict for Qwen3.5-397B RL trainer on 128 v7x: MoE and GDN speed options as config keys #5758 against itself (0 differing bodies) and a different one against cp-as-ep-dense-no-cp.
    • Every collective of 1 MiB or more that does not move a dense weight or its gradient is unchanged, by body, kind, group size and shapes: 128 of 128 under both rules. The rule without the RoutedMoE change fails this check (its ring-of-experts collectives go from 8-way to 2-way); Qwen3.5-397B RL trainer on 128 v7x: MoE and GDN speed options as config keys #5758 against itself passes.
    • The barrier, both ways: with the routing and no barrier, the backward has the full-size all-reduces described above. With the barrier it has 25 reduce-scatters over 256 devices per iteration, as cp-as-ep has, plus 16 4-way all-gathers, each behind a small collective-permute (0.02-0.5 MiB).
  • TPU: the timing above, in two pods; the bench's numerics A/B, in both pods; the profile check (see TPU validation status).
  • Not run yet: lint at the CI pins, the related regression suites, and cp-as-ep-dense-fsdp on TPU.

TPU validation status

item status
timing, cp-as-ep-dense-no-cp vs cp-as-ep, across pods done: -0.923 s per step in the first pod (MEASURED)
timing replicate in a second pod: parambole-bl2r-v14/o275x-l2-l3-prof done: 81.245 s per step, -0.939 s against the mean of the four (MEASURED)
the bench's six-check numerics A/B in each pod, parambole-bl2-v14/nxl2 and parambole-bl2r-v14/nxl2, against nxref and nxref2, with parambole-bl3-v14/nxprf-l3 (#5758's code, same protocol) as the same-cell reference done: PASS 6/6 in both pods (MEASURED, table below)
profile check written before the run: on TPU, the moved all-gathers run in 4 groups of 64 devices and the GDN and routed-expert collectives are unchanged done: both hold in the executed program of bl2's profile (MEASURED)
cp-as-ep-dense-fsdp on TPU not run

Numerics A/B (MEASURED, read with the bench's readout). Gates-off runs: log-prob gates off, random advantages, 3 steps. The six checks compare the first step with two reference runs, nxref and nxref2: every arm keeps its tokens; loss, relative difference (tolerance 1e-5); mean log-prob, absolute difference (5e-5); the last micro-batch's log-prob, absolute difference (5e-5); entropy, relative difference (1e-4); gradient norm, relative difference (2e-3).

test run (pod) tokens kept loss mean log-prob last micro-batch log-prob entropy gradient norm result
nxl2 (bl2) 1.0 2.69e-7 6.03e-6 5.70e-6 2.81e-7 4.58e-6 PASS 6/6
nxl2 (bl2r) 1.0 2.69e-7 5.18e-6 5.70e-6 2.98e-7 2.22e-5 PASS 6/6
nxprf-l3 (#5758's code) 1.0 2.69e-7 1.87e-6 5.70e-6 5.07e-7 1.11e-4 PASS 6/6
  • The loss is 0.0304453575 in all 3 steps of all three runs, the same to the 10 digits logged; the references log 0.0304453493. The two references' gradient norms differ by 1.92e-5 (relative).
  • Gradient norm per step: 0.0557782 / 0.0558033 / 0.0558073 (bl2) and 0.0557773 / 0.0558073 / 0.0558132 (bl2r), against 0.0557847 / 0.0558033 / 0.0558120 for Qwen3.5-397B RL trainer on 128 v7x: MoE and GDN speed options as config keys #5758's code: at most 1.33e-4 apart (relative).
  • What these checks cannot see: they compare scalars of the first step (the later two are logged, not gated), so they rule out gross errors, not a wrong gradient direction. Exactness rests on the CPU test (see Testing): bit-identical loss, and gradients within bfloat16 reduction-order rounding.

Not in this PR

  • The GDN projections. They stay on fsdp x context x expert. Moving them as well was compiled with the rules only, before the gradient routing existed: state +0.85 GiB per device instead of +0.28. It has not run on TPU, so whether their all-gathers would gain is unmeasured.
  • A 64-way gradient reduction. The moved weights' gradients are still reduce-scattered over all 256 devices, one synchronization of all four context ranks per weight per backward, before the all-gather over context. Reducing them within the four independent fsdp x expert groups instead would take the context ranks out of the large reductions too: a 64-way reduce-scatter, then a 4-way all-reduce over context of the 1/64 shard. GSPMD does not produce that form by itself (left alone it emitted the full-size all-reduce), and it is not tried here.
  • Removing the 300 permutes. Mapping embed_grad to ['fsdp', 'expert', 'context'] instead of ['fsdp', 'context', 'expert'] should leave each 64-way shard's four pieces contiguous after the 256-way reduce-scatter, so the 4-way all-gather would need no permute first (INFERRED; not compiled, not run).
  • Other models. Only the Qwen3.5 hybrid decoder was compiled and tested. q_lora and kv_lora (multi-head latent attention) move with the other dense axes, so the rule's dense set matches cp-as-ep's, but no MLA model was compiled or run.
How it was measured: arms, logs, overlay, profiles
  • Bench: Add an RL trainer bench under experimental/rl_trainer_bench #5701's RL trainer bench, Qwen3.5-397B-A17B on 128 TPU v7x, booking r-371ee. Mesh fsdp 32 x context 4 x expert 2; 9 micro-batches per optimizer step; 294,912 tokens per chip per step; ragged_buffer_factor 2.75. Recipe: Qwen3.5-397B RL trainer on 128 v7x: MoE and GDN speed options as config keys #5758's keys as Qwen3.5-397B RL trainer on 128 v7x: pad-first sequence packing, fused sharded log-prob head VJP, and the tuned trainer recipe google/tunix#2913's TRAINER_TUNED_FLAGS, the fused log-prob head (TUNIX_LOGPS_SHARDED_HEAD=fused), pad first (applied by the bench driver), the staged step boundary, and none of the overlay builds' environment switches.
  • Single variable: the 90 MaxText keys of this PR's arm and of o275x-prf-l3-prof differ only in custom_mesh_and_rule. Their config dumps (932 keys, paths excluded) differ only in that key and in the logical axis rules it selects: embed, embed_attn, q_lora, kv_lora from ('fsdp', 'context', 'expert') to ('fsdp', 'expert'), with embed_gdn and embed_grad added. The replicate (bl2r) ran the same job manifest with only the names changed, with the same 90 keys.
  • Code on TPU: both pods of this PR installed this commit's six source files over Qwen3.5-397B RL trainer on 128 v7x: MoE and GDN speed options as config keys #5758's tree from gs://parambole-397b-replay-nap/win-tpu/team-pr-arms/overlay/parambole-lev-l2-8ed9e36874f514521acf64f598fb6546.tgz (md5 8ed9e36874f514521acf64f598fb6546). Each file in it is byte-identical to its blob at 20c93dc4f: common_types.py d6085955, cp-as-ep-dense-no-cp.yml 3f965a8a, cp-as-ep-dense-fsdp.yml c27af2af, linears.py 1952c83a, moe.py 13e7196c, qwen3.py 600a57d8. All four arm logs of this PR (two timing, two numerics) print custom_mesh_and_rule: CustomRule.CP_AS_EP_DENSE_NO_CP, an enum value that only this commit defines, and none has a Traceback or RESOURCE_EXHAUSTED.
  • Timing: tok/s/chip = 294,912 / (s per step).
  • Logs, under gs://parambole-397b-replay-nap/win-tpu/sched/v14/, with md5:
    • parambole-bl2-v14/o275x-l2-l3-prof.log (916f2fac3ee4c91fe670cce6d2a75e04)
    • parambole-bprf-v14/o275x-prf-l3-prof.log (6c6039ba8d3f470b7070fca0d22be626), parambole-bprf-v14/o275x-prf-l3-r.log (5689ffadccbcfe1428c2eb444bb5b9ef)
    • parambole-bl3-v14/o275x-prfc-l3.log (f87a786767e202d862c5881932bd87dd)
    • parambole-bslow-v14/o275x-prf-roll4c-prof.log (f88f61517c0a1d8629bfb9e71ec93741)
    • numerics, Qwen3.5-397B RL trainer on 128 v7x: MoE and GDN speed options as config keys #5758's code: parambole-bl3-v14/nxprf-l3.log (ed5d4cfa58dbb17495e6312be07623a8); references ../r-06ca7/parambole-bnnp-06ca7/nxref.log (530911504a42b98292fd0c76222cf872) and nxref2.log (acff70f5358e3702cd5ece191fedf2c4)
    • replicate: parambole-bl2r-v14/o275x-l2-l3-prof.log (ac16b85d39a0ab19e8f77455651fc228)
    • numerics, this PR: parambole-bl2-v14/nxl2.log (8851f887348b2db7a3c37c439cbcf40d) and parambole-bl2r-v14/nxl2.log (37e4297b91759bb7feebbf0b15c0b36b), read with the bench's num_readout.py (md5 ba65e0c0, self-test 11 of 11)
  • Profiles (xplane): this PR's arm under .../parambole-bl2-v14/o275x-l2-l3-prof/parambole-bl2-v14-o275x-l2-l3-prof/tensorboard/plugins/profile/2026_10_15_21_38_03/; its control under .../parambole-bprf-v14/o275x-prf-l3-prof/parambole-bprf-v14-o275x-prf-l3-prof/tensorboard/plugins/profile/2026_10_15_21_24_47/. The replicate's profile was captured and not analysed. Dissection of the first two, CPU only, on cmcs-checkpoint: ~/p4600/ag/v14res/l2prof-heavy/ (tables out/l2an.md, collective groups out/census2.txt, SparseCore time per call out/sccmp.txt).
  • Job manifests: manifests-ncp/v14/levers/parambole-bl2-v14.yaml (md5 ef03b68b2863c050f74d711b85c2c5b3; the prediction is header line 9) and parambole-bl2r-v14.yaml (md5 71a8327524a0984820273d936fdf74ac, a renamed copy; header lines 1-2 state the -0.92 s per step it set out to reproduce).
CPU and compile evidence

On the build host cmcs-checkpoint, bench image (local id sha256:dbdcf5f8dcba6e71d7edbba31d1378057ce22ca4771735923a67d12862c662c5), --network none:

  • Unit test: ~/p4600/ag/l2/cpu/num5.log (md5 ced4b54240b4dbad56081c3937bd394d): CP_AS_EP_DENSE_NO_CP_CHECKS_PASSED, rc 0. The script's md5 (7f4ad15e) and the tree's six source files equal the committed blobs.
  • Compiles: ~/p4600/ag/l2/aot/logs/l2g-A.log (cp-as-ep-dense-no-cp, md5 71f775a7372b5e569c55147892ffa474), l2g-B.log (cp-as-ep-dense-fsdp, df96d92d7d65ab54bf8d9f6dd421276a), l2g-cpaep.log (cp-as-ep on this commit, 7a6889d001bd57f5ea35891b4165a04a), each rc 0. Memory-usage reports and optimized HLO under ~/p4600/ag/l2/aot/dumps/l2g-*/; Qwen3.5-397B RL trainer on 128 v7x: MoE and GDN speed options as config keys #5758's compile of the same cell under ~/p4600/ag/base/aot/dumps/prcell-base/.
  • Collective census: ~/p4600/ag/l2/ana/l2g-{A,B}/census_agg.md against ~/p4600/ag/base/ana/base/census_agg.md. Compared with the rest gate ~/p4600/ag/l2/tools/restgate.py (md5 a6873957) and the HLO comparator tools/bodydiff.py.
  • Without the barrier: ~/p4600/ag/l2/ana/l2f-A/ (routing, no barrier) and l2x-Ak/ (rules only) both have the full-size all-reduces.
Predictions written before measuring, and how they did
prediction measured
TPU step time: 0 to +0.5 s per step, probably slower; a win needs <= -0.3 -0.923 and -0.939 s per step (two pods) miss (direction)
controls at 82.7-83.2 s per step 82.111-82.269 miss
state memory +0.26 GiB (no-cp), +0.61 GiB (fsdp) +0.28, +0.63 hit
temporaries within +/-0.25 GiB +4.31, +0.64 miss
GSPMD reduces the moved gradients with a 64-way reduce-scatter and a 4-way all-reduce a full-size 256-way all-reduce; fixed by the gradient routing miss
the moved all-gathers become 64-way (32-way); routed-expert collectives unchanged yes; 128 of 128 other collectives unchanged hit
on TPU, the moved all-gathers run in 4 groups of 64 devices; GDN and routed-expert collectives unchanged both, in the executed program hit
a second pod reproduces about -0.92 s per step -0.939 s per step hit
(not predicted) the gradient routing adds 300 collective-permutes and one small all-reduce over context per micro-batch -
CPU: loss bit-identical bit-identical hit
CPU: gradients within 1e-5 x max the routed bfloat16 backward: 6.0e-3. The tolerance was widened to 2^-6 after the first routed run measured 4.4e-3; the float32 variant (2.07e-7) is what ties the difference to rounding miss
CPU: both positive controls fail the check both fail hit

cp-as-ep shards the dense weights (attention and shared-expert projections,
the MoE router and the shared-expert gate) over fsdp x context x expert, so
every all-gather of them spans all devices and synchronizes the context ranks,
which do unequal attention work. The two new rules keep cp-as-ep's
activations, vocabulary weights and routed experts and take those weights off
context: cp-as-ep-dense-no-cp shards them over fsdp x expert,
cp-as-ep-dense-fsdp over fsdp only. Their all-gathers then run in independent
groups of 64 or 32 devices on the 256-device mesh, at the cost of replicating
the weights and their gradients over the axes they leave.

- RoutedMoE keeps ("context", "expert") as its expert-parallel axes under both
  rules, as it does for cp-as-ep.
- Left to GSPMD, the gradient of a weight replicated over context is
  all-reduced at full size over every device and then sliced. Under a rule set
  that maps the new logical axis embed_grad, linears.route_dense_weight_grad
  reduce-scatters it over every token axis instead, as under cp-as-ep, and
  all-gathers the shard over the replicated axes. An optimization barrier
  between the two keeps XLA:TPU from folding them back into the all-reduce.
  DenseGeneral and GateLogit apply it to kernels with an embed or embed_attn
  dimension.
- The GatedDeltaNet projections take their embed axis from embed_gdn when the
  rules map it. Both new rules keep them on fsdp x context x expert: their
  all-gathers are already overlapped, and replicating them would cost far more
  HBM than the rest of the dense weights.

Rule sets that map neither embed_grad nor embed_gdn compile to the same
program as before.

The CPU test runs a tiny hybrid Qwen3.5 on 16 devices (fsdp 2, context 4,
expert 2) under cp-as-ep and both rules: exactly the dense weights move off
context, the loss is bit-identical, and the gradients agree within bfloat16
reduction order (1e-5 with float32 activations). A rule without the RoutedMoE
change and a perturbed weight both fail the check.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces two new custom mesh rules, 'cp-as-ep-dense-no-cp' and 'cp-as-ep-dense-fsdp', which optimize dense weight sharding by taking them off the context axis. It implements the necessary gradient routing logic in linears.py, integrates the rules into the MoE and Qwen3 models, and adds a unit test. Feedback was provided to propagate sys.path to PYTHONPATH in the test's subprocess environment to prevent potential import failures in CI/CD environments.

Comment on lines +87 to +90
env = os.environ.copy()
env["XLA_FLAGS"] = env.get("XLA_FLAGS", "") + f" --xla_force_host_platform_device_count={_NUM_DEVICES}"
env["JAX_PLATFORMS"] = "cpu"
result = subprocess.run([sys.executable, __file__], env=env, capture_output=True, text=True, check=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In certain CI/CD environments (such as Bazel/Blaze or custom test runners), sys.path is dynamically populated with temporary directories containing the dependencies, but these are not exported to os.environ["PYTHONPATH"]. When subprocess.run is called with sys.executable, the child process starts with a clean sys.path (only containing standard library and site-packages), which will fail to import maxtext or other dependencies, leading to a test failure.

To ensure the child process inherits the exact same module search path as the parent process, propagate sys.path to PYTHONPATH in the copied environment.

Suggested change
env = os.environ.copy()
env["XLA_FLAGS"] = env.get("XLA_FLAGS", "") + f" --xla_force_host_platform_device_count={_NUM_DEVICES}"
env["JAX_PLATFORMS"] = "cpu"
result = subprocess.run([sys.executable, __file__], env=env, capture_output=True, text=True, check=False)
env = os.environ.copy()
env["PYTHONPATH"] = os.pathsep.join(sys.path)
env["XLA_FLAGS"] = env.get("XLA_FLAGS", "") + f" --xla_force_host_platform_device_count={_NUM_DEVICES}"
env["JAX_PLATFORMS"] = "cpu"
result = subprocess.run([sys.executable, __file__], env=env, capture_output=True, text=True, check=False)

Rohan-Bierneni pushed a commit to Rohan-Bierneni/tunix that referenced this pull request Oct 16, 2026
…UNED_FLAGS

Cherry-picked from google#2940 (2cfa895), companion to AI-Hypercomputer/maxtext#5783
(20c93dc4f), which AI-Hypercomputer/maxtext#5721 now carries in place of its earlier 2-line
cp-as-ep.yml edit.

TRAINER_TUNED_FLAGS=true now also passes custom_mesh_and_rule=cp-as-ep-dense-no-cp after the
recipe's default custom_mesh_and_rule=cp-as-ep (MaxText keeps the last value of a repeated key):
the dense weights (attention and shared-expert projections, the MoE router and the shared-expert
gate) are sharded over fsdp x expert and replicated over context, their gradients reduce-scattered
over every device and then all-gathered over context (linears.route_dense_weight_grad); the GDN
projections (embed_gdn), routed experts, embeddings and activations stay as under cp-as-ep.
TRAINER_TUNED_FLAGS=false renders as before.

128 TPU v7x (maxtext#5701 bench, from google#2940): 82.185 -> 81.253 s/step (3,588 -> 3,630 tok/s/chip),
numerics PASS 6/6.

Moves requirements/maxtext_requirements.txt to AI-Hypercomputer/maxtext#5721 at d66a82096.

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