Execution plan to offload export on CPU for a MoE - #32261
Conversation
There was a problem hiding this comment.
Pull request overview
Documents a staged plan for evaluating and implementing CPU expert offloading for CUDA-hosted MoE models.
Changes:
- Defines profiling, simulation, cache, fallback, and evaluation phases.
- Adds discussion and status indexes.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
docs/next_steps/discuss/2026/08-moe-cpu-offload.md |
Adds the implementation proposal. |
docs/next_steps/discuss/index.md |
Lists the proposal. |
docs/next_steps/started/index.md |
Adds the started-status index. |
docs/next_steps/completed/index.md |
Adds the completed-status index. |
Suppressed comments (4)
docs/next_steps/discuss/2026/08-moe-cpu-offload.md:166
- A sequence-length decrease does not identify a new request in onnxruntime-genai. A normal decoder request changes from a multi-token prefill to
sequence_length == 1decoding, so this rule falsely marks the first generated token as a new sequence; subsequent decode shapes can remain constant. Use the explicit request ID for boundaries and record position/cache-length metadata separately if generation progress is needed.
Input dimensions are required even when `request_id` is present. During autoregressive generation the sequence-length dimension normally increases; a decrease relative to the previous iteration marks the transition to the next sequence. The analysis script validates this boundary against `request_id` and reports inconsistent traces.
docs/next_steps/discuss/2026/08-moe-cpu-offload.md:328
- This repeats the invalid boundary heuristic: onnxruntime-genai normally decreases
input_idsfrom the prompt length to one token within the same request and then keeps that shape during decoding. Detect boundaries from request IDs instead of tensor-shape decreases.
- Detect sequence boundaries from decreases in the sequence-length input dimension and validate them against request identifiers.
docs/next_steps/discuss/2026/08-moe-cpu-offload.md:389
- Executing every non-resident Qwen 3.6 expert on CPU is not currently possible for the documented NVFP4 model, because CPU
QMoEdoes not implement NVFP4 block/global scale semantics. Make CPU NVFP4 support an explicit prerequisite for this integration, or scope PR 6 to a Qwen export whose weight format the CPU kernel supports.
- Execute all non-resident experts on CPU for the current MoE invocation, then schedule cache updates after that invocation completes.
docs/next_steps/discuss/2026/08-moe-cpu-offload.md:237
- The hybrid estimate omits costs incurred on every CPU miss. Because the node is CUDA-owned, a miss must move routing/activation data to CPU, synchronize before CPU execution, copy the expert result back to CUDA, and merge it with CUDA-hit outputs. Applying standalone CPU execution cost alone systematically understates miss latency and can select the wrong policy.
A checked-in script combines the CPU and CUDA measurements to estimate hybrid execution. For each expert decision, it applies the measured CPU cost to a simulated miss, the measured CUDA cost to a hit, and the measured host-to-device cost to an admission. It then compares the resulting estimated iteration time with the CPU-only and CUDA-only baselines.
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: xadupre <22452781+xadupre@users.noreply.github.com>
…xadupre/moeplan
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove instrumentation propagation through generation and control-flow subgraphs, and keep CUDA plugin builds compatible without exposing unsupported instrumentation APIs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the synthetic per-session iteration counter and derive generation progress from model input shapes instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explain CUDA stream ordering, deferred buffer ownership, end-of-run synchronization, and pinned-memory bounds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Log compact MoE and QMoE routing decisions through the session logger without requiring general session profiling. Update CPU and CUDA coverage and document the independent logging contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a standalone ONNX Runtime GenAI prompt runner, routing distribution analysis, preliminary result plots, and reproducible commands to the expert offloading plan. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve the QMoE instrumentation conflict while preserving the new DeepGEMM dispatch priority.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply the repository Ruff formatter so the Python format check accepts the new scripts.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove trailing whitespace flagged by repository checks while preserving the intended line break.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The analysis calculations, profiler serialization, and CUDA plugin behavior contain correctness gaps.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
tools/python/qmoe_expert_distribution.py:334
- The complement is off by one expert. At threshold 0 no experts are shortlisted, so the CPU-side complement is
num_experts * bytes_per_rank; at thresholdnum_experts - 1, one expert still remains, not zero. The current value undercounts every complement row and distorts the normalized comparison.
- Files reviewed: 30/32 changed files
- Comments generated: 7
- Review effort level: Balanced
Android routes platform logs to logcat, so stderr capture cannot observe MoE routing events there.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Validate every expert ID before updating distribution counters and cover negative and upper-bound violations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the expert-ID validation finding in |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Restricted MoE routing logging to batch size 1 in
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the remaining trace-integrity and analysis-coverage findings in
The expanded analyzer suite now covers completeness/order, schema failures, invalid IDs, external-data validation, rank ties, final-row extraction, ranked CSV semantics, and threshold totals. |
|
Thanks for the update. The previous review's main issues are substantially addressed: the analyzer test is registered in CI, the trace has ordered prompt/completion markers and a fail-closed footer, invalid expert IDs and router-weight cardinality are rejected, and routing collection is explicitly limited to the documented sequential batch-1 protocol. One blocking analyzer validation gap remains in Two smaller follow-ups would also improve confidence:
The external-data fail-open behavior is the only item above that I consider blocking. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The analyzer has correctness and scalability gaps, and some supported configurations can silently produce no routing data.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 39/41 changed files
- Comments generated: 7
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
The prompt loader rejects valid single-record JSONL files, and the analyzer’s eager repeated parsing is not viable for the documented workload size.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
tools/python/qmoe_expert_distribution.py:113
- This parser materializes every routing event in memory, and
main()retains the first full list in_at line 595 while later passes parse the entire log again. The documented 1,000-prompt workload extrapolates the reported 59,880 records/10 prompts to about 6 million JSON dictionaries, so keeping overlapping copies can consume multiple gigabytes and make the analyzer fail before producing results. Validate/aggregate in streaming passes (at most the ranking pass plus one output pass) instead of returning a full event list.
tools/python/qmoe_prompt_runner.py:64 - A one-record JSON Lines file is rejected here. A single JSONL string or
{ "prompt": ... }object is also valid standalone JSON, sojson.loads(text)succeeds with a scalar/dict and the JSONL fallback never runs; line 66 then rejects it despite the documented JSONL format. Treat a non-list successful parse as one JSONL record (and add a single-record regression case).
- Files reviewed: 39/41 changed files
- Comments generated: 1
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The python script is meant to be used from time to time to analyze. It is meant to evolve over time. This will be addressed if the script is used on a regular basis. |
There was a problem hiding this comment.
🔵 Needs a closer look
The analyzer’s repeated full-trace materialization is unsuitable for the planned scale, and identical runner output paths corrupt both artifacts.
Review details
Suppressed comments (2)
tools/python/qmoe_expert_distribution.py:648
- This initial validation retains every decoded event in
_for the rest ofmain, and each later helper reparses and materializes the complete trace again. The planned 1,000-prompt run scales the 59,880-record/10-prompt example to millions of JSON dictionaries, so peak memory can become several GB and parsing is repeated four times. Use a validation-only streaming pass, or parse once and pass/reuse the events without retaining duplicate lists.
tools/python/qmoe_prompt_runner.py:166 - Reject identical output paths before opening the routing log. If
--outputand--routing-logresolve to the same file, the routing capture is first written and then overwritten bywrite_text; the appended completion footer subsequently makes the results file invalid JSON while reporting zero routing records.
- Files reviewed: 40/42 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
Thanks for the update. The new CUDA MoE and QMoE batch > 1 rejection tests address the requested coverage gap, and the expanded The blocking external-data validation issue remains unchanged in Please fail explicitly when an external initializer has no non-empty Non-blocking question: the analyzer now matches runtime records to raw ONNX nodes using |
Stream routing analysis without retaining decoded events and reject overlapping runner output paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the remaining blocking and follow-up items in
For the NodeIndex question: The merge also includes the current WebGPU |
Use the head size instantiated by Flash Attention quick builds so the INT4 metadata case retains a reachable backend. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Big models with expert may be too big for CUDA, the PR proposes an implementation plan to move experts on CPU. The difficulty is to choose which one we should offload.