Skip to content

CUDA: dedup MoE gate/up activation quantization - #25441

Merged
JohannesGaessler merged 8 commits into
ggml-org:masterfrom
praneshgo:pgonegandla/moe_quant_optimize
Jul 16, 2026
Merged

CUDA: dedup MoE gate/up activation quantization#25441
JohannesGaessler merged 8 commits into
ggml-org:masterfrom
praneshgo:pgonegandla/moe_quant_optimize

Conversation

@praneshgo

@praneshgo praneshgo commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Overview

For MoE gate/up projections the src1 activation is broadcast across the routed experts (ne11 == 1), so the MMQ path re-quantized each token's activation once per routed expert. This deduplicates it: each unique token's activation is quantized once and reused for all of its experts.

Additional information

  • NVFP4: a fused quantize+scatter kernel writes each token's quantized block directly to all of its expert slots.
  • MXFP4: quantize the unique token rows once, then gather the blocks into the expert-sorted layout.

Output is bit-identical to the previous per-expert path (same source data, deterministic per-block quantization). Down projection is unaffected (its rows differ per expert), and dense (non-MoE) models never trigger this path. This removes redundant activation-quant work in MoE prefill.

Verification:

  • test-backend-ops -o MUL_MAT_ID passes (default, gather, and per-expert paths).
  • Coherent end-to-end generation.

Perf result comparison on running with GPU: RTX 5090 · llama-bench -p 8192 -n 128 -r 10 -ngl 99, BS=1, CUDA graphs on

Model phase fused (this PR) per-expert (baseline) gain
Gemma-4-26B-A4B (NVFP4) prefill (pp8192) 9712.9 9168.5 +5.9%
decode (tg128) 175.3 175.6 −0.2%
e2e (8192+128) 5286.7 5127.8 +3.1%
Qwen3.6-35B-A3B (NVFP4) prefill (pp8192) 8229.5 7962.4 +3.4%
decode (tg128) 240.3 236.4 +1.7%
e2e (8192+128) 5444.5 5298.3 +2.8%
Qwen3-30B-A3B (Q4_K_M) prefill (pp8192) 8264.9 8259.7 +0.1%
decode (tg128) 326.5 326.7 −0.1%
e2e (8192+128) 6014.9 6013.3 +0.0%

Q8_1 is perf-neutral, since its activation quant is a cheap, memory-bound amax + int8 round, so there is little redundant work to remove (unlike NVFP4's per-sub-block scale search). We apply the dedup there anyway for parity across quantization types.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: AI partially helped in making the code changes.

praneshgo added 2 commits July 8, 2026 10:24
For MoE gate/up projections the src1 activation is broadcast across the
routed experts (ne11 == 1), so ids_src1 maps every one of a token's
n_expert_used slots to the same physical row. The MMQ path therefore
re-quantized each token's activation n_expert_used times.

For fp4 (NVFP4/MXFP4) src0, quantize each unique token row once instead of
once per expert. For NVFP4 a single quantize+scatter kernel
(quantize_scatter_mmq_nvfp4) quantizes each token once and writes the
resulting block_fp4_mmq straight to all n_expert_used slots, using an
inverse token->compact-row map (build_tok2c). MXFP4, and
GGML_CUDA_MOE_QUANT_GATHER=1, use a two-kernel variant: quantize unique
rows then gather into the expert-sorted layout (gather_mmq_fp4_blocks).
Both are bit-identical to the previous gather-then-quantize path (identical
source data, deterministic per-block quantization), verified by
test-backend-ops MUL_MAT_ID (type_a=nvfp4, broadcast b=1; 790/790 for the
default, gather, and per-expert paths) and by coherent end-to-end
generation. Set GGML_CUDA_NO_MOE_QUANT_DEDUP=1 to force the original
per-expert path.

Same-binary A/B on RTX 5090 (sm_120), Qwen3.6-35B-A3B-NVFP4 prefill @8192
(nsys, graphs-off; the unchanged mul_mat_q GEMM confirms stable clocks):
activation-quant GPU-busy drops 61% (78.2 -> 30.4 ms) with the fused
quantize+scatter, vs 33% (78.2 -> 52.8 ms) for the two-kernel gather. The
fused path avoids materializing and re-reading the 8x compact buffer,
writing the expert copies directly from registers.
Guard against malformed ids_src1: skip out-of-range token ids (t < 0 or
t >= n_tokens) and drop entries beyond n_expert_used per token instead of
writing past the token's tok2c region. No behavior change for valid MoE
routing data; test-backend-ops MUL_MAT_ID 790/790.
@praneshgo
praneshgo requested a review from a team as a code owner July 8, 2026 10:31
@github-actions github-actions Bot added ggml changes relating to the ggml tensor library for machine learning CUDA Related to the CUDA backend labels Jul 8, 2026
@praneshgo

Copy link
Copy Markdown
Contributor Author

@gaugarg-nv can you please help review this PR? Thanks.

@ggml-gh-bot

ggml-gh-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

Hi @praneshgo, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

  • PR Template not respected: Please respect the template when creating a new pull request. Make sure to fill out all required sections.

Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

@praneshgo

Copy link
Copy Markdown
Contributor Author

Hi @praneshgo, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

  • PR Template not respected: Please respect the template when creating a new pull request. Make sure to fill out all required sections.

Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

This is addressed now.

@am17an

am17an commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

How is this different from #18538?

@ORippler

ORippler commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

For MoE gate/up projections the src1 activation is broadcast across the routed experts (ne11 == 1), so the MMQ path re-quantized each token's activation once per routed expert. This deduplicates it: each unique token's activation is quantized once and reused for all of its experts.

Shouldn't this technique be generally applicable to Q8_1 as well? Also, do you have some perf measurements you could share to justify this change?

@praneshgo

Copy link
Copy Markdown
Contributor Author

How is this different from #18538?

These changes are complementary but not duplicates. Both attempt to remove redundant activation quantization for MoE MMQ, but along orthogonal axes.

The goal of #18538 seems to be to not re-quantize the same tensor when a later op reuses it.
The goal of #25441 is to not re-quantize the same token once per routed expert inside a single mul_mat_id.

Let T = tokens, E = n_expert_used.

For models with separate gate/up (two mul_mat_id on the same activation)

For models with merged gate_up (single mul_mat_id)

@praneshgo
praneshgo marked this pull request as draft July 8, 2026 12:31
@praneshgo

praneshgo commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

For MoE gate/up projections the src1 activation is broadcast across the routed experts (ne11 == 1), so the MMQ path re-quantized each token's activation once per routed expert. This deduplicates it: each unique token's activation is quantized once and reused for all of its experts.

Shouldn't this technique be generally applicable to Q8_1 as well? Also, do you have some perf measurements you could share to justify this change?

Yes, this should be applicable to Q8_1 as well. The corresponding changes are in progress, and I can add them as a commit to this PR soon, if we want to track both of them here.

Performance numbers and the associated perf gain with the changes in this PR are updated in the description.

@JohannesGaessler JohannesGaessler 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.

If I understand this PR correctly it adds too much complexity for what it does. On master the quantization kernel is doing duplicated work proportional to n_expert_used. The correct way to fix that is to change grid size and indexing logic of the quantization kernel to instead quantize the activations once and to write back the result n_expert_used times. No extra kernels should be added.

- Removed previously added kernels that were not necessary anymore\
- Added an inverse mapping from (token, slot) to compact row. Each token is quantized once and scattered to its compact rows.
@praneshgo

Copy link
Copy Markdown
Contributor Author

If I understand this PR correctly it adds too much complexity for what it does. On master the quantization kernel is doing duplicated work proportional to n_expert_used. The correct way to fix that is to change grid size and indexing logic of the quantization kernel to instead quantize the activations once and to write back the result n_expert_used times. No extra kernels should be added.

Fair point. I have refactored the code based on your suggestions, by adding indexing logic such that activations are quantized once and the result is written back n_expert_used times. Previously added kernels were removed and no new kernels exist.

@JohannesGaessler JohannesGaessler 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.

If at all possible, modify the existing kernels both for the FP4 and the q8_0 path to use the deduplicated pattern unconditionally. It is not necessary to preserve the pattern on master for MMQ or to make the feature configurable.

Comment thread ggml/src/ggml-cuda/mmid.cu Outdated
__launch_bounds__(ggml_cuda_get_physical_warp_size(), 1)
static __global__ void mm_ids_helper(
const int32_t * __restrict__ ids, int32_t * __restrict__ ids_src1, int32_t * __restrict__ ids_dst, int32_t * __restrict__ expert_bounds,
int32_t * __restrict__ ids_src1_inv,

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.

A kernel will only ever need one out of the current or the inverse mapping. Therefore, rather than a pointer pass a boolean to control whether the inverse mapping should be written to ids_src1_inv.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both fair points. Made the dedup unconditional for both quantization types.
Also, instead of passing the pointer, I am now passing a boolean, which when set to true writes the inverse mapping to ids_src1 (and the forward mapping when false), so it only ever builds the one that is needed.

@praneshgo praneshgo changed the title CUDA: dedup MoE gate/up activation quantization (fp4) CUDA: dedup MoE gate/up activation quantization Jul 11, 2026
@praneshgo
praneshgo marked this pull request as ready for review July 11, 2026 18:53
@ORippler

Copy link
Copy Markdown
Collaborator

Q8_1 is perf-neutral, since its activation quant is a cheap, memory-bound amax + int8 round, so there is little redundant work to remove (unlike NVFP4's per-sub-block scale search). We apply the dedup there anyway for parity across quantization types.

Given we intend to change NVFP4 to do dynamic per-channel amax-quant, does it make sense to withhold this PR until those changes have been made and reevaluate their impact on the new quantization logic?

@praneshgo

Copy link
Copy Markdown
Contributor Author

Q8_1 is perf-neutral, since its activation quant is a cheap, memory-bound amax + int8 round, so there is little redundant work to remove (unlike NVFP4's per-sub-block scale search). We apply the dedup there anyway for parity across quantization types.

Given we intend to change NVFP4 to do dynamic per-channel amax-quant, does it make sense to withhold this PR until those changes have been made and reevaluate their impact on the new quantization logic?

I think they are orthogonal on correctness. The dedup removes the redundant per-expert re-quant within one MUL_MAT_ID, so it composes with per-channel amax and stays bit-exact. The perf side is worth re-checking once amax is in, and I can recheck it then. However, I do not see any harm in this change landing now. It is self-contained and gives a gain under the current kernel.

@JohannesGaessler JohannesGaessler 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.

I'll check this PR for performance regressions but other than that I would consider it essentially good to merge.

return;
// write the block once (normal) or to each of the token's compact rows (scatter)
const int nwrite = scatter ? n_expert_used : 1;
for (int slot = 0; slot < nwrite; ++slot) {

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.

Suggested change
for (int slot = 0; slot < nwrite; ++slot) {
#pragma unroll
for (int slot = 0; slot < nwrite; ++slot) {

Please add explicit #pragma unrolls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added explicit pragma unrolls in the latest commit.

Comment thread ggml/src/ggml-cuda/mmq.cu Outdated
Comment on lines +179 to +181
if (dedup_bcast) {
CUDA_CHECK(cudaMemsetAsync(ids_src1.get(), 0xFF, ne_get_rows*sizeof(int32_t), stream)); // -1 for unused slots
}

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.

Why did you add this call to cudaMemsetAsync? I don't think it's needed, ids_src1 should be completely initialized even with the inverse mapping. I am not getting incorrect results from removing this call, I do however see an end-to-end performance difference of up to 5% on my RTX 5090.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, it is in fact redundant. I had initially added it to initialize -1 for unwritten slots, but mm_ids_helper fills every (token, slot) entry, so all the slots are initialized anyway. Removed it in the latest commit.

@JohannesGaessler

Copy link
Copy Markdown
Contributor
Performance
GPU Model Microbatch size Test t/s b10032 t/s bc67b43 Speedup
MI60 / MI50 gpt-oss 20B MXFP4 MoE 16 pp512 117.02 116.95 1.00
MI60 / MI50 gpt-oss 20B MXFP4 MoE 32 pp512 339.95 339.57 1.00
MI60 / MI50 gpt-oss 20B MXFP4 MoE 64 pp512 373.90 373.75 1.00
MI60 / MI50 gpt-oss 20B MXFP4 MoE 128 pp512 579.00 578.65 1.00
MI60 / MI50 gpt-oss 20B MXFP4 MoE 256 pp512 822.43 822.88 1.00
MI60 / MI50 gpt-oss 20B MXFP4 MoE 512 pp512 1081.13 1082.52 1.00
MI100 gpt-oss 20B MXFP4 MoE 16 pp512 569.58 564.24 0.99
MI100 gpt-oss 20B MXFP4 MoE 32 pp512 820.07 819.79 1.00
MI100 gpt-oss 20B MXFP4 MoE 64 pp512 989.97 991.50 1.00
MI100 gpt-oss 20B MXFP4 MoE 128 pp512 1508.26 1510.66 1.00
MI100 gpt-oss 20B MXFP4 MoE 256 pp512 2117.76 2123.49 1.00
MI100 gpt-oss 20B MXFP4 MoE 512 pp512 3014.89 3018.48 1.00
P40 gpt-oss 20B MXFP4 MoE 16 pp512 345.50 345.51 1.00
P40 gpt-oss 20B MXFP4 MoE 32 pp512 473.79 474.33 1.00
P40 gpt-oss 20B MXFP4 MoE 64 pp512 577.05 578.11 1.00
P40 gpt-oss 20B MXFP4 MoE 128 pp512 856.76 859.02 1.00
P40 gpt-oss 20B MXFP4 MoE 256 pp512 1157.85 1161.78 1.00
P40 gpt-oss 20B MXFP4 MoE 512 pp512 1405.61 1411.56 1.00
RTX 3090 gpt-oss 20B MXFP4 MoE 16 pp512 1101.77 1103.65 1.00
RTX 3090 gpt-oss 20B MXFP4 MoE 32 pp512 1599.18 1601.97 1.00
RTX 3090 gpt-oss 20B MXFP4 MoE 64 pp512 2098.76 2107.19 1.00
RTX 3090 gpt-oss 20B MXFP4 MoE 128 pp512 2532.21 2535.01 1.00
RTX 3090 gpt-oss 20B MXFP4 MoE 256 pp512 3780.07 3814.65 1.01
RTX 3090 gpt-oss 20B MXFP4 MoE 512 pp512 5192.88 5190.17 1.00
RTX 4090 gpt-oss 20B MXFP4 MoE 16 pp512 1740.12 1742.18 1.00
RTX 4090 gpt-oss 20B MXFP4 MoE 32 pp512 2940.40 2946.42 1.00
RTX 4090 gpt-oss 20B MXFP4 MoE 64 pp512 4340.78 4355.05 1.00
RTX 4090 gpt-oss 20B MXFP4 MoE 128 pp512 5619.44 5640.22 1.00
RTX 4090 gpt-oss 20B MXFP4 MoE 256 pp512 8929.50 8950.44 1.00
RTX 4090 gpt-oss 20B MXFP4 MoE 512 pp512 12555.80 12606.90 1.00
RTX 5090 gpt-oss 20B MXFP4 MoE 16 pp512 2102.83 2102.78 1.00
RTX 5090 gpt-oss 20B MXFP4 MoE 32 pp512 3577.22 3586.94 1.00
RTX 5090 gpt-oss 20B MXFP4 MoE 64 pp512 5574.30 5601.84 1.00
RTX 5090 gpt-oss 20B MXFP4 MoE 128 pp512 7720.78 7785.80 1.01
RTX 5090 gpt-oss 20B MXFP4 MoE 256 pp512 14620.07 14828.48 1.01
RTX 5090 gpt-oss 20B MXFP4 MoE 512 pp512 19964.77 20339.63 1.02
RX 6800 gpt-oss 20B MXFP4 MoE 16 pp512 204.82 204.72 1.00
RX 6800 gpt-oss 20B MXFP4 MoE 32 pp512 147.78 147.73 1.00
RX 6800 gpt-oss 20B MXFP4 MoE 64 pp512 448.56 448.93 1.00
RX 6800 gpt-oss 20B MXFP4 MoE 128 pp512 703.92 703.84 1.00
RX 6800 gpt-oss 20B MXFP4 MoE 256 pp512 1006.45 1009.01 1.00
RX 6800 gpt-oss 20B MXFP4 MoE 512 pp512 1296.79 1301.28 1.00
RX 9060 XT gpt-oss 20B MXFP4 MoE 16 pp512 520.70 521.88 1.00
RX 9060 XT gpt-oss 20B MXFP4 MoE 32 pp512 661.51 671.56 1.02
RX 9060 XT gpt-oss 20B MXFP4 MoE 64 pp512 1124.68 1129.89 1.00
RX 9060 XT gpt-oss 20B MXFP4 MoE 128 pp512 1385.26 1392.62 1.01
RX 9060 XT gpt-oss 20B MXFP4 MoE 256 pp512 2123.33 2138.32 1.01
RX 9060 XT gpt-oss 20B MXFP4 MoE 512 pp512 2860.27 2890.94 1.01
V100-PCIE-32GB gpt-oss 20B MXFP4 MoE 16 pp512 553.55 552.87 1.00
V100-PCIE-32GB gpt-oss 20B MXFP4 MoE 32 pp512 756.05 757.17 1.00
V100-PCIE-32GB gpt-oss 20B MXFP4 MoE 64 pp512 1012.87 1018.21 1.01
V100-PCIE-32GB gpt-oss 20B MXFP4 MoE 128 pp512 1359.28 1366.07 1.00
V100-PCIE-32GB gpt-oss 20B MXFP4 MoE 256 pp512 1809.56 1825.41 1.01
V100-PCIE-32GB gpt-oss 20B MXFP4 MoE 512 pp512 2435.38 2454.42 1.01

@JohannesGaessler
JohannesGaessler merged commit 5839ba3 into ggml-org:master Jul 16, 2026
21 of 22 checks passed
CowboyTim pushed a commit to aardbeiplantje/llama.cpp that referenced this pull request Jul 21, 2026
* CUDA: dedup MoE gate/up activation quantization (fp4)

For MoE gate/up projections the src1 activation is broadcast across the
routed experts (ne11 == 1), so ids_src1 maps every one of a token's
n_expert_used slots to the same physical row. The MMQ path therefore
re-quantized each token's activation n_expert_used times.

For fp4 (NVFP4/MXFP4) src0, quantize each unique token row once instead of
once per expert. For NVFP4 a single quantize+scatter kernel
(quantize_scatter_mmq_nvfp4) quantizes each token once and writes the
resulting block_fp4_mmq straight to all n_expert_used slots, using an
inverse token->compact-row map (build_tok2c). MXFP4, and
GGML_CUDA_MOE_QUANT_GATHER=1, use a two-kernel variant: quantize unique
rows then gather into the expert-sorted layout (gather_mmq_fp4_blocks).
Both are bit-identical to the previous gather-then-quantize path (identical
source data, deterministic per-block quantization), verified by
test-backend-ops MUL_MAT_ID (type_a=nvfp4, broadcast b=1; 790/790 for the
default, gather, and per-expert paths) and by coherent end-to-end
generation. Set GGML_CUDA_NO_MOE_QUANT_DEDUP=1 to force the original
per-expert path.

Same-binary A/B on RTX 5090 (sm_120), Qwen3.6-35B-A3B-NVFP4 prefill @8192
(nsys, graphs-off; the unchanged mul_mat_q GEMM confirms stable clocks):
activation-quant GPU-busy drops 61% (78.2 -> 30.4 ms) with the fused
quantize+scatter, vs 33% (78.2 -> 52.8 ms) for the two-kernel gather. The
fused path avoids materializing and re-reading the 8x compact buffer,
writing the expert copies directly from registers.

* CUDA: bounds-check token ids in build_tok2c_kernel

Guard against malformed ids_src1: skip out-of-range token ids (t < 0 or
t >= n_tokens) and drop entries beyond n_expert_used per token instead of
writing past the token's tok2c region. No behavior change for valid MoE
routing data; test-backend-ops MUL_MAT_ID 790/790.

* Refactor the code based on review comments

- Removed previously added kernels that were not necessary anymore\
- Added an inverse mapping from (token, slot) to compact row. Each token is quantized once and scattered to its compact rows.

* Adding q8_1 support for dedup and addressing review comments

* Add pragma unrolls

* Remove redundant cudaMemsetAsync call

* Removing follow up redundancies

---------

Co-authored-by: praneshgo <227579474+praneshgo@users.noreply.github.com>
ggerganov pushed a commit to am17an/llama.cpp that referenced this pull request Jul 28, 2026
* CUDA: dedup MoE gate/up activation quantization (fp4)

For MoE gate/up projections the src1 activation is broadcast across the
routed experts (ne11 == 1), so ids_src1 maps every one of a token's
n_expert_used slots to the same physical row. The MMQ path therefore
re-quantized each token's activation n_expert_used times.

For fp4 (NVFP4/MXFP4) src0, quantize each unique token row once instead of
once per expert. For NVFP4 a single quantize+scatter kernel
(quantize_scatter_mmq_nvfp4) quantizes each token once and writes the
resulting block_fp4_mmq straight to all n_expert_used slots, using an
inverse token->compact-row map (build_tok2c). MXFP4, and
GGML_CUDA_MOE_QUANT_GATHER=1, use a two-kernel variant: quantize unique
rows then gather into the expert-sorted layout (gather_mmq_fp4_blocks).
Both are bit-identical to the previous gather-then-quantize path (identical
source data, deterministic per-block quantization), verified by
test-backend-ops MUL_MAT_ID (type_a=nvfp4, broadcast b=1; 790/790 for the
default, gather, and per-expert paths) and by coherent end-to-end
generation. Set GGML_CUDA_NO_MOE_QUANT_DEDUP=1 to force the original
per-expert path.

Same-binary A/B on RTX 5090 (sm_120), Qwen3.6-35B-A3B-NVFP4 prefill @8192
(nsys, graphs-off; the unchanged mul_mat_q GEMM confirms stable clocks):
activation-quant GPU-busy drops 61% (78.2 -> 30.4 ms) with the fused
quantize+scatter, vs 33% (78.2 -> 52.8 ms) for the two-kernel gather. The
fused path avoids materializing and re-reading the 8x compact buffer,
writing the expert copies directly from registers.

* CUDA: bounds-check token ids in build_tok2c_kernel

Guard against malformed ids_src1: skip out-of-range token ids (t < 0 or
t >= n_tokens) and drop entries beyond n_expert_used per token instead of
writing past the token's tok2c region. No behavior change for valid MoE
routing data; test-backend-ops MUL_MAT_ID 790/790.

* Refactor the code based on review comments

- Removed previously added kernels that were not necessary anymore\
- Added an inverse mapping from (token, slot) to compact row. Each token is quantized once and scattered to its compact rows.

* Adding q8_1 support for dedup and addressing review comments

* Add pragma unrolls

* Remove redundant cudaMemsetAsync call

* Removing follow up redundancies

---------

Co-authored-by: praneshgo <227579474+praneshgo@users.noreply.github.com>
smalinin pushed a commit to smalinin/llama.cpp that referenced this pull request Aug 4, 2026
* CUDA: dedup MoE gate/up activation quantization (fp4)

For MoE gate/up projections the src1 activation is broadcast across the
routed experts (ne11 == 1), so ids_src1 maps every one of a token's
n_expert_used slots to the same physical row. The MMQ path therefore
re-quantized each token's activation n_expert_used times.

For fp4 (NVFP4/MXFP4) src0, quantize each unique token row once instead of
once per expert. For NVFP4 a single quantize+scatter kernel
(quantize_scatter_mmq_nvfp4) quantizes each token once and writes the
resulting block_fp4_mmq straight to all n_expert_used slots, using an
inverse token->compact-row map (build_tok2c). MXFP4, and
GGML_CUDA_MOE_QUANT_GATHER=1, use a two-kernel variant: quantize unique
rows then gather into the expert-sorted layout (gather_mmq_fp4_blocks).
Both are bit-identical to the previous gather-then-quantize path (identical
source data, deterministic per-block quantization), verified by
test-backend-ops MUL_MAT_ID (type_a=nvfp4, broadcast b=1; 790/790 for the
default, gather, and per-expert paths) and by coherent end-to-end
generation. Set GGML_CUDA_NO_MOE_QUANT_DEDUP=1 to force the original
per-expert path.

Same-binary A/B on RTX 5090 (sm_120), Qwen3.6-35B-A3B-NVFP4 prefill @8192
(nsys, graphs-off; the unchanged mul_mat_q GEMM confirms stable clocks):
activation-quant GPU-busy drops 61% (78.2 -> 30.4 ms) with the fused
quantize+scatter, vs 33% (78.2 -> 52.8 ms) for the two-kernel gather. The
fused path avoids materializing and re-reading the 8x compact buffer,
writing the expert copies directly from registers.

* CUDA: bounds-check token ids in build_tok2c_kernel

Guard against malformed ids_src1: skip out-of-range token ids (t < 0 or
t >= n_tokens) and drop entries beyond n_expert_used per token instead of
writing past the token's tok2c region. No behavior change for valid MoE
routing data; test-backend-ops MUL_MAT_ID 790/790.

* Refactor the code based on review comments

- Removed previously added kernels that were not necessary anymore\
- Added an inverse mapping from (token, slot) to compact row. Each token is quantized once and scattered to its compact rows.

* Adding q8_1 support for dedup and addressing review comments

* Add pragma unrolls

* Remove redundant cudaMemsetAsync call

* Removing follow up redundancies

---------

Co-authored-by: praneshgo <227579474+praneshgo@users.noreply.github.com>
satindergrewal pushed a commit to satindergrewal/llama.cpp that referenced this pull request Aug 12, 2026
* CUDA: dedup MoE gate/up activation quantization (fp4)

For MoE gate/up projections the src1 activation is broadcast across the
routed experts (ne11 == 1), so ids_src1 maps every one of a token's
n_expert_used slots to the same physical row. The MMQ path therefore
re-quantized each token's activation n_expert_used times.

For fp4 (NVFP4/MXFP4) src0, quantize each unique token row once instead of
once per expert. For NVFP4 a single quantize+scatter kernel
(quantize_scatter_mmq_nvfp4) quantizes each token once and writes the
resulting block_fp4_mmq straight to all n_expert_used slots, using an
inverse token->compact-row map (build_tok2c). MXFP4, and
GGML_CUDA_MOE_QUANT_GATHER=1, use a two-kernel variant: quantize unique
rows then gather into the expert-sorted layout (gather_mmq_fp4_blocks).
Both are bit-identical to the previous gather-then-quantize path (identical
source data, deterministic per-block quantization), verified by
test-backend-ops MUL_MAT_ID (type_a=nvfp4, broadcast b=1; 790/790 for the
default, gather, and per-expert paths) and by coherent end-to-end
generation. Set GGML_CUDA_NO_MOE_QUANT_DEDUP=1 to force the original
per-expert path.

Same-binary A/B on RTX 5090 (sm_120), Qwen3.6-35B-A3B-NVFP4 prefill @8192
(nsys, graphs-off; the unchanged mul_mat_q GEMM confirms stable clocks):
activation-quant GPU-busy drops 61% (78.2 -> 30.4 ms) with the fused
quantize+scatter, vs 33% (78.2 -> 52.8 ms) for the two-kernel gather. The
fused path avoids materializing and re-reading the 8x compact buffer,
writing the expert copies directly from registers.

* CUDA: bounds-check token ids in build_tok2c_kernel

Guard against malformed ids_src1: skip out-of-range token ids (t < 0 or
t >= n_tokens) and drop entries beyond n_expert_used per token instead of
writing past the token's tok2c region. No behavior change for valid MoE
routing data; test-backend-ops MUL_MAT_ID 790/790.

* Refactor the code based on review comments

- Removed previously added kernels that were not necessary anymore\
- Added an inverse mapping from (token, slot) to compact row. Each token is quantized once and scattered to its compact rows.

* Adding q8_1 support for dedup and addressing review comments

* Add pragma unrolls

* Remove redundant cudaMemsetAsync call

* Removing follow up redundancies

---------

Co-authored-by: praneshgo <227579474+praneshgo@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CUDA Related to the CUDA backend ggml changes relating to the ggml tensor library for machine learning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants