June 2026

By Davide Ciffa

Luce KVFlash: 256K context with 72 MiB of KV on the GPU

Long context has a second memory bill nobody budgets for: the KV cache. On Qwen3.6-27B at 256K tokens it costs 4.6 GiB of VRAM and drags decode down to 13 tok/s, because every new token reads the whole thing. Luce KVFlash keeps a small pool of KV on the GPU, auto-sized to your VRAM, and pages cold 64-token chunks to host RAM, bit-exact and recallable. Decode holds a flat 38.6 tok/s from 64K to the native 256K on an RTX 3090, 2.9x the full cache at 256K, with 72 MiB resident and benchmark accuracy unchanged. One flag, every model family lucebox serves.

Luce KVFlash: a small resident pool of KV on the GPU, the rest of a 256K context paged to host RAM as cold chunks

TL;DR

The problem: the cache that grows whether you like it or not

Weights are a one-time bill. The KV cache is a tax on every token of context: on Qwen3.6-27B with Q8_0 KV it is about 18 KiB per token, so a 256K context costs 4.6 GiB of VRAM before the model generates anything. On a 24 GB card that already holds 15 GiB of weights, the cache is what decides how much context you can actually afford.

The memory is half the problem. Decode reads the entire cache once per generated token, so attention time grows linearly with context. You can watch it happen:

The standard fixes all trade something you want. Quantizing the KV (lucebox already runs Q8_0 by default) shrinks the per-token cost but keeps the linear growth. Sliding windows cap the cache but forget everything outside the window. Compression schemes drop tokens for good. What you actually want is the full context kept somewhere cheap, with the GPU only holding the part attention is about to use.

How it works: page the cache like an OS pages memory

KVFlash allocates the attention KV tensors at a fixed pool size, say 4K slots, instead of the 256K maximum. A pager maps logical token positions to physical pool slots at 64-token chunk granularity. When the pool fills, the coldest chunk's quantized rows are copied to a host backing store (0.6 ms per chunk) and its slots are reused. Attention runs over the pool with a slot-validity mask, so evicted positions are excluded exactly, not approximately.

  logical context  [0 ......................... 256K)
                       │ 64-token chunks
                       ▼
  GPU pool  [4K slots]   hot chunks, sinks, recent tail
  host RAM  [the rest]   cold chunks, bit-exact, recallable

Two properties make the relocation legal. RoPE is baked into the K rows at write time, so a row means the same thing in any physical slot. And paging moves raw quantized bytes, so a chunk that leaves and comes back is bit-identical to one that never left. The first chunk (attention sinks) and the trailing window are never evicted.

The interesting question is which chunks deserve the slots. Two policies ship:

Hard cap, not a threshold. The FlashMemory paper keeps chunks whose score clears a sigmoid threshold, and their own ablation shows the footprint creeping back up at 500K context. A fixed slot pool cannot creep: residency is a budget the GPU allocation enforces by construction, and relevance only decides who occupies it.

Memory: the part of the cache you hold goes flat

Attention KV in VRAM by context length, Qwen3.6-27B, Q8_0 KV, pool of 4K tokens:

At 256K the ledger reads: 72 MiB of KV on the GPU, 4.2 GiB in host RAM, and the roundtrip is bit-exact. Paging costs are small enough to ignore in practice: 0.6-1.3 ms per 64-token chunk, hidden behind decode steps that take 26 ms each, and a full repage during reselect costs about 1% of decode time.

Speed and quality, measured together

Here is what the 128K row feels like. Same model, same card, same 128K prompt, same 240-token answer; the only difference is the flag. Prefill is shown as a labeled timelapse (335.9 s vs 177.8 s); the decode race runs at the measured rates in real time:

Side-by-side terminal race at 128K context: the full KV cache decodes 240 tokens in 12.2 s at 19.6 tok/s while KVFlash decodes the same answer in 6.2 s at 38.6 tok/s, holding 72 MiB of KV in VRAM instead of 2304 MiB.
Full cache vs KVFlash at 128K on the same RTX 3090. KVFlash finishes the 240-token answer in 6.2 s; the full cache takes 12.2 s and holds 32x the VRAM doing it.

Numbers without a quality column are how KV tricks usually hide their bodies. Full A/B on Qwen3.6-27B Q4_K_M, RTX 3090, needle-in-haystack prompt at depth 25%, 240-token timed generation, KVFlash pool of 4K with the drafter policy:

ContextModePrefillDecode tok/sNeedle /16KV in VRAM (Q8_0)
32Kfull47.2 s32.816576 MiB
32KKVFlash41.8 s29.01572 MiB
64Kfull130.6 s27.8161152 MiB
64KKVFlash87.5 s38.61472 MiB
128Kfull335.9 s19.6162304 MiB
128KKVFlash177.8 s38.61472 MiB
256Kfull999.0 s13.1164608 MiB
256KKVFlash354.9 s38.61572 MiB

Read it honestly: at 32K KVFlash is slightly slower (29.0 vs 32.8) because a 32K cache is still fast to read and the pool adds a rescore. The crossover is around 64K, and from there the gap only widens: 1.4x at 64K, 2.0x at 128K, 2.9x at 256K. Recall gives up 1-2 needles out of 16 at 6% residency; the policy decides what those misses cost, which is why the policy is pluggable.

The ablation that justifies the drafter: recency-only LRU scores 0/16 the moment the needle falls outside its tail window. Relevance scoring is what makes 6% residency usable for retrieval, not the paging itself:

ContextResidencyLRU recallDrafter recallFull cache
8K9%0 / 1615 / 1616 / 16
32K9%0 / 1615 / 1615-16 / 16
256K6.25%0 / 1614-15 / 1616 / 16

On benchmarks where the answer depends on the whole prompt rather than a needle, nothing moves: HumanEval 10/10, GSM 10/10, MATH 10/10, agent tasks 6/6, identical pass rates to the full cache, with a base-vs-base control confirming the harness itself is deterministic. Speculative decoding also runs directly on the pool, at acceptance parity (15.4-15.6% vs 15.3%).

Every model family lucebox serves

The pager core is architecture-blind; each backend routes its KV writes and masks through it. Same flag, all four families, all smoked through live eviction with a deliberately small 1K pool:

ModelModeDecodeNotes
Qwen3.6-27Ball-GPU37.4 tok/sreference integration, all numbers above
Qwen3.6 35B-A3BSpark hybrid101.6 tok/spooled KV + offloaded experts compose
Laguna XS.2Spark hybrid137.1 tok/sall 40 layers pooled, SWA window stays exact
Gemma4 26B-A4Ball-GPU119.0 tok/spools the full-attention layers; SWA rings untouched

One caveat worth stating plainly: the drafter scores Qwen tokens, so on laguna and gemma4 it runs through a cross-tokenizer bridge (detokenize the target's history, re-tokenize for the drafter, score, map back by character spans). It is the default there too and strictly better than LRU, but not yet tuned to the Qwen-native 14-16/16, so treat long-range recall on those two as functional but not yet dialed in. They get the memory cap and the flat decode either way.

Usage

# recommended: drafter-scored residency, pool auto-sized from your VRAM.
# pass --prefill-drafter so the drafter is guaranteed (no silent LRU fallback).
dflash_server model.gguf --max-ctx 262144 --kvflash auto \
    --prefill-drafter qwen3-0.6b.gguf

# drop the path to auto-probe (model dir, drafter/, draft/, /opt/lucebox/models/drafter/);
# falls back to LRU if none is found, so check the banner reads policy=drafter
dflash_server model.gguf --max-ctx 262144 --kvflash auto

# explicit pool size, recency-only LRU
dflash_server model.gguf --max-ctx 262144 --kvflash 8192 --kvflash-policy lru

With --kvflash auto the pool sizes itself from free VRAM and the drafter loads on demand; pass --prefill-drafter to be sure it is used, since auto-probe falls back to LRU if no drafter is found. If you'd rather size by hand: without a drafter, LRU is recency-only memory, so size the pool to prompt plus generation headroom or it will evict the question itself; with the drafter, 25% of the expected context is a conservative default and 6-9% is measured safe for retrieval work. Memory-dense tasks that genuinely need every token at once (multi-round co-reference over the whole context) are the paradigm's honest limit, shared with the paper: size the pool up for those.

Bottom line

The KV cache was the last part of long context that scaled linearly with VRAM and decode time. Paged through a fixed pool with relevance deciding residency, it stops doing either: 256K context on a 24 GB card with 72 MiB of KV resident, decode flat at 38.6 tok/s where the full cache sinks to 13, prefill 2.8x faster, and the benchmark column unchanged. It composes with PFlash (which keeps the prompt small) and Spark (which keeps the experts small), because they manage different memory. On a local-inference PC the practical effect is simple: context length stops being the thing you ration.


Source: Luce KVFlash on github.com/Luce-Org/lucebox-hub (docs and measured results in optimizations/kvflash/, core in server/src/common/kvflash_pager.h). Numbers measured on an RTX 3090 24 GB, Qwen3.6-27B Q4_K_M, Q8_0 KV, June 2026. Design follows FlashMemory lookahead sparse attention, arXiv 2606.09079.

Related

Run 256K context on a 24 GB card

Open-source. One flag. Flat decode to the model's native maximum.

GitHub PFlash post Compare hardware Discord