Repository navigation
397B v7x RL trainer: tuned settings, sharded log-prob head, staged step boundary (130.8 -> 98.3 s per optimizer step, trainer bench) - #2878
Conversation
TRAINER_TUNED_FLAGS (true or false) applies the trainer settings measured on the RL trainer bench (AI-Hypercomputer/maxtext#5701) at the recipes' 128-chip trainer cell. They are the settings of runs 59 and 62 in AI-Hypercomputer/maxtext#5409 that the recipe does not use yet: - the trainer libtpu flag --xla_tpu_enable_offloading_copy_to_sparsecore=false, appended to TRAINER_LIBTPU_INIT_ARGS before the trainer env, the Pathways workers and the Pathways proxy copy it (a value already there is kept); - seven MaxText keys: context_parallel_strategy=all_gather sa_fuse_reciprocal=true sa_use_base2_exp=false gdn_cp_mode=seq gdn_cp_state=device moe_route=device, appended last (MaxText keeps the last value of a repeated key), and num_moe_token_chunks=2 through NUM_MOE_TOKEN_CHUNKS. The switch is on in mlperf_gbs1024_submission.sh. It is off in mlperf_397b_256_v7x.sh and in the recipes that source it, whose renders are byte-identical to before; in the submission recipe, TRAINER_TUNED_FLAGS=false renders the trainer as before. With the replay layout (a finite RAGGED_BUFFER_FACTOR with RETRY_WHEN_TOKENS_DROPPED=true), where it was measured, the switch sets two MoE token chunks instead of four. The fully dropless layout (RAGGED_BUFFER_FACTOR=-1) keeps four, and an explicit NUM_MOE_TOKEN_CHUNKS wins. Bench, 128 v7x chips, ragged_buffer_factor 2.75: 130.8 s/step for the team cell; 126.4 with the libtpu flag (another pod); 121.7 with the flag and the seven keys (same pod as 126.4). Tests: nine render tests in mlperf_submission_launcher_test.py. The existing test that pins four chunks for the replay layout now renders with the switch off.
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.
…_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).
…AGE_NEXT_MICROBATCH) Between optimizer steps the trainer chips sat idle while the orchestrator closed step k (metrics, logging, checkpoint, weight sync) and then packed and shipped step k+1's first micro-batch: ~7.7 s per step on the 397B v7x trainer. - --pipeline_step_boundary (PIPELINE_STEP_BOUNDARY): pack the next step's first micro-batch while this step's last ones train, send it as soon as the checkpoint, weight sync (its staging, with --async_weight_sync) and commit are done, and run step k's metrics and logging once that call has returned, while the device computes it. The trainer gets the same calls in the same order. - --stage_next_microbatch (STAGE_NEXT_MICROBATCH, requires the above): send the next step's first micro-batch to the trainer host while the device still computes this step's last one (TrainerWorker.stage_fwd_bwd), so its train call carries no data. - --stage_before_update (default on): stage between the step's last micro-batch and its update, which the trainer defers (fwd_bwd(apply_optimizer=True, defer_update=True), then apply_deferred_update()). Under MaxText's retry_when_tokens_dropped, update() waits for the last micro-batch's overflow flag, so a stage after the update's dispatch would find the device idle and latch staging off. Staging needs MaxText's prepare_fwd_bwd_input and wait_for_device (AI-Hypercomputer/maxtext#5718). Without them, or when a stage measures no overlap, staging latches off and the step runs as before. While an update is deferred the worker refuses whatever needs the step closed (another micro-batch, update, get_metrics, a checkpoint, weight sync, actor log-probs, eval). A stage that fails on its way to the trainer falls back: the micro-batch is sent whole in the next step's first call and staging stays off for the rest of the run. compile and restore_checkpoint drop a stale stage. Measured on the RL trainer bench (AI-Hypercomputer/maxtext#5701) through the equivalent bench-driver switch, same pod, fix off vs on: 105.97 -> 98.31 s per optimizer step on 128 v7x chips. This orchestrator code also ran on TPU in a reduced rig: the real StandardRLProgram and TrainerWorker over gRPC, driving the team cell's MaxText trainer on Pathways with synthetic groups instead of rollouts, 3 micro-batches per step. The exact-order checks were clean, and the step-boundary idle time fell from 12.46 s to 0.02-0.03 s. Tests: rl_program_pipelined_boundary_test.py, rl_program_pipelined_boundary_robustness_test.py, rl_program_stage_before_update_test.py, distributed_rl_engine_staging_test.py, distributed_rl_engine_deferred_update_test.py, trainer_worker_staging_test.py, trainer_worker_deferred_update_test.py, trajectory_queue_manager_test.py, and the submission-launcher test (both flags off by default; STAGE_NEXT_MICROBATCH without PIPELINE_STEP_BOUNDARY is refused).
…e staged step boundary mlperf_gbs1024_submission.sh now sets, unless already set: - TUNIX_LOGPS_SHARDED_HEAD=local: the trainer computes log-probs with the sharded head (k8s_launcher.sh passes it to the trainer); 0 turns it off. - PIPELINE_STEP_BOUNDARY=1 and STAGE_NEXT_MICROBATCH=1: the orchestrator sends the next step's first micro-batch ahead of this step's bookkeeping and stages it on the trainer before this step's deferred update. Staging needs AI-Hypercomputer/maxtext#5718. STAGE_NEXT_MICROBATCH follows PIPELINE_STEP_BOUNDARY unless set, so PIPELINE_STEP_BOUNDARY=false alone turns both off. The other 397B v7x recipes keep all three off. The recipe comments name the code instead of pull request numbers. With these and the trainer settings of the first two commits, the trainer of the RL trainer bench (AI-Hypercomputer/maxtext#5701, 128 v7x chips) measured 98.31 s per optimizer step, against 130.80 s for the team recipe. Tests: the head reaches the trainer JobSet only and 0 turns it off; the staged boundary is on by default here and PIPELINE_STEP_BOUNDARY=false turns both flags off; the refusal test now sets PIPELINE_STEP_BOUNDARY=false; the other 397B v7x recipes keep the head and the boundary off.
…the RL trainer bench TRAINER_TUNED_FLAGS was measured on the bench's replay layout, but the recipe still launched the fully dropless buffers by default, so a launch did not get the measured trainer. With TRAINER_TUNED_FLAGS=true (the default here) the recipe now defaults to that cell: RAGGED_BUFFER_FACTOR=2.75 with RETRY_WHEN_TOKENS_DROPPED=true (an overflowing micro-batch reruns with a dropless program), two MoE token chunks, REMAT_CONTEXT=device, MOE_EXCLUDE_PADDING=true, LOG_MOE_DROPS=true and COMPUTE_LOGPS_CHUNK_SIZE=1024. Each value can be overridden. RAGGED_BUFFER_FACTOR=-1 keeps the fully dropless buffers (context offload, four chunks), and TRAINER_TUNED_FLAGS=false renders the trainer as before. The fully dropless final micro-batch (RETRY_DROPLESS_FINAL_MICRO_BATCH=true) and the first-phase buffer factor (FIRST_PHASE_RAGGED_BUFFER_FACTOR) run the regular program with larger buffers; with either, the layout keeps four chunks and context offload. Rendered, the default trainer now carries the same MaxText and libtpu flags as the bench's fastest 128-chip run. What differs is the batch size (GBS 1,024 here, 1,152 on the bench, with its learning rate and clip) and checkpointing, which the bench turns off. Tests: the default trainer's MaxText and libtpu flags equal the bench cell's; RAGGED_BUFFER_FACTOR=-1 gives the fully dropless layout; a finite buffer factor without the replay is still refused; the padding and audit flags follow the replay layout; the final-micro-batch and first-phase knobs keep four chunks and context offload; TRAINER_TUNED_FLAGS=false renders the fully dropless buffers with chunks of 512.
…ext#5718 The staged step boundary needs MaxText's prepare_fwd_bwd_input and wait_for_device (AI-Hypercomputer/maxtext#5718). With the previous pin (60425bfbd) an image built from this tree has neither, so the orchestrator latches staging off and the step boundary runs pipelined but unstaged. Pin both MaxText lines to c79bcfd16: 60425bfbd plus #5718's six commits, which change only the MaxText training engine, its README and its tests. Re-pin to #5718's merge commit when it lands.
…gnostic prints - Name the sources of the timing numbers in words: the MLPerf GBS-1,024 submission baseline run, and another 397B trainer run, both from their TRAINER_STEP_TIMING lines. Each is explained once, where its costs are defined. Helpers, constants, labels and test names that carried internal run names are renamed to match. - Rename the sibling-module alias to boundary_test, and refer to "the pipelined-boundary suite" in assertion messages. - Say what each test checks instead of pointing at notes readers cannot see, and drop process tags (pre-registered, measured, inferred) from comments and docstrings. - Link AI-Hypercomputer/maxtext#5718 in place of a commit GitHub cannot resolve, and write maxtext#5673 in full. - Remove 19 diagnostic print() calls; nothing reads their output. - Rename test_lane_busy_costs_with_three_or_more_calls_keep_staging to test_busy_costs_with_three_or_more_calls_keep_staging. - rl_program.py, comments and docstrings only: say what the "stage did not hide" latch catches, and that on Pathways an idle device's wait already reads above its threshold; reflow a ragged comment and two lines over 80 columns. The module's AST is unchanged. - trainer_worker.py docstrings and the stage-before-update test's module docstring say the same about device_wait_s: a short wait means the device was idle, and on Pathways an idle wait reads about 2.7-2.9 s, not ~0. Docstrings only.
moe_route=device was in every measured bench run, but the bench image's MaxText (3792fdef8) tags nothing with moe_route, so it changed nothing there. MaxText at this tree's pin tags the ragged sort's integer routing results (kernels/ragged/ragged_sort.py), where device keeps them on the chip at a memory cost no run has measured, in a program that ran close to its HBM budget. Leave it at its default (remat), which is what the bench measured. The submission recipe also says which layout the HBM budget was measured on: the replay layout ran within 84992 MiB on the bench; the fully dropless layout fit that budget before the tuned keys and the log-prob head and has not been compiled with them. Tests: the switch sets exactly the tuned keys (now without moe_route), and the default trainer leaves moe_route unset; that test fails on the previous recipe.
With --pipeline_step_boundary and nothing sent ahead (the intake had not packed the step's first micro-batch by the previous step's commit, or that micro-batch is the step's only one), the loop awaited the intake and only then started the step's clock. Without the flag the clock starts at the loop top, before the queue wait. So step_time, and the RCP train_step_time and valid_tokens_per_sec_per_gpu built from it, left that wait out on exactly those steps. No wall time was lost, and the scored MLPerf timestamps were not affected. Found in review by Shadi Noghabi. Start the clock before the intake wait. A step whose first micro-batch was sent ahead keeps the clock it started at the send, and the wait still counts as exposed generation time. Test: StepTimeTest.test_each_step_starts_where_the_previous_one_ended runs three cases with the boundary off and on: groups up front (the next step's first micro-batch is sent ahead), and groups arriving slower than a step trains, with four groups per step and with one. Every step starts at or after the previous one's end. Sent ahead, the clock starts at the send, before the previous step's bookkeeping, and nothing waits. When the step waits for its groups, it starts before that wait, within half of it. On the previous code only the waiting cases with the boundary on fail: the steps started 0.87 s and 0.42 s after the previous one ended, against limits of about 0.39 s and 0.16 s.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces significant performance optimizations for the Qwen3.5-397B-A17B RL trainer on TPU v7x. By tuning MaxText settings, implementing a sharded log-prob head, and optimizing the step boundary pipeline, the trainer achieves a substantial reduction in optimizer step time. These changes are designed to improve throughput and efficiency for large-scale RL training workloads, with performance validated on 128 TPU v7x chips. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces pipelining for the step boundary and support for staging the next step's first micro-batch on the trainer, alongside a sharded chunked log-prob head (TUNIX_LOGPS_SHARDED_HEAD) to optimize distributed post-training. The review feedback highlights several critical and medium-severity issues: a potential AttributeError from using jax.shard_map instead of jax.experimental.shard_map, multiple violations of the 'One well-lit path' style guide rule due to speculative attribute access and duck-typing on structured schemas, missing or overly broad type annotations (list[Any]), and type-checking bypasses that should be resolved by extending the AbstractRLEngine interface.
Brings in 11 base commits: rollout cancellation of pending requests, gRPC channel refresh and retries, the trajectory store off switch, the warm pool replica default, and Raiden FFI fixes. One conflict, in mlperf_submission_launcher_test.py: both sides appended test methods at the same place; kept both.
e526ea02 is #5718 squash-merged onto atwigg/mlperf. Its tree is identical to the previous pin c79bcfd16 (tree e1330ef9), so the installed code does not change; the pin now names the merged commit instead of the PR branch.
…p boundary With the pipelined boundary (#2878) the next step's first micro-batch is staged on the trainer's slot before the step's update. With the intra-step queue (#2906 / maxtext#5751) the step then stages its later micro-batches only once its first train call is pending, so the queue starts every step empty and the early, slow transfers of a step are never hidden. This stages the next step's second micro-batch through the intra-step queue right after the first one is staged (`_prestage_across_boundary`), so its transfer overlaps the step's update, metrics, checkpoint and weight sync. The next step adopts it (`_StepState.prestaged` / `prestage_task`) as the first entry of its queue, or as its active stage task when still in flight, and keeps filling the queue from there. Same refusals as the first micro-batch (closes its step, rescored under live weights; scored checkpoint boundaries are not staged at all); a stage the trainer declines falls back to the inline call. The intake for the second micro-batch is the step's own (`_intake_task`); `_send_first_microbatch_ahead` no longer starts a second one beside it. The boundary's memory bound holds: the first micro-batch's payload is dropped once staged, so taking the second one ahead holds at most one feed's output, as the plain take-ahead does. On the trainer, `apply_deferred_update` joins `stage_batch` and `fwd_bwd` on the worker's executor pool under `TUNIX_STAGE_BATCH=stage`, so its device wait at the boundary does not hold the stage request back on the loop thread. No MaxText change: the slot (`prepare_fwd_bwd_input`) and the keyed queue (`stage_batch`) coexist on the engine, and `fwd_bwd(prepared)` does not consume keyed entries. Active only with pipeline_step_boundary + stage_next_microbatch + stage_train_batches + max_staged_batches >= 2; no new flag. Tests: rl_program_prestage_across_boundary_test.py runs the pipelined boundary harness with a trainer double carrying maxtext#5751's keyed queue: the trainer calls, checkpoints and metrics match the frozen base loop in every scenario; the second micro-batch of every non-scored boundary is staged after the step's last micro-batch and before the next step's first train call, keyed to the step it belongs to, and before that step's weight sync; nothing is staged ahead when it would close its step, at scored boundaries, without the queue, or with a single-slot queue; a declined stage goes whole; a stage still in flight at the step start is adopted; no unshipped micro-batch is held at any take-ahead. Also fixes rl_program_pipelined_boundary_robustness_test's `_PrepackProbe._take_group` for the `count_exposed` keyword #2906 added (72 of its 100 cases raised TypeError at #2906's head). Modelled on siv-sk8's measured stage times against the merged boundary (image trellis-experimental:1014c): ~46 s over six steps with a ~10 s boundary window, vs ~13 s for the queue without this; to be confirmed by an A/B on hardware. TAG=agy CONV=14094c8e-2be4-4c24-87b4-d7102f101303
TL;DR
mlperf_gbs1024_submission.sh: (1) 1 libtpu flag + 6 MaxText settings; (2) 2 precision keys (same switch); (3) a sharded log-prob head: the GRPO loss gathers the vocab table once per micro-batch and each device scores only its own tokens; (4) a staged step boundary: the next step's first micro-batch is copied to the chips while the current step's last one computes. (5) The recipe's trainer defaults move to the bench cell, and MaxText is pinned toatwigg/mlperf+ feat(training_engine): add prepare_fwd_bwd_input and wait_for_device to overlap the next step's input copy (-7.2% s/step, 397B RL trainer bench, 128 v7x) AI-Hypercomputer/maxtext#5718 (the pin is not behind a switch).e526ea02(baa52b4c); build the image from this branch and runmlperf_gbs1024_submission.sh startwithTUNIX_IMAGEset to the image by digest. The recipe's default image lacks this code and stops at startup.Terms
REMAT_CONTEXT=device, log-prob chunk 1024, MoE buffer factor 2.75 with dropless retry: a micro-batch whose MoE buffers overflow reruns with a dropless program, so no token is dropped.What changes
TRAINER_TUNED_FLAGS=trueTUNIX_LOGPS_SHARDED_HEAD=localPIPELINE_STEP_BOUNDARY=1atwigg/mlperfplus AI-Hypercomputer/maxtext#5718TRAINER_TUNED_FLAGS=truefor the defaults(a) The measured runs also set
moe_route=device. It did nothing in the bench image's MaxText, and this PR leaves it at its default.(b) 105.97 s is the change 1-3 program without staging, in the same pod. Staged through a bench-driver switch (AI-Hypercomputer/maxtext#5719, do not merge) that makes the same trainer calls in the same order, not through this PR's orchestrator.
The 6 MaxText settings of change 1 are
context_parallel_strategy=all_gather,sa_fuse_reciprocal=true,sa_use_base2_exp=false,gdn_cp_mode=seq,gdn_cp_state=deviceandnum_moe_token_chunks=2, next to the libtpu flag--xla_tpu_enable_offloading_copy_to_sparsecore=false. The 2 precision keys of change 2 aregdn_cp_matmul_precision=highandcast_grads_after_all_reduce=true.How it works
Where each change acts in one optimizer step
flowchart TD B1["Step boundary - change 4<br/>this step's first micro-batch was staged<br/>during the previous step"] subgraph MB["Each of the 9 micro-batches, forward and backward"] direction TB MIX["Token mixing, attention or GDN by layer<br/>attention, change 1 - K and V all-gathered<br/>over the context axis, 2 Splash options<br/>GDN, change 1 - sequence-sharded, carries on device<br/>GDN, change 2 - state composed in 3 bf16 passes"] MOE["MoE feed-forward in every layer<br/>change 1 - 2 token chunks instead of 4"] HEAD["Log-prob head and GRPO loss<br/>change 3 - sharded head"] GRAD["Gradient reduction across chips<br/>change 2 - all-reduce-only bf16 gradients<br/>cast to f32 after the all-reduce"] MIX --> MOE --> HEAD --> GRAD end UPD["Optimizer update - change 4<br/>deferred until the next step's<br/>first micro-batch is staged"] B2["Next step boundary"] XLA["Compiler, every trainer program - change 1<br/>copies not offloaded to the SparseCores"] B1 --> MB --> UPD --> B2 XLA -.-> MBOne optimizer step: changes 1-3 act inside each of the 9 micro-batches, change 4 at the step boundary. Bench savings: change 1 -8.79 s (speed only), change 3 -14.41 s (PASS), change 2 -1.36 s (PASS), change 4 -7.66 s (numerics pending); 130.80 -> 98.31 s overall.
Change 3: the log-prob head, before and after
flowchart TB IN["One micro-batch reaches the head<br/>hidden states, 64 rows x 65,536 tokens<br/>rows over fsdp x expert, tokens over 4 context ranks"] subgraph OLD["Before - compute_chunked_logps under GSPMD"] direction TB OA["Scan 64 chunks of 1,024 tokens<br/>every context rank computes every chunk<br/>4x the matmuls"] OB["Every chunk, forward and rematerialized backward<br/>all-gathers the 2 GB vocab table"] OC["Every backward chunk<br/>all-reduces the 2 GB bf16 table gradient"] OD["Per micro-batch<br/>129 table all-gathers, 65 table all-reduces<br/>533 TFLOP of head matmuls per device"] OA --> OB --> OC --> OD end subgraph NEW["After - sharded head, shard_map with a custom VJP"] direction TB NA["Each head parameter all-gathered once<br/>in the forward, reused by the backward"] NB["Each device scans only its own tokens<br/>1 row x 16,384 tokens in 16 steps of 1,024"] NC["Head-parameter gradients summed in f32<br/>on each device, then reduced once"] ND["Per micro-batch<br/>2 table all-gathers, 2 table all-reduces<br/>133 TFLOP of head matmuls per device"] NA --> NB --> NC --> ND end IN -->|"switch off or unset"| OLD IN -->|"TUNIX_LOGPS_SHARDED_HEAD=local"| NEWPer micro-batch, from an AOT compile of the team recipe at the bench cell (128 chips, 256 devices): the old head gathers the 2 GB table 128 times, the new one once; each count in the diagram adds one gather and one all-reduce for the input embedding.
The GRPO loss needs the log-probability of every next token: the hidden state times the vocab table (4,096 x 248,320 in bf16, 2 GB), in chunks of 1,024 tokens. A TPU profile of the old head, on a variant of the team recipe, put it at 1,871.5 ms per micro-batch per TensorCore, against 199.0 ms for the pretraining loss head of the same model. The new head runs the same per-chunk math; its one numerical change is that the vocab-table and final-norm-scale gradients are summed in f32 once, not in bf16 chunk by chunk (XLA compiles that f32 reduce-scatter as an all-reduce plus a slice). It falls back to the old head, logging
[logps-head] sharded head off ..., in four cases: no mesh set withjax.set_mesh, no token layout that fits the mesh, acompute_final_logitsthat reads no model state, or a head parameter with no dimension divisible by the device count.Change 4: the step boundary, before and after
gantt title One step boundary on the bench, in seconds dateFormat ss axisFormat %S todayMarker off section Before Last micro-batch of step k, 10.75 s : 00, 11 Update of step k, 1.6 s : 11, 13 Chips idle 7.7 s, next input copied in : crit, 13, 21 First micro-batch of step k+1 : 21, 32 section Staged Last micro-batch of step k : 00, 11 Next input copied in, overlapped : active, 00, 08 Deferred update of step k : 11, 13 First micro-batch of step k+1, at once : 13, 24Bench, same pod. Bars are rounded to whole seconds. The copy bar is drawn as long as the gap it removes, because the bench does not time the copy itself.
On the bench the gap is mapping and copying step k+1's first micro-batch to the chips (its router-replay routing alone is 5.03 GB: synthetic on the bench, recorded by the rollouts in a run): the call that waits on it took 18.36-18.46 s instead of 10.75 s, and 10.71 s when staged. In the reduced rig, where the orchestrator also closes the step, the chips idled 12.46 s per boundary with the flags off and 0.02 s with them on.
Reduced rig: a 128-chip test setup, not part of this PR: an earlier revision of this PR's orchestrator and trainer worker (see "Corrections to commit messages") drives one MaxText trainer over gRPC on recorded trajectory groups instead of rollouts (3 micro-batches per step, learning rate 0, no weight sync or checkpoint).
Results
On the bench (see TL;DR) at the bench cell, with synthetic sequences of the real shape. 0 MoE tokens dropped and 0 dropless retries in every run below. Each row adds one change.
(a), (b): see What changes. The change 1-3 program measured 106.24, 106.21 and 106.04 s in three pods (mean 106.16). Deltas use unrounded means.
The pretraining target is the MaxText pretraining run of this model on the same kind and number of chips (128 v7x), with the same mesh and micro-batch shape; its rows had no padding. About 9.6% of the bench's token slots are padding (7.0% in each of the first 8 micro-batches, 30.25% in the last), so both rows are shown.
Correctness
Bench speed runs cannot see gradient errors: the bench input's rollout log-probs are synthetic, so the loss's log-prob gates (sequence error threshold, truncated importance sampling) drop every sequence of a random-weight model, and loss and grad_norm are 0. A real-gradient A/B turns the gates off (
LOGP_GATES=off) and gives every sequence segment a seeded random advantage; all arms run in one pod from the same weights and inputs. The forward log-prob metric is the mean absolute per-token gap between the trainer's log-probs and those rollout log-probs (sampler_is/token_logdiff_absmean). Pass bounds for changes 2 and 3, written before each run (relative, except the forward metric): loss 1e-5, forward metric 5e-5, entropy 1e-4, grad_norm 2e-3; fail above 5e-4, 1e-3 and 1e-2 for the last three. Parentheses give the same quantity between two reference runs: the noise floor.prefuse_moe_weights=false,moe_accumulate_wi_dlhs=true,moe_accumulate_chunk_wgrad=true,decoder_layer_input=device) fail the same way without the last one, so the cause isprefuse_moe_weights=falseand/or the two accumulate flags. The launcher tests fail if the switch sets any of them.CPU tests on the pushed code (Linux, CPU JAX):
moe_routecommit (no later commit changes a recipe). Sharded log-prob head: 40 tests OK, 1 skipped by design (head files unchanged since).atwigg/mlperfat the merged commit4c7823f20). The cleanup commit then changed 4 of those test files (names, comments and docstrings; 19 diagnostic prints removed; no behavior change) and only comments and docstrings inrl_program.pyandtrainer_worker.py. The 7 step-boundary test files pass 549 of 549, the same count before and after.moe_routetest fails on the previous recipe; the measured-cell tests fail on the old recipe. For in-order layouts like the bench's, the mesh-order fix leaves the tracedvalue_and_gradprogram unchanged (same jaxpr hash, head on and off).TrainerWorkerin the staged order.How to run end to end
Not yet run end to end; see "Gates before un-drafting".
baa52b4c): both MaxText lines ofrequirements/maxtext_requirements.txt(maxtextandmaxtext-vllm-adapter) moved from feat(training_engine): add prepare_fwd_bwd_input and wait_for_device to overlap the next step's input copy (-7.2% s/step, 397B RL trainer bench, 128 v7x) AI-Hypercomputer/maxtext#5718's head commitc79bcfd16to its squash-merge commite526ea02on atwigg/mlperf. The two commits have the same tree (e1330ef9), so the installed code is unchanged.raiden_wheels/(not in git; theDockerfilecopies it) and put the pinned Raiden wheel in it,tpu_sync_jax-0.0.1.dev20261013212200, sha256c3a34a8dc94e6dbabd4aa5e54bde2de6e1c3c5f616bf04c71cc507241d64ece0; a wheel there is installed without a checksum check. Or leave it empty and build on a GCE host whose service account can read the wheel bucket: the bucket refuses unauthenticated reads, so with neither the build fails at the Raiden step. Then:imagePullPolicy: Always, so a moving tag can mix code across restarts. An emptyTUNIX_IMAGEfalls back togcr.io/cloud-tpu-multipod-dev/atwigg/trellis:latest(built 2026-09-27), which stops at startup: its orchestrator rejects the new flags and its MaxText does not know 13 of the trainer's keys.${VAR:-default}, so an exported value wins. Silent changes: an exportedMAXTEXT_EXTRA_FLAGSreplaces the recipe's base MaxText list (mesh rule, kernels, buffer factor, chunks; the retry, padding, audit and tuned keys are still appended);TUNIX_LOGPS_SHARDED_HEADinsideTRAINER_EXTRA_ENVoverrides the switch;COMPUTE_LOGPS_CHUNK_SIZE=0turns the head off; a boundary switch set to anything but1,trueorTrue(for exampleTRUE) is off.mlperf_gbs1024_submission.shTRAINER_TUNED_FLAGStrueRAGGED_BUFFER_FACTOR=2.75withRETRY_WHEN_TOKENS_DROPPED=true, 2 MoE token chunks,REMAT_CONTEXT=device,MOE_EXCLUDE_PADDING=true,LOG_MOE_DROPS=true,COMPUTE_LOGPS_CHUNK_SIZE=1024. Each can be overriddenfalse: the trainer renders as before this PR (fully dropless buffers, context offload, 4 token chunks, no retry, log-prob chunk 512)RAGGED_BUFFER_FACTOR2.75whenTRAINER_TUNED_FLAGS=true, else-1-1: fully dropless buffers, context offload, 4 chunks, no retry, padding or audit flags. Not run on TPU with these settingsTUNIX_LOGPS_SHARDED_HEADlocal0(orfalse,off,no). An empty value falls back tolocalin this recipePIPELINE_STEP_BOUNDARY1(on for1,trueorTrue)--pipeline_step_boundary, plus--stage_next_microbatchthroughSTAGE_NEXT_MICROBATCH, which follows it unless setfalseturns both off.STAGE_NEXT_MICROBATCH=falsealone keeps the pipelined boundary without stagingEvery other 397B v7x recipe keeps these switches off; the MaxText pin applies to every image built from this branch.
STAGE_NEXT_MICROBATCHwithoutPIPELINE_STEP_BOUNDARYis refused. Keep router replay on (RETURN_ROUTED_EXPERTS=true, the default): on the bench at 256 chips, with it off every micro-batch overflowed the 2.75 buffer and was retried dropless, at 156.0 s per optimizer step against 72.7 s with replay on in the same pod (a setup without the 2 precision keys, so not the 71.47 s arm in Results).What the step-boundary flags do
--pipeline_step_boundarypacks step k+1's first micro-batch while step k's last ones train, and sends it right after step k's trainer-metrics fetch, checkpoint (if one is due) and weight-sync staging; step k's logging and other bookkeeping follow the send. Same trainer calls in the same order, nothing copied early.--stage_next_microbatch(needs the flag above) sends that micro-batch to the trainer while the chips still compute step k's last one (TrainerWorker.stage_fwd_bwd); with feat(training_engine): add prepare_fwd_bwd_input and wait_for_device to overlap the next step's input copy (-7.2% s/step, 397B RL trainer bench, 128 v7x) AI-Hypercomputer/maxtext#5718 the trainer maps it and starts its copy to the chips (prepare_fwd_bwd_input).--stage_before_update(default on, no env var) stages between step k's lastfwd_bwd(apply_optimizer=True, defer_update=True)andapply_deferred_update(). Under dropless retryupdate()returns only after the last micro-batch is done, so staging after it hides nothing: staged that way, the rig still idled 7.43 s per boundary.While an update is deferred, the worker refuses anything that needs the step closed (another micro-batch, an update, metrics, a checkpoint, weight sync, actor log-probs, eval). A stage that fails on its way to the trainer falls back to an unstaged send and turns staging off for the run; a stage that raised on the trainer ends the run, as a failed train call does. Without AI-Hypercomputer/maxtext#5718's methods the boundary runs pipelined but unstaged, with one warning.
Where staging applies in a run. In a 10-step GBS-1,024 run, staging can happen at 6 of the 9 step boundaries (after optimizer steps 1-6). It is refused after steps 7-9, whose checkpoints the MLPerf validation scores (
val_start_step7): a slow stage there could delay the checkpoint's timestamp. It also needs step k+1's first micro-batch packed in time; if rollouts are late, the update is still deferred but nothing is staged.What to check in the first run
--pipeline_step_boundary, the trainer the tuned keys[logps-head] sharded head local: ... 1x16384 local tokens in 16 steps of 1024lines, as on the bench[logps-head] sharded head off, using compute_chunked_logps: <reason>, or no[logps-head]line (head code missing, chunk size 0 or packing off)[pipeline] STAGED next step's first microbatch on the trainer before the update in X s; the last microbatch then still computed for Y s[pipeline] stage_next_microbatch is set but the trainer cannot stage ...(no AI-Hypercomputer/maxtext#5718 in the image) or[pipeline] The trainer cannot defer a step's update .... A Y of about 2.7-2.9 s means the chips were already idleMOE_DROP_AUDIT_STEP step=N dropped_assignments_used=U dropless_replays=Rdropless_replaysnear 0[logps-head]line): check that the rollouts return routing. On the random-weight bench, missing routing made every micro-batch retrydropped_assignments_used=0Gates before un-drafting
This PR stays a draft until all three checks have run on TPU and passed.
moe_route, now left out) against the team recipe, with pass bounds written down before the run.What no run has covered yet:
raiden_20261013212200) only the head A/B was re-measured (120.34 -> 106.60 s, same pod). It ran MaxText60425bfbd, the pin without feat(training_engine): add prepare_fwd_bwd_input and wait_for_device to overlap the next step's input copy (-7.2% s/step, 397B RL trainer bench, 128 v7x) AI-Hypercomputer/maxtext#5718's engine-only commits (the same model, kernel and config code), within the 84,992 MiB budget. Not measured at the pin: the 2 precision keys, the staged boundary, GBS 1,024 and checkpointing.update()after a stage is CPU-tested only.Commits in this PR
recipes(397b v7x): trainer flags measured on the RL trainer benchTRAINER_TUNED_FLAGS: the libtpu flag and seven MaxText keys (the six settings of change 1 plusmoe_route=device, which a later commit removes)recipes(397b v7x): two precision keys join TRAINER_TUNED_FLAGSgdn_cp_matmul_precision=high,cast_grads_after_all_reduce=trueMerge atwigg/mlperf 4c7823f20rl: opt-in sharded log-prob head for packed rows (TUNIX_LOGPS_SHARDED_HEAD)a29991d3f)orchestrator: opt-in staged step boundary (PIPELINE_STEP_BOUNDARY, STAGE_NEXT_MICROBATCH)recipes(gbs1024 submission): turn on the sharded log-prob head and the staged step boundaryrecipes(gbs1024 submission): default to the trainer cell measured on the RL trainer benchrequirements: pin MaxText to atwigg/mlperf plus AI-Hypercomputer/maxtext#5718c79bcfd16:60425bfbdplus six commitstests(orchestrator): describe measured costs in plain words; drop diagnostic printsrl_program.pyandtrainer_worker.py(the no-overlap latch cannot fire on Pathways); no behavior changerecipes(397b v7x): leave moe_route out of TRAINER_TUNED_FLAGSmoe_routestays at its default, as the bench effectively ranorchestrator: start a step's clock before waiting for its groups--pipeline_step_boundary, a step that waits for its groups starts its clock before that wait, as without the flag, sostep_timeand the MLPerftrain_step_timebuilt from it include the wait. No wall time or scored timestamp changes. Found in reviewA dry-run render at the
moe_routecommit (no later commit changes a recipe) gives a trainer whose MaxText settings equal the 98.31 s bench arm's on all 75 shared keys, with all 17 libtpu flags equal. Only the bench hasenable_checkpointing=falseand the inertmoe_route=device. The batch differs (GBS 1,024 here withBATCH_SIZE=64, learning rate 2e-6, clip 0.0625; GBS 1,152 on the bench), and so do the Pathways images and the MaxText build.Superseded PRs (do not merge)
Each is marked DO NOT MERGE and opens with a note that points here.
a29991d3f)Known limitations and follow-ups
Known limitations
moe_exclude_padding_from_routing=true) is now on by default in this recipe (the 256-chip recipe keeps it off). It has CPU parity only (MoE: exclude padding tokens from routing (moe_exclude_padding_from_routing) AI-Hypercomputer/maxtext#5672); its TPU on/off numerics A/B has not run. Every bench row, the team recipe included, ran with it on, so no measurement here isolates it.jax.set_mesh, which MaxText's training engine uses; tunix's own RL stack (with mesh:) falls back with a warning, and Explicit-axis meshes raise. The switch is read at trace time.sa_fuse_reciprocal=trueandsa_use_base2_exp=falsereverse the values [v7x] 397B 256 recipe: restore v7x trainer config after mlperf_base move #2435 set in this recipe (fix(recipes): add TPU v7x XLA/Pathways flags and Qwen3.5 GDN/Splash settings to mlperf_35b_128_v7x.sh #2509 listssa_fuse_reciprocal=falseamong Qwen3.5 stability flags for the 35B recipe); gate 1 covers this.REMAT_POLICY=fullwith the switch on fails MaxText's config validation at startup (gdn_cp_state=deviceneeds custom remat).Follow-ups (not blocking review)
STAGE_NEXT_MICROBATCHis set; today the orchestrator warns once, at the first step boundary, and runs unstaged.MAXTEXT_EXTRA_FLAGS, while an explicitNUM_MOE_TOKEN_CHUNKSor libtpu flag is kept. Pick one rule and pin it with a test.LOG_MOE_DROPS=trueis only a default. The drop audit is the run's record that no tokens were dropped (moe_dropped_assignmentsis written only with it), so a scored run should require it.RAGGED_BUFFER_FACTORis compared to-1as a string, so-1.0counts as a finite factor and MaxText refuses the config at trainer start: loud, but after bring-up instead of at render time.TRAINER_TUNED_FLAGS=falsewith a retry override, both pass the launcher tests.Repro on the bench
Follow "How to reproduce" in #2873 (bench pin, images, DRY_RUN, render, run, read), with these changes:
BATCH_SIZE=72(GBS 1,152). It renders the final trainer flags (minus the inertmoe_route=device), so render the bench withoutREPLACE_MAXTEXT_FLAGS,EXTRA_MAXTEXT_FLAGSandLIBTPU_EXTRA;REPLACE_MAXTEXT_FLAGSrefuses the four keys the render sets twice (context_parallel_strategy,sa_fuse_reciprocal,sa_use_base2_exp,gdn_cp_mode). Checked against the measured arm offline, not re-run on TPU.tunix/rl/common.pyover its own: use this PR's file (md50ca8d1ad11b769de5c18414d7874d9d0) in place of the guide'sa29991d3fdownload and md5 check. Add feat(training_engine): add prepare_fwd_bwd_input and wait_for_device to overlap the next step's input copy (-7.2% s/step, 397B RL trainer bench, 128 v7x) AI-Hypercomputer/maxtext#5718'smaxtext_engine.pyandinflight_throttler.pychanges. The driver needs theSTAGE_BEFORE_UPDATEswitch from [DO NOT MERGE] perf(rl_trainer_bench): opt-in STAGE_BEFORE_UPDATE step boundary (-7.2% step time on the 397B bench; stacked on #5701) AI-Hypercomputer/maxtext#5719, which raises without those methods and refuses a tunix that runs the update inside the lastfwd_bwd(perf(orchestrator): fuse final microbatch fwd_bwd+update RPC and defer get_metrics off weight-sync path #2719, in this branch's base).STEPS=5 MICRO_STEPS=9,DRIVER_ENV="TUNIX_LOGPS_SHARDED_HEAD=local STAGE_BEFORE_UPDATE=on", then the same withSTAGE_BEFORE_UPDATE=off, back to back.completed steplines and theSTAGE_BEFORE_UPDATE step=k ...lines. Expect about 105.97 s off and 98.31 s on, two[logps-head] sharded head locallines, and 0 MoE drops.Images of the measured runs:
gcr.io/tpu-vm-gke-testing/sivaibhav/trellis-experimental@sha256:b4647c188b25638e644c7a6fc53fb8e0751741525a277a1bcf07dca92ba9a6acus-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:e714351cdde50d306dd62f127cffabcdaf98dff80ad22e8b4c66c5a1d1df6ddbThe measured runs installed #2873's head file (
a29991d3f, md5a5bfda56676e92d65261f3e24303101b) and an overlay of AI-Hypercomputer/maxtext#5718's engine methods at pod start, with md5 checks. This PR's head file differs froma29991d3fin docstrings and comments, a clearer error for an unrecognized switch value, one moved pure-Python statement, and the mesh-order line.Evidence: per-step numbers, numerics, the staged boundary on TPU
Per-step numbers, s per optimizer step. Pods are lettered to show which arms ran in the same pod.
moe_route=device.Sharded log-prob head, real gradients. One pod, arms head off, head on, head off again, same random init and input. Step 0 is gated (bounds and results in Correctness; kept fraction 1 in every arm, loss bit-identical at 0.0304453493); steps 1-2 follow updates and are reported only. grad_norm is higher with the head at all three steps: +2.78e-4, +2.01e-4 and +2.89e-4 relative, against off-vs-off spreads of 5.32e-5, 2.65e-5 and 2.01e-5. That fits the f32 summation; the TPU run logs only the global norm, so the attribution rests on CPU. On CPU (real GRPO loss and MaxText head, 8 devices, 4 rows x 65,536 tokens), 0 of 262,144 tokens differ in log-prob or entropy, the loss, dL/dh and 68 other gradient leaves are exactly equal, and 5 deliberately broken heads are caught.
Staged boundary, bench, final 128-chip pair (one pod; arms in the order on, off, on again):
STAGE_BEFORE_UPDATE step=k staged=... stage_s=0.007-0.041 device_wait_s=10.49-10.72: the stage call returned at once, and the update then waited about one micro-batch for the device.apply_optimizer, is pinned by the bench switch's CPU tests.Reduced rig (defined under "Change 4"): an earlier revision of this PR's
StandardRLProgramandTrainerWorker, with timing instrumentation, drives the trainer with the team recipe's settings and dropless retry. Input: 16 recorded trajectory groups from the first optimizer step of a team 397B RL run, each served twice. All arms in one pod, 5 steps each, medians:--pipeline_step_boundary--stage_next_microbatch --no-stage_before_update--stage_next_microbatch(default order)Corrections to commit messages
Pushed history is not rewritten, so the corrections are recorded here. The numbers in the messages are right; some sentences claim more than was measured, or have gone stale.
recipes(397b v7x): trainer flags measured on the RL trainer benchlistsmoe_route=device, which themoe_routecommit removes (it did nothing on the bench). It names the source runs in Speed up Qwen3.5-397B-A17B training on TPU v7x AI-Hypercomputer/maxtext#5409 by number: read that as settings of the MaxText pretraining recipe for this model.recipes(397b v7x): two precision keys join TRAINER_TUNED_FLAGS: since themoe_routecommit the block holds six keys plus these two, and the exact-set test expects eight; the pin is nowc79bcfd16(the keys mean the same there).rl: opt-in sharded log-prob head ...: each head parameter is gathered once, in the forward, and the backward reuses it (not "once per pass"). There are four fallbacks, not three. The old head's 129 and 65 collectives each include one for the input embedding, and XLA compiles the "reduce-scattered once" step as an all-reduce plus a slice.orchestrator: opt-in staged step boundary ...: the reduced rig fed recorded trajectory groups from a real 397B RL run's first optimizer step, not synthetic ones, and ran an earlier revision (26b06c504, before the base merge that brought [mlperf] Add intra-prompt rollout over-generation, straggler cancellation, and 35B recipe fixes #2633). Its boundary code is the same apart from comments, docstrings, one help string and the keyword order in one constructor, but the rig never ran it together with [mlperf] Add intra-prompt rollout over-generation, straggler cancellation, and 35B recipe fixes #2633's changes. The "~7.7 s per step" is the bench's gap; the orchestrator's own idle in the rig was 12.46 s, cut to 0.02-0.03 s. "When a stage measures no overlap, staging latches off" does not hold on Pathways (see Known limitations).recipes(gbs1024 submission): turn on the sharded log-prob head ...: the 98.31 s run used the same head switch, but its boundary ran through the bench driver's switch ([DO NOT MERGE] perf(rl_trainer_bench): opt-in STAGE_BEFORE_UPDATE step boundary (-7.2% step time on the 397B bench; stacked on #5701) AI-Hypercomputer/maxtext#5719), notPIPELINE_STEP_BOUNDARY; the bench has no orchestrator. At that commit the default trainer was still fully dropless with log-prob chunk 512; the next commit changed that.recipes(gbs1024 submission): default to the trainer cell ...: "the same MaxText and libtpu flags as the bench's fastest 128-chip run" held at that commit; since themoe_routecommit they match on the 75 shared keys (see Commits in this PR).