Repository navigation
Batch QMoE CPU QNBit expert GEMMs into grouped MLAS dispatches - #32668
Merged
Merged
Conversation
The QNBit path issues one MlasQNBitGemmBatch call per active expert, so a layer costs 2 * num_active_experts thread-pool barriers. MLAS already flattens a batch into a single parallel domain over (expert x N-block), so the active experts are now bucketed by token count and each bucket runs as one batched dispatch. Decode puts every active expert in one bucket. Only applies where the expert loop is already serialized (fewer active experts than threads); with at least as many active experts as threads, one expert per thread stays faster and is unchanged. Measured on a 96-core x64 host, LFM2.5-8B-A1B int4 (22 QMoE layers, 32 experts, top-k 4): decode 36.5 -> 30.3 ms/token (27.4 -> 33.0 tok/s) and prompt processing 249 -> 142 ms, with identical generated tokens. A single QMoE layer at those dimensions goes from 0.749 to 0.438 ms at 32 threads. Tests cover both dispatch paths against the same reference by capping the intra-op pool so the per-expert path is taken for the same shapes.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Grouped execution can allocate hundreds of MiB of additional scratch memory for long prompts, risking OOM.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Batches active CPU QMoE QNBit expert GEMMs to reduce thread-pool barrier overhead.
Changes:
- Groups experts by token count for batched FC1/FC2 MLAS dispatches.
- Adds per-session thread controls and dispatch-path tests.
- Reuses fp32 bias data while converting fp16 bias once.
File summaries
| File | Description |
|---|---|
moe_quantization_cpu.cc |
Implements grouped QNBit expert execution. |
moe_test.cc |
Adds thread-count controls and path coverage. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Single-thread behavior contradicts the stated dispatch policy, and multi-bucket coverage is hardware-dependent.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
onnxruntime/contrib_ops/cpu/moe/moe_quantization_cpu.cc:1665
- This predicate also selects grouped execution when the session has one intra-op thread and multiple active experts, because
num_expert_threadsis then 1 even thoughnum_active_experts >= max_expert_threads. That contradicts the PR's stated invariant that configurations with at least as many active experts as threads are unchanged, and it adds route-wide staging and changes accumulation order for single-thread sessions. Either gate this onnum_active_experts < max_expert_threadsor document the broader behavior as intentional.
onnxruntime/contrib_ops/cpu/moe/moe_quantization_cpu.cc:1737 - The new multi-bucket path is not exercised deterministically. The existing prefill case uses the default pool, so a host with 2–4 intra-op threads takes the per-expert path, while the newly pinned prefill test explicitly forces that same path with two threads. Add a prefill case with an intra-op count greater than
num_expertsso bucket construction and reuse across distinct token counts are always covered.
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Xavier Dupré (xadupre)
approved these changes
Sep 17, 2026
This was referenced Oct 9, 2026
This was referenced Oct 12, 2026
Franklin Moormann (ooples)
pushed a commit
to ooples/AiDotNet
that referenced
this pull request
Oct 13, 2026
Updated [Microsoft.ML.OnnxRuntime](https://github.com/Microsoft/onnxruntime) from 1.30.0 to 1.31.0. <details> <summary>Release notes</summary> _Sourced from [Microsoft.ML.OnnxRuntime's releases](https://github.com/Microsoft/onnxruntime/releases)._ ## 1.31.0 ONNX Runtime 1.31.0 stabilizes model-package and EPContext data APIs, improves CPU model loading and quantized MoE inference, expands device-based execution-provider selection, and strengthens model-loading and runtime reliability. These notes cover changes since ONNX Runtime 1.30.0. CUDA and WebGPU kernel updates are summarized here; detailed provider notes are covered by their separate plugin EP releases. ## Highlights - Promoted the model-package API and EPContext data callbacks to stable C and C++ APIs, with EPContext callback support added across language bindings ([#33166](microsoft/onnxruntime#33166), [#32265](microsoft/onnxruntime#32265)). - Reduced CPU session-initialization overhead by parallelizing eligible weight prepacking, and accelerated block-wise INT4/INT8 QMoE experts with MLAS QNBit kernels and grouped expert dispatch ([#31691](microsoft/onnxruntime#31691), [#32644](microsoft/onnxruntime#32644), [#32668](microsoft/onnxruntime#32668)). - Added x86 FP16 LayerNorm/RMSNorm acceleration, Arm KleidiAI SVE2.1 FP16 GEMM support, and additional RISC-V vector kernels ([#32715](microsoft/onnxruntime#32715), [#32670](microsoft/onnxruntime#32670), [#32710](microsoft/onnxruntime#32710)). - Enabled CoreML participation in device-based EP selection, including Apple Neural Engine selection through `PREFER_NPU` ([#31975](microsoft/onnxruntime#31975)). - Added workspace-memory accounting and verification for constrained-memory graph partitioning, alongside broader model, graph, and tensor validation ([#31962](microsoft/onnxruntime#31962), [#32189](microsoft/onnxruntime#32189)). ## Announcements & Compatibility - [CUDA Plugin EP v0.3.0](https://github.com/microsoft/onnxruntime/releases/tag/plugin-ep-cuda/v0.3.0) and WebGPU Plugin EP v0.5.0 are matched with ONNX Runtime 1.31.0. For **contrib-op compatibility**, core and the plugin must use the same contributed-operator schemas; building both from the **same ONNX Runtime revision** is recommended. Schema mismatches can cause incorrect execution or crashes and may not be detected during plugin registration. - **`GatherBlockQuantized` behavior change:** out-of-range indices now produce zero output slices across CPU, CUDA, and WebGPU for both integer and floating-point quantized formats. This deliberately differs from ONNX `Gather` error semantics ([#32480](microsoft/onnxruntime#32480)). - **Experimental API migration:** model-package consumers should use `OrtApi::GetModelPackageApi` and the stable `OrtModelPackageApi` table. EPContext callback registration now uses stable APIs; the corresponding experimental lookup entries were removed. The stable native callback contract carries callback/state pointers, with payload limits enforced by application callbacks, bindings, or EPs rather than a native read-options policy ([#33166](microsoft/onnxruntime#33166), [#32265](microsoft/onnxruntime#32265)). - **ACL EP is deprecated** and will be removed in a future release. Source builds using `--use_acl` now receive a deprecation warning ([#32564](microsoft/onnxruntime#32564)). - The ONNX dependency remains at **1.22.0**. The initial ONNX 1.23 integration was reverted pending upstream fixes; this release does not ship that integration's new opset or FLOAT6 support ([#33073](microsoft/onnxruntime#33073)). - Supported native builds now default to **1DS telemetry**. Windows TraceLogging remains selectable with `--use_windows_telemetry` or `onnxruntime_USE_WINDOWS_TELEMETRY=ON`; telemetry can be disabled with `--no_telemetry` ([#32384](microsoft/onnxruntime#32384)). ## New Features ### Core APIs & Runtime - Stabilized the model-package C and C++ APIs for loading packages, selecting compatible model variants, and creating sessions ([#33166](microsoft/onnxruntime#33166)). - Stabilized named EPContext data read/write callbacks for application-managed storage. Added EP support negotiation so registered callbacks do not silently fall back to filesystem I/O ([#32265](microsoft/onnxruntime#32265)). - Extended the Compile API to write external initializers and EP context data to buffers, enabling in-memory compilation workflows for models larger than 2 GB ([#31347](microsoft/onnxruntime#31347)). - Added workspace-memory accounting, source reporting, and post-partition reservation verification. Set `session.strict_workspace_verification=1` to fail initialization on workspace declaration overruns or nonzero orphaned reservations; strict verification is not supported for ORT-format model loads ([#31962](microsoft/onnxruntime#31962), [#32189](microsoft/onnxruntime#32189)). - Added the opt-in source-build option `onnxruntime_DISABLE_DEVICE_DISCOVERY=ON` to skip platform accelerator probing and report only the CPU device ([#32592](microsoft/onnxruntime#32592)). ### Shared Contrib Operators These updates affect core operator schemas and cross-provider behavior, not only the separately released GPU plugins. - Extended `EngramGate` and `NGramHashMapping` for Qwen4-Exp, and added `VarlenNGramHashMapping` for packed DeepSeek Engram batching with request-local history. Packed hashing also supports Qwen4-Exp head offsets, EOS resets, and segment boundaries ([#32285](microsoft/onnxruntime#32285), [#32358](microsoft/onnxruntime#32358), [#32467](microsoft/onnxruntime#32467)). - Added the `activation` attribute to `GatedRMSNorm` for SiLU, its Swish alias, and Sigmoid gating across CPU, CUDA, and WebGPU. SiLU remains the default ([#32512](microsoft/onnxruntime#32512)). - Added weightless hyper-connection operators `BranchwiseRMSNorm`, `ScaledSiLU`, `HyperConnectionPreMix`, and `HyperConnectionPostMix`, with CPU, CUDA, and WebGPU implementations ([#32687](microsoft/onnxruntime#32687)). - Extended `GatherBlockQuantized` to FP8/FP4 data, full-axis blocks, and scale broadcasting, and improved CPU UINT8 dequantization. See the out-of-range-index behavior change above ([#32480](microsoft/onnxruntime#32480), [#32621](microsoft/onnxruntime#32621)). ### Plugin Execution Provider Infrastructure - Added support for statically linked plugin EPs, with a static WebGPU plugin build configuration ([#32395](microsoft/onnxruntime#32395)). - Preserved caller streams in single-tensor plugin data transfers and handled null plugin allocator returns ([#32666](microsoft/onnxruntime#32666), [#32683](microsoft/onnxruntime#32683)). - Deduplicated AutoEP custom-op domains and corrected EP assignment reporting after NHWC layout transformation ([#32711](microsoft/onnxruntime#32711), [#33184](microsoft/onnxruntime#33184)). - Added reference guidance for compiled-model compatibility validation, including multi-device scenarios ([#29168](microsoft/onnxruntime#29168)). ## Execution Provider Updates ### CUDA EP CUDA updates include sparse attention and packed continuous batching, expanded paged speculative decoding, INT2 and mixed-width QMoE execution, opt-in MatMul auto-tuning, improved MXFP4 support on Blackwell consumer GPUs, and reliability fixes. ... (truncated) Commits viewable in [compare view](microsoft/onnxruntime@v1.30.0...v1.31.0). </details> Updated [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) from 10.2.3 to 10.3.0. <details> <summary>Release notes</summary> _Sourced from [Swashbuckle.AspNetCore's releases](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases)._ ## 10.3.0 ## What's Changed * Change nullability handling for schemas with `anyOf`, `allOf`, `oneOf` or `type = null` by @ldeluigi in domaindrivendev/Swashbuckle.AspNetCore#3976 * Handle leading slash on route prefix during index.html GET by @superstator in domaindrivendev/Swashbuckle.AspNetCore#4084 * Move NRT handling to DataContractResolver by @desjoerd in domaindrivendev/Swashbuckle.AspNetCore#3998 * Clarify nullable behaviour by @martincostello in domaindrivendev/Swashbuckle.AspNetCore#4094 * Fix review issues for caching and encoding by @martincostello in domaindrivendev/Swashbuckle.AspNetCore#4093 * Support multiple response types per status code by @DeagleGross in domaindrivendev/Swashbuckle.AspNetCore#4100 * Add coverage for `AddServer` across multiple documents by @RaphaelFakhri in domaindrivendev/Swashbuckle.AspNetCore#4115 * Fix Swagger sort key fallback for empty endpoint tags metadata by @AleksaMCode in domaindrivendev/Swashbuckle.AspNetCore#4125 * Fix parameter matching to consider location when names collide by @nonsoikem in domaindrivendev/Swashbuckle.AspNetCore#4126 * Emit form encoding for properties of a complex form parameter by @RaphaelFakhri in domaindrivendev/Swashbuckle.AspNetCore#4128 * Keep a property's own summary when comments come from several XML files by @RaphaelFakhri in domaindrivendev/Swashbuckle.AspNetCore#4129 * Strip query string from relative document URLs in Swagger UI by @GabeSilvaDev in domaindrivendev/Swashbuckle.AspNetCore#4152 * Document that System.Text.Json ignores `[EnumMember]` by @RaphaelFakhri in domaindrivendev/Swashbuckle.AspNetCore#4127 * Drop the regex route constraint from the default Swagger route by @RaphaelFakhri in domaindrivendev/Swashbuckle.AspNetCore#4130 * Serialize custom values in the UI `ConfigObject.AdditionalItems` by @RaphaelFakhri in domaindrivendev/Swashbuckle.AspNetCore#4132 * Use Minimal APIs JSON options when MVC is not registered by @RaphaelFakhri in domaindrivendev/Swashbuckle.AspNetCore#4116 * Resolve enum contracts against the underlying type under source generation by @RaphaelFakhri in domaindrivendev/Swashbuckle.AspNetCore#4131 ## New Contributors * @ldeluigi made their first contribution in domaindrivendev/Swashbuckle.AspNetCore#3976 * @superstator made their first contribution in domaindrivendev/Swashbuckle.AspNetCore#4084 * @desjoerd made their first contribution in domaindrivendev/Swashbuckle.AspNetCore#3998 * @RaphaelFakhri made their first contribution in domaindrivendev/Swashbuckle.AspNetCore#4115 * @AleksaMCode made their first contribution in domaindrivendev/Swashbuckle.AspNetCore#4125 * @nonsoikem made their first contribution in domaindrivendev/Swashbuckle.AspNetCore#4126 * @GabeSilvaDev made their first contribution in domaindrivendev/Swashbuckle.AspNetCore#4152 * @JulianThoms made their first contribution in domaindrivendev/Swashbuckle.AspNetCore#4182 **Full Changelog**: domaindrivendev/Swashbuckle.AspNetCore@v10.2.3...v10.3.0 Commits viewable in [compare view](domaindrivendev/Swashbuckle.AspNetCore@v10.2.3...v10.3.0). </details> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This was referenced Oct 13, 2026
Open
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.
Description
Follow-up to #32644. That PR put block-wise int4/int8 QMoE CPU experts on the MLAS QNBit (MatMulNBits) kernels, but still issues one
MlasQNBitGemmBatchcall per active expert, so a layer costs2 * num_active_expertsthread-pool barriers.MLAS already flattens a batch into a single parallel domain over (expert × N-block), so this batches the active experts instead: they are bucketed by token count and each bucket runs as one batched dispatch. Decode (one token per active expert) collapses to a single bucket, turning
2 * top_kdispatches per layer into 2.This only applies where the expert loop is already serialized (fewer active experts than threads). With at least as many active experts as threads, one expert per thread remains faster and that path is unchanged.
Key changes
MlasQNBitGemmBatchper bucket for FC1 and FC2, with SwiGLU applied across all gathered rows in between.Motivation
CPU decode of MoE models is dominated by the per-expert GEMMs. The per-expert dispatch pattern is barrier-bound rather than bandwidth-bound: it sits near 30 GB/s regardless of expert count and gets worse above 32 threads, while the batched form scales to 68-126 GB/s.
Measured on a 96-core x64 host.
End-to-end on LFM2.5-8B-A1B int4 (22 QMoE layers, hidden 2048, moe_intermediate 1792, 32 experts, top-k 4) through onnxruntime-genai
model_benchmark, 630 tokens per mode:A single QMoE layer at those dimensions: 0.749 -> 0.438 ms at 32 threads.
The gain scales with
threads / active_experts, since it is barrier cost that is being removed. At top-k 4 decode: 1.20x at 8 threads, 1.38x at 16, 1.78x at 32, 2.11x at 64. With few threads relative to active experts the expert-parallel path is selected instead and nothing changes.Testing
MoETest.*passes (24 tests).*_PerExpertDispatchand*_SingleThreadcases cap the intra-op pool so the same shapes and reference exercise the per-expert path and a single-threaded pool, checking the two dispatch paths against each other.PerExpertDispatchcases really do not.Note on reproducibility
Bucketing sorts experts by token count, which changes the cross-expert accumulation order. Decode output is bit-identical; with multiple tokens per expert results can differ at float rounding level (observed in the 8th significant digit at 16 tokens).