Fix QMoE SM80 FP4 init order: don't let the tactic seed throw - #32194
Fix QMoE SM80 FP4 init order: don't let the tactic seed throw#32194Gopalakrishnan Nallasamy (GopalakrishnanN) wants to merge 1 commit into
Conversation
CutlassMoeFCRunner's constructor seeds a default GEMM tactic with the
static getTactics(sm_), which resolves to getConfigs(sm, use_sm80_fp4)
with use_sm80_fp4 defaulting to false.
On SM80 with wfp4a16 that provisional view is wrong. It skips the early
`return {}` in getTmaWarpSpecializedConfigs() and reaches the ORT_THROW:
TMA WS grouped MoE GEMM for SM80 is not compiled, and this QMoE
configuration has no SM80 fallback
The throw fires during construction, before QMoE can push the real
decision via setUseSm80Fp4() (moe_quantization.cc), so the SM80 FP4
grouped-GEMM path is unreachable on Ampere and every MXFP4 MoE model has
to fall back to the dequant path with ORT_FP4_SM80_GEMM=0.
The constructor query is only a best-effort default seed; the code
already tolerates an empty result via `if (!tactics.empty())`. Throwing
there is the defect, so the seed is now non-fatal. setUseSm80Fp4()
immediately re-queries and re-picks: with use_sm80_fp4=true the TMA WS
list is empty by design and getAmpereConfigs() supplies real tactics. A
genuinely unsupported configuration still throws later at dispatch.
Validated on A100-80GB (SM80) with openai/gpt-oss-20b MXFP4 exported for
paged attention. Before: the SM80 path threw at session init. After: the
model runs on the SM80 grouped GEMM with peak GPU memory 24149 MiB vs
~49800 MiB on the ORT_FP4_SM80_GEMM=0 dequant fallback, a 2.1x
reduction, with exact output parity between concurrent and isolated
runs.
There was a problem hiding this comment.
Pull request overview
Prevents provisional tactic selection from blocking SM80 FP4 QMoE initialization.
Changes:
- Makes constructor tactic seeding non-fatal.
- Allows
setUseSm80Fp4()to select valid Ampere tactics afterward.
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| try { | ||
| auto tactics = getTactics(sm_); |
| gemm1_config_ = tactics[0]; | ||
| gemm2_config_ = tactics[0]; | ||
| } | ||
| } catch (const std::exception&) { |
There was a problem hiding this comment.
This shared constructor is also instantiated by the standard MoE path and by QMoE's dense fallback runners; those callers never invoke setUseSm80Fp4(). The comment's "immediately ... re-picks" invariant therefore holds only for the primary WFP4A16 QMoE runner. That makes it important to scope the bypass to this provisional WFP4A16/SM80 query (or pass the decision into construction) instead of suppressing failures in shared initialization.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (2)
onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_kernels.cu:2122
- Please add automated SM80 session-initialization coverage for this path. The existing
TestQMoEFP4Sm80SingleWeightCopy._skip_unless_sm80_regime()inonnxruntime/test/python/transformers/test_qmoe_fp4_cuda.py:946-951skips all devices below SM90, so it skips A100/SM80 and would not detect the constructor throw this change is intended to fix.
try {
auto tactics = getTactics(sm_);
onnxruntime/contrib_ops/cuda/llm/moe_gemm/moe_kernels.cu:2127
- This suppresses every failure from tactic generation, not just the expected provisional SM80/WFP4 mismatch.
getConfigs()allocates and copies vectors and can also throw for genuinely unsupported configurations, so an OOM or unrelated configuration defect now leaves both tactic optionals unset and converts a precise initialization failure into a delayed failure. Avoid broad exception swallowing; pass the SM80-FP4 decision into construction (or otherwise bypass only this known WFP4/Ampere seed) so all unrelated exceptions still propagate.
} catch (const std::exception&) {
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
One additional maintainability issue is noted inline. The two existing open threads already cover the broader correctness and regression-test concerns, so I have not duplicated them here.
| gemm1_config_ = tactics[0]; | ||
| gemm2_config_ = tactics[0]; | ||
| } | ||
| } catch (const std::exception&) { |
There was a problem hiding this comment.
Nit: please include <exception> directly now that this translation unit names std::exception. It is currently available only through core/common/common.h -> core/common/exceptions.h, so a change to that internal include chain could break this file unexpectedly.
Problem
The SM80 FP4 grouped-GEMM path is unreachable on Ampere. Any MXFP4 MoE model (e.g.
openai/gpt-oss-20b) throws at session initialization:Root cause — an ordering problem
CutlassMoeFCRunner's constructor seeds a default GEMM tactic:That resolves to the static overload
getConfigs(int sm, bool use_sm80_fp4 = false)— the flag defaults tofalse.On SM80 with
wfp4a16, that provisional view is wrong.getTmaWarpSpecializedConfigs()guards its early exit on the flag:With the flag
falsethe earlyreturn {}is skipped and the throw fires. It fires during construction, before QMoE can push the real decision:So the decision made at L449 can never take effect, and users must fall back to the dequant path via
ORT_FP4_SM80_GEMM=0.Fix
The constructor's query is only a best-effort default seed — the surrounding code already tolerates an empty result via
if (!tactics.empty()). Throwing there is the defect, so the seed is now non-fatal.setUseSm80Fp4()re-queries immediately afterward and re-picks. Withuse_sm80_fp4=true,getTmaWarpSpecializedConfigs()returns{}by design andgetAmpereConfigs()supplies real candidate configs, so bothgemm1_config_andgemm2_config_get valid tactics. A genuinely unsupported configuration still throws later at dispatch.I kept this minimal rather than plumbing the flag through the constructor signature, which would be tidier but touches every
CutlassMoeFCRunnerconstruction site.Validation
A100-80GB (SM80),
openai/gpt-oss-20bMXFP4 exported for paged attention, run through ONNX Runtime GenAI's continuous-batching engine.ORT_FP4_SM80_GEMM=0fallback)2.1x memory reduction, with exact output parity between concurrent and isolated runs (0 mismatches) and stable output across repeats.
Note for reviewers
I validated this empirically as above, but I have not run ONNX Runtime's own MoE/QMoE test suites against the change — worth confirming in CI. I'd also welcome a view on whether the non-fatal seed is the preferred shape here versus passing the SM80 decision into the constructor.