Repository navigation
perf(rocm): give the dense mxfp4 qmv kernels the warp-shared word path #2246
Description
Part of #1801
Problem / Background
PR #2235 (#2178) gave gather_qmv_warp_shared_kernel an mxfp4 word path: each lane takes one packed 32-bit word (eight e2m1 nibbles, all inside one 32-value group) per step across the whole shared_x chunk, decodes without a branch through fp4_e2m1_to_float_scaled, and scales the word's dot product once. gpt-oss-20b decode went from 8.32 to 63.62 tok/s. Its results page (docs/benchmark_results/rocm-moe-decode-mxfp4-gfx1151-2026-10-08.md:63) notes that the dense mxfp4 kernels were not changed. Whether dense mxfp4 GEMVs are a meaningful share of any decode is not measured.
Current Behavior
In src/lib/mlx-cpp/patches-rocm/mlx/backend/rocm/quantized/qmm.hip (origin/main 1b4e3657), the PR #2235 page says the dense mxfp4 kernel has the same per-group lane pattern. That holds for the batched kernel only (AFFINE == false, BITS == 4 code below):
qmv_warp_shared_batched_kernel(:2361, used whencan_use_batched_qmv, mxfp4 loop at:2627-2660): same pattern as the old gather kernel. Lanes start atlane * 8inside each group, so at group size 32 only lanes 0 to 3 of 16 do work, and every nibble goes throughdequantize_value<4, false>, which reaches the branchyfp4_e2m1_to_floatswitch (:1892,:2003).qmv_warp_shared_kernel(:2015, non-batched transposed qmv, mxfp4 loop at:2259-2270): all 16 lanes work (strideTHREADS_PER_COL, two values per lane per group), but each value is a separate byte load and shift (unpack_packed_value_fast,:1858) followed by the same branchy decode. So the problem here is the per-nibble decode, not idle lanes.qmv_warp_noshared_kernel(:2766, mxfp4 loop at:2904) does the same per-element loop.
The dispatch for these is the non-affine branch of QuantizedMatmul::eval_gpu (:4313 onwards, fast_threads_per_col 16 or WARP_SIZE).
Proposed Solution
- Profile first. Take a guarded
rocprofv3 --kernel-trace --statsdecode trace (scripts/rocm_decode_profile.shor the perf(rocm): add an mxfp4 arm to the warp-shared gather_qmv kernel #2235 recipe) of gpt-oss-20b and of any other mxfp4 checkpoint on the host, and report the share of decode GPU time inqmv_warp_shared{,_batched}_kernel<*, unsigned char, 4, 32, false, *>andqmv_warp_noshared_kernel<..., 4, 32, false, ...>. If the combined share is under 5%, record the numbers in this issue and close it without code changes. - If it is 5% or more, add the
kMxfp4WordPath(!AFFINE && BITS == 4 && GROUP_SIZE == 32) block fromgather_qmv_warp_shared_kernel(:5217-5247) to each kernel the trace shows, skipping its per-group loop for that case.K % 32 == 0holds for every mxfp4 weight (group size 32), andCHUNK_SIZE(2048) is a multiple of 32, so no word straddles a chunk. KeepMLX_ROCM_QMV_USE_WARP=0as the way back to the old kernel. Add apatches-rocm/LOCAL_FIXES.mdentry (mlxcelverse only).
Acceptance Criteria
- The profile numbers are posted on this issue, whichever branch is taken.
- If implemented: a test in
tests/rocm_mxfp4_quant.rs, modeled onmxfp4_warp_shared_gather_qmv_matches_per_row_and_reference, checks dense mxfp4quantized_matmul(one row, and a batched case) against a CPU-dequantized f32 reference with relative L2 error no worse than the old kernel's, and runs the old kernel viaMLX_ROCM_QMV_USE_WARP=0for comparison; a sign-bit mutation must fail it. - If implemented: guarded decode for the profiled checkpoint, at least three alternated rounds per arm, ranges reported;
logit_tracedefault vs old kernel, 0 decided mismatches.
Verification
scripts/rocm_gpu_guard.sh -- scripts/rocm_decode_profile.sh # see docs/benchmarks.md for arguments
cargo test --profile test-fast --features rocm --test rocm_mxfp4_quant -- --test-threads=1
make verify-rocmRelated: #2178, PR #2235, TECHNICAL_REPORTS/2235-rocm-mxfp4-warp-shared-gather-qmv-20261008.en.md, #2061, #1814.
Activity
inureyes commentedon Oct 10, 2026
Issue #2246's profile-first branch. Guarded
rocprofv3 --kernel-trace --hip-graph-trace --statsdecode profile of gpt-oss-20b-MXFP4-Q4 (the only mxfp4 group-size-32 checkpoint on the host) on gfx1151, pp512 / tg128 greedy: 136088 decode dispatches, 1794.2 ms GPU total, 14.02 GPU ms/token, 60.2 tok/s profiled (62.88 tok/s plain).Dense mxfp4 qmv kernel dispatches:
qmv_warp_shared_kernel<..., unsigned char, 4, 32, false, ...>0,qmv_warp_shared_batched_kernel<..., 4, 32, false, ...>0,qmv_warp_noshared_kernel<..., 4, 32, false, ...>0. Whole-process stats (warmup and prefill included): also 0. Combined share of decode GPU time: 0.00%, under the 5% threshold.Reason: in this checkpoint only the MoE expert weights are mxfp4 group-size-32; the dense projections are group-size-64 4-bit affine quantization (q_proj scales are [4096, 45] over K = 2880, so 64 groups) and dispatch to
qmv_wide_kernel<hip_bfloat16, hip_bfloat16, 64, 4>at 26.87% of decode GPU time. The experts already run the #2235 word path (gather_qmv_warp_shared_kernel<hip_bfloat16, unsigned char, 4, 32, false, 16>, 50.32% of decode GPU time, 9144 calls).Closing without code changes per the issue body. Raw data, driver and README:
docs/benchmark_results/data/mxfp4-dense-qmv-gfx1151-2026-10-10/(PR #2263).docs: record the dense mxfp4 qmv decode profile for #2246 (#2263)