Skip to content

Add packed INT2 and mixed-width CUDA QMoE decode - #32761

Merged
David Fan (jiafatom) merged 9 commits into
mainfrom
jiafa/int2-qmoe-cuda-packed-decode
Sep 28, 2026
Merged

David Fan (jiafatom) merged 9 commits into
mainfrom
jiafa/int2-qmoe-cuda-packed-decode

Conversation

@jiafatom

@jiafatom David Fan (jiafatom) commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Description

Add a decode-only packed CUDA path for INT2 and mixed-width integer QMoE together with a bounded dense dequantization fallback for configurations that are not eligible for packed decode.

The packed path supports FC1/FC2 weight-width combinations (2,2), (2,4), and (4,2) for FP16 and BF16:

  • Extend the existing symmetric MoE GEMV to cutlass::uint2b_t and use FP32 accumulation for INT2.
  • Prepack FC1 and FC2 independently into the SM80 interleaved layout using each layer's actual weight width.
  • Dispatch FC1 and FC2 independently to INT2 or INT4 GEMV.
  • Keep raw weight initializers alive so the same session can use the dense fallback when a later invocation is not eligible for packed decode.

Packed-path eligibility

Packed decode is selected only when both FC GEMV shapes pass is_moe_gemv_supported and all of the following hold:

  • SM80 or newer.
  • Symmetric blockwise quantization with no zero points.
  • Fused interleaved SwiGLU.
  • Block size 64 or 128.
  • Prepacked FC1 and FC2 buffers are available.
  • Expanded routing rows satisfy num_rows * k <= 8.
  • Routing is supported by the fused expert-map builder: at most 256 rows and experts, with k in {1,2,4,6,8}.

CUDA completion status

From the CUDA functionality perspective, the supported QMoE contract is complete: configurations accepted by the operator either use the packed decode path or the dense dequantization fallback. This PR does not complete the fully optimized CUDA kernel matrix. Prefill and packed-ineligible decode configurations still rely on the fallback, so they remain functionally correct but are not yet in their final optimized form.

Why packed decode is faster

The fallback performs the following work on every invocation:

  1. Dequantize all experts for both FC1 and FC2 from INT2/INT4 into FP16/BF16 scratch.
  2. Write those dense weights to global memory.
  3. Read the dense weights again in the generic CUTLASS grouped MoE GEMM runner.
  4. Execute routing finalization after FC2.

This fixed work is particularly expensive during decode: a single token selects only k experts, but fallback dequantization still expands both matrices for every expert. For W weight elements, it writes about 2W bytes of dense FP16/BF16 weights before GEMM begins, in addition to reading the quantized weights and scales.

The packed path instead prepacks each layer once at session initialization into the SM80 interleaved layout. At inference time it builds the routed-row/expert maps and runs FC1 and FC2 directly on packed weights. One CUDA block owns one routed row and an output-column tile; as it traverses K, it loads an activation tile, loads an INT2/INT4 weight tile and its block scale, dequantizes that tile in registers, and immediately accumulates the dot product. No full dense weight matrix is materialized.

This reduces weight traffic relative to FP16 by approximately 8x for INT2 or 4x for INT4, before the comparatively small scale overhead, and only accesses experts selected by routing. FC1 also fuses bias and interleaved SwiGLU into its epilogue and reads source token activations through the routing map, avoiding a full gate/up intermediate and explicit top-k activation replication. FC2 currently writes routed outputs and uses the existing finalize kernel for the routing-weighted top-k reduction.

The GEMV organization is intentional for decode and uses a conservative hard shape gate rather than a runtime GEMV/GEMM crossover: expanded_num_rows = num_rows * k must be at most 8. This permits at most 8, 4, 2, 1, or 1 input rows for k = 1, 2, 4, 6, or 8, respectively. The limit applies to the total top-k-expanded rows for the invocation, not an average per-expert M. Within this envelope, a general grouped-GEMM M tile is underfilled; the packed kernel uses CtaM=1 and exposes parallelism over output columns and K reduction instead. It also skips dense-runner tactic profiling and workspace allocation. For prefill, larger per-expert M allows grouped GEMM to reuse weight tiles across many rows and use Tensor Cores efficiently, so a future packed grouped-GEMM path is preferable to repeatedly reading weights through row-wise GEMV.

Why the dense fallback remains

This PR optimizes a decode subset while providing broader correctness coverage through the bounded dense dequantization fallback. Accepted INT2 and mixed-width configurations that miss any packed-path condition continue through the bounded fallback, including prefill or unsupported GEMV shapes, asymmetric zero points, other supported block sizes, unsupported activation/fusion or routing configurations, older GPUs, and sessions where packed decode is disabled or unavailable.

The fallback independently dequantizes FC1 and FC2 to FP16/BF16 scratch and invokes the existing dense A16 CUTLASS MoE runner. Its scratch allocation remains protected by ep.cuda.qmoe_int_dequant_max_scratch_bytes. This is slower and uses more temporary memory than packed GEMV, but preserves correctness across the broader supported contract.

A future packed grouped-GEMM path can remove fallback usage for prefill and larger decode workloads. The fallback should remain until native packed kernels cover the complete supported configuration matrix or the remaining configurations are explicitly declared unsupported.

Testing

  • Built onnxruntime_provider_test with CUDA 12.8.
  • Passed all 18 related MoETest.QMoETest_MixedWidth* and MoETest.QMoETest_Int2* tests on NVIDIA A100 SM80.
  • Packed-path tests cover FP16/BF16, (2,2), (2,4), (4,2), and block sizes 64/128.
  • Packed tests use a one-byte dense scratch limit to prove dense dequantization is bypassed.
  • clang-format --dry-run --Werror and git diff --check passed.

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

Copilot review overview

🟑 Changes recommended

Unsupported routing configurations can enter the packed path with uninitialized maps, and block-size-128 coverage is missing.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity Β· 1 Medium severity

Open (2)
What changed in this PR

Adds a CUDA packed INT2 QMoE decode path while retaining dense fallback behavior.

Changes:

  • Adds INT2 weight prepacking and FP32-accumulating GEMV kernels.
  • Routes eligible FP16/BF16 SwiGLU decode workloads through packed GEMV.
  • Adds packed-path and fallback CUDA tests.
File Description
onnxruntime/​test/​contrib_ops/​moe_test.cc Adds CUDA packed-decode tests.
onnxruntime/​contrib_ops/​cuda/​moe/​moe_quantization.h Extends QMoE packed-weight state.
onnxruntime/​contrib_ops/​cuda/​moe/​moe_quantization.cc Implements prepacking, dispatch, and packed execution.
onnxruntime/​contrib_ops/​cuda/​llm/​moe_gemm/​moe_gemv.h Documents INT2 GEMV support.
onnxruntime/​contrib_ops/​cuda/​llm/​moe_gemm/​moe_gemv.cu Adds INT2 kernel traits and dispatch.

πŸ’‘ Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@jiafatom David Fan (jiafatom) changed the title Add packed INT2 CUDA QMoE decode Add packed INT2 and mixed-width CUDA QMoE decode Sep 23, 2026
@jiafatom
David Fan (jiafatom) force-pushed the jiafa/int2-qmoe-cuda-packed-decode branch from 8361933 to 5481c42 Compare September 23, 2026 21:49
@jiafatom
David Fan (jiafatom) force-pushed the jiafa/int2-qmoe-cuda-correctness branch from 38ec92f to a9a18c8 Compare September 23, 2026 22:01
@jiafatom
David Fan (jiafatom) force-pushed the jiafa/int2-qmoe-cuda-packed-decode branch from 5481c42 to 52a7d2c Compare September 23, 2026 22:04
@jiafatom
David Fan (jiafatom) force-pushed the jiafa/int2-qmoe-cuda-correctness branch from a9a18c8 to 5dcede4 Compare September 24, 2026 01:21
@jiafatom
David Fan (jiafatom) force-pushed the jiafa/int2-qmoe-cuda-packed-decode branch 2 times, most recently from cefc72b to 1fd163a Compare September 24, 2026 05:16
David Fan (jiafatom) added a commit that referenced this pull request Sep 24, 2026
### Description
Add a bounded CUDA correctness fallback for blockwise INT2 and
mixed-width integer QMoE weights.

The fallback independently dequantizes FC1 and FC2 expert weights to
FP16/BF16 scratch and runs the existing dense A16 CUTLASS MoE path. It
supports FC1/FC2 combinations `(2,4)`, `(4,2)`, and `(2,2)`, including
canonical uniform INT2 via `expert_weight_bits=2` without FC-specific
overrides.

### Fallback input contract
To keep this correctness path bounded and match the CUDA blockwise
dequantizer contract, it requires:

- Raw weights (`weights_prepacked=0`) in canonical `[E,N,K/pack]`
layout; legacy transposed layouts are rejected.
- Three-dimensional blockwise scales in `[E,N,K/block_size]` layout;
row-wise scales are rejected.
- A power-of-two `block_size` in `[16,256]`.
- Both FC reduction dimensions divisible by `block_size`.
- Dequantized FC1+FC2 scratch within
`ep.cuda.qmoe_int_dequant_max_scratch_bytes`.

Input-layout, dimension, and scratch-limit validation runs immediately
after `CheckInputs`, before tactic profiling, workspace allocation, or
stream synchronization.

### Why this is a correctness fallback
This PR establishes CUDA behavior and numerical correctness without
introducing the final performance kernel. Weight
unpacking/dequantization and MoE execution both run on CUDA, but the
implementation materializes complete FP16/BF16 expert weights in
temporary scratch before invoking the existing dense A16 CUTLASS runner.
It therefore uses more memory bandwidth and scratch space than a native
packed INT2 path.

Follow-up #32761 pre-packs constant initializers and directly consumes
packed INT2/INT4 weights for eligible decode, avoiding full dense
materialization on that path. This bounded fallback remains for dynamic
inputs and configurations outside the packed path; packed prefill and
larger decode workloads remain follow-up work. CPU mixed-QMoE support
and cross-provider parity are separate work.

### Testing
- Built `onnxruntime_providers_cuda`.
- Built `onnxruntime_provider_test`.
- Passed all 14 `MoETest.QMoETest_MixedWidth*` tests on NVIDIA A100
(SM80).
- Added regressions for row-wise scales, legacy transposed weights,
invalid block sizes, FC1/FC2 K divisibility, early scratch rejection,
and canonical non-square execution.
- Passed `clang-format --dry-run --Werror`.
- Passed `git diff --check`.
Base automatically changed from jiafa/int2-qmoe-cuda-correctness to main September 24, 2026 07:16

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

Copilot review overview

🟑 Changes recommended

Packed decode retains unnecessary dense profiling and fallback-only prepacking overhead, while nontrivial routing remains untested.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
Resolved since last review (2)

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

Copilot review overview

🟑 Changes recommended

Prepacking can read beyond buffers for valid fallback-only INT2 initializer shapes.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)

@titaiwangms Ti-Tai Wang (titaiwangms) 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.

Reviewed the INT2/mixed-width packed QMoE path (five independent static reviews, then rechecked against head 72687ce). The K=32 prepack/fallback issue previously reported in this PR has been addressed by the new static eligibility guard; I am not re-reporting it as an open finding.

Major: validate INT2/mixed-width scale dtypes before the packed path. QMoE T2 permits Float8 scales for other quantization modes, while the INT2/mixed-width common validation at moe_quantization.cc:825-856 checks scale presence and shape but not dtype. PrePack intentionally skips the scale inputs for INT2/mixed-width (:2379-2381). For an otherwise packed-eligible FP16 INT2 model (e.g. E=1, hidden=inter=512, block=64, fused SwiGLU, k=1), Float8E4M3FN [1,1024,8] FC1 scales can therefore reach prepare_scale_zp (:1584-1590): it allocates the one-byte-per-element SizeInBytes() buffer but selects the float32 transpose for every type other than FP16/BF16 (:1394-1409), accessing four bytes per logical element on both input and output. The packed GEMVs also treat the resulting scales as the activation dtype (:1814-1826). The FP16/BF16 checks in the dense dequantization branch occur too late to protect this path. Please reject non-matching FC1/FC2 scale dtypes in the common INT2/mixed-width validation before scale preparation; make the transpose default reject unsupported types rather than assume float32. This is a static memory-bounds finding, not a CUDA-sanitizer result.

Test coverage (non-blocking): the new multirow/multiexpert packed-vs-dense test exercises (2,2), but (2,4)/(4,2) packed tests are still single-row/single-expert, and the new scale tensors are all 1.0 (moe_test.cc:2090-2091,2317-2318). A mixed-width multirow/multiexpert case with distinct per-block scales would check transpose, expert strides, and routing together.

No CUDA build, sanitizer, performance measurement, or real Olive-produced checkpoint was part of this static review.

@jiafatom

Copy link
Copy Markdown
Contributor Author

Ti-Tai Wang (@titaiwangms) Addressed the scale-dtype bounds finding in 8a80e2c. The common INT2/mixed-width validation now requires FC1/FC2 scales to match the FP16/BF16 activation dtype before scale preparation, and the generic transpose fallback explicitly accepts only float32 instead of treating every unknown dtype as float32. I also added a regression case with schema-valid Float8E4M3FN FC1/FC2 scales that expects the new validation error. The targeted onnxruntime_provider_test target builds and lintrunner passes in jiafa-dev. Runtime execution remains blocked because CUDA initialization in that container currently returns cudaErrorNoDevice. I kept the mixed-width multirow/distinct-scale coverage suggestion out of this safety fix since it was marked non-blocking.

@jiafatom

Copy link
Copy Markdown
Contributor Author

Validation update: restarting jiafa-dev restored CUDA visibility (8x A100, SM80). The new QMoETest_MixedWidthCudaRejectsUnsafeFallbackInputs Float8 scale rejection case now runs and passes. The packed/fallback regression set also passes 4/4: QMoETest_Int2CudaPackedDecode, QMoETest_Int2CudaPackedDecodeBlock128, QMoETest_Int2CudaPackedDecodeMultiRowExpertParity, and QMoETest_Int2CudaPackedDecodeFallback.

@tianleiwu Tianlei Wu (tianleiwu) 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.

Reviewed all five changed files at 8a80e2c. The earlier routing, preprocessing, profiling, and scale-dtype fixes are present. One additional performance concern remains in packed decode scale preparation; see the inline comment. No additional correctness defect was established. Validation was static review plus byte-count checks and git diff --check; no local CUDA operator tests or benchmark were run.

@tianleiwu Tianlei Wu (tianleiwu) 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.

Reviewed all five changed files at the current head and rechecked the prior scale-cache thread, which is resolved by e0be11a. One blocking CUDA Plugin EP compatibility issue remains in the new cached-scale path; see the inline comment. The current-head Linux CUDA Plugin EP run reproduces it in all newly added packed INT2 tests. git diff --check origin/main...HEAD passes.

@tianleiwu Tianlei Wu (tianleiwu) 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.

Re-reviewed all five changed files at the latest head. The earlier routing, preprocessing eligibility, scale-cache, and CUDA Plugin EP dtype issues are resolved, and their threads are already closed. The packed path now has coherent shape/routing gates and preserves the dense fallback inputs; the latest standard CUDA and Linux/Windows CUDA Plugin EP checks pass. No remaining actionable findings.

David Fan (jiafatom) added a commit to microsoft/onnxruntime-genai that referenced this pull request Sep 28, 2026
## Summary
- add structured `quant_config.moe.fc1_type` / `fc2_type` overrides for
mixed-width QMoE export
- extend the existing blockwise quantizer with symmetric raw INT2 QMoE
packing and emit projection-specific runtime attributes
- decode GPT-OSS MXFP4 experts and requantize them for integer
mixed-width export
- preserve existing INT4, INT8, and native MXFP4 paths; no new
projection-specific legacy extra options are introduced

INT2 and mixed-width QMoE currently require the CUDA EP, block size 64
or 128, symmetric weights, and no zero points. A no-op INT4/INT4
projection override remains on the existing INT4 path and keeps block
size 32 valid.

Depends on microsoft/onnxruntime#32761 for CUDA mixed-width QMoE runtime
support.

## Validation
- `172 passed, 3 skipped` across quant config, QMoE weights, and GPT-OSS
loader tests
- added coverage for no-op INT4 overrides and unsigned projection
descriptor rejection
- modified Python files compile successfully
- `git diff --check upstream/main...HEAD` passes

The skipped tests require standalone QMoE/CUDA pack pybind exports that
are not available in the local wheel.
@jiafatom
David Fan (jiafatom) merged commit a11b4e5 into main Sep 28, 2026
95 checks passed
@jiafatom
David Fan (jiafatom) deleted the jiafa/int2-qmoe-cuda-packed-decode branch September 28, 2026 01:26
Ti-Tai Wang (titaiwangms) added a commit to onnxruntime/mobius that referenced this pull request Sep 28, 2026
## Status update (September 28, 2026)

- microsoft/onnxruntime#32743 merged as `ee5f6e7` on September 24, and
microsoft/onnxruntime#32761 merged as `a11b4e5` on September 28. The
latter optimizes packed decode for `(2,2)`, `(2,4)`, and `(4,2)`;
`(4,8)`/`(8,4)` still use the bounded dense correctness fallback. These
merges do **not** automatically update Mobius CI's installed ORT wheel.
- The last successful Mobius GPU fast-integration run (September 26)
installed `onnxruntime-gpu==1.30.0.dev20260812001` from the CUDA 12
feed. The opt-in QMoE CUDA tests in this PR are not part of the normal
CI selections, so the CI checks do not qualify the merged ORT runtime
for mixed-width QMoE. The CPU ORT GenAI E2E lane is separately pinned to
ORT 1.29.0.
- The separate experimental manual RTN recipe in
microsoft/olive-recipes#637 exported the full pinned Qwen3-30B-A3B
checkpoint into an approximately 18 GB ONNX package with 24 `(4,8)` and
24 `(4,4)` QMoE layers. A bounded CUDA `model-qa.py` run returned `391`
for `17 * 23` with `ORT_QMOE_INT_DEQUANT_MAX_SCRATCH_BYTES=2147483648`,
and a 57-question MMLU smoke run completed. This used a locally built
**earlier** ORT commit `50b8fcb`, not the newly merged `a11b4e5`; it
does not establish full-logit parity, production throughput,
benchmark-scale quality, or validation of the final merged runtime.

## Summary

- follow the projection-specific QMoE bit-width contract added by
microsoft/onnxruntime#32697
- ingest Olive fused expert checkpoints with FC1/FC2 INT2/INT4,
INT4/INT8, INT8/INT4, or uniform INT8 weights under a model-wide INT4
fallback
- allocate and validate FC1/FC2 packed weights, scales, and zero points
independently, including adjacent layers with different expert widths
- preserve legacy uniform INT4 QMoE emission and prevent mixed geometry
from being incorrectly rewrite-fused
- support authoritative Qwen3, Qwen3.5, and Qwen3.5-VL checkpoint
prefixes
- preserve pinned GGUF route fingerprints when the new QMoE source
aliases are empty

## Contract

Supported FC1/FC2 expert widths are `(4,4)`, `(2,4)`, `(4,8)`, `(8,4)`,
and `(8,8)` with a model-wide INT4 fallback. Differing-width nodes emit
projection-specific bit attributes, including FC3=FC1 for fused SwiGLU.
Uniform INT8 nodes emit `expert_weight_bits=8` with no
projection-specific overrides. A model-wide INT8 fallback is not
enabled.

Checkpoint preprocessing fails closed for missing, partial, stale,
malformed, or unsupported expert sidecars. Plain and regex overrides
follow Olive producer semantics.

## Validation

- `python -m pytest src/mobius/models/moe_test.py
src/mobius/components/_moe_test.py src/mobius/models/qwen35_test.py
src/mobius/integrations/gguf/_builder_core_test.py
src/mobius/integrations/gguf/_moe_cohort_test.py -q --tb=short` β€” 399
passed
- `python -m pytest src/mobius/rewrite_rules/_qmoe_fusion_test.py
src/mobius/_weight_utils_test.py -q --tb=short` β€” 172 passed
- Opt-in `MOBIUS_QMOE_CUDA_TEST=1 python -m pytest
tests/integration/qmoe_cuda_test.py -q`: **2 passed** on A100 with
isolated ORT CUDA build at merged commit `ee5f6e7`. This exports a small
Qwen3-MoE model using Olive-style fused sidecars, runs adjacent
(4,8)/(8,4) and uniform (8,8) layers, checks full FP16 logits against an
independent dequantized PyTorch reference (`rtol=atol=0.01`), and
confirms each QMoE executes on CUDA via profiling. The full graph
assigns shape/control nodes to CPU; it does not claim an all-CUDA graph.
- Opt-in `MOBIUS_QMOE_REAL_SLICE_CUDA_TEST=1 python -m pytest
tests/integration/qmoe_cuda_test.py -k real_qwen3 -q`: **2 passed** on
A100 using layer-0 experts 0–3 and sliced embedding/router weights from
pinned Qwen3-30B-A3B. Olive RTN tensor producer created mixed
(4,8)/(8,4) sidecars; both reloaded exports ran QMoE on CUDA and matched
independently unpacked full logits at `rtol=atol=0.01` (maximum absolute
errors 0.005148/0.004774). Reduced four-expert top-2 routing and
synthetic surrounding decoder are intentional; no full-checkpoint parity
is claimed.
- touched-file Ruff lint and formatting, `python -m mypy
src/mobius/_weight_utils.py`, and `git diff --check` passed

## Runtime boundary

microsoft/onnxruntime#32743 provides a bounded CUDA correctness fallback
for `(4,8)` and `(8,4)` despite its earlier summary listing only 2/4-bit
cases. It dequantizes expert weights into FP16/BF16 scratch and requires
canonical raw weights, 3-D scales, block size 16–256, divisible
reduction dimensions, and scratch within its configured cap. Uniform
(8,8) uses the native packed INT8 CUDA path: the tested tiny model
needed block size divisible by 32 and both FC reduction dimensions
divisible by 64. The synthetic test uses small weights with zero
attention to isolate routed experts. The separate real-slice test
validates selected checkpoint experts via Olive RTN tensor production,
not complete Olive pass orchestration or the original 128-expert top-8
model. Full-checkpoint parity, prefill/decode generation parity,
throughput, and peak VRAM have **not** been validated. For
Qwen3-30B-A3B, FP16 expert scratch alone is approximately 1.125 GiB per
MoE layer, above the fallback default 1 GiB: configure
`ep.cuda.qmoe_int_dequant_max_scratch_bytes` (or the runtime's
corresponding environment override) and measure before claiming
deployability at production size. CPU mixed-width execution is not
qualified. microsoft/onnxruntime#32761 adds a packed decode subset for
2/4-bit combinations, not the `(4,8)` INT8 path.

Addresses #735.

---------

Signed-off-by: titaiwang <titaiwang@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mirounga pushed a commit that referenced this pull request Sep 29, 2026
…untime/contrib_ops/cuda/moe/moe_quantization.cc. (#32932)

### Description
<!-- Describe your changes. -->

Replace old use of `routing_record` with `routing_snapshot_` in
onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc.

### Motivation and Context
<!-- - Why is this change required? What problem does it solve?
- If it fixes an open issue, please link to the issue here. -->

Fix build error about undeclared identifier `routing_record`. Likely an
unlucky merge artifact from two recent PRs #32761 and #32738.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
David Fan (jiafatom) added a commit that referenced this pull request Oct 1, 2026
### Description
Implements Workstream 4, PR 4a of the delivery plan in #32657: an
independently testable kernel foundation, not the QMoE prefill
integration planned for PR 4b.

- Add packed symmetric W2A16 grouped GEMM for SM80, FP16/BF16
input/output, block size 64 and FP32 accumulation, without full
global-memory A16 expert-weight materialization or caller-supplied GEMM
workspace.
- Support explicit CTA 32x128x64 / warp 32x32x64 and CTA 64x128x64 /
warp 32x64x64, both with three stages. Keep 32 rows as the default; no
automatic tactic selection.
- Add scalar-reference parity tests, production W4/W8 regressions,
GPT-OSS-20B-shaped balanced/skewed cases, and opt-in CUDA-event
benchmarks against W4 and dense FP16 baselines.
- Register tests and implementation dependencies for legacy and plugin
CUDA builds. Dense runner template instantiation also requires the
existing five FP16 SM80 fused launchers.
- Fix stale routing_record API usage in packed INT decode, exposed by
the clean legacy build, in a separate commit.

No QMoE prefill dispatch, bias/activation epilogue, SwiGLU/finalization
integration, or additional architecture support is enabled. Mixed
FC1/FC2 execution, lifetimes, runtime dispatch and model-level
qualification remain PR 4b work. Validation notes are kept in this
description rather than a repository Markdown artifact.

### Motivation and Context
Provide the native packed INT2 grouped-GEMM building block needed to
replace full expert-weight dequantization in mixed INT2/INT4 QMoE
prefill. Related design: #32657; packed decode foundation: #32761.

### Kernel Contract
For E experts, P routed rows, N output columns and K reduction elements:

| Buffer | Layout |
| --- | --- |
| activations | FP16 or BF16 [P,K], contiguous rows grouped by expert |
| packed_weights | E consecutive SM80 fpA_intB prepacked INT2 matrices,
K*N/4 bytes each |
| block_scales | Same dtype as activations [E,K/64,N] |
| expert_row_ends | E device int64 cumulative exclusive row ends |
| output | Same dtype as activations [P,N], same grouped row order |

N and K must be positive multiples of 64; partial M/N tiles and empty
experts are supported. P must be positive and fit in int32, and E must
be positive. IsInt2GroupedGemmSupported checks metadata;
RunInt2GroupedGemm also requires non-null buffers and checks 16-byte
alignment of activations, packed_weights, block_scales, and output
before invoking CUTLASS. The caller supplies the actual SM/SM count,
keeps buffers alive until stream completion, ensures matching capacities
and nonoverlapping output, and guarantees nonnegative, nondecreasing row
ends terminating at P. Repeated ends represent empty experts; device
offset contents are not validated on the host.

Raw unsigned codes represent (code - 2) * scale. Convert raw [E,N,K/4]
weights using unpack_uint2_transposed_to_int8_direct_cuda and
preprocess_weights_for_mixed_gemm_cuda with SM80/W2_A16. Preprocessing
mutates its scratch input. GEMM converts tiles internally and performs
no routing or finalization.

### Local Validation
Latest alignment fix (2edffb5): rebuilt and validated in jiafa-dev on
A100. Both legacy and plugin passed all 20 enabled grouped tests,
including FP16/BF16 rejection tests for each of the four buffers at an
8-byte offset and both tile configurations. Both configurations also
passed all 20 grouped tests under Compute Sanitizer memcheck with zero
errors. Host /tmp inode exhaustion was resolved before these runs.

A100-SXM4-80GB, CUDA 12.8.61, GCC 13.3, Release, SM80, internal tests
enabled, QUICK_BUILD=OFF; September 29-30, 2026. Separate build trees
started empty without old objects or compiler caches, then fixes were
rebuilt in those trees.

- Legacy (2edffb5): all 20 grouped tests passed; all 20 passed under
Compute Sanitizer memcheck with zero errors. Existing QMoE regression:
53 passed, 11 skipped.
- Plugin (2edffb5): all 20 grouped tests passed; all 20 passed under
Compute Sanitizer memcheck with zero errors. The combined
Int2GroupedGemm*:*QMoE* run had 75 passed and 12 skipped (20 grouped
tests plus 55 passing QMoE-related tests, including three directly
registered FP4-to-FP8 internal tests).
- Plugin production compilation used local
CMAKE_CUDA_FLAGS="--diag-suppress=970 --diag-suppress=2189" for
Abseil/Protobuf diagnostics. This is not evidence that default-toolchain
CI passes.
- Small cases compare every output against a scalar oracle, covering
FP16 and BF16, minimum alignment, empty experts, partial tiles, dense
parity, and the 32-row and 64-row candidates. Production W4/W8 block-64
paths are exercised directly.
- Model-shaped cases use E=32, top-k=4, K=2880, FC1 N=5760, FC2 N=2880,
T=128/512/2048 and P=512/2048/8192. FP16 covers balanced and skewed
routing at all three sizes; BF16 covers T=128. They check all outputs
for finiteness and sampled first/middle/last expert rows and columns on
both sides of 64-column boundaries against the oracle.
- Mixtral INT4 skips because its dimensions are too small; it is not
counted as CUDA INT4 coverage. Plugin routing-statistics testing also
skips because that EP does not support it. CPU-specific skips are not
counted as validation.
- Full onnxruntime_test_all suite was not run. Three opt-in benchmarks
are disabled by default. P is routed rows, not model input tokens T.

Run from the appropriate build directory with an available GPU:

```bash
# Legacy internal-test loader
GTEST_FILTER="Int2GroupedGemm*" ./onnxruntime_provider_test --gtest_filter=CUDA_EP_Unittest.All
./onnxruntime_provider_test --gtest_filter="*QMoE*"
GTEST_FILTER="Int2GroupedGemm*" compute-sanitizer --tool memcheck --error-exitcode 99 \
  ./onnxruntime_provider_test --gtest_filter=CUDA_EP_Unittest.All

# Plugin tests are directly registered
./onnxruntime_provider_test --gtest_filter="Int2GroupedGemm*:*QMoE*"
compute-sanitizer --tool memcheck --error-exitcode 99 \
  ./onnxruntime_provider_test --gtest_filter="Int2GroupedGemm*"

# Opt-in legacy benchmark; use DISABLED_GptOssKernelBenchmark for W4 comparison
GTEST_FILTER="Int2GroupedGemmTest.DISABLED_GptOssTacticBenchmark" \
GTEST_ALSO_RUN_DISABLED_TESTS=1 \
./onnxruntime_provider_test --gtest_filter=CUDA_EP_Unittest.All
```

### Initial Performance Evidence
Five warmups after an initial launch; three trials of 30 launches on a
nonblocking stream; median per-launch CUDA-event milliseconds. This
measures stream elapsed time around host-submitted launches, not
isolated profiler kernel duration. Allocation, packing, H2D/D2H
transfers and the reference computation are outside GEMM timing.

First run, balanced routing, E=32, top-k=4, K=2880, FC1 N=5760, FC2
N=2880, tile32:

| T | P | W2 FC1 | W4 FC1 | W4 FC2 |
| --- | --- | --- | --- | --- |
| 128 | 512 | 0.403695 | 0.391612 | 0.212548 |
| 512 | 2048 | 0.760764 | 0.766020 | 0.323072 |
| 2048 | 8192 | 2.476240 | 2.570210 | 1.253720 |

W4 uses a different quantized value distribution, not an equal-quality
model baseline. W2 and W4 are close; the T=2048 W2 trial range was
2.47624-2.98469 ms, so this does not establish a robust speedup.

Second run compares identical INT2 codes, activations, scales and
offsets. The dense path uses production DequantizeNBits and
MoeGemmRunner<half,half,half>. Cached dense dequantizes once before
timing; Dequant+dense dequantizes all experts before every GEMM.
Allocations are excluded. This is not an active-experts-only or
tiled-dequantization baseline.

| Routing | T | W2 tile32 | W2 tile64 | Cached dense tile32 | Cached
dense tile64 | Dequant+dense tile32 | Dequant+dense tile64 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Balanced | 128 | 0.404 | 0.602 | 0.651 | 0.663 | 1.538 | 1.548 |
| Balanced | 512 | 0.761 | 0.605 | 0.884 | 0.702 | 1.765 | 1.586 |
| Balanced | 2048 | 2.728 | 1.905 | 2.375 | 1.905 | 3.678 | 2.765 |
| Skewed | 128 | 0.233 | 0.228 | 0.217 | 0.189 | 1.097 | 1.071 |
| Skewed | 512 | 0.798 | 0.641 | 0.737 | 0.488 | 1.510 | 1.353 |
| Skewed | 2048 | 2.819 | 1.979 | 2.532 | 1.700 | 3.139 | 2.585 |

Balanced experts receive T/8 rows each. Skewed counts are
1,31,T-32,T,T,T, with the other experts empty; total 4T. Each benchmark
case passed sampled parity and finiteness checks. GPU 1 had no competing
compute clients at the first measurement start. Clocks were unlocked,
execution order fixed, and CPU plugin compilation ran concurrently
during the second run. Repeat controlled measurements before selecting
dispatch thresholds or asserting stable speedups.

Tile64 helps larger per-expert M but is slower at balanced T=128;
default remains tile32. Cached dense GEMM can be faster, especially with
skew. A tile128 W2 candidate was rejected by CUTLASS thread-map compile
assertions and is not exposed. These are not full prefill, routing,
activation, finalization or TTFT measurements.

### Prepack and Memory Accounting
One-pass prepack event times were 23.1-24.6 ms (W2 FC1), 24.1-24.3 ms
(W4 FC1) and 10.2-12.5 ms (W4 FC2), including transpose/preprocessing
launches and possible host submission gaps, not steady-state GEMM
overhead.

| Buffer | W2 FC1 bytes | W4 FC1 bytes | W4 FC2 bytes |
| --- | --- | --- | --- |
| Packed weights | 132710400 | 265420800 | 132710400 |
| A16 scales | 16588800 | 16588800 | 8294400 |
| Prepack scratch | 4147456 | 8294656 | 4147456 |

Tests also retain a raw GPU copy equal to packed-weight size,
activations, outputs and offsets. Dense FC1 adds 1,061,683,200 bytes
(1012.5 MiB). These are buffer capacities, not measured peak process
memory. W2 halves FC1 packed-weight bytes, not total model memory.

### Remaining Qualification
- Supported-platform CI, including the default plugin toolchain
configuration, remains separate from local validation.
- Controlled timing repeats are needed before automatic tactic selection
or performance gates.
- Profiler evidence and peak allocation measurements are needed before
end-to-end memory/prefill claims.
- Current commit 2edffb5 passed FP16/BF16 grouped regression,
alignment rejection coverage, and grouped memcheck in both legacy and
plugin builds. The full test suite and opt-in benchmarks were not rerun
for this update.
- PR 4b will integrate routing, mixed FC1/FC2, SwiGLU/finalization,
lifetimes and model-level evaluation.
Gopalakrishnan Nallasamy (GopalakrishnanN) pushed a commit that referenced this pull request Oct 1, 2026
…untime/contrib_ops/cuda/moe/moe_quantization.cc. (#32932)

### Description
<!-- Describe your changes. -->

Replace old use of `routing_record` with `routing_snapshot_` in
onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc.

### Motivation and Context
<!-- - Why is this change required? What problem does it solve?
- If it fixes an open issue, please link to the issue here. -->

Fix build error about undeclared identifier `routing_record`. Likely an
unlucky merge artifact from two recent PRs #32761 and #32738.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
David Fan (jiafatom) added a commit that referenced this pull request Oct 2, 2026
…#33005)

### Description
Implements the initial Workstream 4, PR 4b runtime integration from
#32657. PR #32963 (4a) and PR #33045 (4a2) are merged. This PR targets
main and is rebased onto `f0843448e7`, with only the PR4b commits
replayed; no conflicts occurred.

- Add default-enabled packed INT2 FC1 + INT4 FC2 QMoE prefill for SM80
and later, FP16/BF16 activations/output, symmetric quantization, block
size 64, and interleaved fused SwiGLU.
- Reuse existing routing and expert-map kernels, expand rows by expert,
run the 4a INT2 grouped FC1, apply expert bias and configurable SwiGLU,
run production INT4 grouped FC2, and finalize top-k weighted outputs
with FC2 bias.
- Allocate aligned per-invocation workspace; reuse expanded-input
storage for FC2 output after FC1 completes on the same stream. Avoid
full A16 expert-weight materialization on the selected packed path.
- Reuse packed weights and transposed scales, including runtime scale
inputs. Separate grouped-prefill prepack eligibility from
decode-specific minimum dimensions.
- Keep packed decode priority when eligible. Prefill is enabled by
default; no environment variable is required. Set
`ORT_ENABLE_QMOE_INT2_PREFILL=0` to opt out. Packed GEMV also defaults
on. Unsupported configurations retain existing fallback behavior and
scratch limits; debug logging and the one-byte scratch limit are
validation controls, not normal runtime requirements.

### Motivation and Context
Connect the independently tested grouped-GEMM foundation in #32963 to
the mixed-width QMoE execution path. Delivery plan: #32657. Packed
decode foundation: #32761.

### Current Scope
- SM80 and later, including SM86, SM89, SM90, SM120 and SM121; INT2 FC1
and INT4 FC2; block size 64; no zero points; `swiglu_fusion=1`. Both
production eligibility and grouped FC1 accept the actual device SM >=
80. FC1 retains the SM80 CUTLASS template and packed layout; non-TMA
INT4 FC2 uses the existing Ampere fallback on newer architectures. This
is not a new Hopper/Blackwell-native TMA implementation. The binary must
contain suitable native code or compatible virtual PTX for the target
GPU; an SM80 cubin alone is not cross-major compatible.
- Positive hidden/intermediate dimensions divisible by 64, bounded
int-sized dimensions/routed-row count, and at most 256 experts.
- Current packed path requires cached prepacked weight buffers, obtained
from weight initializers with `weights_prepacked=0`. Raw initializers
are retained for fallback.
- Both stages use the fixed 32-row grouped tactic. FC2 uses four stages,
supported by both QUICK_BUILD and full builds. No automatic
threshold/tactic tuning.
- Eligible packed prefill is row-tiled to keep estimated temporary
scratch within 256 MiB, including packed workspace, tile-local routing
metadata and uncached scale transposes. The configured
`ep.cuda.qmoe_row_tile_size` is an upper bound on tile rows. Full and
final partial tiles are sized explicitly; the candidate tile size is
halved until it fits. Tiles reuse packed weights and stream-ordered
workspace without dense weight dequantization. Long prompts and explicit
row tiling no longer select dense fallback solely because of their row
count. If even one row plus fixed scale scratch exceeds the budget,
execution reports a packed-scratch error instead of attempting larger
dense allocations. This is not a whole-process GPU memory cap.

### Latest Submission: SM80+ and Default Dispatch

- Corrected the earlier workspace-cap fallback after reviewer feedback:
eligible long prompts remain on the packed path and run tile-by-tile,
with input/output row offsets and tile-local routing maps. Dense
fallback remains for ineligible configurations. The workspace-limit test
now requires 8193-row FP16/BF16 inference to succeed with dense scratch
capped at one byte; explicit-tiling tests likewise expect success. New
multi-expert tiled tests cover top-k 1/2/4, channel-varying biases,
cached/runtime scales and partial final tiles. These tests were added
but not run locally, as requested; Docker formatting and diff checks
passed. The historical 4096-token benchmark below predates this tiling
implementation and is not validation of the fix.
- Revision `6c24651036` removes `ORT_ENABLE_QMOE_INT2_GEMV`. INT2 packed
GEMV now follows the existing `ORT_DISABLE_MOE_GEMV` switch: unset/0
permits eligible GEMV dispatch, and 1 disables it for both INT2 and
native INT4/INT8. Set it before starting the process. The INT2 prefill
switch remains independent. Test overrides were migrated and the
disabled-path test now includes a single-token case with a one-byte
dense scratch limit to detect bypass of the shared switch. No local
build or runtime tests were run; Docker clang-format and `git diff
--check` passed. Old INT2 GEMV environment settings below describe
historical benchmark revisions only and are no longer read by current
code.
- Revision `78294ec898` fixes all eight clang-format suggestions and the
BF16 mixed-width initialization failure reported by Linux/Windows plugin
CI. QUICK_BUILD now constructs the already-built BF16 dense fallback
runner for mixed-width/INT2 models; native BF16 INT4/INT8 runner
restrictions remain unchanged. Existing packed-prefill tests cover the
failed cases.
- No local build or runtime tests were run for this revision, as
requested. Only clang-format 20.1.8 checks in Docker `jiafa-dev` and
`git diff --check` were run and passed. New CI results are pending; no
plugin runtime pass is claimed for this fix.
- Grouped numerical tests pass the actual device SM and no longer skip
GPUs newer than SM86. Metadata coverage explicitly accepts
SM80/86/89/90/100/103/110/120/121 and rejects pre-SM80 devices. Prefill
operator tests now run on any SM80+ GPU, including BF16 in QUICK_BUILD;
forced packed tests retain the one-byte dense scratch guard. Metadata
checks are not hardware execution evidence.
- Added an unset-environment test covering FP16/BF16 at 1/33/257 rows
with dense fallback blocked. Explicit opt-out and
unsupported-configuration tests remain.
- No local build or runtime tests were run for the SM80+ extension, as
requested. Docker clang-format and diff checks passed.
SM86/89/90/120/121 execution of this extension remains unverified; CI
results must be tied to the actual runner GPU before claiming
architecture coverage. The following validation and benchmark records
are historical SM80 results, not evidence for this new revision or its
four-stage FC2/default-enable changes.
- Previous-head WebGPU CI (run 36932358189) remains unresolved: all
compile jobs passed; the Windows novcpkg test lost its device during an
Einsum test, causing subsequent failures and two result-summary jobs to
fail. The vcpkg test passed. No WebGPU code fix is included here, and a
transient-driver cause is not yet established. The new push will trigger
fresh CI; old-head jobs are not manually duplicated.

### BF16 QUICK_BUILD Guard: Rationale and Behavior Change

In `moe_quantization.cc`, changing the outer guard from `#if
!defined(ORT_QUICK_BUILD) && defined(ENABLE_BF16)` to `#if
defined(ENABLE_BF16)` does not enable every BF16 integer runner in quick
builds. The `#if !defined(ORT_QUICK_BUILD)` restriction is retained
inside that branch around the native BF16 INT4/INT8 runner construction.
Only the dense BF16 runner used when `use_int_dequant_fallback` is true
is made available in quick builds.

`use_int_dequant_fallback` applies to integer-quantized models with
mixed FC weight widths or `expert_weight_bits=2`. This branch constructs
`CutlassMoeFCRunner<__nv_bfloat16, __nv_bfloat16, __nv_bfloat16>`, whose
dense BF16 implementation is already instantiated under `ENABLE_BF16`,
including QUICK_BUILD. It does not require enabling an omitted native
BF16 integer specialization.

| Configuration | Before this guard change | After this guard change |
| --- | --- | --- |
| BF16 + QUICK_BUILD + INT2/mixed-width | No runner was constructed;
session initialization failed at `m_moe_runner != nullptr`, before
packed-kernel dispatch. | Construct the existing dense BF16 fallback
runner, removing that initialization failure. Runtime dispatch still
checks packed-path eligibility. |
| BF16 + QUICK_BUILD + ordinary uniform INT4/INT8 | Native BF16 integer
runner construction excluded. | Unchanged: still excluded by the inner
QUICK_BUILD guard. |
| BF16 + full build | Dense/native runners constructed according to the
existing branch conditions. | Runner construction unchanged. |
| FP16, or builds without ENABLE_BF16 | Existing FP16 behavior/BF16
exclusion. | Unchanged by this guard change. |

Constructing a dense fallback runner does **not** mean dense execution
is forced. The constructor requires a non-null runner even when
inference later takes packed INT2 GEMV or packed INT2/INT4 prefill.
Eligible calls use those packed paths; unsupported calls retain dense
fallback and its scratch limits. This is an intentional behavior change
for previously failing BF16 QUICK_BUILD INT2/mixed-width configurations,
not a change to the existing pure INT4 default dispatch. The table
describes the guard change specifically, separately from this PR's
default-enabled packed prefill feature. Build success alone does not
establish runtime correctness; no additional local build or runtime
tests were run for this explanation.

### Historical Validation
Local A100-SXM4-80GB / SM80, CUDA 12.8, GCC 13.3, Release,
QUICK_BUILD=OFF, in Docker container `jiafa-dev` on September 30, 2026.

- Legacy build succeeded. All 6 focused packed-prefill tests passed;
they also passed Compute Sanitizer memcheck with `ERROR SUMMARY: 0
errors`. After formatting the new implementation files, rebuilt and
reran all 6 focused tests successfully.
- Legacy QMoE regression: 59 passed, 11 skipped. CPU-specific and
unsupported small Mixtral cases are not counted as coverage.
- Forced packed tests disable INT2 GEMV and cap dense dequantization
scratch at 1 byte, preventing silent decode/dense fallback from
satisfying parity checks.
- FP16/BF16: 1, 33, and 257 rows. Independent scalar-reference
multi-expert tests cover top-k 1/2/4, balanced and skewed routing, empty
experts, unequal hidden/intermediate dimensions, nonuniform block/output
scales, expert-specific biases, and nondefault SwiGLU alpha/beta/limit.
- Runtime-scales tests exercise transposition and 64/128-dimensional
cases in both orientations. Negative cases verify fallback for zero
points, block size 128, reversed weight widths, non-SwiGLU activation,
runtime weights, and disabled prefill.
- `git diff --check` passed.
- Plugin rebuild and tests are still in progress; no plugin result is
claimed for this commit. The existing plugin configuration uses local
`CMAKE_CUDA_FLAGS="--diag-suppress=970 --diag-suppress=2189"`; it does
not establish default-toolchain CI success.

Run from the build output directory on an available SM80 GPU:
```bash
./onnxruntime_provider_test --gtest_filter="MoETest.QMoETest_MixedWidthCudaPackedPrefill*"
compute-sanitizer --tool memcheck --error-exitcode 99 \
  ./onnxruntime_provider_test --gtest_filter="MoETest.QMoETest_MixedWidthCudaPackedPrefill*"
./onnxruntime_provider_test --gtest_filter="*QMoE*"
```

### Full-Model Validation (October 1, 2026)

All model execution, dependency installation, benchmarking and NVML
sampling below ran inside Docker `jiafa-dev` on an A100-SXM4-80GB. Used
a clean non-plugin CUDA wheel, Release commit `463fa42cb5`, CUDA 12.8,
cuDNN 9.7, SM80, QUICK_BUILD=OFF. The installed wheel's build metadata
was checked; all six focused packed-prefill tests passed in the clean
build, and an installed-wheel CUDA MatMul smoke test passed.

- Full mixed-width GPT-OSS-20B export: 24 QMoE layers, INT2 FC1 / INT4
FC2, block size 64, top-k 4, fused SwiGLU, FP16 activations and no zero
points.
- GEMM-only validation (`ORT_ENABLE_QMOE_INT2_PREFILL=1`,
`ORT_ENABLE_QMOE_INT2_GEMV=0`): 19-token prefill followed by eight
single-token decode calls; generated `**Paris** is the capital of
France.` All logits/KV outputs were finite. ORT profiling recorded 216
QMoE CUDA events (24 layers x 9 runs).
- Combined-path validation (both flags set to 1): 309-token prefill
followed by two single-token decode calls, ending naturally at EOS;
generated `Paris.` Actual branch-log assertions confirmed 24
`packed_int_prefill` calls at 309 rows followed by 48 `packed_int_gemv`
calls at one row. All logits/KV outputs were finite; profiling recorded
72 QMoE CUDA events. This confirms same-session long-prompt GEMM prefill
-> GEMV decode, not repeated transitions or concurrency/capture safety.
- Both mixed validation runs kept
`ep.cuda.qmoe_int_dequant_max_scratch_bytes=1`, preventing the full
dense expert-weight dequantization fallback. This is not a one-byte cap
on all workspace or GPU allocations.
- GPU I/O binding retains KV on device. Even-numbered sliding-window
layers use 128-position cache capacity with shared past/present buffers;
odd-numbered layers retain full-history KV. The 309-token test exceeds
the sliding-window size.

### Model Artifact Size

Measured logical file sizes of `model.onnx` plus every unique referenced
external-weight file, excluding tokenizer/configuration files. Each
export references one `model.onnx.data` file. GiB = bytes / 2^30.

| Export | ONNX graph bytes | External weights bytes | Total bytes |
Total GiB |
| --- | ---: | ---: | ---: | ---: |
| Mixed INT2 FC1 / INT4 FC2 | 212,376 | 8,012,132,352 | 8,012,344,728 |
7.462 |
| INT4 FC1 / INT4 FC2 | 209,568 | 11,794,870,272 | 11,795,079,840 |
10.985 |

The mixed export is **3,782,735,112 bytes (3.523 GiB, 32.07%) smaller on
disk**. This measures the whole existing export, not just QMoE weights.
The two exports differ in QMoE block size as well as bit width, so this
is not a bit-width-only ablation. Smaller artifacts did not translate
into lower measured process GPU memory in these runs.

### Initial Direct ORT Performance Baseline

Same GPU 0, installed wheel, harness and tokenizer for both exports;
tokenizer files were byte-identical. Batch size 1, input lengths
128/512/2048/4096, 128 generated tokens, three warmups and ten measured
requests per configuration. Each configuration ran in a fresh process.
Each model completed 40 measured requests / 5,120 generated tokens; all
ten token sequences within each configuration matched. Throughput was
recomputed from the raw timing totals.

This is a synthetic direct-ORT benchmark, **not tau2-bench**. Prompts
repeat `Paris is the capital of France. Its museums and public parks
attract visitors.`, followed by `Summarize the context in detail.` and
GPT-OSS chat delimiters. Context tokens are repeated/truncated to obtain
exact total input lengths. Greedy autoregressive generation ignores EOS
to enforce the output length; this is not a quality evaluation.

| Input tokens | Model | TTFT P50 ms | TTFT P95 ms | Prefill tokens/s |
Decode tokens/s | Sampled process peak GiB |
| ---: | --- | ---: | ---: | ---: | ---: | ---: |
| 128 | Mixed INT2/INT4 | 21.31 | 21.39 | 6,418.11 | 196.44 | 15.02 |
| 128 | INT4 | 26.39 | 26.76 | 5,099.97 | 203.12 | 13.82 |
| 512 | Mixed INT2/INT4 | 43.62 | 43.75 | 12,141.54 | 191.55 | 16.00 |
| 512 | INT4 | 56.50 | 56.64 | 9,279.55 | 195.97 | 14.61 |
| 2048 | Mixed INT2/INT4 | 137.01 | 137.11 | 15,118.97 | 180.10 | 16.04
|
| 2048 | INT4 | 171.45 | 171.73 | 12,052.99 | 187.88 | 15.57 |
| 4096 | Mixed INT2/INT4 | 268.66 | 269.06 | 15,336.09 | 169.55 | 18.01
|
| 4096 | INT4 | 329.69 | 330.08 | 12,478.59 | 174.80 | 17.82 |

Measurement boundaries:

- **TTFT:** warm session, pretokenized input and initial GPU KV ready
until the first greedy token is available on CPU. Includes binding,
synchronized prefill, final-position logits D2H and CPU argmax. Excludes
model loading, tokenization, initial KV reset, queuing and networking;
not service-level TTFT.
- **Prefill TPS:** total input tokens / total synchronized ORT prefill
run time. Excludes binding setup and token selection. The original graph
still computes logits for every input position.
- **Decode TPS:** 1,270 tokens / total measured decode time across ten
requests (127 decode steps per request). Excludes the first token
produced by prefill; includes Python orchestration, binding, GPU
execution, last-position logits transfer and CPU sampling.
- **Memory:** NVML process-resident bytes including weights, KV,
workspace and retained ORT arena allocations. Requested sampling
interval was 5 ms; observed maximum gaps were about 11-19 ms. These are
sampled peaks and may miss brief allocations, not exact allocator
high-water marks. Loaded-session residency was about 15.0 GiB for mixed
versus 13.5-13.8 GiB for INT4. Device-wide peaks were
15.78/16.76/16.80/18.77 GiB for mixed and 14.57/15.37/16.33/18.57 GiB
for INT4, respectively.
- Profiling and QMoE branch logging were disabled in measured runs. No
full logits/KV CPU copies were performed; only the last-position logits
were transferred for sampling. Both opt-in flags and the one-byte
dense-dequantization limit were retained for both exports; enabled flags
do not prove path selection.

Findings and limits:

- Mixed prefill throughput was **22.9%-30.8% higher**, with
**18.5%-22.8% lower TTFT P50**, than this existing INT4 export. TTFT P50
reductions were **19.26%, 22.80%, 20.09%, and 18.51%** for 128, 512,
2048, and 4096 input tokens, respectively, calculated from unrounded
measurements as `(INT4 TTFT P50 - mixed TTFT P50) / INT4 TTFT P50 *
100%`. Mixed decode throughput was **2.3%-4.1% lower**, not faster.
- Mixed sampled process peak memory was **0.20-1.39 GiB higher**,
despite the 32.07% smaller disk artifact. The cause has not been
attributed; no runtime-memory reduction is claimed.
- Existing INT4 QMoE uses **block size 32**, while mixed uses **64**. A
separate INT4 diagnostic run confirmed `grouped_moe` for both prefill
and decode (72 prefill calls and 216 decode calls across three
four-token generations). Block size 32 fails the current packed INT GEMV
eligibility gate. Mixed uses the packed prefill/GEMV routes verified
above. This compares existing model configurations and execution paths,
not isolated INT2-versus-INT4 kernels.
- Synthetic repeated text and differing greedy continuations can produce
different MoE expert routing. Results do not establish real-workload
throughput, tau2 task success or accuracy preservation.
- Measurements were sequential, not alternating A/B trials. GPU 0 had no
other compute process, but other GPUs on the shared host were occupied
and their utilization differed between model runs. Small decode
differences require controlled repeated measurements before claiming a
stable regression. P95 is based on only ten requests.

Local artifacts on the validation machine (not checked into this PR):
`gpt_oss_20b_pr4b_benchmark.py`, `pr4b-benchmark-20261001/input-*.json`,
`pr4b-int4-benchmark-20261001/input-*.json`, corresponding logs, and
`gpt-oss-20b-pr4b-combined.log`, under
`/datadisks/disk1/jiafa/accuracy/`. Export directories are
`int4_cuda_int2_int4_qmoe` and `int4_cuda_int4_qmoe` under
`/sunghcho_data/jiafa/gpt-oss-20b-export/`.

Example reproduction using the local harness; replace the model
directory with the INT4 export for the comparison and repeat for each
input length:

```bash
docker exec -w /tmp \
  -e CUDA_VISIBLE_DEVICES=0 \
  -e LD_LIBRARY_PATH=/datadisks/disk1/jiafa/cudnn9.7/lib:/datadisks/disk1/jiafa/cuda-12.8/lib64 \
  jiafa-dev /datadisks/disk1/jiafa/accuracy/onnxruntime/.venv/bin/python -u \
  /datadisks/disk1/jiafa/accuracy/gpt_oss_20b_pr4b_benchmark.py \
  --model-dir /sunghcho_data/jiafa/gpt-oss-20b-export/int4_cuda_int2_int4_qmoe \
  --input-length 512 --output-length 128 --warmups 3 --repeats 10 \
  --output /datadisks/disk1/jiafa/accuracy/rerun-mixed-512.json
```

### Remaining Before Ready
- Complete plugin build, focused tests, regression and memcheck; run
supported-platform CI. The October 1 full-model results above are
non-plugin only.
- Extend the successful same-session prefill-to-decode smoke test to
repeated transitions and concurrency/capture qualification.
- Run controlled alternating A/B measurements and matched-block-size
comparisons, realistic workloads, accuracy/task-success evaluation, and
exact peak-allocation attribution before broad performance claims or
default dispatch changes.
- The full test suite and tau2-bench have not been run for this
integration. The initial synthetic performance baseline above is
complete but does not replace those qualifications.
Sayan Shaw (sayanshaw24) added a commit that referenced this pull request Oct 5, 2026
## Description

Adds WebGPU execution support for INT2 and mixed-width integer `QMoE`,
following the operator contract introduced for CUDA in #32761.

- Accepts 2-bit expert weights on WebGPU.
- Passes FC1, FC2, and FC3-specific bit widths to the existing
`MatMulNBits` implementation.
- Replaces the mixed-width rejection test with a nonzero INT2 FC1 / INT4
FC2 numerical test for the optimized single-token path.

The underlying WebGPU `MatMulNBits` shaders already implement 2-bit
unpacking, so no new shader path is required.

## Validation

- [x] `clang-format --dry-run --Werror` on changed C++ files
- [x] `git diff --check`
- [x] VS Code diagnostics clean
- [x] Release `onnxruntime_provider_test` build with WebGPU/D3D12
- [x] `QMoETest_WebGPU_MixedWidth_SingleToken` in hardware-backed WebGPU
CI
- [x] All WebGPU build/test CI jobs passed

Local virtual D3D12 execution is unstable with
`DXGI_ERROR_DEVICE_REMOVED`; hardware-backed CI is the numerical
validation source.

## Review follow-up (October 5)

Added H200/Vulkan coverage for homogeneous inherited INT2, k=2
mixed-width indirect selection of two distinguishable experts, two-token
mixed-width routing to different experts, and separate non-fused SwiGLU
FC3 with an independent INT2 width. FC1, FC2, and optional FC3 now
reject packed rows/blocks that cannot be indexed as u32 words; tests
cover the 12-element INT2 boundary and FC3. All 16 focused WebGPU QMoE
tests pass locally; all six review threads have replies and are
resolved. CI is rerunning on the new head. The previous Android minimal
binary-size check exceeded its 1,597,440-byte threshold by 509 bytes;
this is not claimed fixed by the WebGPU-only changes and must be
reassessed on the rerun.

---------

Co-authored-by: Sayan Shaw <sayanshaw@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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