[CUDA] Skip FP4 QMoE fc1 activation expansion - #31479
[CUDA] Skip FP4 QMoE fc1 activation expansion#31479Tianlei Wu (tianleiwu) wants to merge 1 commit into
Conversation
c62b6ae to
f713813
Compare
f713813 to
77dc84d
Compare
77dc84d to
b964e92
Compare
b964e92 to
f46110e
Compare
Multi-model review (5 reviewers: readability, correctness, adversarial, deep/spec, integration)Verdict: the index math is correct — I tried hard to break it and could not. Three reviewers independently confirmed the Confirmed correct (so nobody re-litigates it)
Major1. The The already-merged INT-path equivalent guards the identical rewrite, and even documents the mapping: // moe_gemv.cu:20-36
template <int CtaM>
__device__ __forceinline__ int act_source_row(const int* permuted_row_to_source_row, int num_rows,
int row, int offset_m) {
if (permuted_row_to_source_row == nullptr) return offset_m;
static_assert(CtaM == 1, "source-row indirection assumes one expanded row per block");
return permuted_row_to_source_row[row] % num_rows;
}The FP4 version inlines the expression with neither the guard nor the comment. It is correct today only because Fix: hoist 2. The env knob is parsed on every
Fix: latch it in the constructor as 3. Every other 4. The new parity test cannot distinguish the indirection it exists to protect. With
The shape validates the modulus but not the indirection — which is the entire reason 5. The test has no positive control — it passes green if the fused GEMV never dispatched. The dispatch gate at 6. The header doc now describes the wrong buffer. The doc block still inherits Minor
Questions
Nice result on the perf side, and the fact that the INT path already landed the same transformation makes this a low-risk port. The two things I'd genuinely want before merge are #1 (the guard — it's three lines and prevents a silent-wrong-answer regression) and #4/#5 (make the test actually able to fail). 🤖 Reviewed with a 5-model review team (Claude Opus 5 · GPT-5.3-Codex · GPT-5.6 · Gemini 3.1 Pro), findings verified against the source. |
| f"ORT_FP4_GEMV_DEFAULT_TILING=0 run failed:\n{proc.stdout}\n{proc.stderr}", | ||
| ) | ||
|
|
||
| def test_nvfp4_fp16_gemv_skip_expand_parity(self): |
There was a problem hiding this comment.
From Copilot:
Could we extend this parity test to cover at least one MXFP4 case and one BF16 case as well? The skip-expand implementation is shared across MXFP4/NVFP4 and FP16/BF16 activation paths, but this test currently exercises only FP16 NVFP4. A format- or dtype-specific scale/layout regression could therefore pass unnoticed.
| else: | ||
| os.environ[env_name] = previous_value | ||
|
|
||
| self.assertTrue( |
There was a problem hiding this comment.
From Copilot:
Could we also validate both outputs against the existing dequantized reference, rather than relying only on torch.equal between the two runs? Since both executions share the same generated weights, routing inputs, and GEMV implementation, this assertion primarily verifies that the two paths address the activation rows identically. Comparing each result against the reference would independently confirm the numerical correctness of the skip-expand path as well. If exact equality is intentional here, please add a comment explaining why it is expected and what this assertion is meant to cover.
| static_cast<const T*>(input->DataRaw()), static_cast<T*>(p_act_buf.get()), | ||
| nullptr, nullptr, p_r2u, num_rows, hidden, static_cast<int>(k_), num_experts, | ||
| quant_params, false, p_efto, nullptr, nullptr, nullptr, stream); | ||
| const bool skip_expand = !Fp4GemvSkipExpandDisabled(); |
There was a problem hiding this comment.
From Copilot:
Could we either capture the skip_expand decision at session construction time or document that ORT_DISABLE_FP4_GEMV_SKIP_EXPAND is intentionally read at inference time? Most neighboring FP4 GEMV environment options are latched when the session is created, whereas this option can change between calls. Runtime mutability is useful for A/B testing, but the behavior should be explicit and consistent with the intended configuration model.
| fc1_gemv_sm80_layout, skip_expand ? p_r2u : nullptr, num_rows, stream); | ||
| }; | ||
| auto launch_fc2 = [&](MoeGemvConfig cfg) { | ||
| gemv::launch_moe_gemv_fp4_symmetric<T>( |
There was a problem hiding this comment.
From Copilot:
Could you update this comment to account for the skip-expand path? With skip-expand enabled, FC1 reads directly from the original input using p_r2u rather than from p_act_buf, so the current wording may imply that p_act_buf must be initialized during autotuning.
| f"ORT_FP4_GEMV_DEFAULT_TILING=0 run failed:\n{proc.stdout}\n{proc.stderr}", | ||
| ) | ||
|
|
||
| def test_nvfp4_fp16_gemv_skip_expand_parity(self): |
There was a problem hiding this comment.
From Copilot:
Could we make the routing inputs deterministic for this regression and include cases with repeated source rows and varied expert ordering? The current test relies on one random router result, so it may not exercise all % num_rows mappings. Explicitly covering repeated tokens routed to multiple experts and nontrivial expert orderings would better validate that the permutation map is populated and interpreted correctly.
Description
Stacked on #31159; review only the top commit.
This removes the standalone FP4 QMoE fc1 activation expansion during GEMV decode. Instead, fc1 maps each permuted row back to its source token with
permuted_row_to_source_row[row] % num_rows; fc2 remains unchanged because it consumes the expanded fc1 output.Summary of Changes
ORT_DISABLE_FP4_GEMV_SKIP_EXPAND=1for same-binary comparison.num_tokens=3,top_k=8) with skip-expand enabled and disabled.Performance
H200, Qwen3.6 35B A3B NVFP4, MTP N=3, paired same-binary A/B:
expandInputRowslaunches per decoding step.Testing
lintrunner -aon the five changed files.moe_gemv_fp4.cuandmoe_quantization.ccagainst the exact [CUDA] Speed up the NVFP4 QMoE decode GEMV and enable it for MTP verify #31159 head.Checklist