Skip to content

[CUDA] Add an opt-in FP8 DeepGEMM MoE decode path for QMoE - #32122

Merged
Tianlei Wu (tianleiwu) merged 12 commits into
mainfrom
tlwu/20260816/qmoe_deepgemm
Sep 4, 2026
Merged

[CUDA] Add an opt-in FP8 DeepGEMM MoE decode path for QMoE#32122
Tianlei Wu (tianleiwu) merged 12 commits into
mainfrom
tlwu/20260816/qmoe_deepgemm

Conversation

@tianleiwu

@tianleiwu Tianlei Wu (tianleiwu) commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Description

Adds an opt-in DeepGEMM-based MoE decode path for QMoE, behind ORT_QMOE_FP4_DEEPGEMM (default off). The DSV4 MoE decode GEMMs are DRAM bound, so the prepacked weight format sets their cost; this path stores an fp8 e4m3 mirror of the MXFP4 weights with a power-of-two block scale and runs DeepGEMM's sm90 fp8 masked grouped kernel.

Measured 11.5% lower decode step time on 8xH200 and 32 GiB per rank of weights freed relative to the bf16 mirror it replaces.

Summary of Changes

Build

File Change
cmake/external/deep_gemm.cmake New. Header-only FetchContent of deepseek-ai/DeepGEMM.
cmake/onnxruntime_providers_cuda.cmake Include the dependency and route deep_gemm_sm90.cu to the SM90 TMA target.
cmake/onnxruntime_providers_cuda_plugin.cmake Same wiring for the plugin EP build.
cmake/onnxruntime_cuda_source_filters.cmake Classify deep_gemm_sm90.cu as SM90-only.

Kernels and dispatch

File Change
onnxruntime/contrib_ops/cuda/llm/moe_gemm/deep_gemm_sm90.{cu,h} New. Pack / masked grouped FC1 / interleaved SwiGLU / FC2 / unpack, with the validated 32-local-expert (eight-rank) configuration instantiated.
onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.{cu,h} New. MXFP4 → fp8 e4m3 prepack with power-of-two block scales.
onnxruntime/contrib_ops/cuda/moe/moe_quantization.{cc,h} Gate and dispatch the path; the FP4 GEMV keeps priority when DeepGEMM is not selected.
onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_kernels.{cu,h} Supporting entry points.
docs/contrib_ops/cuda/moe_qmoe.md Document the env var, the weight format, and the memory cost.

Testing

  • python test_qmoe_fp4_cuda.py — passes with the path both enabled and disabled.
  • Weight conversion is bit exact. An E2M1 code carries at most two significant bits and e4m3 carries four, so a power-of-two block scale only shifts exponents and never disturbs a mantissa. Checked over all 256 experts of all 46 layers: every weight reproduces the fp32 dequantization bitwise, with no underflow and no clipping.
  • Padded-row skip verified against a poisoned workspace (NaN, ±Inf, 1e30 across 128 patterns, ragged and zero counts): the compact output is identical even though 40–60% of padded FC2 rows become NaN.
  • MMLU-Pro (800 samples) 0.660 vs 0.6625 baseline — 2 disagreements, McNemar p=0.48. Rank argmax disagreements 0.
  • Builds clean with and without NCCL; deep_gemm_sm90.cu compiles into the SM90 TMA target.

Motivation and Context

Per decode step on 8xH200, prompt 1024 / generation 512:

Metric Before After
Step time 27.59 ms 24.43 ms (−11.5%)
FC1 GEMM (standalone) 70.9 us 37.9 us
FC2 GEMM (standalone) 38.6 us 22.3 us

Smaller tiles also soften the wave-quantization cliff: going from 8 to 9 active experts costs bf16 35% but fp8 only 18%.

Two things reviewers should know:

  1. The conventional amax/448 block scale is not usable here. It is not a power of two, so every weight would acquire a full mantissa before being rounded back to three bits (4.8% max relative error). The power-of-two scale is load-bearing for exactness, not an optimization. Activations do use amax/448 per token per 128-channel chunk, where the full e4m3 range is worth more than an exact scale.
  2. The path is off by default and gated to a narrow shape. It requires sm90, SwiGLU fusion, no bias, and matching hidden/inter sizes. At four ranks (64 experts), the e4m3 mirror would cost 64.5 GiB per rank over 43 layers and exhaust memory during PrePack, so this configuration is excluded from the DeepGEMM gate and continues through standard QMoE. This is documented in moe_qmoe.md.

The main reviewer-facing consideration is the new third-party FetchContent dependency; it is header-only and confined to the SM90 build.

Checklist

  • Tests added/updated
  • No breaking changes (new path is opt-in and off by default)
  • Documentation updated

Gate a persistent-BF16 SM90 masked grouped GEMM path behind ORT_DSV4_FP4_DEEPGEMM=1. K=6 verify improves from 27.040 to 21.043 ms; real-text decode improves from 19.0 to 17.1 ms/step with zero rank disagreements. Direct FC1/FC2 comparison is bit-exact to cuBLAS.
The decode MoE chain lays tokens out as [32 experts, 64 padded rows, K] but
only masked_m[expert] rows are real, typically 1-3 of 64. PackInputKernel and
InterleavedSwiGLUKernel both walked the full padded extent, so their cost was
constant regardless of how much work there actually was: ncu measured them at
a flat 11.0 us and 14.4 us per layer with DRAM SOL of 0.18% and 20.9%.

Bound both loops by the per-expert row count instead. The padded rows are now
left stale rather than zero-filled, which is safe because the GEMMs are
row-independent and UnpackOutputKernel copies back only the first count rows.
This was verified bitwise against a poisoned workspace (NaN, +/-Inf, 1e30
across 128 patterns, ragged and zero counts): the compact output is identical,
even though 40-60% of the padded FC2 rows do become NaN. Also give SwiGLU a
2D grid over experts and load the adjacent gate/linear pair as one
__nv_bfloat162; that part is an exact refactor, bit-identical on valid rows.

The two kernels now scale with real work and sit within 0.35 us of the empty
kernel launch floor: 1.55 us and 1.51 us at the realistic decode point.

Bounding by count removes an implicit clamp the old padded loop bound gave for
free, so pin the invariant with a static_assert alongside the alignment one the
vectorized load now needs.

DeepSeek-V4-Flash, 8xH200, world 8, 1024 prompt / 128 gen:
decode 92.12 -> 96.23 tps, 10.86 -> 10.39 ms/token. Acceptance rate 0.329 and
2.65 tokens/step are unchanged, and rank argmax disagreements stay 0.
The DSV4 MoE decode GEMMs are DRAM bound, so the prepacked weight format
sets their cost. TryBuildDsv4DeepGemmWeights dequantized the stored MXFP4
weights all the way to bf16, moving 2 bytes per weight. Convert to fp8
e4m3 with a per-[128 N, 128 K] block scale instead and run DeepGEMM's
sm90 fp8 masked grouped kernel.

The conversion is bit exact. An E2M1 code carries at most two significant
bits and e4m3 carries four, so a *power-of-two* block scale only shifts
exponents and never disturbs a mantissa. Checked over all 256 experts of
all 46 layers: every weight reproduces the fp32 dequantization bitwise,
with no underflow and no clipping. The headroom is the group exponent
spread within a block, which may reach 14 binades; the measured maximum
is 6. Note the conventional amax/448 block scale is *not* usable here:
it is not a power of two, so every weight would acquire a full mantissa
before being rounded back to three bits (4.8% max relative error).

The sm90 fp8 kernel takes both operands in fp8, so activations are
quantized too. That work folds into the existing pack and SwiGLU kernels,
which already read and write exactly this data, so it is nearly free.
Activations use an amax/448 scale per token per 128-channel chunk, where
the full e4m3 range is worth more than an exact scale.

Measured on 8xH200 at prompt 1024 / generation 512, per decode step:
27.59 ms -> 24.43 ms, a 11.5% reduction, and 32 GiB per rank of weights
freed. Standalone the FC1 GEMM goes 70.9 -> 37.9 us and FC2 38.6 -> 22.3
us. Smaller tiles also soften the wave quantization cliff: going from 8
to 9 active experts costs bf16 35% but fp8 only 18%.

MMLU-Pro (800 samples) is unchanged at 0.660 vs 0.6625, 2 disagreements,
McNemar p=0.48. Rank argmax disagreements remain 0.
… 64)

A DSV4 rank owns 256 / world experts, so an eight-rank export has 32 and a
four-rank one has 64. kNumExperts was hardcoded to 32, which silently routed
every four-rank decode to the generic FP4 grouped GEMM. Both counts are now
instantiated and the count travels as a runtime argument.

The masked grouped GEMM does not care how many experts there are: its scheduler
takes num_m_blocks = ceil_div(masked_m[g], BLOCK_M), so an expert with no rows
contributes no tile. Only the workspace, the tensor-map extents and the pack /
SwiGLU / unpack grids scale with the count.

This does not make 64 experts usable on a 141 GiB H200. DeepGEMM reads an e4m3
mirror of the MXFP4 weights, 1 byte per weight against the checkpoint's 0.5,
which at 64 experts is 1,536 MiB per MoE layer and 64.5 GiB per rank over 43
layers -- on top of the SM80-interleaved e2m1 that prefill still reads. The
session dies in PrePack. ORT_DSV4_FP4_DEEPGEMM must stay 0 at four ranks until
a grouped GEMM reads the 4-bit weights directly.
Copilot AI balanced review requested due to automatic review settings August 17, 2026 01:54

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.

Pull request overview

Adds an opt-in FP8 DeepGEMM decode path for fixed-shape CUDA QMoE workloads on Hopper GPUs.

Changes:

  • Converts MXFP4 expert weights into persistent E4M3 buffers.
  • Adds masked grouped FP8 FC1/FC2 kernels and dispatch.
  • Integrates DeepGEMM and documents configuration.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
cmake/external/deep_gemm.cmake Fetches DeepGEMM.
cmake/onnxruntime_providers_cuda.cmake Configures bundled CUDA build.
cmake/onnxruntime_providers_cuda_plugin.cmake Configures CUDA plugin build.
cmake/onnxruntime_cuda_source_filters.cmake Classifies the SM90 source.
docs/contrib_ops/cuda/moe_qmoe.md Documents the new path.
onnxruntime/contrib_ops/cuda/llm/moe_gemm/deep_gemm_sm90.cu Implements FP8 grouped GEMMs.
onnxruntime/contrib_ops/cuda/llm/moe_gemm/deep_gemm_sm90.h Declares fixed-shape interfaces.
onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_kernels.cu Adds workspace and execution dispatch.
onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_kernels.h Extends runner state and interfaces.
onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc Adds gating, prepack, and routing.
onnxruntime/contrib_ops/cuda/moe/moe_quantization.h Stores configuration and packed buffers.
onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.cu Implements MXFP4-to-E4M3 conversion.
onnxruntime/contrib_ops/cuda/moe/qmoe_kernels.h Declares the conversion launcher.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmake/external/deep_gemm.cmake Outdated
Comment thread cmake/onnxruntime_providers_cuda.cmake Outdated
Comment thread cmake/onnxruntime_providers_cuda_plugin.cmake Outdated
Comment thread onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc Outdated
Comment thread onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc Outdated
Comment thread docs/contrib_ops/cuda/moe_qmoe.md Outdated
Comment thread docs/contrib_ops/cuda/moe_qmoe.md Outdated
Comment thread docs/contrib_ops/cuda/moe_qmoe.md Outdated
Comment thread docs/contrib_ops/cuda/moe_qmoe.md Outdated
Comment thread docs/contrib_ops/cuda/moe_qmoe.md Outdated

@github-actions github-actions Bot 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.

You can commit the suggested changes from lintrunner.

Comment thread onnxruntime/contrib_ops/cuda/moe/moe_quantization.cc Outdated
Factor the arbitrary per-expert global scale out before FP8 quantization so power-of-two scaling remains bit-exact. Fixes the CUDA and TensorRT QMoEFp4ToFp8RoundsScaleUp failure.
@hariharans29

Copy link
Copy Markdown
Member

Review — PR #32122: [CUDA] Add an opt-in FP8 DeepGEMM MoE decode path for QMoE

Scope

Large PR (17 files, +1121/-9) adding an opt-in specialized decode path for QMoE on Hopper. Behind ORT_QMOE_FP4_DEEPGEMM, default off. Fixed-shape gate: SM90 + ≥120 GiB (H200), MXFP4 W4A16, BF16 activation/output, hidden=4096, inter=2048, k=6, activation=swiglu + swiglu_fusion=1, 32 or 64 local experts, num_tokens ≤ 8, TP=EP=cluster=1, no bias, no AWQ/groupwise/wo scales. Every miss falls back transparently to the standard MXFP4 dispatch.

Author is a trusted CUDA EP contributor and the accuracy validation (MMLU-Pro 800 samples, McNemar p=0.48, 0 rank-argmax disagreements, per-weight bit-exact round-trip check across all 256 × 46 experts) is thorough. Testing story is unusually strong for a change of this size.

Reviewing across four axes:

  1. Correctness of the MXFP4 → E4M3 quantization theory.
  2. Runtime dispatch gating and fallback safety.
  3. Third-party dependency addition (DeepGEMM).
  4. Build integration and lint / cross-platform concerns.

Axis 1 — MXFP4 → E4M3 correctness (the load-bearing insight)

The PR description spells out why the conventional amax/448 scale is unusable and why the power-of-two block scale is load-bearing for exactness, not an optimization. Walking the math:

  • E2M1 encodes ≤ 2 significant mantissa bits.
  • E4M3 encodes ≤ 4 significant mantissa bits.
  • A power-of-two block scale only shifts the exponent field — it never touches the mantissa.
  • Therefore for any E2M1 value v with block scale s = 2^n, s · v is representable in E4M3 iff s · v fits in E4M3's dynamic range and its mantissa's leading nonzero bits still fit in ≤ 4 significant bits — the mantissa constraint holds trivially because E2M1's mantissa is already ≤ 2 bits.
  • Losslessness fails only when block-level MXFP4 group exponents span more than E4M3's usable range (~14 exponent codes with E2M1's 2 exponent bits factored in). Measured spread on DeepSeek V4: 6. Well inside the budget.

The proof discipline is correct. The runtime inexact_flag verification in the second kernel pass is the critical safety net: exact = exact && (static_cast<float>(quantized) * block_scale == value) per-element. If any weight fails to round-trip, inexact_flag is set to 1. Critical follow-up: what does moe_quantization.cc do when it sees inexact_flag == 1? A silent path-selection collapse (falling back to standard MXFP4) would be acceptable; a silent continue with corrupted weights would be a critical bug. My workspace snapshot doesn't yet have the moe_quantization.cc handler for TryBuildFp4DeepGemmWeights, so I can't verify from the diff. Please confirm in the PR description or a code comment that inexact_flag == 1 at PrePack results in either (a) a hard ORT_ENFORCE failure of session init, or (b) a documented fall-through to the standard MXFP4 dispatch with a warning log. This is the one place where a subtle bug could ship silently.

The kernel implementation itself in qmoe_kernels.cu:

  • Pass 1 (QMoEFp4ToFp8BlockScaleKernel): one CUDA block per [128 N, 128 K] weight block, BlockReduce for amax, and the power-of-two rounding via frexpf + ldexpf with the mantissa == 0.5f adjustment (which rounds down when amax is exactly at a power-of-two boundary — i.e., frexpf returns 0.5 exactly and we want the next-lower exponent). Correct handling of the boundary case. ✓
  • Pass 2 (QMoEFp4ToFp8WeightsKernel): 16-byte uint4 vector store per warp per 16-K stride matches the alignment of the DeepGEMM SFB layout. The #pragma unroll on the K=16 loop is aggressive but bounded. static_assert(32 % kQMoEFp8VecK == 0) guards the "one thread's kQMoEFp8VecK values share one MXFP4 group scale" invariant — good structural guard.
  • Preservation of the arbitrary per-expert global_scale outside the power-of-two block scale is correct: only the block-level factor gets rounded, the expert-level factor stays as fp32, so the effective per-weight scale retains its full precision.

One micro concern on the launcher's ORT_ENFORCE:

ORT_ENFORCE(n % kQMoEFp8BlockN == 0 && k % kQMoEFp8BlockK == 0 && (n % kQMoEFp8TileN) == 0, ...)

Throws OnnxRuntimeException on shape mismatch. Called from PrePack, so a throw aborts session init — acceptable. But given n = kHiddenSize = 4096 and kQMoEFp8BlockN = kQMoEFp8TileN = 64/128 are compile-time constants, this can never fire in the gated path. Fine as a defensive guard.

Axis 2 — Dispatch gating (both static and runtime)

Static gate lives in getWorkspaceBufferSizes and mirrors the runtime gate in runMoe:

if (use_fp4_deep_gemm_ && num_rows > 0 && num_rows <= deep_gemm_sm90::kMaxTokensPerExpert &&
    hidden_size == deep_gemm_sm90::kHiddenSize && inter_size == deep_gemm_sm90::kInterSize &&
    deep_gemm_sm90::NumExpertsSupported(num_experts_per_node) && experts_per_token == 6 &&
    activation_type == ActivationType::Swiglu && !use_awq) {
  fp4_deep_gemm_workspace_size = deep_gemm_sm90::GetWorkspaceSize(num_experts_per_node);
}

The runtime gate in runMoe adds several more conditions on top: fc1/fc2_expert_biases == nullptr, input_sf == nullptr, parallelism_config.{tp_size,ep_size,cluster_size} == 1, quant_params.groupwise.group_size <= 0, and both quant_params.wo.fc{1,2}_weight_scales == nullptr. The workspace-sizer gate is the superset — any run-gate miss falls through to the standard dispatch with an unused-but-allocated workspace. That's a small wasted allocation but semantically safe.

Small inconsistency worth flagging: the sizer gate checks activation_type == ActivationType::Swiglu but the runtime gate checks activation_params.swiglu_fusion == 1 in addition. This means an unfused-swiglu call reserves workspace it won't use. Not a bug, just a wart — the sizer could tighten the swiglu_fusion check too if the field is available at that layer, and eliminate the wasted allocation. Non-blocking.

Runtime path: deep_gemm_sm90::Run handles pack → quantize → FC1 → SwiGLU → FC2 → unpack in one launch sequence. Standard structure. The sync_check_cuda_error(stream) before return is the only thing between the DeepGEMM path and the fallback, so failure recovery is limited to error propagation. Fine.

Axis 3 — Third-party dependency (DeepGEMM)

New addition to cmake/deps.txt:

deep_gemm;https://github.com/deepseek-ai/DeepGEMM/archive/559d79fb6994a58b8a15b4b93bf13ccc16edf247.tar.gz;76a0076386991cac8e5d32c3e7e74d9bb8102115

Concerns:

  1. Pin is to an arbitrary git SHA rather than a release tag. DeepGEMM's release cadence is fast and the API is not stable yet; a SHA pin is defensible for a young library, but a refs/tags/v* pin would be safer if one is available. If not, the ThirdPartyNotices entry (Copyright (c) 2025 DeepSeek, MIT) should document the exact commit for legal traceability, which it doesn't currently do — just the license text. Add the SHA to the notice. ThirdPartyNotices needs commit-level attribution to comply with the "source distribution" requirement of many MIT-derivative audits.
  2. Header-only + include(deep_gemm) guarded by ORT_HAS_SM90_OR_LATER AND NOT onnxruntime_CUDA_MINIMAL AND NOT onnxruntime_DISABLE_CONTRIB_OPS — correct. Non-SM90 builds don't fetch, ensuring the dep doesn't leak into unrelated CI legs. ✓
  3. Xcompiler=/wd4068 / Xcompiler=-Wno-unknown-pragmas on deep_gemm_sm90.cu — DeepGEMM uses pragmas that MSVC/GCC don't recognize. Standard for CUDA third-party code. Fine.
  4. CMP0169 policy handling in deep_gemm.cmake — correct pattern for newer CMake versions where FetchContent_Populate becomes deprecated.
  5. cgmanifest.json addition — I don't see it in the diff. Any new third-party dep should be added to cgmanifests/cgmanifest.json too for Microsoft's SBOM compliance. Please add.

Axis 4 — Build integration and lint

  • Cpplint flagged include order in deep_gemm_sm90.h (3 warnings) and qmoe_kernels.cu (1 warning). Google style wants C system headers before C++ system headers. All are #include <cuda_bf16.h> / <cuda_fp8.h> / <cuda_runtime_api.h> — reorder to precede <cstddef> / <cstdint> etc.
  • The moe_quantization.cc prior lintrunner warning (commit 5d78d93) was addressed by 30eebd9. ✓
  • Test file wired into onnxruntime_unittests.cmake under the CUDA-plugin-internal-test list, correctly gated by NOT onnxruntime_DISABLE_CONTRIB_OPS. ✓

Documentation quality

§9.12 of moe_qmoe.md is unusually thorough:

  • Full constraint matrix (11 rows).
  • Execution sequence (4 numbered steps).
  • Memory cost math (0.75 GiB per node/rank + 44.4 MiB per invocation, with a call-out that the standard FP4 buffers may still coexist).
  • File pointers into the implementation.

The one gap: the 64-expert (4-rank) variant is instantiated (kNumExpertsWorld4 = 64) but not documented in the constraint table (which shows 32 experts). The 4-rank case is mentioned in the header comment ("eight and four ranks are the supported splits") but readers of the docs alone will miss it. Add a row or a footnote for the 64-expert variant — otherwise a downstream user with a 4-rank deployment might not realize the path can serve them.

Also flagged in the description: "At four ranks (64 experts) the e4m3 mirror costs 64.5 GiB per rank over 43 layers and the session dies in PrePack, so ORT_DSV4_FP4_DEEPGEMM must stay 0 there". If this constraint is real, the runtime gate should probably reject the 4-rank config outright with a clear error, rather than letting session init OOM. Or at minimum, the doc should call this out as a hard-warning to users. Currently the header comment says "both are instantiated" without noting the 4-rank case is memory-infeasible. Reconcile the header, doc, and the code's actual capability: either remove kNumExpertsWorld4 and its NumExpertsSupported case, or add a memory-budget gate at PrePack that produces a clear error when the E4M3 mirror can't fit.

Test coverage

Three new unit tests in qmoe_fp4_to_fp8_kernel_test.cc:

  1. QMoEFp4ToFp8PreservesExactPowerOfTwoScale — global_scale = 448/6, expects block scale = 448/(6·64). Exact power-of-two, inexact == 0.
  2. QMoEFp4ToFp8RoundsScaleUp — global_scale = 300/6, expects block scale = 300/(6·64). Non-power-of-two amax but scale still rounds correctly. Verified inexact == 0 because a single-scale block has zero exponent spread.
  3. QMoEFp4ToFp8ReportsWideExponentSpread — sets one exceptional group scale to UE8M0 code 147, spanning 2^20 exponent difference. inexact == 1. Confirms the safety net fires.

The three tests together validate the arithmetic + safety net. Not covered by unit tests: multi-block and multi-expert quantization (though the kernel is per-block per-expert, so 1-block/1-expert is representative), interaction with the masked_m row counts, and the FC1/SwiGLU/FC2 sequence itself. The larger integration is covered by the Python E2E test the author references (test_qmoe_fp4_cuda.py) which passes with the path both enabled and disabled — good.

One test-quality nit: EXPECT_FLOAT_EQ is used everywhere. For the power-of-two scale, EXPECT_EQ on the bit pattern would be even stronger (since the promise is bit-exactness, not float-close). Non-blocking.

API surface additions

Two new virtual methods on CutlassMoeFCRunnerInterface:

virtual void setUseFp4DeepGemm(bool /*use_fp4_deep_gemm*/) {}
virtual void setFp4DeepGemmWeightScales(const float* /*fc1_scales*/, const float* /*fc2_scales*/) {}

Default no-op in the base, real impl in CutlassMoeFCRunner. Mirrors the pre-existing setUseSm80Fp4 pattern. ✓

CI status

88/91 checks OK on 176e4cd. 3 non-green. Given the pattern in other recent PRs, these are almost certainly the standard bot-scaffolding (Azure Pipelines /azp run gate, doc gen, license/cla). But for a change touching CUDA build integration + new third-party dep, worth explicitly confirming the Windows GPU CUDA CI leg is green — this is exactly the CI leg that would fail on include-order or FetchContent issues, and it wasn't obviously broken in the annotations I could see.

Environment variable name change

ORT_DSV4_FP4_DEEPGEMMORT_QMOE_FP4_DEEPGEMM between commits. Good rename — drops the model-family-specific DSV4 prefix in favor of the generic op name, which matches ONNX Runtime's env-var naming conventions elsewhere. However, the PR description still uses the old name in "so ORT_DSV4_FP4_DEEPGEMM must stay 0 there"; this is a stale line in the PR description that will confuse users trying to find the flag. Update the PR description before merge — the flag name is ORT_QMOE_FP4_DEEPGEMM.

Summary of items to address before merge

Blocking:

  1. Confirm/document the inexact_flag == 1 handling in moe_quantization.cc's TryBuildFp4DeepGemmWeights — this is the safety net for the entire bit-exactness argument. Silent continue would be a correctness bug.
  2. Add DeepGEMM to cgmanifests/cgmanifest.json for SBOM compliance.

Non-blocking but strongly recommended:

  1. Reconcile the 64-expert (4-rank) case: header claims both 32 and 64 are instantiated; doc only describes 32; PR description says 4-rank OOMs in PrePack. Either add a PrePack memory-budget guard with a clear error, or remove kNumExpertsWorld4 entirely if it's not actually supportable. As-is, a 4-rank deployment enabling the flag would fail confusingly.
  2. Cpplint include-order fixes in deep_gemm_sm90.h and qmoe_kernels.cu — 4 warnings total, mechanical.
  3. Add the commit SHA to the ThirdPartyNotices entry for DeepGEMM (currently only the license text is present).
  4. Update the PR description to use ORT_QMOE_FP4_DEEPGEMM (not the old ORT_DSV4_FP4_DEEPGEMM).
  5. Confirm Windows GPU CUDA CI leg is green on tip commit before merge.

Nice to have:

  1. Document the 4-rank / 64-expert case in §9.12 (either as supported with a memory footnote, or as explicitly unsupported).
  2. Tighten getWorkspaceBufferSizes static gate with the swiglu_fusion == 1 check that the runtime gate uses, to avoid the small wasted workspace allocation on unfused-swiglu paths.
  3. EXPECT_EQ on bit pattern in the "PreservesExactPowerOfTwoScale" test to strengthen the exactness claim.

Recommendation

Approve — pending #1 (the inexact-flag handling documentation/confirmation) and #2 (cgmanifest.json addition). Everything else is polish. The core theory (power-of-two block scale → bit-exact E2M1 → E4M3 conversion) is correct, the safety net is in place, the gates are conservative, the documentation is thorough, and the accuracy validation is rigorous. Well-executed opt-in specialization.

The bit-exact PrePack conversion trick is a genuinely nice insight — it's the kind of numerical-representation reasoning that's worth generalizing. Worth writing up as a design note or blog post separately.

@tianleiwu
Tianlei Wu (tianleiwu) requested a review from a team as a code owner August 31, 2026 20:00
@tianleiwu

Copy link
Copy Markdown
Contributor Author

Thanks Hariharan Seshadri (@hariharans29) for the detailed review. I addressed the ten follow-ups in 85eb5bb3 (and updated the PR description):

  1. Inexact conversion fails closed: TryBuildFp4DeepGemmWeights now enforces inexact == 0 during PrePack, so session initialization fails before the staged MXFP4 inputs are released; an inexact FP8 mirror cannot be used silently.
  2. SBOM: added the pinned DeepGEMM commit to cgmanifests/cgmanifest.json.
  3. 64-expert configuration: removed the 64-local-expert instantiation/support claim. The DeepGEMM gate now accepts only 32 local experts; 64 experts use the standard QMoE path instead of attempting the memory-infeasible mirror.
  4. Lint: fixed the CUDA/system include ordering in deep_gemm_sm90.h and qmoe_kernels.cu.
  5. Third-party notice: added the exact DeepGEMM commit SHA to ThirdPartyNotices.txt.
  6. PR wording: replaced the stale ORT_DSV4_FP4_DEEPGEMM reference with ORT_QMOE_FP4_DEEPGEMM and reconciled the 64-expert wording.
  7. Windows CUDA CI: the new-tip Windows GPU CUDA CI Pipeline is currently queued at https://github.com/microsoft/onnxruntime/actions/runs/33433704689/job/99624950141. I am treating it, including its Test Job once created, as a before-merge gate and am not relying on old-tip results.
  8. Docs: section 9.12 now explicitly documents 32 local experts as the supported DeepGEMM shape and 64 experts as standard-QMoE fallback.
  9. Workspace sizing: threaded swiglu_fusion into getWorkspaceBufferSizes and require fusion mode 1 before reserving DeepGEMM workspace.
  10. Bit-exact test: the exact-power-of-two case now compares the IEEE-754 bit patterns with EXPECT_EQ.

Validation on the feedback commit:

  • clang-format, cpplint, SBOM registration, ThirdPartyNotices SHA, and whitespace checks passed.
  • CUDA provider build passed for SM80, and the refreshed CUDA 13 provider-test build passed with both SM80 and SM90 enabled.
  • On A100/SM80, exactly these three nested CUDA internal tests ran and passed: QMoEFp4ToFp8PreservesExactPowerOfTwoScale, QMoEFp4ToFp8RoundsScaleUp, and QMoEFp4ToFp8ReportsWideExponentSpread.

@tianleiwu
Tianlei Wu (tianleiwu) merged commit aeb1a16 into main Sep 4, 2026
106 of 109 checks passed
@tianleiwu
Tianlei Wu (tianleiwu) deleted the tlwu/20260816/qmoe_deepgemm branch September 4, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants