Repository navigation
Conversation
…DED_HEAD) compute_chunked_logps scans the sequence under GSPMD. On the MLPerf Qwen3.5-397B trainer cell (fsdp32 x cp4 x ep2 on 256 devices, 64 x 65,536-token packed micro-batches, compute_logps_chunk_size 1024) the compiled program all-gathers the 2 GB vocab table in every scan step (forward and rematerialized backward) and all-reduces a 2 GB table gradient in every backward step: 129 table all-gathers and 65 table all-reduces per micro-batch. Because the scan runs over a context-sharded sequence, every context rank also computes every chunk, so the vocab matmuls are 4x redundant. With TUNIX_LOGPS_SHARDED_HEAD=local (or 1), packed rows go through a shard_map'ed head that runs the same per-chunk math: - each head parameter is all-gathered once per pass; - each device scans only the tokens it holds (dynamic slices, no reshape of the sequence); - the table gradient is accumulated per device in float32 and reduce-scattered once; - the one-token shifts (targets left, outputs right) are explicit collective-permutes inside the shard_map, so they do not depend on how XLA partitions a shift of a sequence-sharded array. The head falls back to compute_chunked_logps, with a log line, when there is no mesh in context, when some mesh axis shards neither the batch nor the sequence, or when a head parameter does not divide over the mesh. Unset (the default), nothing changes.
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
This was referenced Oct 14, 2026
Closed
parambole
added a commit
that referenced
this pull request
Oct 14, 2026
TRAINER_TUNED_FLAGS=true now also appends gdn_cp_matmul_precision=high and cast_grads_after_all_reduce=true to the trainer's MaxText flags, in the same block as the seven keys: - gdn_cp_matmul_precision=high: under gdn_cp_mode=seq, the float32 matmuls that compose the GDN state across the sequence shards run 3 bf16 passes instead of 6. - cast_grads_after_all_reduce=true: a gradient that the forward/backward pass sums by an all-reduce alone, and that is not already float32, leaves it in the parameter's dtype and is cast to float32 after the all-reduce. Both keys have these meanings at this branch's MaxText pin (60425bf) and in the bench image's MaxText (3792fdef8). Bench (AI-Hypercomputer/maxtext#5701, 128 v7x chips), measured with the sharded log-prob head of #2873 on in both arms: 107.59 -> 106.24 s/step in one pod, and 107.71 -> 106.41 in a second pod that trained on real gradients. In that pod the gradient norm moved 1.47e-5 relative (two runs with the keys differ by 1.92e-5), the forward log-prob metric moved 1.1e-6 (3.6e-7), and the loss was identical as printed. Four MoE settings measured alongside them (prefuse_moe_weights=false, moe_accumulate_wi_dlhs=true, moe_accumulate_chunk_wgrad=true, decoder_layer_input=device) moved the gradient norm by -0.80% and the forward metric by 9.7e-4, so they are left out. Tests: the exact-set test expects nine keys, and _MAXTEXT_DEFAULTS has the two keys' MaxText defaults.
- Docstrings and comments in tunix/rl/common.py now match the code: the four fallbacks and the inputs that raise instead, the per-device chunk_size, the gathered parameters kept for the backward, why check_vma=False, and the greedy layout search. The switch's error message lists every accepted value, and shard_shapes is computed only in the branch that reads it. The value_and_grad jaxprs hash the same as before with the switch off and on. - The tests no longer set the device count at import. Run directly they use 8 CPU devices; under pytest they run in-process only when the process already has 8 CPU devices, and otherwise in a child process, so other test modules keep their device count. - Every parity test checks that the head engaged and compares all outputs. New tests pin the token layouts, the fallback reasons, the legacy `with mesh:` fallback, the switch's scope and its parsing.
…ise condition; parameter discovery gap)
The deepswe_dist launcher gave no way to turn the sharded log-prob head on in an RL run. TRAINER_EXTRA_ENV cannot carry it from the 397B v7x recipes: a value set there replaces mlperf_397b_256_v7x.sh's default TRAINER_EXTRA_ENV, including the trainer's LIBTPU_INIT_ARGS. When TUNIX_LOGPS_SHARDED_HEAD is set, the launcher now adds it to the trainer's start command only. Unset, nothing is rendered, so every recipe renders as before. Tests: unset, the variable appears nowhere in the render; set to local, it reaches the trainer JobSet and not the orchestrator or rollout JobSets.
parambole
added a commit
that referenced
this pull request
Oct 14, 2026
…_HEAD) compute_chunked_logps scans the sequence under GSPMD. On the MLPerf Qwen3.5-397B trainer cell (fsdp32 x cp4 x ep2 on 256 devices, 64 x 65,536-token packed micro-batches, compute_logps_chunk_size 1024) the compiled program all-gathers the 2 GB vocab table in every scan step (forward and rematerialized backward) and all-reduces a 2 GB table gradient in every backward step: 129 table all-gathers and 65 table all-reduces per micro-batch. Because the scan runs over a context-sharded sequence, every context rank also computes every chunk, so the vocab matmuls are 4x redundant. With TUNIX_LOGPS_SHARDED_HEAD=local (or 1, true, on, yes), packed rows go through a shard_map'ed head that runs the same per-chunk math: - each head parameter is all-gathered once per pass; - each device scans only the tokens it holds (dynamic slices, no reshape of the sequence); - the table gradient is accumulated per device in float32 and reduce-scattered once; - the one-token shifts (targets left, outputs right) are explicit collective-permutes inside the shard_map, so they do not depend on how XLA partitions a shift of a sequence-sharded array. The head falls back to compute_chunked_logps, with a log line, when there is no mesh in context, when some mesh axis shards neither the batch nor the sequence, or when a head parameter does not divide over the mesh. Sequence axes are used in mesh order, the order in which jax.lax.ppermute numbers shards, so a logical-axis rule that lists them in another order reshards the input instead of shifting tokens across the wrong shards. Unset (the default), nothing changes. k8s_launcher.sh passes TUNIX_LOGPS_SHARDED_HEAD to the trainer process when it is set. TRAINER_EXTRA_ENV cannot carry it from the 397B v7x recipes: a value set there replaces mlperf_397b_256_v7x.sh's default TRAINER_EXTRA_ENV, including the trainer's LIBTPU_INIT_ARGS. Measured on the RL trainer bench (AI-Hypercomputer/maxtext#5701, 128 v7x chips), same pod, only the switch differs: 122.00 -> 107.59 s per optimizer step. Real-gradient A/B on TPU (reference, head, reference): loss bit-identical; grad_norm +2.8e-4 relative to the references' mean, whose own difference is 5.3e-5. Tests: tests/rl/sharded_logps_head_test.py on an 8-device CPU mesh (under pytest it runs in a child process unless the process already has 8 CPU devices): parity with compute_chunked_logps with the head engaged, token layouts, fallbacks, the legacy `with mesh:` fallback, the switch's scope and parsing, sequence axes listed out of mesh order, non-finite hidden states at masked positions, and GRPO's pattern of differentiating the log-probs only (and the entropy only). Launcher: unset, the variable appears nowhere in the render; set, it reaches the trainer JobSet only. The file measured on TPU is #2873 at a29991d. This version adds the mesh-order line, docstrings and tests; for in-order layouts the traced value_and_grad program is unchanged (same jaxpr hash, head on and off).
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.
TL;DR
shard_mapversion of that head incompute_per_token_logps, for packed rows (segment_idsset). It gathers the table once per micro-batch and keeps it for the backward, each device scans only its own tokens, and the table gradient is summed in f32 and reduced once. Off by default: setTUNIX_LOGPS_SHARDED_HEAD=localin the process that traces the trainer step (on Pathways, the client / trainer driver); with it off, the traced program is the same as today's (jaxpr hashed and compared on CPU).grad_normis 2-3e-4 relative higher, consistent with summing the table gradient in f32 once instead of in bf16 per chunk.jax.set_mesh, which MaxText's training engine uses; tunix's own RL stack useswith mesh:and falls back to today's head. It has not run end to end with real weights, and it needs a full-size f32 gradient accumulator per device (see Risks).How it works
flowchart TD D["compute_per_token_logps: packed rows, chunk_size above 0"] subgraph TODAY["Today, or switch off: compute_chunked_logps under GSPMD"] A1["1,024-token chunks, each on all 4 context-parallel ranks"] A2["each chunk: all-gather the 2 GB table (64 fwd + 64 bwd)"] A3["head matmuls: 533.3 TFLOP per device per micro-batch"] A4["each backward chunk: all-reduce the bf16 table gradient (64)"] end subgraph LOCAL["TUNIX_LOGPS_SHARDED_HEAD=local: shard_map with a custom VJP"] B2["one-token shifts: ppermute between neighbouring shards"] B1["all-gather the head parameters once, kept for the backward"] B3["scan only local tokens: 1x16384 in 16 steps of 1024"] B4["same per-chunk math: 133.3 TFLOP per device per micro-batch"] B5["backward: per-device f32 gradient accumulator (4.07 GB)"] B6["one psum_scatter of the gradient"] end D -->|switch off| A1 D -->|switch on| B2 D -.->|switch on but a checked precondition fails| A1 A1 --> A2 --> A3 --> A4 B2 --> B1 --> B3 --> B4 --> B5 --> B6If a checked precondition fails, the helper logs a
[logps-head] sharded head offwarning and today's path runs; some inputs raise instead, such as explicit-axis meshes (limitations 2 and 9). When it engages, it logs[logps-head] sharded head local: ....Results
grad_normat step 0, real gradientskept_frac0), so loss andgrad_normare 0. Pod 7 keeps every sequence with seeded random advantages (real gradients) and measured -14.23 s/step (-11.7%) over steps 1-2.Changes
tunix/rl/common.pycompute_per_token_logps: the only edit to today's path. Packed, chunked calls (segment_idsset,chunk_size > 0) first trymaybe_compute_sharded_chunked_logps; onNonethey runcompute_chunked_logpsas before. Today's path pads packed log-probs and entropy with one zero at the front; the sharded head's output is already aligned, so that pad is skipped.sharded_head_mode()(new): parses the switch.local,1,true,on,yesturn it on; unset, empty,0,false,off,noleave it off; anything else raisesValueError._token_partition()(new): mesh axes for the batch and sequence dims, from the flax logical-axis rules (activation_batch,activation_length) when valid, else from the mesh order._head_parameters(),_use_dtypes()(new): tracecompute_final_logitsonce, abstractly, to find the parameters it reads and the dtype to gather each in.maybe_compute_sharded_chunked_logps()(new): checks the preconditions (else warns and returnsNone), then runs ashard_mapwithhead_local, a custom VJP (forward: gather once, scan local tokens; backward: f32 accumulation, onepsum_scatter), and theneighbourppermuteone-token shifts. One long function; its per-chunk math duplicatescompute_chunked_logps's.tests/rl/sharded_logps_head_test.py(new): 19 tests on an 8-device CPU mesh: parity withcompute_chunked_logps, engagement, fallbacks and switch parsing.Testing
tests/rl/common_test.py83/83. Broken copies of the head (input or output boundary token dropped, accumulator overwrite, logits x1.001) each fail 5-7 tests.algo_core.grpo_loss_fnand MaxText head on 8 CPU devices. At 4 rows x 65,536 tokens, 0 of 262,144 tokens differ; the loss, dL/dh (the gradient for the hidden states) and 68 other gradient leaves are exactly equal, and only the table and norm-scale gradients differ, both closer to an all-f32 reference; every deliberately broken head fails.value_and_gradthroughcompute_per_token_logpshashes identically to unmodified tunix.main), a single-processpytest tests/rl/with the new module, pylint (pyink is clean on the changed lines), any end-to-end RL run.Risks and limitations
jax.set_mesh. tunix's own RL stack (with mesh:) falls back to today's head with a warning, so it gets no speedup yet (limitation 1).Related
compute_logps_chunk_sizeabove 1024 under context parallelism.TPU profile of today's head
Compared op by op with a MaxText pretraining run of the same model at 3,193 tok/s/chip (its own loss head,
num_vocab_tiling=2), a TPU profile puts the GRPO head at +1.67 s per micro-batch per TensorCore. The profiled cell is the baseline cell plus two SparseCore all-gather/reduce-scatter latency-multiplier flags and the TensorCore ragged-sort MoE keys (unprofiled median 13.21 s per micro-batch, against 13.505-13.522 s for the baseline cell).A paper sum from this profile (not a model) estimated -1.3 to -1.6 s per micro-batch; the same-pod A/B, run on arm B, measured -1.590 s.
Design details: gather dtype, chunking, fallbacks, explicit shifts
Each chunk runs today's math:
compute_final_logits, f32 logits, thenselective_log_softmaxand entropy.(4096,)and the table(4096, 248320)in bf16.chunk_sizebecomes tokens per scan step per device, so per-device logits memory per step is unchanged. Chunks aredynamic_slices of the local block, not today'sreshape+swapaxes.[logps-head] sharded head off, using compute_chunked_logps: <reason>and returnsNone, and today's path runs): no mesh in context (jax.set_mesh); no token layout, because some mesh axis of size > 1 shards neither batch nor sequence; a head parameter that does not divide over the mesh; or acompute_final_logitsthat reads no model state. Explicit-axis meshes raise instead (limitation 2). Unpacked rows and switch-off calls take today's path.ppermuteshifts: while testing this, the XLA SPMD partitioner (CPU, jax/jaxlib 0.11.1) miscompiledjnp.concatenate([x[:, 1:], 0]).at[:, k].set(0)on a context-sharded[B, T]array: the first token of every later context block kept its unshifted value (got 4097, want 4098). The shift alone, the set alone, and the shift thenjnp.whereare correct. It hit the 8-device fsdp2 x context2 x expert2 mesh (4 rows x 1 boundary = 4 wrong tokens) and an 8-way context-only mesh (4 rows x 7 boundaries = 28 wrong tokens), not a 2-device one. This PR does both shifts inside theshard_map, so its correctness never depends on how the partitioner shifts a sequence-sharded array.TPU evidence: speed (pods 3, 4, 5, 6 and 10)
Setup: the TPU cell of "Results" (arm B), plus
ragged_buffer_factor2.75 and 9 micro-batches per bench step, which #5701 calibrates to one optimizer step of the recipe (about 1,152 trajectories packed into 64-row micro-batches). Client imagetrellis-experimental@sha256:b4647c18...(pod 10: newer images) with this PR'stunix/rl/common.py(md5a5bfda56) installed over the image's copy. "Head off" arms ran this PR's file withTUNIX_LOGPS_SHARDED_HEAD=off, except the pod-3 reference (stock file). Step 7 of "How to reproduce" defines both tok/s/chip rulers.Two more measurements of the head effect, besides pods 5 and 10 in "Results":
gdn_cp_matmul_precision=high,cast_grads_after_all_reduce=true), head on + profiler. Within-arm spread over steps 1-4: 0.09 s (off), 0.31 s (on). tok/s/chip rises 14.5% (micro-batch ruler) and 13.4% (step ruler). MoE dropped / replayed: 0 / 0. Head on logged two engaged[logps-head]lines and no fallback; head off logged none.sampler_is/token_logdiff_absmean(trainer per-token log-prob against the payload's rollout log-prob, mean over the 40 regular payloads of steps 0-4) reads 12.2219079 (off) and 12.2219074 (on): 5.1e-7 apart, against a within-arm range of about 2e-5. Speed arms are forward only (loss andgrad_normare 0, see "Results"); pod 7 checks gradients.raiden_20261013212200(unsanitized_server@sha256:7cd4dbed...,unsanitized_proxy_server@sha256:8679cb5c...), clientgcr.io/cloud-tpu-multipod-dev/atwigg/trellis-experimental:1013b(sha256:f30c7b2f...). Sametunix/rl/common.pyasb4647c18(md5be286d30), different MaxText build (854 resolved config keys instead of 934, noembed_routeraxis rule,gdn_pin_in_proj_output_layout=True), so only the A/B inside pod 10 is comparable. s/step, steps 1-4: off 120.337 / 120.319 / 120.307 / 120.393, on 106.585 / 106.610 / 106.588 / 106.633; regular micro-batch 12.357 to 10.813 s (-1.544 s, -12.5%). tok/s/chip (micro-batch / step ruler): 2,652 / 2,451 off, 3,031 / 2,766 on. 0 MoE drops, 0 replays;diff_config.pyshows only the 8 path and name keys between the arms (keys,ragged_buffer_factor, chunk, mesh and batch as in arm B); head on logged two engaged lines, no fallback. Forward check: 12.2219074 (off), 12.2219072 (on), within-arm ranges 2.3e-5 and 1.7e-5. Here the 9th dispatch entry reads about two regular micro-batches (21.65 s on, 24.74 s off) and the time after the last dispatch shrinks by one (head on: 1.60 s, against 12.52 s in pod 4); the median of entries 3-9 still picks a regular entry.TPU evidence: numerics with real gradients (pod 7)
Numerics mode:
LOGP_GATES=offkeeps every sequence (kept_frac1);ADVANTAGES=randomgives a seeded N(0, 1) advantage per segment, the same in every arm, with the same random init and payload. Three arms in one pod: head off, head on, head off again (the replicate). The step-0 pass rules were written before the run, and at step 0 the weights are identical; steps 1-2 follow one and two updates with each arm's own gradient, so they are report-only.kept_fractoken_logdiff_absmean, on minus mean(off)grad_norm, relgrad_normis higher with the head on at all three steps: 5.2x the off-vs-off spread at step 0, then 7.6x and 14.4x at steps 1-2, after the weights diverged. This is the head's one numerical change: the table gradient is summed in f32 once, not in bf16 over 64 all-reduces per micro-batch. On CPU only the table and norm-scale gradients differ from today's (relative Frobenius difference 1.63e-3 / 5.34e-3), both closer to an all-f32 reference ("CPU validation").Other MaxText levers measured on top of the head (not part of this PR)
Each arm adds MaxText settings to head on ("GDN-CP keys": see the pod 5 bullet above). Changes are in s/step against the same pod's reference: head on at 107.59 (pod 5) or 107.94 (pod 6), head on + GDN-CP keys at 106.21 (pod 9). Differences under about 0.3 s/step are step-to-step noise.
grad_normwithin 1.5e-5 relative against a 1.9e-5 replicate spread);decoder_layer_input=device(cell:offload) 107.11 (-0.49);--compute_logps_chunk_size=4096(cell: 1024) 107.84 (+0.25, no gain). Forward checks within noise (+1.2e-6, +1.0e-6, +1.6e-6). The chunk-4096 arm shows the sharded head compiles and runs at chunk 4096 on TPU, where today's head fails TPU compile ("CPU validation", item 4).moe_accumulate_wi_dlhs=true,moe_accumulate_chunk_wgrad=trueanddecoder_layer_input=device: 105.72, replicate 105.85 (-2.22, -2.09, pod 6); withoutdecoder_layer_input=device106.69 (-1.24, pod 6); with GDN-CP keys 104.01, replicate 103.94 (-2.19, -2.26, pod 9). All three shift the forward check by +9.7e-4, and a real-gradient A/B on TPU measuredgrad_norm0.80% lower (about 420x the replicate spread) with or withoutdecoder_layer_input=device, so these settings are not used.weight_dtype=float32(cell:bfloat16), with GDN-CP keys: 108.05 (+1.85); forward check +7.5e-4; fp32 confirmed by the compiled program's arguments (5.80 GiB per device, against 2.92 GiB for this pod's bf16 lever arm).CPU validation: numerics gate, unit tests, compiles
1. Numerics gate through the real loss, with positive controls. Unmodified tunix
algo_core.grpo_loss_fnand MaxTextTunixMaxTextAdapterhead (decoder norm +logits_dense); tunix'sGRPOAdapter+SequencePackedBatchAssemblerpack the micro-batch, with completion masks interleaving model and environment turns, an overlong rollout, andseq-mask-tis0.999/1.002 keeping some sequences and dropping others. Submission-recipe GRPO config; fsdp2 x context2 x expert2 on 8 CPU devices,cp-as-eprules, chunk 1024, 4 packed rows of T tokens; pass rules written before any treatment arm ran. A halo is the boundary token a sequence shard gets from its neighbour for the one-token shift.The gate ran on an earlier revision whose
localand switch-off training jaxprs hash identically to this commit's (jaxpr ofvalue_and_gradthroughcompute_per_token_logpsat the unit-test cell; a one-line mutant changes the hash). Switch-off also hashes identically to unmodified tunix.2. Unit tests, 19/19. Parity with
compute_chunked_logps: f32 and bf16; uneven and single-step chunks; log-probs only; the no-gradient forward with and without entropy; the mesh-order layout without logical rules; a sequence-only layout over 8 devices (7 shard boundaries). Also engagement, fallbacks and switch parsing. Broken heads (input halo dropped, output halo dropped, accumulator overwrite, logits x1.001) each fail 5-7 tests; a no-gradient-path bug fails exactly the 2 forward-only tests.3. AOT at the baseline cell (arm A): a CPU-hosted compile for tpu7x-256 (libtpu 0.0.46; GRPO + router replay + fp8 fake-quant +
ragged_buffer_factor2.75 + the dropless retry program). No TPU run used the head at this cell (they used the arm B/C cell). It compiled an earlier revision that shifted with a globalconcatenateinstead of theppermutes; the head scan and table collectives are the same code. Between the columns MaxText differs by one commit (#5568, engine and metrics files only) and tunix, apart from this change, only outsiderl/common.pyandalgo_core.py. The cell'sxla_tpu_max_hbm_size_mib=84992(83.0 GiB) caps temporaries + arguments + outputs (80.05 GiB today, 73.43 GiB withlocal); 94.74 GiB is the per-device HBM, the fit limit including state and code. Counts and FLOPs: see "Results".localfwd_bwdfwd_bwdtemp / total (with arguments, outputs, state, code), of 94.74 GiBreshapefrom the chunking transpose (the op that fails TPU compile at chunk 2048/4096)XLA rewrote the head's f32 reduce-scatter into one all-reduce plus a dynamic-slice. Instruction by instruction the head's FLOPs fall exactly 4.00x; the only other FLOP change is one extra rematerialization of a decoder projection the compiler chose (+30.8 TFLOP, about 27 ms at peak).
4. Head-only compile with the TPU-matched compiler: libtpu 0.0.51.dev20261010 reproduces the TPU compiler's pass and fail verdicts on this cell. The real
compute_per_token_logpsruns undervalue_and_gradwith a stand-in head at the real dims and shardings (hidden[64, 65536, 4096]bf16, table[4096, 248320]bf16) on 256 tpu7x devices. Today's head passes at chunk 1024 and fails at 4096 with the TPU's exact signature (Reshape should have supported layout ... s32[4,1,4,1024] ... compute_per_token_logps/transpose);localpasses at 1024 (0 token-id reshapes, 0 all-to-alls) and at 4096, as does the TPU run at 4096.How to reproduce the TPU A/B (pins, commands, expected numbers)
The bench (AI-Hypercomputer/maxtext#5701) runs the exact trainer program of an e2e GRPO launch (same MaxText and libtpu flags, mesh, GRPO loss and chunked log-prob pass) on one trainer slice, with random weights and a synthetic micro-batch whose MoE routing is calibrated to a production run. Its input is the e2e launcher's DRY_RUN output. The A/B is three bench runs; this PR's effect is C vs B:
ragged_buffer_factor2.75, logps chunk 1024)REPLACE_MAXTEXT_FLAGS,EXTRA_MAXTEXT_FLAGS,LIBTPU_EXTRAtunix/rl/common.py+TUNIX_LOGPS_SHARDED_HEAD=localIMAGE(a derived image),DRIVER_ENVPinned inputs
70d64441eba957fe5fe934cc4346a26df4706719(branchsivaibhav/rl-trainer-benchofultrons/maxtext), directorysrc/maxtext/experimental/rl_trainer_benchac001198872b0212351b4c58bc8932720487bf14(atwigg/mlperf, merge of #2858):tunix/experimental/examples/recipes/mlperf_gbs1024_submission.sha29991d3fef51325fe4a306e3590ca52fdad80b3. Its one runtime file istunix/rl/common.py, md5a5bfda56676e92d65261f3e24303101b, 77,339 bytesgcr.io/tpu-vm-gke-testing/sivaibhav/trellis-experimental@sha256:b4647c188b25638e644c7a6fc53fb8e0751741525a277a1bcf07dca92ba9a6ac. tunix is an editable install at/app. The image'stunix/rl/common.pyis md5be286d30a3071d180676ef56b5c89782, byte-identical to the file in this PR's base (atwigg/mlperf8d245c561)us-docker.pkg.dev/cloud-tpu-v2-images-dev/pathways/gke/datenglin/unsanitized_server@sha256:662f33b0ca6cd236a12534e4c78a8b7d715868669366738a015401925a39cb86us-docker.pkg.dev/cloud-tpu-v2-images-dev/pathways/gke/datenglin/unsanitized_proxy_server@sha256:e714351cdde50d306dd62f127cffabcdaf98dff80ad22e8b4c66c5a1d1df6ddbgcr.io/cloud-tpu-multipod-dev/yixuannwang_google_com-runner@sha256:6682fc0f364d2246a8768265c8c06ef50e213d86e1639d367543c78a90a8f0f2tpu7x:4x4x8(128 chips = 256 devices, 32 hosts of 4 chips); trainer mesh fsdp 32 x context 4 x expert 2,custom_mesh_and_rule=cp-as-ep; micro-batch 64 packed rows x 65,536 tokensUse the client image by digest: it carries a specific MaxText and tunix build.
Requirements
tpu7x4x4x8 slice for the 32 Pathways worker pods and a CPU node pool for the Pathways head pod (recipe defaultcpu-np); pull access to the images above and a registry the cluster can pull arm C's image from.SERVICE_ACCOUNT, defaultxpk-sa) can write: each arm saves a final checkpoint there after its timed steps, about 5 TiB (740 GiB of parameters + 4.3 TiB of optimizer state). Delete it afterwards.Qwen/Qwen3.5-397B-A17Btokenizer in/opt/hf_cacheand setsHF_HUB_OFFLINE=1.Set these once (bash; in zsh, first run
setopt interactivecomments, because the commands contain#comments):1. Check out the bench and the launcher at the pinned commits
Check (
md5 -qon macOS):2. Build the client image for arm C
The bench image's
tunix/rl/common.pyis byte-identical to this PR's base, so copying the PR's file over it applies the PR. Only the client image (themaincontainer, whosebench_driver.pytraces the head) changes;IMAGEswaps it and leaves the Pathways containers alone.Both
md5sum -clines print/app/tunix/rl/common.py: OK, and the build deletes the image's stalecommon.cpython-312.pyc. The base image is linux/amd64 only (elsewhere add--platform linux/amd64; the verified builds ran on x86-64 Linux). Optional checks:3. Render the e2e launch with DRY_RUN
Step 1 of the bench's README, with the measured launch's environment. It writes YAML to stdout and applies nothing; all three arms use it. Keep it local: it describes your environment.
BATCH_SIZE=72(GBS 1,152: lr 2.121e-6, clip 0.0589),RAGGED_BUFFER_FACTOR=2.75withRETRY_WHEN_TOKENS_DROPPED=true(recipe default: dropless, -1),REMAT_CONTEXT=device(defaultoffload),MOE_EXCLUDE_PADDING=true,LOG_MOE_DROPS=true,COMPUTE_LOGPS_CHUNK_SIZE=1024(default 512). The rest are recipe defaults.K8S_NAMESPACE;PRIORITY_CLASS(must exist on your cluster; the measured runs usedlow); if your cluster differs,KUEUE_QUEUE(defaultmultislice-queue),TPU_RESERVATION(defaultghostfish-tb2qe8ysltros),SERVICE_ACCOUNT(defaultxpk-sa),HEAD_NODEPOOL(defaultcpu-np). AnyJOB_PREFIXworks: it survives only in four orchestrator paths, which the bench parses but never writes to.env -i,KUBECONFIG=/dev/null: even withDRY_RUN=truethe recipe runskubectl config use-context(to its own cluster),kubectl config set-context --current --namespace=...andkubectl get workload; with an empty kubeconfig these fail, the recipe ignores that, and the output is byte-identical.env -ikeeps variables such asMAXTEXT_EXTRA_FLAGSorLIBTPU_INIT_ARGSout of the render.4. Render the three bench JobSets
Each command writes only
rendered/<NAME>.yaml.REPLACE_MAXTEXT_FLAGSedits the client'sMAXTEXT_EXTRA_FLAGSin place, failing unless the key occurs once:num_moe_token_chunks4→2,context_parallel_strategyring→all_gather,sa_fuse_reciprocalfalse→true,sa_use_base2_exptrue→false,gdn_cp_modehead→seq.EXTRA_MAXTEXT_FLAGSappendsgdn_cp_state=deviceandmoe_route=device(absent from the baseline cell).LIBTPU_EXTRAreaches the client, thepathways-proxy(where XLA compiles) and thepw-nodeworkers.IMAGEreplaces only themain(client) container;DRIVER_ENVis the inline environment ofpython /tmp/bench_driver.py, which traces the trainer.STEPS/MICRO_STEPSgive 5 steps (step 0 is warm-up; 1-4 are timed) of 9fwd_bwdcalls;COMPUTE_LOGPS_CHUNK_SIZE=1024is already in the DRY_RUN and kept explicit. LeaveTRAINER_SLICE, the payload knobs (PAYLOAD,ROUTING*,PAD_FRACTION,FINAL_PAD_FRACTION,PAYLOAD_SEED,EP_SIZE,ADVANTAGES,LOGP_GATES) and the profiler knobs unset, as the measured runs did. Optional arm A2 separates the libtpu flag from the 7 keys:NAME=tb-base-sc LIBTPU_EXTRA="$SC_FLAG" bash "$BENCH/render_bench.sh".5. Check the renders before you spend the slice
B vs A lists the 7 MaxText keys, the libtpu flag at the client, proxy and worker sites, and the JobSet name, gang id, output folder and worker id. C vs B must list exactly these 6 (long lines cut):
6. Run each arm
One arm at a time, in any order, for each
NAMEintb-base,tb-tunedandtb-tuned-head(the bench README's commands):If the pod is still pending,
logsreturns at once; rerun it. Keep--tail=-1, or a late attach with a label selector starts at the last 10 lines and misses the config and[logps-head]lines. Each attach replays the whole log, soteeoverwrites$NAME.log. The client log shows, in order (times from arm C in pod 4; ranges over the measured runs):Trainer worker initialized in ... on 256 devices.295.0 s (6-379 s).Bench micro-batch: ... completion_ids=(64, 65536) ...[logps-head] sharded head local: ...twice, arm C only (step 7).Trainer compile finished in ...539.0 s (52-584 s; a program compiled before is faster).Realistic payloads built in ...182.1 s of host time, thenPAYLOAD_ROUTING regular pad_fraction=0.0700 ...andfinal ... pad_fraction=0.3025.completed step: 0tocompleted step: 4, about 108 s each, each followed bySTEP_LOSS step=N loss=0 grad_norm=0 ...(expected: Caveats, "Speed only").Saving checkpoint at step 5....Finished save ...91.8 s.Program End: ...,EXIT_CODE=0(the bench's PID-1 wrapper). A stack trace at interpreter shutdown (IFRT proxy server disconnected, thenPy_FinalizeEx) is expected; the measured runs exited 0.Then delete the checkpoints:
gcloud storage rm -r "$OUTPUT_PREFIX/$NAME".7. Read the result
One line per step; verbatim from arm C in pod 4 (the measured runs used the earlier driver, tagged
[E2EBench]; at #5701's head the tag is[TrainerBench]):secondsis the whole step, from the firstfwd_bwddispatch until its device work has drained, including the optimizerupdate; use steps 1-4 (step 0 includes first execution).fwd_bwd dispatch sis how long each of the 9 calls blocked the driver: entry 1 returns at once (the previous step drained), entry 2 is longer, and from entry 3 on they settle to one regular micro-batch.Two throughput rulers; do not mix them. A micro-batch is 64 rows x 65,536 tokens on 128 chips = 32,768 tokens per chip. A bench step runs 8 regular payloads (7.0% padding), then one final payload (30.25% padding); both rulers count padded rows.
secondsof steps 1-4; it also pays the update and step boundary.bench_readout.py(save it next to the logs) computes both, plus the head and forward checks:bench_readout.py (stdlib only)
Its output on arm C's log from pod 4:
Proof that the head ran (arm C): exactly two such lines during the trainer compile (presumably one per traced program,
fwd_bwdand its dropless retry variant; not verified), and nosharded head offline:That is 1 row x 16,384 tokens per device (64 rows over fsdp 32 x expert 2; 65,536 tokens over context 4), and both head parameters are gathered once, in bf16. Arms A and B print no
[logps-head]line; neither does arm C if its client lacks this PR's file (the stock image ignores the switch).Other checks:
python3 "$BENCH/diff_config.py" tb-tuned.log tb-tuned-head.logshould list only 8 path and name keys of the resolved MaxText configs (base_output_directory,checkpoint_dir,dump_hlo_gcs_dir,dump_jaxpr_gcs_dir,managed_mldiagnostics_dir,metrics_dir,run_name,tensorboard_dir).grep -c 'MOE_DROP_AUDIT_STEP .*dropped_assignments_used=0 dropless_replays=0' <log>gives 5 in every arm: no token dropped, no micro-batch replayed.Expected numbers
Measured 2026-10-13/14 with the step 7 rulers, steps 1-4. The A/B of this PR is the pod 5 pair (B, then C, in one JobSet on one slice); the other rows show how far the same program moves between pods.
Every cross-pod B/C pairing gives -13.80 to -14.46 s/step and -1.54 to -1.61 s per regular micro-batch. Pods 5 and 6 ran with
enable_checkpointing=false(the final checkpoint comes after the timed steps), and pod 5's B ran this PR's file withTUNIX_LOGPS_SHARDED_HEAD=off, the same path as unset; otherwise their B and C arms match the step 4 renders in every field that defines the trainer program, anddiff_config.pyon pod 5's B and C shows only the 8 path and name keys.How close to expect: within a run the 28 dispatch entries span 0.07-0.08 s. Across pods the same program moved 0.03 s per step for the baseline cell (pods 1, 2 and 8), 0.27 s for B (two pods) and 0.40 s for C (three pods); the regular micro-batch time moved 0.04 s (B) and 0.03 s (C). Your B and C will usually run in different pods: on the pinned images, expect C vs B within about 0.6 s per step of pod 5's -14.4 s, as the cross-pod pairings were (pod 10, newer images: -13.74).
How the measured runs differed from these steps
None of these changes the trainer program. (1) Driver:
e2e_driver.pyat #5701d6db22f99(md5e78c082f) instead ofbench_driver.pyat70d64441(md5a2fdaf1a); they differ in 3 lines, the log tag ([E2EBench]→[TrainerBench]) and the two argv variable names (E2E_TRAINER_ARGV/E2E_ORCH_ARGV→TRAINER_ARGV/ORCH_ARGV). (2) Wrapper: a multi-arm wrapper instead of the bench's PID-1 wrapper; timing is computed inside the driver, and the baseline cell measured 130.804 s per step under the bench's wrapper (pod 1) and 130.805 / 130.780 under the multi-arm one (pods 2 and 8). (3) This PR's file was copied over the image's at pod start with md5 guards (raw in pod 4, gzip+base64 in pods 5-7) instead of baked in: same bytes (md5a5bfda56), same path, stale.pycremoved. (4) The switch was exported before the driver (pod 4) or set inline on the driver line (pods 5-7) instead ofDRIVER_ENV; both reach the driver's environment. (5) Keys, flag, images: keys re-appended, the flag inserted mid-list and image tags swapped for these digests after rendering, with the same result: the same 74 keys and values, flag sets (17 client, 21 proxy, 19 worker) and images. (6) Cluster fields (namespace, JobSet-level slice-topology annotation,activeDeadlineSeconds, names): scheduling only.Rendered from a DRY_RUN made as in step 3, all three arms match the launched manifests field by field: the MaxText flags as a set and as last-wins values, every libtpu flag at the three sites, the trainer argv (53 items, apart from the output folder and worker id), the orchestrator argv (88 items, apart from the four
JOB_PREFIXpaths), the 35 other client variables, and every image and YAML field apart from (1)-(6).Without a registry, install the file at pod start as the measured runs did: render arm C without
IMAGE(keepDRIVER_ENV); in the client script, before the driver heredoc, write the file, exit 3 unless it is md5a5bfda56and/app/tunix/rl/common.pyis md5be286d30, copy it over that path and delete/app/tunix/rl/__pycache__/common.*.pyc. The client script is one exec argument, limited to 131,071 bytes: pod 4's single-arm script fit with the file raw (114.5 KB); pods 5-7's multi-arm script needed gzip+base64.Caveats
sample_mask/kept_frac=0), so loss andgrad_normare exactly 0 in every speed arm. For gradients use the numerics mode, as pod 7 did:DRIVER_ENV="TUNIX_LOGPS_SHARDED_HEAD=local LOGP_GATES=off ADVANTAGES=random"for C, the same without the switch for B.--xla_tpu_enable_sparse_core_reduce_scatter_v2=trueis logged by the Pathways proxy as deprecated on every compile, in all arms; whether it still takes effect is unknown.PROFILER_SKIP=12 PROFILER_STEPS=2tracesfwd_bwdcalls 12 and 13 (regular payloads of step 1; calls count from 0, 9 per step) to$OUTPUT_PREFIX/<NAME>/tensorboard/. A step that overlaps the window carries the profiler's cost.Limitations and follow-ups
Line numbers are at
a29991d3f(tunix/rl/common.pymd5a5bfda56676e92d65261f3e24303101b, test md5060eac1d95da6bfa14da01d6f1f95d76). None of these gives a wrong result on the measured path: the MaxText training engine (jax.set_mesh) with bf16 weights.jax.set_mesh. The head readsjax.sharding.get_abstract_mesh()(common.py:1188-1190), which tunix's own RL stack (RLEngine, aliasRLCluster; legacywith mesh:atrl_cluster.py:1293-1312) leaves empty, so there it falls back (... no mesh in context (jax.set_mesh)). Follow-up: when it is empty, usepxla.thread_resources.env.physical_mesh, whichtunix/sft/peft_trainer.pyalready reads.jax.shard_map(common.py:1393,ValueError: in_specs passed to shard_map ... does not match the specs of the input). jax 0.11.1'sjax.make_meshbuilds explicit axes unlessaxis_typesis given; tunix'screate_mesh(tunix/utils/mesh.py:114,119) and the bench's MaxText mesh useAuto. The docstring (common.py:1159-1162) promises a fallback for any failed precondition and omits the fourth (:1216); errors while tracing the head abstractly (in_head_parametersand the abstract leaf list) also propagate. Follow-up: fall back on explicit axes (orjax.sharding.reshardfirst), turn precondition errors into fallbacks, list all four fallbacks.head_local_fwd). The arm B/C TPU runs fit. Follow-up (optional): gather again in the backward instead of keeping the residual.sharded_head_mode(),common.py:1036-1043, reached via:644-657); elsewhereTUNIX_LOGPS_SHARDED_HEAD=loclais silently ignored. Follow-up: parse it on every call.jax.clear_caches()). Follow-up: a typed config field next tocompute_logps_chunk_size, passed as a static argument, would close 4 and 5._use_dtypes(common.py:1101-1119) sees direct consumers and inner jaxprs only. If a head passes the table unchanged out of an inner call (for examplejax.jit(lambda w: w)(table)) and uses that in f32, while another use casts it to bf16, the table is gathered in bf16 and the f32 use silently gets bf16-rounded weights: a test head built this way was off by up to 1.19 nats on 251 of 256 tokens, with no warning. Nothing is narrowed at bf16 weights; in the one fp32-weight TPU arm (pod 9) only the table was, to the bf16 its dot uses. Follow-up: narrow only when every use is a top-levelconvert_element_type.maybe_compute_sharded_chunked_logps(common.py:1149):dynamic_sliceclamps, so hidden states 8 positions longer than the tokens return a result without an error. From the source,compute_per_token_logpscannot trigger this with the models in this repo. Follow-up: fall back whenhidden_states.shape[:2] != input_tokens.shape.tests/rl/sharded_logps_head_test.py:19overwritesXLA_FLAGSwith--xla_force_host_platform_device_count=8at import. The CI command, a single-processpytest tests/rl/(.github/workflows/cpu-tests.yml:76), imports it last, so every RL test there sees 8 CPU devices, not the 4rl_utils_test.pyset before; on a backend already up with fewer devices,_mesh()fails with an unhelpfulreshapeerror. Follow-up: run the module in its own pytest process (its own CI step,--ignored in thetests/rl/step) withtunix.tests.test_common.safe_set_n_cpu_devices(8)insetUpClass; in the shared process that call raises once JAX is up with fewer devices._token_partitionkeeps the rule's order andneighbourpasses it tojax.lax.ppermute, which in jax 0.11.1 raises at lowering when it differs from the mesh order (jax_raise_on_ppermute_sort_diff, on by default). From the source; not run. MaxText's built-in rules are in mesh order, and the TPU runs used one sequence axis. Follow-up: sort both axis tuples into mesh order.Tests worth adding: sharded head parameters (the tests replicate them, MaxText shards the table; a one-off CPU check matched); the
with mesh:fallback and its warning; tied embeddings, whose two table cotangents must add (a one-off CPU check matched); the two unreached fallbacks, asserting the warning text; one test per fix above; the backward's entropy-only branch (never exercised), its neither-output branch (matched in a one-off CPU check) and GRPO's case, entropy requested but kept out of the gradient (covered by the CPU gate and TPU runs, not by unit tests); and in every parity test, checks of engagement, layout and gather dtypes over every output.Not tested
raiden_20261013212200, the tag this PR's base8d245c561/ build(raiden): update Raiden wheel to dev20261013212200 and Pathways images to raiden_20261013212200 #2862 moves the recipe to) ran only the speed A/B and the forward check. The gradient A/B (pod 7) ran only onraiden_20261009183300_cl996655201with clientb4647c18.1x16384 local tokens in 16 steps of 1024and2x8192 local tokens in 8 steps of 1024) and 0 MoE drops. No other model.weight_dtype=float32) engaged and gathered the norm scale in float32 and the table in bfloat16, the logits dot's dtype (gathers [((4096,), 'float32'), ((4096, 248320), 'bfloat16')]). With no head-off fp32 arm, its forward check (+7.5e-4 against bf16 weights) does not test the head; no fp32 gradient was compared.perturbedflags, so the backward builds and reduce-scatters the f32 accumulator even for an untrained head. Whether XLA removes that dead work was not checked.