Skip to content

ggml-cuda: pad matmul weight rows to avoid cache-set aliasing - #78

Merged
roberteg16 merged 2 commits into
gfx11from
rogarcia.padd-gemm-weight-rows-to-avoid-cache-set
Jul 30, 2026
Merged

ggml-cuda: pad matmul weight rows to avoid cache-set aliasing#78
roberteg16 merged 2 commits into
gfx11from
rogarcia.padd-gemm-weight-rows-to-avoid-cache-set

Conversation

@roberteg16

@roberteg16 roberteg16 commented Jul 30, 2026

Copy link
Copy Markdown

Overview

A weight whose packed row size is a multiple of the cache set-index stride puts every row in the same cache sets, so a matmul walking down a column thrashes a single set. Adding one cache line to the row stride spreads the rows out again.

ggml_backend_cuda_buffer_init_tensor rewrites nb[1..3] for an eligible weight and get_alloc_size reserves the extra bytes, zeroed so the gaps cannot hold NaNs. The loader marks candidates with a new GGML_TENSOR_FLAG_PAD_ROWS and uploads the packed file data with a strided 2D copy. The matmul paths are unchanged: cuBLAS, mmf, mmvf and mmq all take their leading dimension from nb[1] already.

Quantized weights are excluded: those kernels read the row stride as nb[1]/type_size, and a 128 B pad would break that division being exact.

Tuning

The alias stride and the pad follow from the cache geometry (line size times number of sets), which neither CUDA nor HIP report — hipDeviceProp_t::l2CacheSize is documented as always returning 0 — so they are tabulated per architecture in ggml_cuda_row_pad_params_for(). Only RDNA3.5 has an entry (2048/128); any other architecture keeps packed rows until it has been measured.

variable effect
GGML_CUDA_ROW_ALIAS_STRIDE override the row size that triggers padding
GGML_CUDA_ROW_PAD override the bytes added to the row stride
GGML_CUDA_NO_PAD_WEIGHTS disable the padding

Settings are resolved and validated once per device. A stride that is not a power of two, a pad that is a multiple of it, or a pad that would violate the nb[1] % (2*type_size) requirement of mmf/mmvf all fall back to no padding rather than degrading silently.

Results

gfx1151, llama-bench -ngl 999 -p 128,4096 -n 128 -d 128 -ub 2048 -r 10, padding on vs off. Each measurement is the median of its repetitions, each cell the median across 4-6 rounds that alternate which side runs first. spread is the largest range between rounds on either side, so a delta below it is not separable from run-to-run variation.

model test off on delta spread
gemma-3-4b BF16 pp128@d128 1144.57 2024.34 +76.86% 4.85%
gemma-4-E2B BF16 pp128@d128 2056.70 2590.53 +25.96% 1.15%
gemma-3-4b BF16 pp4096@d128 2275.95 2546.84 +11.90% 0.77%
gemma-4-E2B BF16 tg128@d128 42.04 43.40 +3.23% 2.52%
Qwen3.6-35B-A3B UD-Q4_K_M tg128@d128 59.75 60.57 +1.37% 0.10%
Qwen3.5-35B-A3B Q4_K_M tg128@d128 59.15 59.88 +1.23% 0.27%
gemma-3-4b BF16 tg128@d128 26.97 27.25 +1.07% 0.17%

The gain concentrates in small-batch prefill, where a short run of output rows streams the whole weight matrix and the weight-side cache behaviour dominates.

Q4_K_M models are largely unaffected: their large GEMM weights are quantized and therefore excluded, leaving only the MoE router and ssm_alpha/ssm_beta, which show up as the ~+1.2-1.4% decode gain on the two MoE models. Across all 21 model/test cells the worst delta is -0.47%, within its own spread.

Two notes on scope: the +3.23% figure is the least separated from its spread and should be read as indicative, and the two BF16 models are the only non-quantized GGUFs that fit in 32 GiB of VRAM, so the large-gain result rests on two models of the same family.

@roberteg16

Copy link
Copy Markdown
Author

@mgehre-amd padding works amazing for bf16/f16/f32 checkpoints but for GGUF-quantized ones only a handful of layers stay in f32. For these models, this optimization is mostly shown only on decode and you can see it is around +1-1.4% . Do you have any thoughts on whether we ought to merge it? Should we wait for bf16/f16 use cases?

@mgehre-amd

Copy link
Copy Markdown
Collaborator

@mgehre-amd padding works amazing for bf16/f16/f32 checkpoints but for GGUF-quantized ones only a handful of layers stay in f32. For these models, this optimization is mostly shown only on decode and you can see it is around +1-1.4% . Do you have any thoughts on whether we ought to merge it? Should we wait for bf16/f16 use cases?

A win for bf16/f16/f32 checkpoints is still a considerable win. I think we should get it merged and upstreamed.

@roberteg16

Copy link
Copy Markdown
Author

Added a second commit with the idea of making this more upstreamable. Now every arch can define their desired values.

@roberteg16
roberteg16 marked this pull request as ready for review July 30, 2026 14:11

@mgehre-amd mgehre-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks!

roberteg16 and others added 2 commits July 30, 2026 09:27
Weight matrices whose packed row size is a multiple of 2048 bytes map every row
onto the same L2 cache sets, so a matmul walking down a column thrashes a single
set. On RDNA3.5 this shows up as lost bandwidth in the FFN and attention GEMMs.

Add one cache line (128 B) to the row stride of such weights at buffer-init time
so consecutive rows land in different sets. get_alloc_size reserves the extra
bytes, and the loader uploads the packed file data with a strided 2D copy since
the destination rows are no longer adjacent. The gaps are zeroed so they cannot
hold NaNs.

Quantized weights are excluded: 128 is not a multiple of their block size and
would misalign the block-indexed kernels. The matmul paths need no change --
cuBLAS, mmf and mmvf all take the leading dimension from nb[1] already.

The loader flags the eligible tensors with GGML_TENSOR_FLAG_PAD_ROWS, reusing
llm_tensor_info_for() to tell a matmul weight from a bias or a norm; the
token-embedding/output duplication rule it shares with the buffer-type selection
is factored into resolve_tn_tensor() so the two cannot drift.

Gated to RDNA3.5; disable with GGML_CUDA_NO_PAD_WEIGHTS.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The alias stride and the pad were compile-time constants gated to RDNA3.5. Both
follow from the cache geometry -- line size times number of sets -- so another
architecture needs different values, and neither CUDA nor HIP report enough to
derive them: hipDeviceProp_t::l2CacheSize is documented as always returning 0,
and set associativity is not exposed at all.

Move the pair into cuda_device_info, resolved once per device from a table keyed
on the compute capability, then overridden by GGML_CUDA_ROW_ALIAS_STRIDE and
GGML_CUDA_ROW_PAD. Porting to another architecture becomes a table entry, and
the values can be swept without rebuilding. Architectures with no entry keep
packed rows, so each one is opted in only after being measured.

Resolving once per device is also where the settings are validated: an alias
stride that is not a power of two, or a pad that is a multiple of it and so
leaves the rows aliasing, warns and falls back to no padding.
GGML_CUDA_NO_PAD_WEIGHTS now resolves to a zero pad, leaving a single way for
the padding to be off.

Add the per-type check that was missing: ggml_cuda_should_use_mmf and
ggml_cuda_should_use_mmvf reject a src0 whose strides are not a multiple of
2*type_size, so a pad that misses this would not fail or corrupt anything, it
would quietly stop those kernels from being selected.

Also correct the reason quantized weights are excluded. The matmul kernels do
read the row stride, as nb[1]/type_size in mmq.cu and mmvq.cu; what a 128 byte
pad breaks is that division being exact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@roberteg16
roberteg16 force-pushed the rogarcia.padd-gemm-weight-rows-to-avoid-cache-set branch from 6a01e6b to 0f6fe20 Compare July 30, 2026 16:30
@roberteg16
roberteg16 merged commit 7e03b88 into gfx11 Jul 30, 2026
5 checks passed
@roberteg16
roberteg16 deleted the rogarcia.padd-gemm-weight-rows-to-avoid-cache-set branch July 30, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants