feat(moe): R3 rollout routing replay — slot-indexed expert-routing pool (scaffold)#737
Draft
qywu wants to merge 2 commits into
Draft
feat(moe): R3 rollout routing replay — slot-indexed expert-routing pool (scaffold)#737qywu wants to merge 2 commits into
qywu wants to merge 2 commits into
Conversation
…ol (scaffold) R3 (arXiv:2510.11370) stabilizes MoE RL by having the training forward replay the exact expert selection the rollout made, instead of letting the inference and training routers pick experts independently (numerical drift flips top-k choices and inflates train/inference KL). Integrates with slime/veRL. Core design: persist per-token, per-MoE-layer top-k expert ids in a pool indexed by the KV slot, alongside the KV cache. Unlike a transient per-forward capturer (vllm-ascend), slot-indexed storage makes routing follow the KV: on a prefix-cache hit the shared prefix tokens are dropped from the forward, but their routing is recovered from the reused slots with zero recompute and guaranteed consistency with the reused KV. Lands the tested engine core: * RoutedExpertsPool — [size+1, num_moe_layers, top_k] int32 buffer, slot-indexed on dim 0 (row 0 reserved, mirroring the KV pools); store_layer() scatters by slot like set_kv_buffer, gather() is the prefix-hit retrieval. * RoutedExpertsCapturer — per-forward begin/capture/commit controller; no-op when inactive; skips (and counts) TP/EP-misaligned rows rather than corrupt the pool. * Process-global accessor (mirrors moe/distribution_recorder.py). * GenerateReqInput.return_routed_experts opt-in flag (propagated via __getitem__). 18 CPU unit tests cover store/gather, reserved slot, sizing, the prefix-hit retrieval property, capture/commit, inactive no-op, TP-misalignment skip, and flag propagation. Model-forward + serving wiring (allocate pool via ServerArgs, capture hook at topk.py, out_cache_loc into the MoE block, TP/EP alignment, BYPASSED fused routing, prefix-hit retrieval + return path across the gRPC/gateway boundary) are documented as follow-ups in docs/guides/routing-replay-r3.md — they need a running model. Draft for design review. Signed-off-by: Qingyang Wu <willqywu@gmail.com>
Wires the R3 capture path end-to-end behind --enable-routing-replay (off by default; every hook is a no-op when unset, so default behavior is byte-identical): * server_args: --enable-routing-replay flag. * event_loop: after the KV pool is created, build_routed_experts_capturer() sizes a RoutedExpertsPool to max_total_num_tokens (layers/top_k from the model config; None for dense models) and installs the global capturer. * model_runner.forward: begin_forward(out_cache_loc) before model.forward and commit() after (finally), keying capture to this forward's exact KV slots. * topk.select_experts: capture_in_order(topk_ids) next to the existing on_select_experts hook. Layer index is assigned by per-forward MoE invocation order (layers fire in order), avoiding edits to every model file. Only the standard (non-BYPASSED) path materializes topk_ids in Python, so fused-routing models are naturally skipped. capture_in_order skips under torch.cuda.is_current_stream_capturing() so it can never corrupt a CUDA graph — only eager forwards are captured this increment. Adds capturer layer-counter + builder tests (25 CPU tests total). Verified all edited modules import cleanly (no circular imports). Remaining follow-ups — CUDA-graph decode capture, TP/EP row alignment (safe-skipped today), and the prefix-hit retrieval + return path — are documented in docs/guides/routing-replay-r3.md. Not yet GPU-validated end to end. Signed-off-by: Qingyang Wu <willqywu@gmail.com>
Collaborator
Author
|
Update: forward wiring landed (commit
Now 25 CPU tests; all edited modules verified to import cleanly (no circular imports). Not yet GPU-validated end to end (this venv can't run a model). Remaining, documented in the guide: CUDA-graph decode capture, TP/EP row alignment (safe-skipped today; TP=1 works), and the prefix-hit retrieval + return path across the gRPC/gateway boundary. |
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 & why
R3 (arXiv:2510.11370) stabilizes RL training of MoE models: the training forward replays the exact expert selection the rollout made, instead of letting the inference and training routers pick experts independently. Independent routers + tiny numerical drift flip a token's top-k choice → train/inference KL blows up. R3 records per-token, per-layer top-k expert ids at rollout and replays them in training (KL cut ~50%; integrates with slime/veRL).
Design — store routing in the KV memory pool
The distinctive choice vs. vllm-ascend's transient
RoutedExpertsCapturer: persist routing in a pool indexed by the KV slot, alongside KV.On a prefix-cache hit the shared prefix tokens are dropped from the forward entirely — their MoE layers never run (
out_cache_loccovers only new tokens). A transient capturer therefore returns nothing for the prefix; you'd have to force a re-forward. A slot-indexed pool makes routing follow the KV: a prefix hit that reuses slots also reuses the routing captured when those slots were first written — zero recompute, guaranteed consistent with the reused KV. Cost ≈num_moe_layers * top_k * 4B/token (~1-5% of KV).This builds on infra the repo already has (
moe/distribution_recorder.pyalready captures per-token per-layertopk_idsfor offline analysis + tracks the current layer); R3 adds slot-indexed persistence and a return path.What this PR lands (tested engine core)
runtime/cache/routed_experts_pool.py:RoutedExpertsPool—[size+1, num_moe_layers, top_k]int32, slot-indexed on dim 0 (row 0 reserved, mirrors the KV pools).store_layer()scatters by slot likeset_kv_buffer;gather()is the prefix-hit retrieval.RoutedExpertsCapturer— per-forwardbegin_forward/capture/commit; no-op when inactive; skips + counts TP/EP-misaligned rows rather than corrupt the pool.GenerateReqInput.return_routed_expertsopt-in flag (propagated via__getitem__).18 CPU unit tests: store/gather, reserved slot, sizing, the prefix-hit retrieval property, capture/commit, inactive no-op, TP-misalignment skip, detach-on-capture, flag propagation.
Remaining wiring (needs a running model) — documented follow-ups
In
docs/guides/routing-replay-r3.md: allocate the pool via aServerArgsflag; capture hook attopk.py::select_experts(beside the existingon_select_experts); threadout_cache_locinto the MoE block; TP/EP all-gather alignment;BYPASSEDfused-routing capture; prefix-hit retrieval + return path across the gRPC/gateway boundary (proto + servicer + Rust gateway, same boundary as thereturn_token_idsfollow-up).Docs
Adds
docs/guides/routing-replay-r3.md+ sidebar entry.🤖 Draft opened for design review.