ggml-cuda: GEMM weight row padding + one-time K-padded f16 dequant for prefill#57
Draft
roberteg16 wants to merge 3 commits into
Draft
ggml-cuda: GEMM weight row padding + one-time K-padded f16 dequant for prefill#57roberteg16 wants to merge 3 commits into
roberteg16 wants to merge 3 commits into
Conversation
When a GEMM weight's packed row size is a multiple of 2048 bytes, consecutive rows collide in the same last-level cache sets, throttling the weight-bound matmuls. Pad such a weight's row stride by one 128-byte cache line so the rows no longer alias, speeding up F16/BF16 prefill and decode. Enabled by default; set GGML_CUDA_NO_PAD_WEIGHTS to restore the previous layout. The model loader tags mul_mat / mul_mat_id weights with GGML_TENSOR_FLAG_PAD_ROWS and uploads their rows with a strided 2D copy; the CUDA backend reserves and applies the padded nb[1] and feeds the strided weight to cuBLAS via lda without repacking. Quantized weights are excluded, since a 128-byte pad is not a multiple of their block size and would misalign the block-indexed kernels. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Add an opt-in GGML_PREFILL_DEQUANT=<min_tokens> path that, for large-batch (prefill) quantized dense GEMMs, routes src0 through the dequantize-to-f16 + hipBLAS path instead of MMQ. The weight is dequantized ONCE into a persistent, K-padded (+1 cache line, avoids cache-set aliasing) f16 buffer cached by weight pointer; subsequent calls reuse it, so only the per-call activation convert and the GEMM run. Decode (small batch) is untouched and stays on MMQ/MMVQ. On gfx1151 the padded f16 hipBLAS kernel reaches ~80% of the f16 WMMA roofline vs MMQ's ~61%; combined with ROCBLAS_USE_HIPBLASLT=1 this is a small prefill win. Off by default. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Replace the runtime, pointer-keyed dequant cache in ggml_cuda_mul_mat_cublas_impl with a persistent f16 shadow built once when the quantized weight is uploaded (ggml_backend_cuda_buffer_set_tensor). The shadow is K-padded only when its f16 row size aliases at 2048 B, so weights with K not a multiple of 1024 (e.g. a down_proj with K=3584) also get a shadow instead of being re-dequantized on every forward pass. The shadow is owned by the buffer context and freed with the model. The dispatch only skips MMQ/MMVQ for weights that actually have a shadow, so mul_mat_id expert slices (no shadow) stay on MMQ. Dense 2D weights only; still opt-in via GGML_PREFILL_DEQUANT. Prefill improves over the runtime-cache version (e.g. gemma-3 4B pp1024 +23% vs baseline, up from +10%), and the per-call dequantize_block_* kernels disappear from the timed graph. Decode is unchanged.
This was referenced Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports two prefill GEMM optimizations onto the refactored
gfx11CUDA matmul path (the oldggml_cuda_op_mul_mat/ggml_cuda_op_mul_mat_cublaswere replaced by the templatedggml_cuda_mul_mat_cublas_impl<>+ggml_cuda_mul_mat), plus a follow-up that moves the quantized-weight dequant from inference time to load time.GGML_CUDA_NO_PAD_WEIGHTS). When a float weight's packed row size is a multiple of 2048 bytes, consecutive rows collide in the same LLC sets. The loader tags such weights withGGML_TENSOR_FLAG_PAD_ROWS; the CUDA buffer reserves and applies a one-cache-line (128 B) paddednb[1]. The three kernel paths (mmf, mmvf, cuBLAS impl) all derive their leading dimension fromnb01, so the padded stride is honored natively. Quantized weights are excluded (128 B is not a multiple of their block size).GGML_PREFILL_DEQUANT=<min_tokens>, off by default). For large-batch quantized dense GEMMs (ne11 >= min_tokens) the prefill path skips MMQ/MMVQ and runs an f16 hipBLAS GEMM. Each eligible quantized 2D dense weight is dequantized once at load time (ggml_backend_cuda_buffer_set_tensor) into a persistent f16 shadow owned by the buffer context (freed with the model), stored intensor->extra. The shadow is K-padded by one cache line only when its f16 row size aliases at 2048 B, so weights withK % 1024 != 0(e.g. adown_projwith K=3584) also get a shadow instead of being re-dequantized per call. The dispatch only bypasses MMQ for weights that actually have a shadow, somul_mat_idexpert slices stay on MMQ (MoE out of scope in v1). Decode (small batch) is untouched.This supersedes an earlier runtime, pointer-keyed dequant cache: it removes the per-call
dequantize_block_*kernels from the timed graph entirely (verified via roofline) and covers theK % 1024 != 0weights the runtime cache missed.Measured impact (gfx1151, Radeon 8060S 32 GB, Q4_K_M,
-p 128,1024,4096 -n 128 -d 128)Padding alone (default): no regression on quantized models (within run-to-run noise), as expected since quantized weights are excluded — it targets F16/BF16/F32 weights.
Prefill with
GGML_PREFILL_DEQUANT=32 ROCBLAS_USE_HIPBLASLT=1(load-time shadow) vs baseline (t/s). pp1024/pp4096 at default reps; pp128 averaged over-r 40(matched baseline) since it is small-batch and jittery:*= Qwen3.5 0.8B pp128 stays noise-dominated even at-r 40(±~1130 t/s); the tiny GEMMs are bound by launch/scheduling overhead, so treat it as inconclusive. Decode (tg128) is flat across all configs, as expected (the shadow only feeds prefill).Moving the dequant to load time notably improves the models whose
down_projhasK=3584(missed by the earlier runtime cache): gemma-3 4B pp1024 went from +10% to +23%, Qwen2.5 3B pp4096 from +6% to +11%. Roofline confirms the per-calldequantize_block_q4_K/q6_Kkernels no longer appear in the timed prefill graph.Batch-size dependence: the f16 path is a large-batch win (pp1024/pp4096) but is a small, consistent regression at pp128 (-2% to -10% with
-r 40), where the activation-convert + GEMM overhead is not amortized and MMQ is already efficient. Notemin_tokensis compared against the per-u-batch token count (ne11, capped atn_ubatch, default 512), not the full prompt, so pp1024 and pp4096 both run M=512 GEMMs and only differ in u-batch count. The crossover is therefore M=128 (loses) vs M=512 (wins):min_tokensshould be tuned between 128 and 512 (e.g. ~256);32is used here only to exercise the path, and any value >n_ubatchdisables the path entirely (verified:GGML_PREFILL_DEQUANT=2048at pp1024 stays on MMQ with zero runtimedequantize_block_*).u-batch sensitivity: raising
-ubto 2048 (so pp1024/pp4096 run M=1024/2048 GEMMs) does not improve throughput — the f16 advantage over MMQ persists (Llama-3.1 8B +21-22%, Qwen2.5 3B +8-11%) but absolute t/s is flat-to-slightly-worse vs the default u-batch (Llama 8B pp4096 1693 -> 1625). The M=512 GEMMs already saturate compute, so the defaultn_ubatch=512is the sweet spot for this path.Known limitation
The f16 shadows are kept resident alongside the quantized originals (decode still uses MMVQ on the quantized weight), so enabling
GGML_PREFILL_DEQUANTroughly quadruples weight VRAM. On 32 GB, models >=12B OOM at load time (fails early and clearly, instead of mid-inference). Off by default, so unaffected unless opted in.Test plan
test-backend-ops test -o MUL_MAT(F16/BF16/F32 + quantized)llama-benchacross 8 GGUF models, default flags: no crashes / no NaN, no regressionllama-bench(4 models <=8B) withGGML_PREFILL_DEQUANT=32 ROCBLAS_USE_HIPBLASLT=1,-p 128,1024,4096: results aboveGGML_ROOFLINE_OUT) with the flag: per-calldequantize_block_*gone from timed prefill (incl. K=3584 down_proj)