cpu-attn
Single-token decode attention over an INT8-quantized KV cache, aarch64-first. K and V quantize per token per head (absmax int8) at append time; decode runs integer q.k logits, an f32 softmax, and f32 accumulation over the int8 V stream. Versus an fp16 cache this halves both cache RAM and per-token cache bandwidth, which is the decode wall on LPDDR4-class boards. Companions on the Hub: phanerozoic/bitnet-cpu and phanerozoic/quant-matmul (linears), phanerozoic/decode-ops (fused glue and sampling).
logits[s] = sdot(q_i8, k_i8[s]) * q_scale * k_scale[s] * softmax_scale
out[d] = sum_s softmax(logits)[s] * v_scale[s] * v_i8[s, d]
Logit dots are exact int32, accumulation order is fixed, and the NEON polynomial exp satisfies exp(0) == 1 exactly, so uniform-logit constructions are bit-exact end to end.
| path | instruction | selected when |
|---|---|---|
| AVX2 | 16-bit widening vpmaddwd + Cephes exp |
x86-64 with AVX2 and FMA: Haswell and newer |
| SDOT | sdot (dotprod) |
aarch64 with asimddp: Cortex-A76+ (Pi 5), Neoverse, Apple silicon |
| NEON | smull + sadalp |
any other aarch64 (Pi 4, Pi Zero 2) |
| scalar | portable C++ | everything else |
Chosen once at runtime from CPUID / HWCAP; CA_CPU_ISA demotes for A/B
runs. The int8 q.k dot is signed x signed, which AVX-VNNI does not
accelerate, so both x86 tiers share the AVX2 path.
Usage
import torch
from kernels import get_kernel
ca = get_kernel("phanerozoic/cpu-attn", version=1, trust_remote_code=True)
cache = ca.Int8KVCache(n_kv_heads=8, max_seq=4096, head_dim=128)
for k, v in kv_stream: # [Hkv, D] f32 per token
cache.append(k, v)
out = cache.decode(q) # q [H, D] -> [H, D] f32
GQA follows from shapes: query head h reads kv head h // (H // Hkv).
API
| Function | Purpose |
|---|---|
kv_append(k_cache, k_scale, v_cache, v_scale, k_new, v_new, pos) |
quantize one token into the caches |
attn_decode(q, k_cache, k_scale, v_cache, v_scale, seq_len, scale) |
decode attention for one token |
Int8KVCache(n_kv_heads, max_seq, head_dim) |
allocating wrapper with append / decode / dequant_k / dequant_v |
Performance
Raspberry Pi 5 (4x Cortex-A76 2.4 GHz, SDOT path) and Raspberry Pi 4 Model B (4x Cortex-A72 1.8 GHz, NEON path), 64-bit Raspberry Pi OS, torch 2.13 CPU, D=128:
| H, Hkv, S | Pi 5 | Pi 4 |
|---|---|---|
| 8, 8, 512 | 0.089 ms | 0.558 ms |
| 8, 8, 2048 | 0.420 ms | 1.69 ms |
| 8, 8, 8192 | 1.80 ms | 7.51 ms |
| 32, 8, 2048 | 1.12 ms | 3.88 ms |
The Pi 5 streams KV at 9-12 GB/s, the board's memory ceiling; an fp16 cache would move twice the bytes for the same context.
Requirements
- Head dim a multiple of 16, at most 1024; f32 queries; H % Hkv == 0.
- This kernel is the decode path; prefill attention stays in torch SDPA.
- Fast paths cover aarch64 (NEON; SDOT with dotprod) and x86-64 (AVX2 + FMA). Anything else uses the correct scalar fallback.
Scope
The KV-cache half of a quantized CPU inference stack: quantized linears (bitnet-cpu, quant-matmul) make weights cheap to stream, this kernel does the same for the cache, and long-context decode stops being the first thing that dies on an 8 GB board.
License
Apache-2.0.
- Downloads last month
- -
- OS
- linux
- Arch
- x86_64aarch64
- Kernel Builder
- 19aaa64