ggml-cuda: pad matmul weight rows to avoid cache-set aliasing - #78
Conversation
|
@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. |
|
Added a second commit with the idea of making this more upstreamable. Now every arch can define their desired values. |
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>
6a01e6b to
0f6fe20
Compare
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_tensorrewritesnb[1..3]for an eligible weight andget_alloc_sizereserves the extra bytes, zeroed so the gaps cannot hold NaNs. The loader marks candidates with a newGGML_TENSOR_FLAG_PAD_ROWSand uploads the packed file data with a strided 2D copy. The matmul paths are unchanged: cuBLAS,mmf,mmvfandmmqall take their leading dimension fromnb[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::l2CacheSizeis documented as always returning 0 — so they are tabulated per architecture inggml_cuda_row_pad_params_for(). Only RDNA3.5 has an entry (2048/128); any other architecture keeps packed rows until it has been measured.GGML_CUDA_ROW_ALIAS_STRIDEGGML_CUDA_ROW_PADGGML_CUDA_NO_PAD_WEIGHTSSettings 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 ofmmf/mmvfall 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.spreadis the largest range between rounds on either side, so a delta below it is not separable from run-to-run variation.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.