Skip to content

2, 3, 5, 6 bit quantization exploration - #32657

Draft
David Fan (jiafatom) wants to merge 25 commits into
mainfrom
jiafa/2bit-6bit-quantization-exploration
Draft

David Fan (jiafatom) wants to merge 25 commits into
mainfrom
jiafa/2bit-6bit-quantization-exploration

Conversation

@jiafatom

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

Copy link
Copy Markdown
Contributor

Description

Documents the design space for 2-, 3-, 5-, and 6-bit weight-only quantization and defines the end-to-end delivery plan for mixed-width INT2 QMoE. The prioritized QMoE recipe uses INT2 expert gate/up weights and INT4 expert down weights while keeping the portable model format separate from provider-specific runtime prepacking.

QMoE Progress (October 6, 2026)

  • Mixed-width QMoE contract and shared validation merged in Add mixed-width weight contract for QMoE #32697; bounded CUDA correctness fallback merged in Add CUDA correctness fallback for INT2 and mixed-width QMoE #32743.
  • Packed CUDA INT2 and mixed INT2/INT4 decode merged in Add packed INT2 and mixed-width CUDA QMoE decode #32761 on September 28.
  • PR4a [CUDA] Add SM80 FP16/BF16 INT2 grouped GEMM foundation #32963 merged on October 1 (a4d55e2b6f): SM80 FP16/BF16 INT2 grouped GEMM foundation, including the corrected byte-address alignment contract.
  • PR4a2 [CUDA] Follow up INT2 grouped GEMM test coverage and plugin build (PR4a2) #33045 merged on October 1 (78c834918d): SM8x test coverage, plugin test build footprint, diagnostic options and requested file headers. The QUICK_BUILD baseline tactic issue was addressed before merge; it is no longer an outstanding delivery task. The redundant quantization fix is already in main via Replace old use of routing_record with routing_snapshot_ in onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc. #32932.
  • Historical PR4a2 local A100 validation in Docker jiafa-dev: actual CUDA 12 legacy provider-test target built successfully; 23 grouped tests passed (3 benchmarks disabled), QMoE regression 53 passed / 11 skipped. Both passed memcheck with zero errors/leaks. Actual plugin internal-test target and fixture compiled; a standalone harness linked from these generated objects passed 16 tests under memcheck. Standalone CUDA 12/13 plugin and CUDA 12 legacy harnesses also passed memcheck/synccheck/initcheck and three focused racecheck cases. These results do not establish SM86/SM89 coverage. That local full CUDA 12 plugin provider build was blocked by third-party-header warning-as-error diagnostics; the historical standalone results are not a full plugin integration pass.
  • PR4b [CUDA] Enable SM80+ FP16/BF16 mixed INT2/INT4 QMoE prefill by default #33005 merged on October 2 (2cdff3ca8e): default-enabled SM80+ FP16/BF16 packed INT2 FC1 / INT4 FC2 prefill, symmetric quantization, block size 64 and interleaved fused SwiGLU. Set ORT_ENABLE_QMOE_INT2_PREFILL=0 to opt out. Packed decode retains priority; ORT_DISABLE_MOE_GEMV=1 disables eligible INT2 and existing INT4/INT8 GEMV dispatch. Set environment controls before process startup.
  • Eligible packed prefill now tiles within a 256 MiB estimated temporary-scratch budget, including packed workspace, tile-local routing metadata and uncached scale transposes. Explicit row tiling and long prompts stay packed rather than triggering full dense weight dequantization. Full/final tile sizes are accounted for; a one-row overflow reports a packed-scratch error. This is not a whole-process GPU memory cap. Unsupported configurations retain the bounded fallback. Regression coverage includes 8193-row FP16/BF16 cases with a one-byte dense scratch guard, top-k, bias, runtime scales and partial final tiles.
  • SM80+ eligibility includes later architectures but retains the SM80 CUTLASS template/layout; suitable target code or compatible virtual PTX is required. Merge and metadata tests do not establish hardware execution coverage on every architecture.
  • PR4c Extend CUDA QMoE packed INT2 prefill block sizes and weight combinations #33092 merged on October 5 (0c67e77bb7cb): extends packed prefill to block sizes 32, 64 and 128 and (FC1, FC2) widths (2,4), (2,2) and (4,2), dispatching each projection to the INT2 or INT4 grouped GEMM. Existing (4,4), INT8, zero-point and non-fused paths and width inheritance defaults are unchanged. Block size 32 has no GEMV path. Final-head local validation at db8a7625b8 on A100 with non-plugin Release CUDA 12.8, BF16 and internal tests enabled passed 15 outer tests and 26 internal tests, with no failures/skips (three benchmarks disabled). Coverage includes the 216-invocation FP16/BF16 integration matrix, direct grouped-GEMM block/tile cases, cached-scale decode-to-prefill transition, packed-decode fallback and block32 dense fallback with prefill disabled. Validation details: Extend CUDA QMoE packed INT2 prefill block sizes and weight combinations #33092 (comment) . This is local A100 correctness evidence, not plugin integration, every-GPU coverage or a performance claim.
  • Historical PR4b validation: six focused tests passed in a clean non-plugin wheel build at 463fa42cb5. Full mixed GPT-OSS-20B validation demonstrated GEMM prefill -> GEMV decode in one session, with finite logits/KV and explicit branch counts (24 prefill and 48 decode calls for a 309-token prompt). These results predate the final default-dispatch and packed-tiling changes and must not be treated as validation of those revisions.
  • Initial direct-ORT A100 benchmark compared existing mixed INT2/INT4 and INT4 GPT-OSS-20B exports: batch 1, 128/512/2048/4096 input tokens, 128 generated tokens, three warmups and ten measured requests per configuration. Mixed prefill TPS was 22.9%-30.8% higher; TTFT P50 was 18.5%-22.8% lower; decode TPS was 2.3%-4.1% lower. Sampled process peak GPU memory was 0.20-1.39 GiB higher, not lower. Full tables and measurement boundaries are in [CUDA] Enable SM80+ FP16/BF16 mixed INT2/INT4 QMoE prefill by default #33005. These historical timings also predate the final packed-tiling changes.
  • Mixed model artifacts total 7.462 GiB versus 10.985 GiB for INT4: 3.523 GiB / 32.07% smaller. This is ONNX graph plus referenced external weights, not GPU residency.
  • Performance results are an initial synthetic baseline, not tau2-bench or accuracy qualification. Existing exports differ in block size (mixed 64, INT4 32) and dispatch; sequential runs on a shared machine are not an isolated bit-width ablation. Repeated prompts and differing greedy continuations can affect expert routing.
  • Remaining work: controlled alternating and matched-block-size benchmarks for the merged implementation; exact runtime-memory attribution; broader fused export integration, CPU/CUDA model parity, deployment-platform qualification, real-workload and target-model quality evaluation; repeated transition/concurrency/capture validation. Default packed prefill enablement is merged; generalized speedup, lower runtime memory and end-to-end product qualification are not claimed.

Branch and Documentation Update

Rebased onto main at 2cdff3ca8e (including PR4a2 and PR4b), without conflicts. Both design documents record the merged work and distinguish historical estimates from remaining qualification tasks. The October 6 update records merged PR4c (#33092, 0c67e77bb7cb) and its final-head local A100 validation in both design documents, including the expanded support boundary and remaining qualification limits. This roadmap update is documentation-only; no new build or runtime tests were run for this update.

Motivation and Context

Expert weights dominate storage and memory traffic in the target Qwen MoE models. This plan prioritizes a deployable mixed-width QMoE path while reusing dense MatMulNBits packing, validation, and CUDA primitives where useful. It does not treat the bounded dense fallback as production performance support, and it does not claim compatibility with GGUF IQ2_XS or IQ4_NL formats.

Copilot AI balanced review requested due to automatic review settings September 16, 2026 20:45
@jiafatom
David Fan (jiafatom) marked this pull request as draft September 16, 2026 20:45

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.

🟡 Changes recommended

The support matrix incorrectly reports that WebGPU MatMulNBits lacks 2-bit support.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Explores INT2/INT6 weight-only quantization and recommends a scoped CUDA INT2 implementation.

Changes:

  • Documents current support, external approaches, packing options, evaluation criteria, and delivery estimates.
  • Proposes phased CUDA INT2 development with INT6 as follow-up.
File summaries
File Description
docs/design/2bit-6bit-weight-only-quantization-exploration.md Adds the quantization exploration and roadmap.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/design/2bit-6bit-weight-only-quantization-exploration.md Outdated
@titaiwangms

Copy link
Copy Markdown
Contributor

Thanks for putting this exploration together. From the Olive/Mobius side, we want to clarify the end-to-end model-production dependency for the proposed INT2 CUDA work.

Even after ORT implements the CUDA MatMulNBits(bits=2) kernels, Olive and Mobius may need corresponding work before users can produce deployable INT2 models. There are two possible workflows:

  1. ONNX-native: FP ONNX → Olive OnnxBlockWiseRtnQuantization → MatMulNBits(bits=2).
  2. PyTorch/Mobius: HF/PyTorch → Olive RTN/GPTQ/SMP INT2 or mixed-precision checkpoint → Mobius → MatMulNBits(bits=2/4/8).

The current codebase has different gaps for these paths. Olive's PyTorch RTN/GPTQ/KQuant and SelectiveMixedPrecision already accept 2-bit configurations, but the INT2 checkpoint-to-ONNX path still needs end-to-end qualification. Olive's ONNX RTN and built-in quantized-linear export paths also need additional work for complete INT2 support. Mobius can construct MatMulNBits(bits=2), but Olive-format uniform INT2 and mixed INT2/INT4/INT8 checkpoint export need end-to-end graph, weight-binding, and numerical-parity coverage.

We expect both workflows to be supported eventually, but could the document clarify which one is required for the initial CUDA vertical slice? Given the GPTQ, SMP, and mixed-precision quality goals, the PyTorch/Mobius path may be the more relevant first milestone, with ONNX-native INT2 RTN following for ONNX-only workflows.

Could the scope also explicitly confirm whether the initial target is limited to dense MatMulNBits projections? If fused MoE/QMoE expert weights are included, that introduces a separate Olive/Mobius/runtime contract and should be tracked as a distinct workstream.

@jiafatom

David Fan (jiafatom) commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for clarifying the model-production dependency. Updated in f20da36 with two explicit scope decisions:

  1. The initial vertical slice uses the HF/PyTorch -> Olive RTN/GPTQ/SelectiveMixedPrecision checkpoint -> Mobius -> dense mixed-bit MatMulNBits ONNX workflow, including graph conversion, initializer binding, tensor-wise bit-width, and numerical-parity qualification. ONNX-native Olive INT2 RTN/export follows as a separate milestone.
  2. The initial CUDA target is limited to individual dense MatMulNBits projections. Expert gate/up INT2 is included only when exported as independent dense nodes; fused MoE/QMoE export, packing, and runtime contracts are explicitly out of scope and tracked separately.

The committed scope and dated schedule now reflect the export qualification work.

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.

🔵 Needs a closer look

The support matrix inaccurately describes existing 2-bit QDQ packing and runtime fusion support.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

docs/design/2bit-6bit-weight-only-quantization-exploration.md:37

  • This note misidentifies the remaining QDQ gap. A native 2-bit QDQ packer is already exported as quantize_qdq_matmul_2bits (onnxruntime/python/onnxruntime_pybind_quant.cc:210-211), and runtime DQ→MatMulNBits(bits=2) fusion is implemented and tested (onnxruntime/core/optimizer/qdq_transformer/selectors_actions/qdq_actions.cc:24-26,296-300; onnxruntime/test/optimizer/qdq_matmulnbits_transformer_test.cc:763-768). What remains 4-bit-only is the high-level Python quantizer's QDQ branch (matmul_nbits_quantizer.py:1032-1037). Please describe that distinction so the roadmap does not plan already-completed native/runtime work.
  • Files reviewed: 1/1 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@jiafatom

Copy link
Copy Markdown
Contributor Author

Added the INT2 QMoE investigation in e751729. The document now:

  • keeps CUDA MatMulNBits(bits=2) as the first implementation priority;
  • identifies the single expert_weight_bits attribute as the blocker for the target FC1 INT2 / FC2 INT4 recipe;
  • records the current CPU, CUDA, WebGPU, and export gaps;
  • treats full expert dequantization only as a correctness oracle, not a production milestone; and
  • defines QMoE contract/export and fused decode as the next track after the MatMulNBits packing, correctness, and M=1 primitives stabilize.

The November dense MatMulNBits commitment remains unchanged; fused QMoE is still explicitly follow-up scope.

@jiafatom

Copy link
Copy Markdown
Contributor Author

Added a standalone QMoE-first delivery plan in 19a723d: docs/design/int2-qmoe-end-to-end-delivery-plan.md.

It defines the end-to-end path from Olive calibration and Mobius export through CPU reference, CUDA correctness, packed decode, bounded/native prefill, and Qwen quality/performance qualification. The initial target is mixed FC1 INT2 / FC2 INT4, with backward-compatible schema semantics and explicit separation from IQ2_XS/IQ4_NL.

The proposed eight-week milestone is contract + reproducible export + CPU reference + bounded CUDA correctness + packed decode on one architecture. Production grouped-GEMM prefill and broad architecture coverage require follow-up time or additional CUDA staffing. The original exploration document now points to this plan and identifies QMoE as the product priority.

@jiafatom

Copy link
Copy Markdown
Contributor Author

Extended the MatMulNBits exploration in 929ee53 to include INT3 and INT5 alongside INT6. The update adds:

  • current support-matrix gaps for INT3/INT5/INT6;
  • one canonical LSB-first contiguous bitstream contract;
  • candidate runtime prepacking as 2+1 planes for INT3, 4+1 for INT5, and 4+2 for INT6;
  • a common 2/3/4/5/6/8-bit quality and effective-size gate; and
  • a cost model that selects at most one new width for initial CUDA decode/prefill prototypes instead of committing to three kernel families.

INT2 QMoE remains the product priority. INT3/INT5/INT6 are explicitly a parallel measured research track, with INT3 the first optimization candidate only if the data supports it.

@jiafatom David Fan (jiafatom) changed the title 2bit 6bit quantization exploration 2, 3, 5, 6 bit quantization exploration Sep 17, 2026
@titaiwangms

Copy link
Copy Markdown
Contributor

Thanks for making mixed-width QMoE the explicit product priority. We aligned the
existing Olive and Mobius tracking with the updated plan:

  • microsoft/Olive#2638 owns fused-expert SMP/recipe work and qualification of
    an Olive checkpoint with FC1 gate/up INT2 and FC2 down INT4.
  • onnxruntime/mobius#735 owns checkpoint ingestion, independent FC1/FC2 packed
    layouts, gate/up interleaving, down-projection orientation, graph emission,
    initializer/external-data binding, and ORT CPU parity.

The existing microsoft/Olive#2671 and onnxruntime/mobius#740 qualify the
dense checkpoint-to-MatMulNBits(bits=2/4/8) path only; they do not claim fused
mixed-width QMoE support.

From the downstream side, we propose treating the ORT mixed-width QMoE contract
as the graph-emission gate. Could the contract work settle:

  1. FC-specific attributes versus a versioned descriptor.
  2. FC1/FC2 scale and zero-point shapes and dtypes.
  3. Packed tensor ordering and whether the initial configuration shares one
    block size.
  4. Inheritance and backward compatibility with expert_weight_bits.
  5. The schema commit that Olive/Mobius should pin for implementation and shared
    fixtures.

Before that contract is fixed, we will keep work schema-neutral: checkpoint
qualification, expert target classification, packing oracles, quality/effective
size measurements, and a tiny deterministic source fixture. Once pinned, the
Mobius export work can proceed in parallel with the ORT CPU reference and use
the same fixture for CPU/CUDA parity.

Please also confirm whether native Mobius QMoE construction is sufficient for
the first exporter milestone, with dense-graph-to-QMoE fusion following
separately.

David Fan (jiafatom) added a commit that referenced this pull request Sep 21, 2026
### Description
Add the schema and validation contract needed for mixed-width QMoE
expert weights.

- Add optional `fc1_expert_weight_bits`, `fc2_expert_weight_bits`, and
`fc3_expert_weight_bits` attributes, inheriting from
`expert_weight_bits` when omitted.
- Pass effective bit widths through shared validation and compute packed
byte dimensions as `logical_elements * bits / 8`.
- Require byte-aligned weight rows while allowing zero-point rows to pad
to a whole byte.
- Preserve legacy single-`pack_size` helper overloads and existing 2/4/8
validation diagnostics.
- Wire effective widths through CPU, CUDA, and WebGPU QMoE providers.
- Require matching FC1/FC3 widths for fused SwiGLU.
- Bypass legacy prepacking and return an explicit `NOT_IMPLEMENTED`
status for mixed-width execution. Execution support will follow
separately.
- Preserve the existing uniform-width behavior and schema version.

### Attribute semantics
`expert_weight_bits` remains the legacy global value and the fallback
for each optional projection override:

```text
effective_fc1_bits = fc1_expert_weight_bits if present, otherwise expert_weight_bits
effective_fc2_bits = fc2_expert_weight_bits if present, otherwise expert_weight_bits
effective_fc3_bits = fc3_expert_weight_bits if present, otherwise expert_weight_bits
```

If an override is omitted, it is never left uninitialized: it inherits
`expert_weight_bits`, whose schema default is 4. If all three overrides
are present, each override takes precedence for its corresponding
projection. All effective values are currently restricted to 2, 4, or 8
bits. Fused SwiGLU additionally requires effective FC1 and FC3 widths to
match.

Examples:

- `expert=4`, no overrides -> effective widths `(4, 4, 4)`, existing
uniform 4-bit path.
- `expert=4`, `fc1=2`, `fc2=4`, `fc3=2` -> effective widths `(2, 4, 2)`,
mixed-width contract path.
- `expert=4`, `fc1=2`, `fc2=2`, `fc3=2` -> effective widths `(2, 2, 2)`,
but currently still treated as an override/mixed configuration because
the effective widths differ from the legacy global value.
- For a uniform 2-bit model, use `expert_weight_bits=2` and omit the
overrides.

### Current execution boundary
For this contract-only change, `is_mixed_width` means that at least one
effective projection width differs from `expert_weight_bits`:

```text
is_mixed_width =
    effective_fc1_bits != expert_weight_bits ||
    effective_fc2_bits != expert_weight_bits ||
    effective_fc3_bits != expert_weight_bits
```

This intentionally rejects even `expert=4, fc1=2, fc2=2, fc3=2`. Shape
and zero-point validation use the effective 2-bit widths, while the
existing execution and dispatch paths still use the legacy global width.
Allowing execution in that state could interpret 2-bit data as 4-bit
data. The explicit rejection prevents silent incorrect results.

Follow-up execution support must make prepacking, scale/zero-point
interpretation, packed layouts, workspace sizing, and kernel dispatch
projection-aware. Once dispatch consumes effective widths, uniformly
overridden configurations can be normalized to one effective width,
while genuinely mixed configurations can dispatch FC1/FC2/FC3
independently.

### Testing
- Built `onnxruntime_provider_test` with CUDA enabled.
- Added CPU contract and invalid-shape tests.
- Added row-wise and block-wise mixed-width zero-point validation,
including invalid FC2 shapes.
- Added availability-gated CUDA and WebGPU provider contract tests.
- Preserved the legacy INT2 invalid-hidden-size diagnostic.
- Covered arbitrary 3/5/6-bit packed-byte calculations in the shared
helper.
- Repository lintrunner passes for the changed files.

Related to #32657.
Ti-Tai Wang (titaiwangms) added a commit to onnxruntime/mobius that referenced this pull request Sep 22, 2026
## Summary

- qualify synthetic Olive-format uniform INT2 checkpoints through the
public local `build()` path
- qualify mixed default INT2 with INT4 `down_proj` and INT8 `gate_proj`
overrides
- save and reload the Mobius package, then verify exact `MatMulNBits`
attributes, input wiring, packed bytes, scales, and initializer binding
- execute representative emitted INT2/INT4/INT8 projections explicitly
on `CPUExecutionProvider` against an independent Olive-compatible decode

## Scope

This validates the Olive checkpoint format/import/export contract and
CPU operator semantics. It does **not** claim CUDA INT2 kernel readiness
or performance, and it does not cover QMoE/fused MoE. No Mobius
production changes were required after #733.

## Validation

- `python -m pytest src/mobius/_component_quantization_loading_test.py
-q --tb=short` (61 passed)
- `python -m ruff check
src/mobius/_component_quantization_loading_test.py`
- `python -m ruff format --check
src/mobius/_component_quantization_loading_test.py`
- `git diff --check`

Related: microsoft/onnxruntime#32657, microsoft/Olive#2669

---------

Signed-off-by: titaiwang <titaiwang@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jiafatom
David Fan (jiafatom) force-pushed the jiafa/2bit-6bit-quantization-exploration branch from 929ee53 to 8831424 Compare September 22, 2026 16:50
Ti-Tai Wang (titaiwangms) added a commit to microsoft/Olive that referenced this pull request Sep 23, 2026
## Describe your changes

Add offline qualification coverage for the Olive side of the initial
dense INT2 model-production workflow discussed in
microsoft/onnxruntime#32657.

- Verify native PyTorch RTN, GPTQ, and KQuant produce real uniformly
packed INT2 weights and preserve them across checkpoint save/reload.
- Verify `SelectiveMixedPrecision` drives each quantizer with an INT2
default, an SMP-selected INT4 projection, and an explicit INT8
projection override.
- Inspect reloaded `QuantTensor` settings and packed safetensors buffers
instead of relying on checkpoint metadata alone.
- Check RTN weights against the original floating-point weights within
the quantization-scale bound.
- Check GPTQ checkpoint reconstruction and module execution using
deterministic, varied, fully local calibration sequences.
- Check KQuant checkpoint materialization against a directly recomputed
KQuant result.
- Keep all tests offline with a locally constructed tiny dense Llama
model and tokenizer.

No production code changes were required: the existing native RTN, GPTQ,
KQuant, SMP, serialization, and reload paths already materialize dense
INT2 and mixed INT2/INT4/INT8 checkpoints correctly. Mobius export
qualification, ONNX-native INT2 RTN, and MoE/QMoE remain separate
follow-up work.

Validation:

```text
python -m pytest -q \
  test/passes/pytorch/test_dense_int2.py \
  test/passes/pytorch/test_rtn.py::test_rtn_int2_dense_checkpoint_packing_and_roundtrip \
  test/passes/pytorch/test_rtn.py::test_selective_mixed_precision_rtn_int2_int4_int8_checkpoint

6 passed

python -m ruff format --check \
  test/passes/pytorch/test_dense_int2.py \
  test/passes/pytorch/quantization_test_utils.py \
  test/passes/pytorch/test_rtn.py

python -m ruff check --ignore CPY001 \
  test/passes/pytorch/test_dense_int2.py \
  test/passes/pytorch/quantization_test_utils.py \
  test/passes/pytorch/test_rtn.py

git diff --check
```

## Checklist before requesting a review
- [x] Add unit tests for this change.
- [x] Make sure all targeted tests can pass.
- [x] Update documents if necessary. (No user-facing behavior changed.)
- [ ] Lint and apply fixes to your code by running `lintrunner -a`.
(Targeted Ruff checks pass; the existing `test_rtn.py` has a
pre-existing CPY001 finding.)
- [x] Is this a user-facing change? If yes, give a description of this
change to be included in the release notes. (Tests only; no release note
required.)

## (Optional) Issue link

Supports the INT2 model-production scope in microsoft/onnxruntime#32657.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jiafatom
David Fan (jiafatom) force-pushed the jiafa/2bit-6bit-quantization-exploration branch from 2eb9d05 to 1d78f64 Compare September 24, 2026 14:18
@jiafatom

Copy link
Copy Markdown
Contributor Author

Thanks for aligning the downstream tracking. The mixed-width contract is now fixed by #32697, merged as 090f6ba1cfd6f4a0cb2481bac4b92156828a0e33. Answers to the five contract questions:

  1. We chose FC-specific optional attributes on the existing QMoE v1 schema: fc1_expert_weight_bits, fc2_expert_weight_bits, and fc3_expert_weight_bits; there is no versioned descriptor in the initial contract.
  2. For integer QMoE, weights and packed zero points are uint8; scales may be float, float16, or bfloat16. FC1 scales are [E, fusion_size * inter_size] row-wise or [E, fusion_size * inter_size, hidden_size / block_size] block-wise. FC2 scales are [E, hidden_size] or [E, hidden_size, inter_size / block_size]. Row-wise FC1/FC2 zero points are [E, ceil(fusion_size * inter_size * fc1_bits / 8)] and [E, ceil(hidden_size * fc2_bits / 8)]; block-wise forms are [E, fusion_size * inter_size, ceil((hidden_size / block_size) * fc1_bits / 8)] and [E, hidden_size, ceil((inter_size / block_size) * fc2_bits / 8)]. Zero-point rows pad to whole bytes.
  3. The portable raw weight form (weights_prepacked=0) is [E, N, K * effective_bits / 8], with K-contiguous values packed low bits first within each byte. Provider prepacking is runtime-internal and is not an exporter contract. The initial schema has one shared block_size attribute for FC1/FC2/FC3.
  4. Each omitted FC override inherits expert_weight_bits (default 4), preserving existing uniform models. Effective widths are restricted to 2/4/8. For fused SwiGLU, effective FC3 must equal effective FC1 because FC3 is stored in FC1.
  5. Olive/Mobius should pin schema and graph-emission fixtures to merge commit 090f6ba1cfd6f4a0cb2481bac4b92156828a0e33. For executable ORT parity, Add CUDA correctness fallback for INT2 and mixed-width QMoE #32743 is merged as ee5f6e7cfa1f9e4e4253154e7c719a846280ca83; Add packed INT2 and mixed-width CUDA QMoE decode #32761 adds packed CUDA decode and remains a runtime optimization, not an exporter contract dependency.

Native Mobius QMoE construction is sufficient for the first exporter milestone. Dense-graph-to-QMoE fusion should follow separately and should not block the deterministic fused-QMoE fixture/export path. I recorded that milestone boundary and the Olive#2638/Mobius#735 ownership in 35f76b8ce5.

@jiafatom
David Fan (jiafatom) force-pushed the jiafa/2bit-6bit-quantization-exploration branch from 35f76b8 to 83581ec Compare September 28, 2026 02:04
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.
@gramalingam

Copy link
Copy Markdown
Contributor

Please see onnx/onnx#8576 ... whether it makes sense

@jiafatom
David Fan (jiafatom) force-pushed the jiafa/2bit-6bit-quantization-exploration branch from a05da07 to 1340936 Compare October 14, 2026 21:06
@jiafatom

Copy link
Copy Markdown
Contributor Author

G. Ramalingam (@gramalingam) Thank you for the feedback in #32657 (comment) and for pointing me to onnx/onnx#8576 in #32657 (comment).

I reviewed the current Pack/Unpack proposal and updated the INT3 QMoE delivery plan in commit 1340936:

  • The proposed portable INT3 weight format already matches its UINT8, last-axis, row-local LSB-first packing, including the [0,1,2,3,4,5,6,7] -> [0x88,0xc6,0xfa] example. Explicit zero points use the same packing with count=B (number of scale blocks), while weights use count=K.
  • P0 now requires interoperability checks against a recorded proposal revision and an executable standard-operator unpack/dequantization reference, rather than deferring that reference until the committee submission.
  • P2 will compare the independent scalar oracle and CUDA results with that reference: unpack unsigned codes, cast before zero-point subtraction, and apply scales/zero points using logical K-block indexing.
  • P5 now includes numerical equivalence between the native fused QMoE export and a small standard-operator MoE reference using the same packed initializers. Automatic graph recognition/fusion remains follow-up work.
  • Section 7.3 clarifies that generic Unpack ignores high padding bits, whereas the proposed INT3 QMoE profile requires canonical zero padding; that stricter check belongs at the profile/export boundary.

This keeps the standard interchange semantics separate from backend execution. CUDA may still fuse decoding with computation or convert the portable bytes into a provider-private prepack; the reference graph does not require production execution to materialize all experts as floating-point weights.

Since #8576 is still an open proposal, the plan records the reviewed revision and does not treat its draft operator/opset as a released dependency. Until it is approved and supported, the reference unpack stage can use an equivalent decomposition of existing standard operators. Native INT3/UINT3 datatype standardization remains outside this delivery sequence.

These are plan and acceptance-criteria updates, not a claim that INT3 QMoE kernels or the interoperability tests are already implemented.

Ti-Tai Wang (titaiwangms) added a commit to onnxruntime/mobius that referenced this pull request Oct 15, 2026
## Summary

Add projection-specific QMoE export for Olive fused expert checkpoints.
FC1 and FC2 independently support INT2, INT3, INT4, and INT8: all 16
pairs
are accepted subject to format validation, without an experimental
opt-in.

This is **export support, not runtime qualification**. INT3 follows the
proposed
raw-weight direction in microsoft/onnxruntime#32657 and consumes the
native Torch
INT3 checkpoint format introduced by microsoft/Olive#2716. INT3 output
retains
draft-format/runtime-warning metadata; this PR does not provide INT3 ORT
kernels
or claim that released ORT can execute it.

## Changes

- Resolve FC1/FC2 widths independently instead of using an explicit pair
whitelist.
This intentionally includes `(2,3)`, `(3,2)`, `(3,8)`, `(8,3)`, `(2,2)`,
and
  `(2,8)`, in addition to the other valid pairs.
- Remove the experimental INT3 flag and opt-in rejection. Export
requires an
explicitly quantized checkpoint with compatible projection
configurations;
  ordinary unquantized models do not become INT3 automatically.
- Require exact Olive expert overrides and a supported model-wide INT4
fallback
for projection-specific layouts. Preserve the existing uniform INT4
path.
- Preserve row-local, LSB-first packed unsigned INT3 codes with
symmetric zero
point 4. For pairs containing INT3, interleave FC1 gate/up payloads
lazily during
serialization in public HF builds; direct preprocessing remains eager by
default. Otherwise retain graph-side interleaving. Preserve FC2's K-last
layout
  without requantization.
- Validate shared power-of-two group sizes, aligned dimensions, sidecar
shapes,
packed zero points, and finite nonnegative scales. Reject scale
conversion
overflow when the adapter supplies the activation dtype. INT3-containing
pairs
additionally require block size 32/64/128 and FP16/BF16 activation/scale
agreement. Non-INT3 layouts retain the standard loader's dtype
conversion.
- Apply the same numeric scale checks to the legacy uniform INT4 path
without
imposing the projection-specific shape ABI. Rejecting previously
accepted
negative, NaN, infinite, or activation-cast-overflowing scales is an
intentional
  validation tightening.
- Bind deferred FC1 weights, scales, and asymmetric zero points through
uncached
lazy tensors, rather than retaining transformed copies for every layer.
Keep transform accounting component-scoped when prefix stripping
produces
  identical local initializer names.
- Preserve INT2/INT4/INT8 zero-point semantics and validate optional
explicit
  offset-4 INT3 zero points before omitting them from the graph.
- Emit projection-width attributes where needed and retain INT3
draft-format
  metadata without assigning an unapproved schema version.
- Cover all 16 pairs across FP16/BF16, independent
packing/dequantization oracles,
CPU execution of graph-side interleaving, invalid scales,
local-checkpoint
  public builds, and external-data save/reload.
- Pass the model activation dtype through all three Qwen3.5-MoE
preprocessing
paths: text, VL global quantization, and VL decoder component
quantization.
Add 57 regression cases covering all seven INT3-containing pairs across
FP16/BF16, incompatible dtypes, and non-INT3 scale conversion behavior.

## Validation

Latest offline CPU validation includes the merged `main`, Qwen3.5-MoE
dtype
propagation, uniform scale checks, deferred serialization, and component
routing:

```bash
PYTHONPATH="$PWD/src" HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 \
  TOKENIZERS_PARALLELISM=false PYTHONDONTWRITEBYTECODE=1 CUDA_VISIBLE_DEVICES='' \
  timeout 300 /home/titaiwang/miniconda3/envs/dev/bin/python -m pytest \
  --rootdir="$PWD" -c "$PWD/pyproject.toml" \
  src/mobius/models/int3_qmoe_test.py src/mobius/models/qwen35_test.py \
  src/mobius/models/moe_test.py \
  src/mobius/_weight_utils_test.py src/mobius/_flags_test.py \
  src/mobius/components/_moe_test.py \
  src/mobius/_model_package_test.py::TestModelPackageApplyWeights \
  src/mobius/weights/_adapters_test.py \
  src/mobius/integrations/_weight_loading_test.py \
  src/mobius/integrations/transformers/_builder_test.py \
  -q -m 'not integration and not arch_validation and not golden and not generation' \
  --tb=short
```

**Passed: 728 tests, 1 deselected, in 52.36 seconds.** Coverage includes
distinct per-layer payloads, source-storage aliasing, release of
transformed
tensors between callbacks, repeated saves, asymmetric FC1 zero points,
and
positive/negative component-local-name collision regressions.

```bash
lintrunner src/mobius/_model_package.py src/mobius/_model_package_test.py \
  src/mobius/_weight_utils.py src/mobius/integrations/_weight_loading.py \
  src/mobius/integrations/transformers/_builder.py src/mobius/models/granite.py \
  src/mobius/models/int3_qmoe_test.py src/mobius/models/llada.py \
  src/mobius/models/moe.py src/mobius/models/qwen35.py \
  src/mobius/models/qwen35_test.py src/mobius/weights/_adapters.py
git diff --check
```

**Both passed.** Ruff formatting and checking also passed for the same
12 Python
files. Scoped mypy reported 77 errors outside changed hunks; no
project-wide
type-check success or fully established clean baseline is claimed.

## Draft boundaries

- All 16 pairs are exporter-supported layouts, not a runtime-qualified
matrix.
  Execution depends on the target ORT version and execution provider.
- Source checkpoint loading remains eager. Uncached lazy interleaving
bounds
additional FC1 payload retention, not total checkpoint or serialization
peak
memory. No full-checkpoint RSS or concurrent-writer qualification was
run.
- Direct callers opting into deferred preprocessing must forward the
populated
transform map when binding the returned weights. Explicit adapters and
older
  preprocessing hooks retain their existing behavior.
- No INT3 ORT kernel execution, checkpoint-to-runtime logits parity,
real-model
quality, or performance qualification was run. The CPU interleave tests
  exercise standard ONNX graph operations, not QMoE kernels.
- Dense INT3 `MatMulNBits`, quantized embeddings, unsupported source
layouts, and
  schema-unapproved tails are not enabled.
- Final QMoE schema/version agreement, complete quantization provenance,
and
runtime qualification remain follow-up gates. This does not complete the
  end-to-end plan's P5 acceptance gate.
- INT2 runtime/CI qualification is separate in #765
and is not
  included in this PR.

---------

Signed-off-by: titaiwang <titaiwang@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fba8a408-cd63-49f0-b8b4-5daa19ed42d4

This branch has not been deployed

No deployments
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