CUDA: adding switch points per HW and quant type to tune the mvq->MMQ decode crossover - #26079
Conversation
|
@gaugarg-nv @ORippler can you please review this PR? Thanks. |
ORippler
left a comment
There was a problem hiding this comment.
Once the batch goes past 1 the vector kernel becomes compute-bound and MMQ is often faster, but the switch point is fixed at compile time.
If this were the case, we would expect perf gain for all quants & models, not necessarily just a selected few. My feeling is we should look to add a dedicated small-BS kernel instead of tweaking thresholds. @JohannesGaessler mentioned he optimized MMQ only for the large-BS regime. If we want to go for this, imo we need a more complete sweep on HW & quantization types. Other comments:
- How did you generate the perf numbers? Please paste the CLI. Also, ensure
-embd 1is set so you generate logits for all elements of the seq - The env variable should be added here https://github.com/ggml-org/llama.cpp/blob/3a19db0185bf7d7517992fd813640a0ff2ca0a23/docs/build.md#runtime-cuda-environmental-variables
Fair point. I have now updated the PR description to reflect a more accurate description of the PR.
The exact commands used are as below. The description now shows updated perf numbers obtained with the commands containing -embd 1. Also using --pure in quantization step now, so as to avoid mixtures of multiple quant types in the results. CLI
Quantized into necessary quant types using commands similar to this Clocks pinned so near-crossover deltas aren't smeared by boost/thermal drift: A/B, identical except the env var (8 = stock, 1 = force MMQ from ne11>=2):
Removed the runtime env variable in the latest commit and switched to a per-HW switch points that switch based on the quant type. There are existing examples in the modified files, doing the same for AMD HW. |
Add a runtime override of the mul_mat_vec_q -> MMQ batch crossover (default MMVQ_MAX_BATCH_SIZE). Lowering it routes batches above the threshold from the CUDA-core vector kernel to the int8 MMQ tensor-core path, which is faster once quantized decode becomes compute-bound at B>1 (measured +23-41% at B=8 on RTX 5090 for Q4_K dense, no low-batch loss). The value is parsed once and clamped to [1, MMVQ_MAX_BATCH_SIZE], since mul_mat_vec_q asserts ncols_dst <= that; invalid input warns and falls back to the default. The override is applied consistently in both the mul_mat_vec_q and MUL_MAT_ID dispatch paths. Default behavior unchanged.
54f4199 to
1cb7ee6
Compare
|
Change looks good to me. We will have to revisit these thresholds if/when MMVQ kernels are optimized. |
|
How would these thresholds change based on #26705, if any? |
On incorporating changes from #26705 and rerunning the experiment (on RTX 5090), I see that the switch points move by 2 for Q4_K and Q5_K quants and remains same for all the remaining quants. #26705 does not however apply to DGX Spark in its current state, so I do not expect any change in its switch points.
I can make the corresponding switch point changes when #26705 PR lands. |
ORippler
left a comment
There was a problem hiding this comment.
Please clarify the intention. Currently, MUL_MAT_ID has an independent get_mmvq_mmid_max_batch() helper, which would need to reflect the changes here as well if the intent is for the new crossover points to apply to MoE/MUL_MAT_ID too.
Might make sense to convert this to a stacked PR then, if there is a clear dependence. Not sure if stacked PRs can span cross-forks yet though |
I ran the sweep with MoE as well, but it was not giving the benefits as we see in dense models, so the change does not apply to MoE. Specifically, on Qwen3-30B-A3B Q4_K_M on an RTX 5090, forcing MUL_MAT_ID onto MMQ costs 38-62% across batches 2-8 — mvq wins the entire supported range and there is no crossover to find.
I can explore stacking these PRs if we prefer that, however the intention of both these changes is independent from each other, as well as the changes needed to make due to the switch point dependency are small anyway, so I can also submit the PRs independently. |
… decode crossover (ggml-org#26079) * CUDA: runtime GGML_CUDA_MMVQ_MAX to tune the mvq->MMQ decode crossover Add a runtime override of the mul_mat_vec_q -> MMQ batch crossover (default MMVQ_MAX_BATCH_SIZE). Lowering it routes batches above the threshold from the CUDA-core vector kernel to the int8 MMQ tensor-core path, which is faster once quantized decode becomes compute-bound at B>1 (measured +23-41% at B=8 on RTX 5090 for Q4_K dense, no low-batch loss). The value is parsed once and clamped to [1, MMVQ_MAX_BATCH_SIZE], since mul_mat_vec_q asserts ncols_dst <= that; invalid input warns and falls back to the default. The override is applied consistently in both the mul_mat_vec_q and MUL_MAT_ID dispatch paths. Default behavior unchanged. * Added Blackwell specific switch point, to reduce dependence on runtime env var. * Add per-HW switch point values for DGX Spark and removing runtime env var * Adding switch points for Ada, tested on RTX 4090 * Modifying DGX Spark numbers based on latest run and adding some comments and small functional changes relating to MoE * Reverting an unnecessary conditional * Update ggml/src/ggml-cuda/mmvq.cu --------- Co-authored-by: praneshgo <227579474+praneshgo@users.noreply.github.com> Co-authored-by: Oliver Simons <osimons@nvidia.com>
ggml_cuda_should_use_mmvq() gained per-architecture switch points in ggml-org#26079, but the NVIDIA branches test cc for exact equality (Ada 890, Blackwell 1200, DGX Spark 1210). Ampere (sm_80 cc 800, sm_86 cc 860, sm_87) matches none of them and falls through to ne11 <= MMVQ_MAX_BATCH_SIZE (8). ggml_cuda_mul_mat() tests MMVQ before MMQ and returns on the first match, so on Ampere every batch of 8 or fewer takes MMVQ even though ggml_cuda_should_use_mmq() early-returns true for turing_mma_available(cc). This is most visible in speculative decoding, where the verify batch is ne11 = 1 + n_draft (3-8 for typical draft depths) - squarely inside the range routed to the slower kernel on K-quants. Measured on RTX 3090 (sm_86), Qwen3.8-27B UD-Q4_K_XL, llama-server with --spec-type draft-mtp, greedy, ignore_eos, 8 real prompts, 512 output tokens: n_max verify batch MMVQ (current) MMQ delta 2 3 60.13 t/s 62.87 +4.6% 3 4 58.38 68.31 +17.0% 4 5 56.68 73.96 +30.5% 5 6 51.66 73.82 +42.9% Draft acceptance is unchanged (0.721 -> 0.722 at n_max=2), confirming this is a dispatch effect rather than a change in drafting behaviour. Threshold chosen by llama-bench sweep (-n 0 -p 1..8 -r 12), MMVQ vs MMQ per batch size: MMVQ wins at B=2 (71.8 vs 63.2), ties at B=1 and B=3, and MMQ wins from B=4 up (+14.8% at B=4 rising to +67% at B=8). The crossover is therefore at 3, making 2 the correct threshold - CDNA1 independently uses 2 for Q4_K. Restricted to Q4_K/Q5_K/Q6_K, the types present in the measured model. Q2_K and Q3_K were not measured and keep the default.
|
I'm unsure if this is relevant but for a greedy run of 128 tokens, this PR causes a difference in mtp generation, fixed-seed, on Blackwell cards. I tested this on a 5070ti. The changes are due to floating point differences. Perplexity is essentially the same, and because this was merged I am unsure whether or not this is an issue in the first place. I noticed that prior to this commit, earlier MTP generation still deviated from non-mtp output (again greedy, fixed-seed testing) but only further into the generation. I can provide reproduction if necessary. |
… decode crossover (ggml-org#26079) * CUDA: runtime GGML_CUDA_MMVQ_MAX to tune the mvq->MMQ decode crossover Add a runtime override of the mul_mat_vec_q -> MMQ batch crossover (default MMVQ_MAX_BATCH_SIZE). Lowering it routes batches above the threshold from the CUDA-core vector kernel to the int8 MMQ tensor-core path, which is faster once quantized decode becomes compute-bound at B>1 (measured +23-41% at B=8 on RTX 5090 for Q4_K dense, no low-batch loss). The value is parsed once and clamped to [1, MMVQ_MAX_BATCH_SIZE], since mul_mat_vec_q asserts ncols_dst <= that; invalid input warns and falls back to the default. The override is applied consistently in both the mul_mat_vec_q and MUL_MAT_ID dispatch paths. Default behavior unchanged. * Added Blackwell specific switch point, to reduce dependence on runtime env var. * Add per-HW switch point values for DGX Spark and removing runtime env var * Adding switch points for Ada, tested on RTX 4090 * Modifying DGX Spark numbers based on latest run and adding some comments and small functional changes relating to MoE * Reverting an unnecessary conditional * Update ggml/src/ggml-cuda/mmvq.cu --------- Co-authored-by: praneshgo <227579474+praneshgo@users.noreply.github.com> Co-authored-by: Oliver Simons <osimons@nvidia.com>
… decode crossover (ggml-org#26079) * CUDA: runtime GGML_CUDA_MMVQ_MAX to tune the mvq->MMQ decode crossover Add a runtime override of the mul_mat_vec_q -> MMQ batch crossover (default MMVQ_MAX_BATCH_SIZE). Lowering it routes batches above the threshold from the CUDA-core vector kernel to the int8 MMQ tensor-core path, which is faster once quantized decode becomes compute-bound at B>1 (measured +23-41% at B=8 on RTX 5090 for Q4_K dense, no low-batch loss). The value is parsed once and clamped to [1, MMVQ_MAX_BATCH_SIZE], since mul_mat_vec_q asserts ncols_dst <= that; invalid input warns and falls back to the default. The override is applied consistently in both the mul_mat_vec_q and MUL_MAT_ID dispatch paths. Default behavior unchanged. * Added Blackwell specific switch point, to reduce dependence on runtime env var. * Add per-HW switch point values for DGX Spark and removing runtime env var * Adding switch points for Ada, tested on RTX 4090 * Modifying DGX Spark numbers based on latest run and adding some comments and small functional changes relating to MoE * Reverting an unnecessary conditional * Update ggml/src/ggml-cuda/mmvq.cu --------- Co-authored-by: praneshgo <227579474+praneshgo@users.noreply.github.com> Co-authored-by: Oliver Simons <osimons@nvidia.com>
… decode crossover (ggml-org#26079) * CUDA: runtime GGML_CUDA_MMVQ_MAX to tune the mvq->MMQ decode crossover Add a runtime override of the mul_mat_vec_q -> MMQ batch crossover (default MMVQ_MAX_BATCH_SIZE). Lowering it routes batches above the threshold from the CUDA-core vector kernel to the int8 MMQ tensor-core path, which is faster once quantized decode becomes compute-bound at B>1 (measured +23-41% at B=8 on RTX 5090 for Q4_K dense, no low-batch loss). The value is parsed once and clamped to [1, MMVQ_MAX_BATCH_SIZE], since mul_mat_vec_q asserts ncols_dst <= that; invalid input warns and falls back to the default. The override is applied consistently in both the mul_mat_vec_q and MUL_MAT_ID dispatch paths. Default behavior unchanged. * Added Blackwell specific switch point, to reduce dependence on runtime env var. * Add per-HW switch point values for DGX Spark and removing runtime env var * Adding switch points for Ada, tested on RTX 4090 * Modifying DGX Spark numbers based on latest run and adding some comments and small functional changes relating to MoE * Reverting an unnecessary conditional * Update ggml/src/ggml-cuda/mmvq.cu --------- Co-authored-by: praneshgo <227579474+praneshgo@users.noreply.github.com> Co-authored-by: Oliver Simons <osimons@nvidia.com>
Overview
On CUDA, quantized decode uses mul_mat_vec_q while ne11 <= MMVQ_MAX_BATCH_SIZE (a build-time constant, 8), and MMQ (int8 tensor cores) above that. The switch point is fixed at compile time.
mul_mat_vec_q re-decodes the weights for every output column; MMQ decodes them once into shared memory and reuses them. So mvq's decode work grows with ne11 and MMQ's doesn't, which means the best switch point depends on how expensive the quant type is to decode as well as on the batch size.
This PR adds per-quant switch points on various HW families and batches above this switch point use MMQ instead of mvq.
This largely seems to help dense K-quant decode. The gains shown in the Performance section below are for K-quants (Q2_K/Q3_K / Q4_K / Q5_K / Q6_K) on dense models. Legacy Q4_0 / Q8_0 and IQ2_XS do not seem to benefit — for those, mvq stays ahead across the whole batch range. Lowering the crossover there would only slow things down.
Performance
RTX 5090 (sm_120)
GPU: NVIDIA RTX 5090 (Blackwell sm_120, 32 GB). Decode t/s from
llama-bench -p 1..8 -n 0 -embd 1 -r 50on--puresingle-type ggufs, clocks pinned.ne11 equals the
-pvalue.mvq=mul_mat_vec_q,MMQ= the tensor-core path.Q2_K (this branch keeps mvq for ne11 <= 5, MMQ above)
Q3_K (this branch keeps mvq for ne11 <= 5, MMQ above)
Q4_K (this branch keeps mvq for ne11 <= 5, MMQ above)
Q5_K (this branch keeps mvq for ne11 <= 5, MMQ above)
Q6_K (this branch keeps mvq for ne11 <= 7, MMQ above)
DGX Spark GB10 (sm_121)
GPU: NVIDIA GB10 (Blackwell sm_121, 48 SM, 128 GB LPDDR5 unified - no carveout), driver 610.43.02, Ubuntu 24.04, CUDA 13.3. SM clock pinned at 2400 MHz.
Decode t/s from
llama-bench -p 1..8 -n 0 -embd 1 -r 50on the same--puresingle-type ggufs, A-B-B-A ordering, 4 runs per arm. The mvq arm is upstream at the branch base (sm_121 falls through to the default of 8 there); the MMQ arm is the same commit with a one-line local patch forcing MMQ from ne11=2. ne11=1 therefore runs identical code in both arms - that control came in at +0.00% mean, 0.3% worst over 32 blocks.Only Q2_K crosses within ne11 <= 8 on this hardware, at 6; that is the single entry the sm_121 branch carries. Every other quant type keeps mvq ahead through ne11=8 and falls through to the default.
Q2_K (crosses at ne11 = 6; this branch switches there)
Q3_K (never crosses within ne11 <= 8; stays on the default of 8)
Q4_K (never crosses within ne11 <= 8; stays on the default of 8)
Q5_K (never crosses within ne11 <= 8; stays on the default of 8)
Q6_K (never crosses within ne11 <= 8; stays on the default of 8)
RTX 4090 (sm_89)
GPU: NVIDIA RTX 4090 (Ada Lovelace sm_89, 128 SM, 24 GB), driver 610.44, Windows 11, CUDA 12.9. SM clock pinned at 2400 MHz.
Decode t/s from
llama-bench -p 1..8 -n 0 -embd 1 -r 50on--puresingle-type ggufs, A-B-B-A ordering. ne11 equals the-pvalue.mvq=mul_mat_vec_q,MMQ= the tensor-core path. The mvq arm is a stock build (sm_89 previously fell through to the default of 8), the MMQ arm forces MMQ from ne11=2, so ne11=1 runs identical code in both and its delta is the measurement noise floor: mean +0.03%, worst 0.8% over 31 blocks.Q2_K (this branch keeps mvq for ne11 <= 4, MMQ above)
Q3_K (this branch keeps mvq for ne11 <= 6, MMQ above)
Q4_K (this branch keeps mvq for ne11 <= 7, MMQ above)
Q5_K (this branch keeps mvq for ne11 <= 7, MMQ above)
Requirements