Skip to content

[CUDA] Add SM80 FP16/BF16 INT2 grouped GEMM foundation - #32963

Merged
David Fan (jiafatom) merged 10 commits into
mainfrom
jiafa/int2-qmoe-sm80-grouped-gemm
Oct 1, 2026
Merged

David Fan (jiafatom) merged 10 commits into
mainfrom
jiafa/int2-qmoe-sm80-grouped-gemm

Conversation

@jiafatom

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

Copy link
Copy Markdown
Contributor

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 (d3d60ac): corrected the shared CUTLASS tensor_aligned pointer check to use alignment * sizeof_bits::value / 8 bytes, while retaining stride checks in elements and the entry-point 16-byte guard. Added positive packed-weight suballocation tests at +16/+32/+48 bytes for FP16/BF16 and both INT2 tactics, plus production INT4/INT8 offset regressions. The new INT2 tests reproduced Error Misaligned Operand before the fix and passed afterward on A100. Existing +8-byte rejection tests remain.

Rebuilt and validated in jiafa-dev on October 1, 2026. Both legacy and plugin passed all 23 enabled grouped tests and all 23 under Compute Sanitizer memcheck with zero errors. Forced recompilation of the MoE CUDA translation units because some cached nvcc objects recorded no header dependencies; the new INT4 offset test also detected the stale object before recompilation.

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

  • Legacy (d3d60ac): all 23 grouped tests passed; all 23 passed under Compute Sanitizer memcheck with zero errors. Existing QMoE regression: 53 passed, 11 skipped.
  • Plugin (d3d60ac): all 23 grouped tests passed; all 23 passed under Compute Sanitizer memcheck with zero errors. The combined Int2GroupedGemm*:QMoE run had 78 passed and 12 skipped (23 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:

# 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.

Copilot AI balanced review requested due to automatic review settings September 30, 2026 03:29

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

🔵 Needs a closer look

Low-level CUTLASS kernel changes and pending default-toolchain CI require human validation, and one diagnostic remains inaccurate.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
What changed in this PR

Adds the SM80 FP16/INT2 grouped-GEMM foundation for future QMoE prefill integration.

Changes:

  • Adds 32-row and 64-row packed INT2 GEMM tactics.
  • Adds correctness, regression, validation, and opt-in benchmark coverage.
  • Registers legacy/plugin tests and updates routing snapshot usage.
File Description
moe_gemm_int2_test.cc Adds kernel tests and benchmarks.
moe_quantization.cc Updates routing snapshot capture/consume flow.
moe_gemm_template_dispatch.h Enables constrained UINT2 dispatch.
moe_gemm_kernels_fp16_uint2.cu Implements validation and SM80 launchers.
moe_gemm_int2.h Defines the kernel entry-point contract.
moe_cutlass_kernel.h Enables UINT2 scale validation.
moe_int2_grouped_gemm.md Documents usage, validation, and measurements.
onnxruntime_unittests.cmake Registers plugin tests and dependencies.

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

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

🟢 Approval recommended

The implementation and coverage match the scoped kernel contract; only a non-blocking diagnostic mismatch remains.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Resolved since last review (1)

@jiafatom

Copy link
Copy Markdown
Contributor Author

CI triage: the only outstanding non-success/non-neutral check is React Native CI iOS E2E Tests (cancelled). Both the original attempt and the manual rerun show the same simulator initialization/cleanup failure signature:

  • iOS app build succeeds.
  • Detox starts the iPhone 15 / iOS 17.4 simulator and invokes xcrun simctl bootstatus.
  • About four minutes later, cleanup reports CoreSimulator.SimError, code=405: Unable to shutdown device in current state: Shutdown.
  • Jest reports that it did not exit because asynchronous operations remain, and the job stays alive until cancelled. The E2E steps lasted roughly 79 minutes and 53 minutes respectively.

Original job: https://github.com/microsoft/onnxruntime/actions/runs/36667939436/job/109741418017
Rerun job: https://github.com/microsoft/onnxruntime/actions/runs/36667939436/job/109906898589

The available logs do not show application tests or ORT inference reaching a failure. This points to simulator setup/teardown rather than the CUDA changes, but the underlying simulator boot failure is not established by these logs. Other recent workflow runs have succeeded, so this is not evidence of a platform-wide outage.

Could a React Native CI maintainer check the macOS runner/CoreSimulator environment and advise on a targeted rerun on a healthy runner? Leaving the CUDA code and check requirements unchanged; another identical rerun without environment diagnosis may reproduce the same hang. Separate CI hardening could add simulator boot diagnostics and bounded cleanup timeouts.

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

🔵 Needs a closer look

The low-level architecture-specific CUDA kernel and toolchain-sensitive plugin build require final human and CI validation.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@jiafatom David Fan (jiafatom) changed the title [CUDA] Add SM80 FP16 INT2 grouped GEMM foundation [CUDA] Add SM80 FP16/BF16 INT2 grouped GEMM foundation Sep 30, 2026
@jiafatom
David Fan (jiafatom) requested a balanced review from Copilot September 30, 2026 17:46

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

The new entry point can admit insufficiently aligned caller buffers into vectorized CUTLASS accesses.

Review effort: Balanced
Findings: 1 High severity

Open (1)

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

🔵 Needs a closer look

The low-level CUDA kernel and latest alignment changes still require completed legacy and plugin build/runtime validation.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@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 commit 2edffb5ee626c5c4ca9e702cd040f7952360c3db. Requesting changes for one remaining P2 alignment-contract issue, detailed in the follow-up on the existing alignment thread.

Validation: a compiled host-side harness exercised the actual CUTLASS can_implement for FP16/BF16 and both tactics. Current GitHub CUDA/plugin checks pass. I did not rerun the SM80 numerical or sanitizer suite because the local GPUs are H200/SM90.

@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 d3d60acf26. The earlier alignment P2 is resolved: tensor_aligned now checks pointer alignment in bytes (alignment * sizeof_bits<Element> / 8) and stride in elements. This loosens the over-strict UINT4 (32 B) and UINT2 (64 B) pointer checks to the actual 16 B access width and tightens FP16/BF16 A/C/D from 8 B to the 16 B that the vectorized loads/stores need, so no valid production configuration is newly rejected. Per-expert B offsets already use int64_t.

Remaining items are non-blocking (inline):

  • Coverage: the numerical tests require device 8.0 exactly, but CUDA and plugin CI run on A10 (SM86), so they all skip in CI, including the W4/W8 regressions for the shared alignment change.
  • Build: the plugin test object library recompiles about 15 CUTLASS TUs, mostly for test-only baselines, and adds diagnostic suppressions without the CUDA 13 gate.
  • Rebase: the moe_quantization.cc change is already on main via #32932.
  • Nit: the new files are missing the Microsoft copyright/MIT license header.

I did not rerun the SM80 numerical or sanitizer suites for this round.

Comment thread cmake/onnxruntime_unittests.cmake Resolved
Comment thread cmake/onnxruntime_unittests.cmake Resolved
@jiafatom
David Fan (jiafatom) merged commit a4d55e2 into main Oct 1, 2026
95 of 96 checks passed
@jiafatom
David Fan (jiafatom) deleted the jiafa/int2-qmoe-sm80-grouped-gemm branch October 1, 2026 17:29
David Fan (jiafatom) added a commit that referenced this pull request Oct 1, 2026
…4a2) (#33045)

## Summary

PR4a2 follows up the five non-blocking comments from the final review of
#32963 at `d3d60acf26`. Based on main after #32963 was squash-merged as
`a4d55e2b6f`. No production kernel math, alignment contract, or runtime
eligibility changes.

## Review Follow-ups

- **Coverage**
([comment](#32963 (comment))):
allow compute capability major 8 in the direct-kernel fixture instead of
exactly 8.0. Test parameters continue to select the SM80 implementation.
Legacy W4/W8 alignment regressions remain enabled, removing their
fixture-level skip on A10/SM86.
- **Plugin build footprint**
([comment](#32963 (comment))):
restrict dense/W4/W8 baselines and disabled benchmarks to legacy tests;
add source-specific plugin/adapter definitions. Reduce plugin
internal-test contrib implementation sources from 16 to 4, retaining the
adaptor needed for INT2 unpacking. Plugin retains INT2 FP16/BF16
numerical, metadata, valid-offset and misalignment coverage. No measured
CI wall-time improvement is claimed.
- **Diagnostic options**
([comment](#32963 (comment))):
remove unconditional duplicate suppressions. CUDA 13+ inherits 970/2189
from shared options. CUDA <13 retains target-local suppressions with an
explanatory comment because the internal INT2 TU exposes third-party
Abseil/Protobuf header diagnostics under warning-as-error. Removing them
reproduced a CUDA 12.8 compile failure; the explicit version guard fixes
it.
- **Rebase**
([comment](#32963 (comment))):
#32932 is already in the main base. This PR does not replay that fix and
has no `moe_quantization.cc` diff.
- **Headers**
([comment](#32963 (comment))):
add the requested Microsoft copyright/MIT license headers to the helper
header, implementation CU and test TU.

## Validation

All compilation, formatting, CMake checks and GPU tests ran inside
Docker `jiafa-dev` on an A100-SXM4-80GB.

| Check | Result |
| --- | --- |
| CUDA 12.8 plugin standalone harness | 16 passed, zero skipped |
| CUDA 12.8 legacy standalone harness | 23 enabled passed, zero skipped;
3 benchmarks disabled |
| CUDA 13.0 plugin standalone harness | 16 passed, zero skipped |
| Fresh legacy/plugin test TU compilation with CUDA 12.8 and 13.0
headers | Passed |
| Fresh four plugin implementation CUs with CUDA 12.8 and 13.0,
warning-as-error | Passed |
| Fresh legacy INT2 implementation CU, CUDA 12.8 | Passed |
| Scoped execution of actual CMake source/definition and option blocks |
Passed; four contrib sources enabled / zero disabled, exactly one
suppression per diagnostic per toolkit |
| Symbol audit | Plugin baseline dependencies/benchmarks absent; legacy
coverage retained |
| Production body comparison | Unchanged apart from requested headers |
| clang-format dry run and git diff --check | Passed |

### Limitations

- Focused standalone validation, not a full top-level CMake
configure/build or provider integration run. Existing Ninja recipes were
adapted to compile current sources and new options into fresh objects;
actual changed CMake blocks were extracted and executed with capture
functions. Full target integration remains for CI.
- Plugin harnesses freshly compile the test and all four contrib
implementation CUs, reusing unchanged core/static libraries and CUDA
support objects. Legacy freshly compiles the test and INT2 CU, reusing
unchanged baseline kernel objects and standalone plugin support shims.
- Only SM80 hardware was available. SM86/SM89 execution, Windows, full
regression and sanitizer runs were not exercised for this follow-up.
CUDA 13 legacy test compilation passed; legacy runtime execution used
CUDA 12.8.
- Exact local commands and compiler/test logs are recorded under
`build/pr4a2-review-validation/` on the validation machine; these
ignored scratch artifacts are not committed.
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.
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