Ollama and llama.cpp predict a model’s GPU memory footprint during startup to determine how many transformer layers can be placed into GPU VRAMThe GPU's onboard memory, where the weights, KV cache, and compute buffer all have to fit.. If the estimator underestimates memory consumption, the runtime either forces unnecessary layer offloading to CPU or triggers out-of-memory (OOM) allocation failures during runtime.
This post analyzes Ollama’s memory estimation accounting alongside GPU kernel execution in llama.cpp. A rewrite of Ollama’s MoE memory estimator (PR #17201) corrected a -20.6% memory under-estimation down to -6.0%. Additionally, we examine an initial 3.43× speedup benchmark in llama.cpp’s MMQ kernel that failed numerical correctness checks on large matrix shapes.
Auditing Ollama’s MoE Memory Footprint Estimator
When placing model layers between GPU VRAM and system RAM, Ollama calculates expected allocations per layer. For mixture-of-expertsA model whose layers hold many expert sub-networks and fire only a few per token, so its footprint is dominated by weights that differ from layer to layer. (MoE) architectures, legacy allocation estimates ran ~20% below actual memory allocations.
Comparing estimated allocations against actual llama-server runtime allocations revealed two primary sources of error:
- KV Cache Sizing: The KV cacheThe key and value vectors stored for every token, sized by layers, KV-head count, head dimension, and context length. was calculated using a single averaged head dimension across all layers.
- Compute Scratch Buffer: Memory allocated for intermediate tensor execution (compute bufferScratch space llama.cpp reserves for the largest intermediate tensors in one graph step; it grows with context and batch.) was modeled as zero.
Using a single averaged head dimension fails on modern architectures. DeepSeek-V2’s Multi-Head Latent Attention (MLA) specifies asymmetric key and value dimensions (key_length = 192, value_length = 128); averaging these dimensions under-sized the KV cache by ~20%. Similarly, Grouped-Query Attention (GQAGrouped-query attention: several query heads share one key/value head, so the KV cache is far smaller than the query-head count suggests.) shares key/value heads across query groups; averaging head counts obscured this ratio.
PR #17201 replaced global averaging with per-layer metadata extraction, reading exact key/value dimensions and KV-head counts per layer while assigning a non-zero lower bound to compute scratch buffers.
| Estimate | KV Cache (MiB) | Compute Buffer (MiB) | Total (MiB) | Error vs. Actual |
|---|---|---|---|---|
| Actual (measured) | 8640 | 2376 | 19392 | baseline |
| Legacy estimator | 6912 | 0 | 15405 | −20.6% |
| PR #17201 estimator | 8640 | 1103 | 18235 | −6.0% |
KV cache estimation matches the measured 8,640 MiB exactly. The remaining 6.0% delta reflects a conservative compute buffer floor (1,103 MiB vs 2,376 MiB peak).
Mathematical impact of averaged head dimensions on KV cache sizing
KV cache allocation is defined as:
Memory = 2 × layers × kv_heads × head_dim × context_length × bytes_per_element
Averaging head_dim across asymmetric key () and value () tensors under-sizes memory by ~20%. Reading exact per-layer metadata ensures exact KV cache accounting.
Compute buffer scratch space accounting
In addition to weight matrices and KV caches, llama.cpp allocates scratch space for graph intermediates (attention logits, expert routing buffers, feed-forward activations). Setting this buffer to zero in legacy code created memory estimation deficits that grew proportionally with context length. PR #17201 assigns a conservative non-zero baseline buffer to prevent unexpected OOM errors.
Evaluating llama.cpp MMQ Kernel Tile Sweeps and Correctness Validation
llama.cpp’s quantized matrix multiplication kernel (MMQ) uses a fixed default tile configThe compile-time tuple that sets how a kernel splits a matmul into tiles across threads, registers, and VRAM. (nthreads=256, occupancy=1, I=128, K_vram=256, stream_k=true) across multiple NVIDIA GPU generations (A100, RTX 3090, RTX 4090, H100).
We evaluated candidate tile configurations to test whether hardware-specific tuning could improve throughput. Increasing VRAM staging tile size to K_vram=512 produced a 3.43× throughput jump in microbenchmarks (138.9 TFLOP/s vs 40.5 TFLOP/s on Q4_K) and a 1.60× speedup in llama-bench.
However, validating the candidate configuration against llama.cpp’s test-backend-ops suite revealed numerical failures. The K_vram=512 tile configuration failed 25 out of 78 correctness tests for Q4_K and Q6_K tensor shapes, returning Normalized Mean-Squared Error (NMSENormalized mean-squared error, MSE divided by the variance of the true output. Zero is exact; 1.0 means the output is uncorrelated with the correct answer.) values near 1.0 on large-matrix prefill shapes.
| Tile Configuration | Q4_K Throughput | Speedup vs Default | MUL_MAT Correctness Test |
|---|---|---|---|
| Shipped default (K_vram=256) | 40.5 TFLOP/s | 1.00× | 78 / 78 Passed |
| Candidate (K_vram=512) | 138.9 TFLOP/s | 3.43× micro / 1.60× bench | 53 / 78 Passed (25 Failed, NMSE ≈ 1.0) |
The 25 failing test cases correspond to large prefill matrix shapes. The 3.43× microbenchmark speedup was an artifact of incomplete matrix computations, and the configuration was retracted.
Interpretation of NMSE = 1.0 error metrics
Normalized Mean Squared Error is defined as NMSE = MSE(output, reference) / Var(reference). An NMSE value near 1.0 indicates that kernel outputs share no correlation with reference outputs, indicating missing or uncalculated matrix tile passes.
Benchmarking INT8 k-Quant Kernels vs. FP16 cuBLAS
llama.cpp provides INT8 quantization paths for Q4_K and Q6_K weights (k-quant). Upstream documentation notes that at larger batch sizes (e.g. batch ), FP16 cuBLAS GEMMGeneral matrix multiply, the dense linear-algebra operation at the core of every transformer layer. execution can outperform custom INT8 dequantization kernels once workload intensity becomes compute-bound.
We benchmarked INT8 k-quant against FP16 cuBLAS on a Qwen2.5-1.5B model using an RTX 3090 GPU:
On smaller models or memory-bound batch sizes (batch size 1 decode), the INT8 k-quant path maintains a clear advantage (1.74× speedup over FP16 cuBLAS) by reducing HBM weight transfer volume.
Crossover dynamics between k-quant and cuBLAS
At small batch sizes, inference is constrained by HBM read speeds, making weight volume reduction (INT8/Q4_K) the primary driver of performance. At high batch sizes (batch ), execution shifts to compute-bound Tensor Core execution, where FP16 cuBLAS routines sustain higher peak compute density.
DeepSeek Multi-Head Latent Attention (MLA) Execution Paths
DeepSeek architectures utilize Multi-Head Latent Attention (MLA) to compress key and value projections into latent representations. We analyzed MLA execution paths within llama.cpp’s FlashAttention implementation (fattn.cu).
MLA operates in two structural representations within llama.cpp:
| MLA Representation | Head Dims (K / V) | GQA Ratio | Tensor Core (MMA) Support |
|---|---|---|---|
| Absorbed representation | 576 / 512 | gqa = n_head | Supported (MMA kernel active) |
| Uncompressed representation | 192 / 128 | gqa = 1 | Fallback path (gated at fattn.cu:412) |
Absorbed MLA representations dispatch to Tensor Core MMA kernels. Uncompressed representations with GQA ratio = 1 trigger non-tensor-core fallback execution.
MLA execution dispatch in fattn.cu
In absorbed MLA representations, latent up-projection matrices are combined into the attention layer, yielding key/value dimensions of . This shape satisfies llama.cpp’s MMA FlashAttention kernel constraints.
Uncompressed MLA representations ( dimensions, ) are restricted by a condition at fattn.cu:412, forcing execution onto non-tensor-core fallback paths. Re-enabling double-buffering on this fallback path provides a projected 1.15–1.4× performance increase.
Next Steps
- Gated MMQ Tile Sweeps: Re-evaluate MMQ tile parameters across GPU microarchitectures with inline
test-backend-opscorrectness validation. - Compute-Bound Crossover Benchmarking: Benchmark INT8
k-quantkernels against FP16 cuBLAS on 30B+ parameter models at batch sizes . - MLA Fallback Optimization: Optimize memory double-buffering for uncompressed MLA fallback execution paths in
fattn.cu.
Conclusion
Systematic profiling of memory accounting and kernel paths ensures predictable deployment behavior. Correcting Ollama’s MoE memory estimator (PR #17201) prevents premature CPU offloading and OOM failures, while combining throughput benchmarks with numerical correctness gates prevents false performance conclusions during kernel optimization.
Glossary
Glossary of memory and kernel optimization terms
- Mixture of experts (MoE)
- A model whose layers hold many expert sub-networks and fire only a few per token. Its footprint is dominated by weights that differ from layer to layer, so one averaged number describes it badly.
- VRAM
- The GPU's onboard memory. Weights, the KV cache, and the compute buffer all have to fit inside it, or a layer moves to the CPU.
- Memory estimate
- ollama's prediction of a model's GPU footprint, used to decide how many layers to place on the card before loading it.
- Offload
- Placing part of a model on the CPU when the GPU estimate says it will not fit. Correct when a model genuinely does not fit, needless when an under-estimate triggers it.
- KV cache
- The stored keys and values for every token seen, sized as
2 × layers × kv_heads × head_dim × context × bytes. The term the ollama fix makes exact. - Compute buffer
- Scratch space llama.cpp reserves for the largest intermediate tensors in one graph step. It grows with context and batch, and the old estimate counted it as zero.
- MLA (multi-head latent attention)
- DeepSeek attention that compresses keys and values into a latent space, with asymmetric key and value dimensions (
192 / 128uncompressed,576 / 512absorbed). - GQA (grouped-query attention)
- Several query heads share one key/value head, so the KV cache is far smaller than the query-head count implies. A single averaged head hides the ratio.
- MMQ
- llama.cpp's quantized matrix-multiply kernel (Matrix Multiply Quantized), which runs quantized weights on the GPU using one compile-time tile config per GPU family.
- k-quant
- llama.cpp weight formats such as
Q4_KandQ6_Kthat pack weights into 4 to 6 bits with per-block scales. - GEMM
- General matrix multiply, the dense linear-algebra operation at the core of every transformer layer.
- Tile config
- The compile-time tuple (
nthreads, occupancy, I, K_vram, stream_k) that sets how a kernel splits a matmul into tiles across threads, registers, and VRAM. - NMSE
- Normalized mean-squared error: mean-squared error divided by the variance of the true output. Zero is exact; 1.0 means the output carries no more information than a constant would.
- Quantization
- Storing weights in fewer bits, here 4 to 8, to shrink memory and speed up the matmul. The kernels above run on quantized weights directly.
- Prefill / decode
- Prefill processes the prompt in one large batch and is compute-bound; decode generates one token at a time and is memory-bound. Which kernel wins often flips between them.
- Roofline
- The hardware ceiling on a kernel: once it saturates compute or memory bandwidth, no scheduling change makes it faster. A 3× jump past it signals skipped work, not a win.