llama.cpp -ctk and -ctv: The Default CUDA Build Supports Four Values
llama-bench accepts eight KV cache types and llama-server accepts nine, but the default CUDA build compiles FlashAttention kernels for only f16, bf16, q8_0 and q4_0, and only when K and V match. Every other setting has no kernel and prefills at 83-284 tokens per second against about 4,600. Measured on llama.cpp 69320fe on an A100.

llama.cpp `-ctk` and `-ctv`: The Default CUDA Build Supports Four Values
-ctk and -ctv set the data type of the KV cache. llama-bench's help prints the default and nothing else; llama-server's does list the nine allowed values, but neither says what any of them costs. I measured every accepted value on one A100.
Short answer: the accepted list depends on which binary you run, and the default CUDA build compiles FlashAttention kernels for only four of the values — and none at all when K and V differ. That is decided at compile time, so nothing warns you at runtime.
| Type | Accepted by llama-bench | Accepted by llama-server / llama-cli | Cache at 32K | Prefill (4096 tokens) |
|---|---|---|---|---|
| f32 | no | yes | 9.00 GiB | not measured |
| f16 | yes | yes | 4.50 GiB | 4,707 t/s |
| bf16 | yes | yes | 4.50 GiB | 4,652 t/s |
| q8_0 | yes | yes | 2.39 GiB | 4,584 t/s |
| q5_1 | yes | yes | 1.69 GiB | 83 t/s |
| q5_0 | yes | yes | 1.55 GiB | 88 t/s |
| q4_1 | yes | yes | 1.41 GiB | 140 t/s |
| q4_0 | yes | yes | 1.27 GiB | 4,594 t/s |
| iq4_nl | yes | yes | 1.27 GiB | 88 t/s |
The two lists are not the same
llama-bench parses these flags itself, in tools/llama-bench/llama-bench.cpp, where ggml_type_from_name knows eight names. Everything else goes through common/arg.cpp, whose kv_cache_types vector holds nine — the same eight plus f32. So llama-server -ctk f32 starts and llama-bench -ctk f32 exits with error: invalid parameter for argument: -ctk. Nothing in the help output says so.
Both flags also take a comma-separated list in llama-bench, so one command sweeps them:
llama-bench -m model.gguf -fa on -ngl 99 -ctk f16,q8_0,q4_0 -ctv f16,q8_0,q4_0 -p 4096 -n 128 -d 0 -d 8192Four of the eight fall off the CUDA FlashAttention path
Qwen3-8B Q4_K_M on one A100 80GB, -fa on -ngl 99, K and V set to the same type:
| Type | Prefill 4096 | Prefill @ 8K depth | Decode 128 | Decode @ 8K | Decode @ 32K |
|---|---|---|---|---|---|
| f16 | 4,707 | 3,950 | 152.0 | 134.6 | 104.6 |
| bf16 | 4,652 | 3,822 | 144.9 | 91.4 | 44.3 |
| q8_0 | 4,584 | 3,797 | 142.3 | 110.1 | 67.4 |
| q4_0 | 4,594 | 3,788 | 141.9 | 107.2 | 63.4 |
| q4_1 | 140.2 | 25.1 | 93.6 | 7.2 | — |
| q5_0 | 88.2 | 17.8 | 90.6 | 6.1 | — |
| q5_1 | 83.2 | 18.1 | 94.0 | 7.2 | — |
| iq4_nl | 88.0 | 18.7 | 82.9 | 6.6 | — |
All figures are tokens per second, two repetitions each. The empty @32K cells are configurations that never reached that depth inside the 15-minute per-configuration limit. The @8K column shows why: prefilling 4,096 tokens on top of an 8K cache at 18.7 tokens per second takes 3 minutes 39 seconds.
The gap between the two groups is not a tax, it is a cliff. The four slow types hold 1.27 to 1.69 GiB of cache at 32K and prefill 33 to 55 times slower than q4_0. iq4_nl and q4_0 store exactly the same 40.5 KiB per token, and one is 52 times faster to fill. Generation collapses too once depth accumulates: at 8K the slow four decode at 6.1 to 7.2 tokens per second against 107.2 for q4_0.
The cause is a build option
No guessing needed this time. ggml/src/ggml-cuda/fattn.cu checks two things before it picks a kernel:
#ifndef GGML_CUDA_FA_ALL_QUANTS
if (K->type != V->type) {
return BEST_FATTN_KERNEL_NONE;
}
#endifand ggml_cuda_fattn_kv_type_supported in the same file returns true for f32, f16, bf16, q8_0 and q4_0, while q4_1, q5_0 and q5_1 return true only when GGML_CUDA_FA_ALL_QUANTS is defined. iq4_nl is in neither list, so it is always false.
That option defaults to OFF in ggml/CMakeLists.txt, and my build's CMakeCache.txt confirms OFF. So the four slow types and every mismatched pair have no FlashAttention kernel compiled at all: kernel selection returns BEST_FATTN_KERNEL_NONE and the work falls to the slow general path, -fa on notwithstanding.
This is a build configuration, not a limit of the card. If you need those combinations, rebuild with -DGGML_CUDA_FA_ALL_QUANTS=ON and pay for it in compile time and binary size. iq4_nl stays unsupported either way. The next section measures how much comes back.
Mixing K and V breaks it too
-ctk and -ctv are separate flags, so nothing stops you from quantizing the V cache harder than the K cache. On this build, any mismatch drops off the fast path, including pairs where both types are fast alone:
| K / V | Prefill 4096 | Decode 128 |
|---|---|---|
| f16 / f16 | 4,707 | 152.0 |
| q8_0 / q8_0 | 4,584 | 142.3 |
| q8_0 / f16 | 283.8 | 89.3 |
| f16 / q8_0 | 213.0 | 89.7 |
| q8_0 / q4_0 | 125.9 | 92.9 |
| f16 / q4_0 | 122.1 | 91.3 |
In the default build the rule is simple: set both flags to the same value. An earlier post here reported the q8_0 / q4_0 mix as slow and treated it as a bad pair. It is not the pair. It is the mismatch.
How much the option brings back
If the cause is a build option, turn it on. I rebuilt the same commit with -DGGML_CUDA_FA_ALL_QUANTS=ON and measured under the same conditions. Both builds are CMAKE_BUILD_TYPE=Release, sm80.
| K / V | Default prefill | ALL_QUANTS prefill | Default decode | ALL_QUANTS decode |
|---|---|---|---|---|
| f16 / f16 (control) | 4,708 | 4,710 | 153.8 | 153.8 |
| q8_0 / q8_0 (control) | 4,587 | 4,588 | 144.3 | 144.3 |
| q4_1 / q4_1 | 136.2 | 4,593 | 93.1 | 144.6 |
| q5_0 / q5_0 | 100.7 | 4,582 | 91.4 | 140.2 |
| q5_1 / q5_1 | 94.8 | 4,589 | 93.5 | 143.5 |
| q8_0 / q4_0 | 143.9 | 4,591 | 93.0 | 143.7 |
| iq4_nl / iq4_nl | 102.1 | 101.6 | 82.6 | 83.4 |
The two control rows agree to the decimal, which is what makes the rest comparable. With the option on, the three slow types and the mismatched pair all run at full speed: 4,582 to 4,593 tokens per second of prefill against 4,599 for q4_0.
iq4_nl does not move, exactly as the source predicts. There is no path to using it as a KV cache type at this commit.
The price is compile time and binary size, since the build emits many more kernel combinations. Unless you specifically need q5_1's footprint (1.69 GiB at 32K), staying on the default build with q8_0 or q4_0 is the simpler answer.
bf16 is the one that looks safe and is not
bf16 costs the same memory as f16, 144 KiB per token, and keeps the fast prefill path. It looks like a free swap. At depth it is the worst of the four working types: 44.3 tokens per second at 32K against 104.6 for f16, and below both quantized types. If you are holding 4.5 GiB of cache anyway, f16 decodes 2.4 times faster at that depth.
What to set
- The cache fits in VRAM. Keep
f16. It is the fastest at every depth measured. - You need the memory back.
-ctk q8_0 -ctv q8_0halves the cache to 2.39 GiB at 32K and costs 36% of decode throughput at that depth.q4_0gives 1.27 GiB and costs 39%. - Anything else in the list, or a K/V mismatch. The default build has no FlashAttention kernel for it. Rebuild with
-DGGML_CUDA_FA_ALL_QUANTS=ONand measure before shipping. The failure is silent: the model loads, answers correctly, and feeds a long prompt at a walking pace.
The memory column is arithmetic, not a measurement: 36 layers × 8 KV heads × 128 dims × 2 (K and V) × bytes per element, times the context length. Qwen3-8B uses grouped-query attention with 8 KV heads, which is why 32K of f16 cache is 4.5 GiB rather than tens of gigabytes.
The trap that cost me this table once
The first sweep produced f16 prefill at 1,976 tokens per second. The real figure is 4,707. A second benchmark process was still on the same GPU from a run I thought I had stopped, and the two shared the card. Nothing in the output says so — llama-bench reports a clean mean with a small standard deviation, because both processes were slowed steadily.
If you benchmark on a shared box, check nvidia-smi --query-compute-apps=pid,used_memory --format=csv before you trust a number, and run one sweep at a time. The sweep script in the harness kit now refuses to start if another llama-bench is alive — though that only guards against my own runs, not someone else's process on a shared card.
Harness and raw results: the sweep script and the per-configuration JSON ship with the free KV cache benchmark harness kit.
Setup: llama.cpp 69320fe (CUDA, sm80, CMAKE_BUILD_TYPE=Release), Qwen3-8B Q4_K_M, one A100 80GB PCIe. The main tables use the default build with GGML_CUDA_FA_ALL_QUANTS=OFF; the comparison section rebuilds the same commit with it ON and measures on the same card. Settings: -fa on -ngl 99 -p 4096 -n 128 -d 0 -d 8192 -d 32768 -r 2, one process at a time; the mixed K/V pairs were run at depth 0 with -r 1. Accepted-value lists read from tools/llama-bench/llama-bench.cpp (ggml_type_from_name) and common/arg.cpp (kv_cache_types) at the same commit. Measured on 2026-09-21.
Want this measured on your own model?
Tell us the model and the constraints. We design the conditions, run them, and write down which choice to make. Default hardware is an A100 80GB; other GPUs are possible.
Subscribe to Newsletter
Related Posts

llama.cpp KV Cache Quantization: Why q8_0 Costs 9% of Throughput — or 22%
Mainline llama.cpp on one A100, Qwen3-8B Q4_K_M, llama-server with 1 to 32 concurrent slots. On a 32K prompt, q8_0 cost 9% of server throughput when each request generated 128 tokens and 22% when it generated 1,024, because prefill dominates the short case and prefill is unaffected by the KV type. Per-token decode was 34% slower, matching llama-bench. VRAM in use after startup fell from 41.0 GiB to 25.1 GiB at four 64K slots.

llama.cpp KV Cache Quantization, Measured on One A100 — q8_0 Is Free at 4K and Costs Half Your Decode Speed at 64K
Mainline llama.cpp, Qwen3-8B, one A100: -ctk q8_0 -ctv q8_0 matches f16 perplexity and cuts the 32K cache by 2.1 GiB, but decode at 64K depth drops to 55% of f16 (q4_0: 50%). Two other settings, q5_1 and a q8_0/q4_0 mix, silently ran prefill on the CPU at 43 and 63 tokens per second.

Jev's Speed Claims: Benchmarking Label-Only Alternatives on BANKING77
Four ways to get only a label on BANKING77's 77-intent test set, compared on the same 154 messages. Pretrained MiniLM embeddings with a logistic regression reached 90.3% accuracy at a 6.8 ms median on CPU. Jev itself has not been measured yet.