MoE post-training with Rollout Routing Replay (R3), validated on Qwen3-30B-A3B (MATH-500 86.95 -> 92.35) - #24
Open
iamziyuzhao wants to merge 8 commits into
Open
iamziyuzhao wants to merge 8 commits into
iamziyuzhao wants to merge 8 commits into
Conversation
iamziyuzhao
force-pushed
the
feat/moe-r3
branch
from
September 7, 2026 22:36
5927a8c to
d46edba
Compare
Rollout Routing Replay (R3, arXiv:2510.11370) needs the expert ids the rollout used for every token. With gconfig.return_routed_experts the SGLang backend asks for them (sglang>=0.5.13 native capture) and decodes the reply's base64 int32 blob into a flat array; the engine accumulates the chunks across interrupt/resume iterations (routed_experts_start_len resumes at the last forwarded position) and reshapes them to [seq_len - 1, num_hidden_layers * top_k]. numpy raises if the payload does not cover every forwarded position. SGLangConfig.build_args refuses launches that would capture nothing: an implicit or non-capturing moe_runner_backend (sglang's "auto" resolves to a backend that bypasses select_experts on sm_100) and a disabled chunked prefill (the capturer buffer is sized from it). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY6dyNDbj9pEbDtfyecegF
The RLVR workflow attaches the recorded expert ids to each sequence as routed_experts [1, seq_len, num_hidden_layers * top_k]. The final position is never forwarded during rollout, so its row repeats the last recorded one; it carries no loss. return_routed_experts is added to GenerationHyperparameters and excluded from the OpenAI argument set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY6dyNDbj9pEbDtfyecegF
concat_padded_tensors, micro-batch splitting and packed-sequence padding generalise from [batch, seq] to [batch, seq, ...] so per-token tensors with trailing dims travel with the batch unchanged. The mask is int32 end to end, which NCCL carries natively. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY6dyNDbj9pEbDtfyecegF
actor.megatron.moe_router_replay patches TopKRouter.forward so the top-k choice is the recorded one and probs = softmax(logits.masked_fill(~mask, -inf)) in fp32, which equals the post-softmax top-k renormalisation and keeps the router differentiable. Records are installed per micro-batch and per model chunk right before that chunk's forward; every layer keeps a forward and a backward cursor because activation recompute re-forwards a layer during its backward and 1F1B interleaves micro-batches. The pass asserts every record was consumed. Router configurations the masked softmax does not reproduce (pre-softmax scoring, sigmoid, capacity or group-limited routing, expert bias, load balancing) are rejected at init; context parallelism and sequence parallelism are not supported yet and raise. The reference policy inherits the actor's setting. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY6dyNDbj9pEbDtfyecegF
transformers 5 moved rope_theta into rope_parameters; mbridge 0.1.0 still reads hf_config.rope_theta when it builds the GPTModel. Backfill the attribute on the bridge's config right after AutoBridge.from_pretrained. Also pass the computed worker count to the HF save thread pool instead of the raw argument. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY6dyNDbj9pEbDtfyecegF
… the served version A Qwen3-30B-A3B full sync is ~57 GiB and takes 76-130 s, so the 60/90/180 s waits sized for 8B dense models fire spuriously. One WEIGHT_SYNC_TIMEOUT_SEC (300 s) now backs the buffer-ready ack, wait_delta_ready and the RaaS health-monitor grace. The sender always serves its newest buffer, so RaaS labels the engine with the version actually served: queued notify_version requests then hit the existing "already loaded" check and coalesce instead of pulling each version in turn. A sender-agent startup failure surfaces with its own traceback instead of a TypeError. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY6dyNDbj9pEbDtfyecegF
dataflow.buffer.max_buffered_samples pauses prompt submission while that many fresh samples are already buffered, sized by the running accepted-samples- per-prompt ratio. Without it rollout runs open loop: the fresh heap fills, put() evicts the oldest sample and the trainer receives data that is many versions old. Evictions are exported as buffer/evicted, the eval block resumes the flow on every exit path, and the buffer no longer sleeps while holding its lock after a wake. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY6dyNDbj9pEbDtfyecegF
Trainer TP1/PP1/DP4/EP4 on four GPUs beside an SGLang DP2 rollout server, GRPO (clip 0.2/0.28, lr 1e-6, 256 samples per step, no KL), closed loop at 512 buffered samples. MATH-500 avg@4 86.95 -> 92.35 over 800 steps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY6dyNDbj9pEbDtfyecegF
iamziyuzhao
force-pushed
the
feat/moe-r3
branch
from
September 9, 2026 19:46
d46edba to
4950f29
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
AstraFlow could not post-train Mixture-of-Experts models: the trainer's routers re-decide expert assignment, so gradients are computed against different experts than the rollout used, and MoE RL destabilises. This PR adds Rollout Routing Replay (R3, arXiv:2510.11370) end to end, plus the loop-control and weight-sync changes a validated Qwen3-30B-A3B run turned out to require. It is deliberately limited to what runs when you train a MoE model with the recipe below, and written as the minimum change to the existing code: one contract per hop instead of guard code, and a loud failure at the first place a contract violation reaches.
raas/) —gconfig.return_routed_expertsasks sglang (>= 0.5.13) for per-token top-k expert ids; the backend decodes the base64 blob into a flat int32 array, the engine accumulates the chunks across interrupt/resume iterations and reshapes them to[seq_len - 1, num_hidden_layers * top_k](numpy raises if the payload does not cover every forwarded position).SGLangConfig.build_argsrefuses the two launches that capture nothing: an implicit or bypassingmoe_runner_backend(sglang'sautopicks a non-capturing kernel on Blackwell) and a disabled chunked prefill.core/workflow/,dataflow/utils.py,train_worker/utils/data.py) — the mask rides the trajectory as int32[1, S, num_hidden_layers * top_k], aligned toinput_ids(the never-forwarded final position repeats the last row); padding, micro-batch splitting and packing generalise over trailing dims.train_worker/utils/mcore/routing_replay.py, Megatron engine) —actor.megatron.moe_router_replaypatchesTopKRouter.forwardtosoftmax(logits.masked_fill(~mask, -inf))in fp32, which equals top-k renormalisation and keeps router gradients. Records are installed per micro-batch and per model chunk right before that chunk's forward and read through a forward and a backward cursor (activation recompute, 1F1B); every pass asserts all records were consumed. Router configurations the masked softmax does not reproduce are rejected at init; context/sequence parallelism raiseNotImplementedError. The reference policy inherits the actor's setting.hf_config.rope_theta, which transformers 5 moved intorope_parameters; the engine backfills it right afterAutoBridge.from_pretrained(two lines).dataflow.buffer.max_buffered_samplespauses prompt submission while enough samples are already buffered (one_submit_budgetmethod). Without it the fresh heap fills,put()evicts the oldest, andedfhands the trainer the oldest survivor: two open-loop 30B runs trained on data ~22 versions old and eroded/collapsed once staleness passed ~17.buffer/evictedis exported; the eval block resumes the flow on every exit path.WEIGHT_SYNC_TIMEOUT_SEC = 300replaces waits tuned for 8B models (a 57 GiB sync is 76 s on the loopback, 110-130 s next to a checkpoint write). RaaS labels the engine with the version the sender actually served, so queuednotify_versionrequests hit the existing "already loaded" check and coalesce instead of pulling every version in turn. A sender-agent startup failure raises with its own traceback instead of aTypeError.examples/math/qwen3-30b-a3b-m2po/: one 8xB200 node, trainer TP1/PP1/DP4/EP4 on 4 GPUs + SGLang DP2 on 2, GRPO (clip 0.2/0.28, lr 1e-6, 256 samples/step, no KL).Validation
Qwen3-30B-A3B, 800 steps (previous revision of this branch; MATH prompts, 4x B200 trainer + 2 rollout GPUs, ~14 h wall clock including one GPU-contention crash and an HF-checkpoint resume at step 299):
+5.40 over baseline (pass@4 97.0), still rising at the final eval. Sample staleness held at 2-5 versions throughout; zero buffer evictions; zero staleness drops.
This revision, Qwen3-30B-A3B, 40 steps on the same node and recipe (2026-09-09, 4x B200 trainer + 1 rollout GPU): MATH-500 avg@4 87.45 at step 0 and 86.55 at step 25, against 86.95 and 86.70 for the previous revision's run at the same steps (eval noise is about +-0.7). Importance-weight abs delta 0.0074-0.0102 (previous: ~0.009), sample staleness 2.0-4.2 avg / <= 5 max (previous: 2.8-4.9 / <= 5), zero evictions and zero stale drops, entropy 0.23-0.34, grad norm 0.0035-0.0088, clip ratio 0.04-0.06 %, replay armed on all 48 layers in every rank, weight-update coalescing fired 7 times, no tracebacks in any log, clean shutdown.
This revision, 800 steps, same node and recipe: in progress (started 2026-09-09; saves every 100 steps, evals every 25). The full curve will be added here next to the previous revision's when it finishes.
This revision vs the previous one, same GPU smoke (tiny 4-layer Qwen3-MoE, sglang rollout with capture -> dataflow closed loop (cap 16) -> Megatron actor + ref with replay -> TCP weight sync, 8 steps, same seed): both runs report replay armed on 4/4 layers, importance weights within [0.992, 1.008] and approx-KL max ~0.0078 on every step, entropy 11.88, zero evictions, staleness 1-2, one queued weight update coalesced. The same ids reach the router: the flat int32 layout is a reshape of the previous int16
[rows, L, K], checked bit-for-bit on CPU.Radix cache: a GPU probe (sglang 0.5.13.post1) showed routing rows and tokens bit-identical between radix cache on and off, on full and partial prefix hits, and after a weight reload (which flushed the cache). The earlier refusal of the radix cache was wrong and is gone; the recipe still leaves it disabled.
Replay equivalence (previous revision, same math): record-vs-replay max logit delta 0.0078 at Qwen3-30B-A3B shapes, bitwise 0.0 at 4L/top-8 and 48L/top-2.
Unit tests: CPU tests live on
iamziyuzhao/astraflow@feat/moe-r3-v2-extras, stacked on this branch; not part of this PR.Using it
yaml/experiment_b200_1node.yaml+yaml/raas_b200_1node.yaml; the README lists the prerequisites and what to watch during bring-up.Scope
Rewritten on 2026-09-08 from +3004/-93 (35 files) to +904/-38 (30 files) with the same behaviour for the recipe: the rollout side no longer needs the model config (the trainer views the flat mask with its own layer count), the replay context lost its debug stage machine and memory-release bookkeeping, validation of internally produced tensors and duplicated guards were removed in favour of the contract above, and the mbridge compatibility module collapsed to two lines. The previous revision is preserved as
feat/moe-r3-full/feat/moe-r3(d46edba) on the fork.