Skip to content

397B v7x RL trainer: tuned settings, sharded log-prob head, staged step boundary (130.8 -> 98.3 s per optimizer step, trainer bench) - #2878

Merged
niting merged 13 commits into
atwigg/mlperffrom
parambole/ours275-trainer-config
Oct 15, 2026
Merged

niting merged 13 commits into
atwigg/mlperffrom
parambole/ours275-trainer-config

Conversation

@parambole

@parambole parambole commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

TL;DR

Terms

  • Optimizer step: one weight update. On the bench: about 1,152 trajectories packed into 9 micro-batches of 64 rows x 65,536 tokens (at 256 chips: 5 micro-batches of 128 rows), each run forward and backward, then one update. Times are the mean of steps 1-4; step 0 is warm-up.
  • tok/s/chip: token slots per optimizer step (micro-batches x rows x 65,536) / chips / seconds, padding included. At 128 chips: 294,912 / seconds per optimizer step.
  • Bench cell: mesh fsdp 32 x context 4 x expert 2, 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.
  • Team recipe: the team's trainer settings before this PR, run at the bench cell. The recipe's old default (fully dropless buffers, context offload, 4 token chunks, no retry, log-prob chunk 512) was not measured here.
  • Same pod / cross-pod: A/B arms that ran back to back in one TPU job / in separate jobs.

What changes

# Change What it does Switch (default in the submission recipe) Bench, s per optimizer step Numerics on TPU
1 Tuned settings: 1 libtpu flag, 6 MaxText settings Attention all-gathers K and V over the context axis (not a ring), with 2 Splash kernel options; GDN context parallelism shards the sequence and keeps its carries on device; MoE runs 2 token chunks, not 4; XLA stops offloading copies to the SparseCores TRAINER_TUNED_FLAGS=true 130.80 -> 122.00, cross-pod (a) speed only (gate 1)
2 2 precision keys GDN state composition runs 3 bf16 passes, not 6. Gradients summed by an all-reduce alone are cast to f32 after it same switch 107.59 -> 106.24, same pod, on top of change 3 PASS
3 Sharded log-prob head Gathers the vocab table once per micro-batch, scores only each device's own tokens, sums the head-parameter gradients in f32 once TUNIX_LOGPS_SHARDED_HEAD=local 122.00 -> 107.59, same pod PASS
4 Staged step boundary Copies step k+1's first micro-batch to the chips while step k's last one computes; step k's update is held back (deferred) until the copy has started PIPELINE_STEP_BOUNDARY=1 105.97 -> 98.31, same pod, through the bench-only switch (b) order checks PASS; real-gradient check pending (gate 2)
5 Recipe defaults and MaxText pin Moves the recipe's trainer defaults to the bench cell. Pins MaxText to atwigg/mlperf plus AI-Hypercomputer/maxtext#5718 TRAINER_TUNED_FLAGS=true for the defaults not measured: every row ran at the bench cell no A/B against the old default; padding exclusion, now on by default, has CPU parity only and no TPU on/off A/B

(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=device and num_moe_token_chunks=2, next to the libtpu flag --xla_tpu_enable_offloading_copy_to_sparsecore=false. The 2 precision keys of change 2 are gdn_cp_matmul_precision=high and cast_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 -.-> MB
Loading

One 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"| NEW
Loading

Per 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 with jax.set_mesh, no token layout that fits the mesh, a compute_final_logits that 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, 24
Loading

Bench, 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.

Setup s per optimizer step Change How it was compared
Team recipe 130.80 - mean of 3 pods (130.78-130.81)
+ change 1: libtpu flag, 6 MaxText settings (a) 122.00 -8.79 cross-pod
+ change 3: sharded log-prob head 107.59 -14.41 same pod
+ change 2: 2 precision keys 106.24 -1.36 same pod
same program, in the boundary pod 105.97 reference same pod as the next row
+ change 4: staged step boundary (b) 98.31 -7.66 same pod; a repeat read 98.26

(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.

  • Total: -32.48 s per optimizer step (-24.8%). The stage deltas come from different pods and add up to 32.22 s.
  • The boundary saving is only the boundary. A regular micro-batch takes 10.754 s with staging and 10.755 s without (medians).
  • 256 chips (fsdp 64 x context 4 x expert 2, 5 micro-batches of 128 rows, changes 1-3 on): 71.47 -> 56.12 s per optimizer step with the staged boundary, same pod. It is the same work as at 128 chips (560 real rows, in 640 row slots instead of 576). With the boundary on at both sizes, 128 -> 256 chips is 1.75x; perfect scaling would be 49.2 s.
tok/s/chip Team recipe This PR Pretraining target
All token slots, padding included 2,255 (70.6% of target) 3,000 (93.9%) 3,193
Real tokens only about 2,039 (63.8%) about 2,712 (84.9%) 3,193

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.

Change Check on TPU Result Verdict
1. libtpu flag and 6 MaxText settings None against the team recipe. They ran with real gradients as the reference arms of the head A/B loss and grad_norm finite; two runs agree to 5.3e-5 speed only (gate 1)
2. 2 precision keys Same pod, real gradients, step 0: one run without the keys, two with them grad_norm 1.47e-5 relative (1.92e-5); forward log-prob metric 1.1e-6 (3.6e-7); entropy 4.2e-9 (8.4e-9); loss identical as printed PASS, measured with the head on
3. Sharded log-prob head Same pod, real gradients, step 0: head off, on, off again loss bit-identical; grad_norm +2.78e-4 relative to the mean of the head-off runs (5.32e-5); forward metric +3.7e-6 (3.8e-6); entropy -1.7e-8 relative PASS
4. Staged step boundary Exact checks on the bench and in the reduced rig; no real-gradient A/B yet Same micro-batches in the same order, one update per step, every micro-batch of step k+1 after update k, staged before every update; regular micro-batch and forward metric unchanged order PASS; numerics pending (gate 2)
Left out: a bundle of 4 MoE settings Same pod, real gradients forward metric 9.74e-4, above the 5e-4 fail bound; grad_norm -0.80%, about 420x the spread between two reference runs FAIL, not in this PR
  • The bit-identical loss does not test the head: at an importance ratio of 1 the GRPO loss depends only on advantages and masks, while the gradient goes through the head's log-probs. The +2.78e-4 grad_norm shift fits the head's one numerical change: on CPU only the table and norm-scale gradients differ from the old head's, both closer to an all-f32 reference (0.93x and 0.13x of the old head's error).
  • The 4 MoE settings (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 is prefuse_moe_weights=false and/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):

  • Launcher: 35 of 35 at the moe_route commit (no later commit changes a recipe). Sharded log-prob head: 40 tests OK, 1 skipped by design (head files unchanged since).
  • Orchestrator, queue and worker suites, at "requirements: pin MaxText ...": 1,315 passed; the 28 failures and 1 collection error also fail on the base (atwigg/mlperf at the merged commit 4c7823f20). 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 in rl_program.py and trainer_worker.py. The 7 step-boundary test files pass 549 of 549, the same count before and after.
  • Shown to fail on deliberately wrong code: the head before the mesh-order fix fails both out-of-order tests (log-probs off by up to 10.65 and 13.99); deleting the head's non-finite guard fails the NaN test; the moe_route test 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 traced value_and_grad program unchanged (same jaxpr hash, head on and off).
  • Step boundary, on [DO NOT MERGE] perf(orchestrator): opt-in pipelined step boundary that stages the next micro-batch before the deferred update #2877's branch: 599 tests; 31 of 31 deliberately broken hooks caught. With the flags off, the tests compare every trainer call with a frozen copy of the old loop. 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 tests train bit-identically through this PR's TrainerWorker in the staged order.
  • The counts above were taken before the last commit (the step clock). It adds one test, with the boundary off and on; its message reports that the test fails on the previous code in the two cases where the boundary is on and a step waits for its groups.

How to run end to end

Not yet run end to end; see "Gates before un-drafting".

  1. Merge 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.
  2. Re-pin (done in baa52b4c): both MaxText lines of requirements/maxtext_requirements.txt (maxtext and maxtext-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 commit c79bcfd16 to its squash-merge commit e526ea02 on atwigg/mlperf. The two commits have the same tree (e1330ef9), so the installed code is unchanged.
  3. Build, push and launch. Create raiden_wheels/ (not in git; the Dockerfile copies it) and put the pinned Raiden wheel in it, tpu_sync_jax-0.0.1.dev20261013212200, sha256 c3a34a8dc94e6dbabd4aa5e54bde2de6e1c3c5f616bf04c71cc507241d64ece0; 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:
bash build_docker.sh --maxtext --raiden --deepswe   # --maxtext adds the MaxText vLLM adapter
docker tag tunix_base_image "gcr.io/<project>/<user>/trellis:<tag>"
docker push "gcr.io/<project>/<user>/trellis:<tag>"   # prints the image digest
export TUNIX_IMAGE="gcr.io/<project>/<user>/trellis@sha256:<digest>"
export WANDB_API_KEY="<key>" MAXTEXT_OUTPUT_DIR="gs://<bucket>/trellis/maxtext"
JOB_PREFIX="<prefix>" K8S_NAMESPACE="<namespace>" \
  bash tunix/experimental/examples/recipes/mlperf_gbs1024_submission.sh start
  • Pin by digest. Client containers pull with imagePullPolicy: Always, so a moving tag can mix code across restarts. An empty TUNIX_IMAGE falls back to gcr.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.
  • Launch from a clean shell. Recipe settings are ${VAR:-default}, so an exported value wins. Silent changes: an exported MAXTEXT_EXTRA_FLAGS replaces 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_HEAD inside TRAINER_EXTRA_ENV overrides the switch; COMPUTE_LOGPS_CHUNK_SIZE=0 turns the head off; a boundary switch set to anything but 1, true or True (for example TRUE) is off.
Switch Default in mlperf_gbs1024_submission.sh What it turns on To turn it off
TRAINER_TUNED_FLAGS true Changes 1 and 2, and the bench cell: RAGGED_BUFFER_FACTOR=2.75 with RETRY_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 overridden false: the trainer renders as before this PR (fully dropless buffers, context offload, 4 token chunks, no retry, log-prob chunk 512)
RAGGED_BUFFER_FACTOR 2.75 when TRAINER_TUNED_FLAGS=true, else -1 The buffer factor of the bench cell, with dropless retry -1: fully dropless buffers, context offload, 4 chunks, no retry, padding or audit flags. Not run on TPU with these settings
TUNIX_LOGPS_SHARDED_HEAD local Change 3, in the trainer JobSet only 0 (or false, off, no). An empty value falls back to local in this recipe
PIPELINE_STEP_BOUNDARY 1 (on for 1, true or True) Change 4: --pipeline_step_boundary, plus --stage_next_microbatch through STAGE_NEXT_MICROBATCH, which follows it unless set false turns both off. STAGE_NEXT_MICROBATCH=false alone keeps the pipelined boundary without staging

Every other 397B v7x recipe keeps these switches off; the MaxText pin applies to every image built from this branch. STAGE_NEXT_MICROBATCH without PIPELINE_STEP_BOUNDARY is 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_boundary packs 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 last fwd_bwd(apply_optimizer=True, defer_update=True) and apply_deferred_update(). Under dropless retry update() 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_step 7): 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

What Where Healthy Problem
The image has this PR the run starts the orchestrator accepts --pipeline_step_boundary, the trainer the tuned keys an older image stops at argument or config parsing
The log-prob head engaged trainer log, warm-up compile two [logps-head] sharded head local: ... 1x16384 local tokens in 16 steps of 1024 lines, 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)
Staging engaged orchestrator log at each eligible boundary, [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 idle
Router replay reaches the trainer trainer log, MOE_DROP_AUDIT_STEP step=N dropped_assignments_used=U dropless_replays=R dropless_replays near 0 a retry on every micro-batch, or a recompile after warm-up (a third [logps-head] line): check that the rollouts return routing. On the random-weight bench, missing routing made every micro-batch retry
No token was dropped the same lines dropped_assignments_used=0 any other value

Gates before un-drafting

This PR stays a draft until all three checks have run on TPU and passed.

  1. Change 1 against the team recipe. A same-pod real-gradient A/B of the libtpu flag and the six MaxText settings (the first commit's seven keys minus moe_route, now left out) against the team recipe, with pass bounds written down before the run.
  2. The staged step boundary with real gradients, at 128 chips. Staging on against two staging-off runs in the same pod, on the bench through the bench-driver 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). Pass if loss, grad_norm, entropy and the forward log-prob metric at steps 0, 1 and 2 stay inside the band between the two staging-off runs. This PR's orchestrator path is exercised on TPU only by gate 3.
  3. One end-to-end smoke run from an image built at the pin: checkpointing on, real rollouts, and if possible a forced overflow and dropless retry at 128 chips.

What no run has covered yet:

Commits in this PR

Commit What it does How it was checked
recipes(397b v7x): trainer flags measured on the RL trainer bench TRAINER_TUNED_FLAGS: the libtpu flag and seven MaxText keys (the six settings of change 1 plus moe_route=device, which a later commit removes) Bench: 130.8 -> 126.4 s with the flag (another pod), 121.7 s with the keys (same pod)
recipes(397b v7x): two precision keys join TRAINER_TUNED_FLAGS gdn_cp_matmul_precision=high, cast_grads_after_all_reduce=true 107.59 -> 106.24 s, same pod; real-gradient A/B PASS
Merge atwigg/mlperf 4c7823f20 Base sync; brings in #2633 Adds exactly the base's change
rl: opt-in sharded log-prob head for packed rows (TUNIX_LOGPS_SHARDED_HEAD) #2873's head, squashed, plus sequence axes in mesh order and three tests 122.00 -> 107.59 s, same pod; real-gradient A/B PASS (on #2873's file a29991d3f)
orchestrator: opt-in staged step boundary (PIPELINE_STEP_BOUNDARY, STAGE_NEXT_MICROBATCH) #2877's change, squashed 105.97 -> 98.31 s, same pod, via the bench-driver switch; rig idle 12.46 -> 0.02 s
recipes(gbs1024 submission): turn on the sharded log-prob head and the staged step boundary Head and staged boundary on by default, in this recipe only Render tests, incl. off switches and other recipes
recipes(gbs1024 submission): default to the trainer cell measured on the RL trainer bench The default trainer is the bench cell Render tests for each default and override
requirements: pin MaxText to atwigg/mlperf plus AI-Hypercomputer/maxtext#5718 Both MaxText lines at c79bcfd16: 60425bfbd plus six commits git: the six touch only MaxText's training engine, README and tests
tests(orchestrator): describe measured costs in plain words; drop diagnostic prints Plain-word test names, comments and docstrings; 19 diagnostic prints removed; comment and docstring edits in rl_program.py and trainer_worker.py (the no-overlap latch cannot fire on Pathways); no behavior change 549 of 549 before and after
recipes(397b v7x): leave moe_route out of TRAINER_TUNED_FLAGS moe_route stays at its default, as the bench effectively ran Launcher 35 of 35; the new test fails on the previous recipe
orchestrator: start a step's clock before waiting for its groups With --pipeline_step_boundary, a step that waits for its groups starts its clock before that wait, as without the flag, so step_time and the MLPerf train_step_time built from it include the wait. No wall time or scored timestamp changes. Found in review New test, boundary off and on; per the commit message it fails on the previous code when the boundary is on and a step waits

A dry-run render at the moe_route commit (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 has enable_checkpointing=false and the inert moe_route=device. The batch differs (GBS 1,024 here with BATCH_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.

PR What it was Status / why kept
#2873 The sharded log-prob head Its review history, and the exact file measured on TPU (a29991d3f)
#2877 The staged step boundary Its review history and the reduced-rig write-up
#2876 Lets the old head compile at log-prob chunks above 1024 under context parallelism Reference only; with the sharded head, chunk 4096 gave no gain (107.84 vs 107.59 s)
#2887 Integration build of every tunix change of the 128-chip run Superseded by this PR
AI-Hypercomputer/maxtext#5719 The bench-driver switch used to measure the staged step boundary Bench only
AI-Hypercomputer/maxtext#5723 MaxText integration build Superseded by AI-Hypercomputer/maxtext#5718, which merges first

Known limitations and follow-ups

Known limitations

Follow-ups (not blocking review)

  1. Make a missing 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 fail loudly at bring-up when STAGE_NEXT_MICROBATCH is set; today the orchestrator warns once, at the first step boundary, and runs unstaged.
  2. The tuned keys replace the same keys in a caller's MAXTEXT_EXTRA_FLAGS, while an explicit NUM_MOE_TOKEN_CHUNKS or libtpu flag is kept. Pick one rule and pin it with a test.
  3. LOG_MOE_DROPS=true is only a default. The drop audit is the run's record that no tokens were dropped (moe_dropped_assignments is written only with it), so a scored run should require it.
  4. RAGGED_BUFFER_FACTOR is compared to -1 as a string, so -1.0 counts as a finite factor and MaxText refuses the config at trainer start: loud, but after bring-up instead of at render time.
  5. Two test gaps: a recipe that changes which layouts get the 1,024-token log-prob chunk, and one that adds the padding mask and drop audit under TRAINER_TUNED_FLAGS=false with 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:

  1. DRY_RUN this branch with the bench images and BATCH_SIZE=72 (GBS 1,152). It renders the final trainer flags (minus the inert moe_route=device), so render the bench without REPLACE_MAXTEXT_FLAGS, EXTRA_MAXTEXT_FLAGS and LIBTPU_EXTRA; REPLACE_MAXTEXT_FLAGS refuses 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.
  2. Client image: the bench image with this PR's tunix/rl/common.py over its own: use this PR's file (md5 0ca8d1ad11b769de5c18414d7874d9d0) in place of the guide's a29991d3f download 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's maxtext_engine.py and inflight_throttler.py changes. The driver needs the STAGE_BEFORE_UPDATE switch 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 last fwd_bwd (perf(orchestrator): fuse final microbatch fwd_bwd+update RPC and defer get_metrics off weight-sync path #2719, in this branch's base).
  3. Two arms, one pod: STEPS=5 MICRO_STEPS=9, DRIVER_ENV="TUNIX_LOGPS_SHARDED_HEAD=local STAGE_BEFORE_UPDATE=on", then the same with STAGE_BEFORE_UPDATE=off, back to back.
  4. Read steps 1-4 of the completed step lines and the STAGE_BEFORE_UPDATE step=k ... lines. Expect about 105.97 s off and 98.31 s on, two [logps-head] sharded head local lines, and 0 MoE drops.

Images of the measured runs:

  • client gcr.io/tpu-vm-gke-testing/sivaibhav/trellis-experimental@sha256:b4647c188b25638e644c7a6fc53fb8e0751741525a277a1bcf07dca92ba9a6ac
  • Pathways server us-docker.pkg.dev/cloud-tpu-v2-images-dev/pathways/gke/datenglin/unsanitized_server@sha256:662f33b0ca6cd236a12534e4c78a8b7d715868669366738a015401925a39cb86
  • Pathways proxy us-docker.pkg.dev/cloud-tpu-v2-images-dev/pathways/gke/datenglin/unsanitized_proxy_server@sha256:e714351cdde50d306dd62f127cffabcdaf98dff80ad22e8b4c66c5a1d1df6ddb

The measured runs installed #2873's head file (a29991d3f, md5 a5bfda56676e92d65261f3e24303101b) and an overlay of AI-Hypercomputer/maxtext#5718's engine methods at pod start, with md5 checks. This PR's head file differs from a29991d3f in 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.

Arm Pod Steps 1-4 Mean
Team recipe A 130.872 / 130.807 / 130.720 / 130.815 130.80
Team recipe B 130.785 / 130.754 / 130.812 / 130.869 130.81
Team recipe C 130.723 / 130.719 / 130.766 / 130.910 130.78
+ libtpu flag only D 126.434 / 126.407 / 126.360 / 126.253 126.36
+ 6 MaxText settings (and the flag) D 121.759 / 121.773 / 121.776 / 121.634 121.74
same, repeat E 121.949 / 122.035 / 122.042 / 121.980 122.00
+ sharded log-prob head E 107.595 / 107.640 / 107.415 / 107.723 107.59
+ 2 precision keys E 106.329 / 106.310 / 106.126 / 106.185 106.24
same program, reference for the boundary F 106.001 / 105.944 / 105.993 / 105.950 105.97
+ staged step boundary F 98.297 / 98.303 / 98.327 / 98.319 98.31
+ staged step boundary, repeat F 98.271 / 98.271 / 98.262 / 98.236 98.26
  • The libtpu flag alone was -4.43 s against the team recipe (cross-pod) and the MaxText settings -4.63 s on top (same pod D); the tables use pod E's repeat, which shares a pod with the head arms. Every arm from the 6 MaxText settings on also carried the inert moe_route=device.
  • 98.31 sits inside the band written down before the run (98.48, range 97.5-99.8).
  • Regular micro-batch, median, down the five stages: 13.51, 12.53, 10.94, 10.78 and 10.75 s.
  • 256 chips, same pod: boundary off 71.683 / 71.465 / 71.374 / 71.372, on 56.127 / 56.116 / 56.122 / 56.095. Predicted -11.7 s (range -12.4 to -7.6), measured -15.36: a miss on the good side, as the stage hid completely.

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):

  • Every boundary logged 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.
  • Forward log-prob metric 12.2219073 off, 12.2219076 on (within-arm range about 2e-5). 0 drops, 0 retries.
  • Exact checks ran on an earlier same-pod pair without the 2 precision keys (107.73 -> 100.01 s, same bench switch) and all passed: same micro-batches in the same order (per-micro-batch input fingerprint), one update per step, staging lines and call counts as expected. The call sequence, including apply_optimizer, is pinned by the bench switch's CPU tests.

Reduced rig (defined under "Change 4"): an earlier revision of this PR's StandardRLProgram and TrainerWorker, 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:

Arm Flags Boundary device idle, s Step time, s
A0 off 12.46 64.45
A0p off, repeat 11.72 61.85
A1 --pipeline_step_boundary 11.34 62.26
A2 + --stage_next_microbatch --no-stage_before_update 7.43 59.23
A3 + --stage_next_microbatch (default order) 0.02 50.82
  • In every arm, each step trained the same micro-batches in the same order (content digests), with the same summed loss denominator (2,040,178), and every micro-batch of step k+1 entered after update k. In A3 every stage reached the trainer before the update. After warm-up, no arm ran a backend compile of 5 s or more.
  • Written down before the run (predicted value, range): A0 idle 12.0 s (9-15), A3 0.016 s (0-3), step time A0 64.6 s (59.6-70.6), A3 52.6 s (48.1-57.6). All hit. A stricter estimator puts A3's idle at 0.026 s.
  • The rig's scorer printed an overall FAIL with 74 of 79 checks passed. No ordering, data or decision check missed: the 2 misses are grad_norm bands of two non-default arms, and 3 loss checks could not be scored because at learning rate 0 the flags-off loss does not repeat within 2% from step to step.
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 bench lists moe_route=device, which the moe_route commit 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 the moe_route commit the block holds six keys plus these two, and the exact-set test expects eight; the pin is now c79bcfd16 (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), not PIPELINE_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 the moe_route commit they match on the 75 shared keys (see Commits in this PR).

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.
Comment thread tunix/experimental/orchestrator/rl_program.py Outdated
@parambole parambole changed the title 397B v7x RL trainer, 128-chip best run: tuned settings, sharded log-prob head, staged step boundary (130.8 -> 98.3 s per optimizer step) 397B v7x RL trainer, 128-chip best run: tuned settings, sharded log-prob head, staged step boundary (130.8 -> 98.3 s per optimizer step on the trainer bench) Oct 14, 2026
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.
@parambole parambole changed the title 397B v7x RL trainer, 128-chip best run: tuned settings, sharded log-prob head, staged step boundary (130.8 -> 98.3 s per optimizer step on the trainer bench) 397B v7x RL trainer: tuned settings, sharded log-prob head, staged step boundary (130.8 -> 98.3 s per optimizer step, trainer bench) Oct 14, 2026
@parambole
parambole marked this pull request as ready for review October 14, 2026 23:22
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • Performance Optimization: Reduced optimizer step time by approximately 25% (130.8s to 98.3s) through tuned MaxText settings, precision adjustments, and staged step boundaries.
  • Sharded Log-Prob Head: Implemented a sharded log-prob head to reduce memory overhead and compute time by gathering vocab tables once per micro-batch.
  • Staged Step Boundary: Introduced staged step boundaries to overlap data copying with computation, effectively reducing device idle time.
  • Recipe & Dependency Updates: Updated trainer defaults and pinned MaxText to atwigg/mlperf to ensure stability and support for the new performance features.
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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

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

Copy link
Copy Markdown
Contributor

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 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.

Comment thread tunix/rl/common.py
Comment thread tunix/rl/common.py
Comment thread tunix/experimental/orchestrator/distributed_rl_engine.py
Comment thread tunix/experimental/orchestrator/distributed_rl_engine.py
Comment thread tunix/experimental/orchestrator/distributed_rl_engine.py
Comment thread tunix/experimental/orchestrator/distributed_rl_engine.py
Comment thread tunix/experimental/orchestrator/rl_program.py
Comment thread tunix/experimental/orchestrator/rl_program.py
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.
@niting
niting merged commit affb1c6 into atwigg/mlperf Oct 15, 2026
2 checks passed
A9isha added a commit that referenced this pull request Oct 15, 2026
…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
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.

4 participants