Prefill Performance Roadmap
The current CK prefill path is correct and auditable, but the Q4_K_M gap against llama.cpp is now mostly a layout and microkernel problem.
31 tok/s while llama.cpp was around 78 tok/s on the local i7.
After x8 testing, the dispatch matrix showed x8 winning for short/medium prefill shapes and losing for the
longer M=128,N=896,K=4864 compact-down shape. Treat the exact numbers as host-local; the durable
signal is that layout and shape-gated dispatch, not math, are the bottlenecks.
6682.61 ms (4.34 tok/s) to
2588.18 ms (11.20 tok/s) — about 61% less prompt time and 2.6x the previous CKE
batched-prefill throughput, measured on an ICX native AVX-512/VNNI build inside a 14-CPU cgroup.
The lane is still opt-in (CK_V8_FORCE_BATCHED_PREFILL=1) until 1K-context logit parity is certified.
Operator steps: v8 Runbook — Qwen3.6 Q4_K_M optimized prefill.
54.32 s to 34.75 s and mixed-prefill wall time fell from 77.21 s to 56.87 s. The real llama.cpp production oracle remains bit-exact.
Why Prefill Is Different
Decode is mostly GEMV: one activation vector times many weight rows. Prefill is GEMM: many token rows times many output rows. A fast decode kernel can still leave prefill slow because prefill needs to reuse both activation rows and weight blocks across a two-dimensional tile.
The key mistake is to treat prefill as many independent GEMV calls. That preserves correctness, but it reloads metadata and activation blocks too often. llama.cpp's faster path uses repacked/interleaved layouts so the hot loop computes a block of rows and columns together.
Optimization Ladder
| Step | What Changed | Why It Matters | Status |
|---|---|---|---|
| Q8 activation contract | Quantize prefill activations to Q8_K before mixed-quant GEMM. |
Stops repeatedly feeding FP32 into quantized weights and aligns CK with llama-style quantized dot products. | Active |
| Persistent threadpool | Use CK's pthread pool instead of creating OpenMP regions around every kernel. | Reduces launch overhead and gives the runtime explicit control over active threads, row splitting, and phase-specific dispatch. | Active |
| Shape-gated dispatch | Choose serial, row-split, N-split, or 2D dispatch based on M,N,K and thread count. |
Small shapes can regress when parallel overhead or activation rereads dominate; wide shapes need more jobs to fill cores. | Active for Q6; experimental for Q4 |
| Packed Q4_K metadata | Pre-unpack Q4_K scales/mins into a cached packed-meta layout. | Moves repeated metadata decoding out of the hottest dot-product path. | Active, but not enough |
| Q4_K 8x8 repacked GEMM | Pack eight Q4_K output rows together and compute an output-row tile for each Q8_K token row. | This captures the first llama.cpp-style layout win: each Q8 activation load is reused across multiple output lanes instead of one row at a time. | Shape-gated default for measured short/medium Q4_K prefill shapes |
| Token-tile x output-tile GEMM | Schedule small token tiles against eight-row output tiles, with a separate implementation for the exact repacked split-min contract. | This reuses the same packed Q4_K group across multiple Q8_K token rows and is the first step toward a true register-blocked GEMM microkernel. | Active for aligned, large grouped-prefill shapes; other arithmetic contracts remain separately gated |
| Exact head-parallel DeltaNet + Q4 x16 prefill | Partition independent DeltaNet value heads across the threadpool once per prompt, and route the measured Qwen3.6 Q4_K projection shapes through the AVX-512 VNNI x16 provider with weights packed at load time. | Removes serial head execution and repeated per-token dispatch for Qwen3.6 prefill without changing the certified serial arithmetic. | Completed for measured Qwen3.6 Q4_K_M shapes (PR #277); opt-in until long-context parity is certified |
| Fused MLP kernels | Fuse norm/projection/SwiGLU/down or similar sequences after the base GEMM is competitive. | Useful, but premature while the Q4_K prefill GEMM itself is still the dominant gap. | Later |
The Q4_K Repack Target
The current CK packed-meta path is shaped like this:
for each token m:
for each output row n:
for each Q4_K block b:
dot_one_q4k_block_with_one_q8k_block()
That is simple and auditable, but it is still scalar at the tile level. The next target is shaped more like this:
for token tile Mx4 or Mx8:
pack Q8_K activations into an interleaved tile
for output tile Nx8:
read one interleaved Q4_K weight tile
accumulate an MxN output tile in registers
apply Q4_K scale/min metadata once per tile lane
This mirrors the idea behind llama.cpp's ggml_gemm_q4_K_8x8_q8_K: the math is the same, but the memory layout changes so the CPU hot loop does less bookkeeping per useful multiply.
The important part is not copying llama.cpp blindly; the important part is matching the dataflow: interleave weights, interleave activations, compute a small output tile, and keep metadata close to the SIMD lanes.
CK keeps the canonical for token -> for output row -> dot() kernel as the reliable fallback.
The packed-x8 lane groups eight Q4_K output rows per K block and uses a SIMD tile accumulator that loads each Q8 activation chunk once, then reuses it across the active output lanes.
It is now the default for measured short/medium Qwen/Nemotron-family Q4_K prefill shapes, with
CK_DISABLE_Q4K_PACKED_META_X8_PREFILL=1 as the escape hatch and
CK_FORCE_Q4K_PACKED_META_X8_PREFILL=1 for new hardware sweeps.
The current x8 shape is still closer to:
for token:
for output_row_group_of_8:
compute 8 output rows for one Q8_K token row
The stronger target remains:
for token_tile:
for output_tile:
compute multiple tokens x multiple output rows together
Exact Split-Min M-Reuse Result
The promoted Qwen3-VL path does not use the older interleaved minimum-correction kernel. It keeps two FP32 accumulators per output, traverses K blocks in ascending order, performs one final value-minus-minimum subtraction, and applies bias afterward. Only independent output scheduling changes. Complete four-row groups follow the repacked GEMM contract; residual rows continue through the distinct repacked GEMV contract.
| Measurement | Before | After | Change |
|---|---|---|---|
| Q4 projection kernel total | 54.32 s | 34.75 s | 36.0% lower |
| Profiled mixed prefill | 77.21 s | 56.87 s | 26.3% lower |
| 128-token production mixed prefill | 85.58 s | 65.84 s | 23.1% lower |
| llama.cpp production oracle | Bit-exact | Bit-exact | No numerical regression |
These are host-local GCC/AVX2 measurements with 20 CK worker threads and explicit hybrid-core affinity. The durable result is the provider design and exact oracle, not an expectation that every CPU will reproduce the same percentage.
The current experimental x8mt path takes the first step:
CK_ENABLE_Q4K_PACKED_META_X8MT_PREFILL=1 \
CK_Q4K_PACKED_META_X8MT_TILE_M=2 \
make profile-v8-prefill-perf-stat
In the local dispatch matrix, x8mt with tile_m=2 edged out plain x8 on the measured
Q4_K shapes, including the longer M=128,N=896,K=4864 compact-down shape. It remains opt-in because
end-to-end Qwen2 timings on the laptop were noisy and not yet a stable promotion signal. The next real kernel
improvement is to keep the tile accumulators in registers across the token tile instead of using x8mt mostly as
a scheduling/data-reuse step.
Why 2D Scheduling Was Not Enough
CK now has an opt-in Q4_K packed-meta 2D scheduler:
CK_ENABLE_Q4K_PACKED_META_2D_PREFILL=1 \
CK_FORCE_Q4K_PACKED_META_2D_PREFILL=1 \
CK_Q4K_PACKED_META_TILE_M=8 \
CK_Q4K_PACKED_META_TILE_N=512 \
CK_V8_PREFILL_PERF_ENGINE=ck \
make profile-v8-prefill-perf-stat
Local tests showed only a small and noisy improvement. That is expected: 2D scheduling creates better work distribution, but the hot loop still computes one output value at a time. The x8 layout fixed the more important part first: reuse one activation chunk across multiple output lanes. The next step is a true token-tile/output-tile microkernel.
Exact Qwen Recurrent Q/K Normalization Parallelism
The Qwen3.5 recurrent Q/K L2 provider now partitions complete
(Q-or-K, row, head) jobs through the persistent CK thread pool for sufficiently large prefill
calls. Every head retains the scalar FP64 accumulation, FP32 square root, epsilon clamp, and element-update
order. No reduction is split between workers. Decode, small calls, nested dispatch, and overlapping Q/K
ranges continue through the serial loops.
Native tests compared one-thread and four-thread execution bit-for-bit over 257 rows, unequal Q/K head
counts, and eight repetitions. The llama.cpp production oracle covered five shapes, including a 65-row case
above the dispatch threshold, at 1, 16, 20, and 24 threads: all 20 comparisons passed exactly. Three baseline
and three candidate full-model runs retained first-logit hash 7fedeeccedd89648 and token IDs
[100, 100].
| Q/K normalization measurement | Baseline | Parallel provider |
|---|---|---|
| Isolated production shape, 1 thread | 2.142 ms | 2.109 ms |
| Isolated production shape, 16 threads | 2.142 ms | 0.404 ms |
| Combined-stack provider median | 40.537 ms | 9.002 ms |
| Combined-stack full prefill median | 2160.170 ms | 2117.058 ms |
The isolated shape was rows=1024, q_dim=k_dim=2048, and
head_dim=128. The combined result was measured on an Intel Core i7-14700T with AVX2/FMA and 16
physical-core representatives, using a Qwen3.5 0.8B Q4_K_M artifact with SHA-256
9297be150ac361e95e27387f5bd4b17a55a24c5428467e64bb304734f6be097d. The baseline engine SHA-256 was
d865e043cf826603b1a21267d01b695e435346fe5b69c4dfd306796d1ce1e5b6; the candidate engine SHA-256 was
824c41b3c5716a70bb5a29d38ffd250583c67c2cbcc5b7ac4a7359f64c2227be. Ryzen and long-context
measurements remain open.
Exact Attention-Gate Parallelism
The llama.cpp-compatible FP32 attention gate now schedules complete token rows through the persistent CK
thread pool for sufficiently large prefill calls. Each output retains the scalar libm.so.6
expf call and multiplication order. Decode, small calls, nested dispatch, and unsupported overlap
cases stay serial. Qwen3.5, Qwen3.8 dense, and Instella MoE consume this provider; the separately selected
Qwen3.8 Flash PyTorch BF16-storage gate is unchanged.
Native tests compared one-thread and four-thread output bit-for-bit over 257 rows, 16 heads, and 128 values
per head, including separate and exact in-place output across eight repetitions. A production-size
65 x 2048 comparison was bit-exact with the pinned llama.cpp oracle at 16 threads. Five baseline
and five candidate Qwen3.5 full-model runs retained first-logit hash 7fedeeccedd89648 and token IDs
[100, 100].
| Attention-gate measurement | Baseline | Parallel provider |
|---|---|---|
| Isolated 1024 x 2048 call | 7.391 ms | 2.677 ms at 16 threads |
| Full-model gate region median | 46.844 ms | 4.933 ms |
| Full-model prefill median | 2165.770 ms | 2108.758 ms |
These measurements used the same Intel Core i7-14700T host and Qwen artifact described above with 1,024
repeated token IDs, two generated IDs, and context capacity 2,048. The baseline engine SHA-256 was
d865e043cf826603b1a21267d01b695e435346fe5b69c4dfd306796d1ce1e5b6; the candidate engine SHA-256 was
fe4c1af0514d5120038d6393363f7fa7c78c6ec2abca5196790ff4db67653cd4. Ryzen, long-context, Instella
full-model, and matched llama.cpp end-to-end measurements remain open.
Measurement Contract
Use the safe perf lane for routine iteration:
make profile-v8-prefill-perf-stat
The report records prefill/decode tok/s, milliseconds, cycles, instructions, IPC, cache misses, cache miss rate, and context switches. This is the default lane because VTune/Advisor deep collection can load kernel drivers that destabilized the local Linux profiling host.
Promotion Rule
- Keep every new layout behind a shape gate and an escape hatch.
- Prove parity against canonical CK for synthetic Q4_K/Q8_K blocks.
- Benchmark micro-shapes: Qwen3.5 gate/up, down, qkv/out, compact MLP, and wider MLP shapes.
- Benchmark end-to-end Qwen3.5 v8 prefill against llama.cpp with the same cached GGUF.
- Promote only the shapes where the sweep wins; keep canonical dispatch as the fallback.
Remaining Performance Work
The Qwen3.6 Q4 x16 prefill shapes and the exact head-parallel DeltaNet items above are completed, measured work. The next measured targets, in order, are planned work — none of them are implemented yet:
- Q6 recurrent QKV and MLP-down short-wide GEMM providers.
- Q8 recurrent output projection.
- Reduction of threadpool barriers and intermediate-buffer traffic.
- Per-layer CKE-versus-llama.cpp performance attribution.
- Separate first-token and steady-state decode analysis.
- Resolution of the 353-token / 1K-token numerical accumulation drift that currently keeps Qwen3.6 batched prefill opt-in.
Related Files
src/kernels/gemm_kernels_q4k_q8k_vnni.c- Q4_K x Q8_K packed-meta kernels.version/v8/src/ck_parallel_prefill_v8.c- v8 prefill dispatch policy.scripts/perf_stat_v8_prefill_compare.py- safe CK-vs-llama perf counter lane.benchmarks/compare_ck_llama_v8.py- standardized v8 model-family comparison.- v8 Runbook — Qwen3.6 optimized prefill - operator commands, dispatch verification, and current parity limitation.
docs/notes/QWEN36_EXACT_BATCHED_PREFILL_2026-07-30.md- authoritative engineering note for PR #277.- Q6_K Prefill Roofline Notes - shape-gating precedent for Q6_K.
- Profiling Guide - safe profiling commands and VTune/Advisor warning.