KV-Cache Mechanics
DantinoX uses three distinct caching strategies depending on the model type and generation paradigm.
AR Static KV-Cache
The autoregressive KV-cache is statically pre-allocated at model construction:
shape: [B, H_kv, max_context, head_size] per layer
Static allocation means no dynamic shapes and no XLA recompilation between decode steps.
# Prefill — full forward pass fills the cache
logits, kv_cache, _ = model(
prompt_ids,
use_cache=True, kv_caches=init_cache, cache_index=0
)
# Decode — each step processes a single new token
logits, kv_cache, _ = model(
next_token,
use_cache=True, kv_caches=kv_cache, cache_index=T_prompt
)
Memory Formula
where:
\(L\) = number of layers
\(S\) = sequence length (tokens)
\(H_{kv}\) = number of KV heads
\(d_h\) = head dimension
bytes_per_element= 2 for bfloat16, 4 for float32
Reference table (12 layers, head_size=32, bfloat16)
Attention |
\(H_{kv}\) |
KV-MB @ 512 tok |
KV-MB @ 1024 tok |
KV-MB @ 4096 tok |
|---|---|---|---|---|
MHA (\(H=8\)) |
8 |
0.375 MB |
0.750 MB |
3.0 MB |
GQA (\(H_{kv}=H/4=2\)) |
2 |
0.094 MB |
0.188 MB |
0.75 MB |
GQA (\(H_{kv}=H/8=1\)) |
1 |
0.047 MB |
0.094 MB |
0.375 MB |
MLA (\(d_c^{KV}=96\)) |
— |
~0.027 MB |
~0.054 MB |
~0.216 MB |
MLA stores a compressed latent vector per token instead of full K/V tensors, then decompresses on the fly during attention.
Batch size calculator
For a single A100 40 GB, estimate the maximum batch size:
Attention |
Weights (256d 12b) |
KV/seq @ 512 tok |
Max BS @ 40 GB |
|---|---|---|---|
MHA |
~32 MB |
0.375 MB |
~100 |
GQA (×4) |
~30 MB |
0.094 MB |
~400 |
MLA |
~32 MB |
~0.027 MB |
~1500 |
!!! note
These are rough estimates. Actual VRAM usage depends on activations, intermediate buffers, and JAX XLA padding. Use jax.devices()[0].memory_stats() for real measurements.
MHA vs GQA vs MLA Comparison
MHA |
GQA |
MLA |
|
|---|---|---|---|
KV heads |
\(H\) |
\(H / r\) |
compressed latent |
KV cache size |
\(L \cdot 2 \cdot S \cdot H \cdot d_h\) |
\(L \cdot 2 \cdot S \cdot (H/r) \cdot d_h\) |
\(L \cdot S \cdot d_c^{KV}\) |
Decode throughput |
baseline |
~1.0× (similar) |
~0.8× (absorb overhead) |
Prefill speed |
baseline |
~same |
~10–30% slower |
Cache at 512 tok, 12L, bf16 |
0.375 MB |
0.094 MB (r=4) |
~0.027 MB |
Max batch @ 40 GB |
~100 |
~400 |
~1500 |
Config flag |
|
|
|
MLA’s slower per-step latency is offset by fitting more sequences in VRAM simultaneously, making it the highest-throughput option at large batch sizes.
Diffusion DualCache
For diffusion, the cache consists of two parts:
class DualCache(NamedTuple):
prefix_kvs: tuple # per-layer (k, v) for the prompt — computed once
suffix_kvs: tuple # per-layer (k, v) for remaining MASK tokens — refreshed per block
The suffix KV adds overhead proportional to the number of remaining MASK blocks. Averaged over a full generation this adds approximately 20–40% to the peak cache size vs. the static AR cache.
See Fast-dLLM DualCache for the full architecture description.
Cache Memory vs Throughput
At large batch sizes, the cache footprint determines how many sequences fit in VRAM simultaneously.
Measured throughput on A100 40 GB, 256d 12-layer model, bfloat16, seq_len=512:
Attention |
Decode tok/s (BS=1) |
Decode tok/s (BS=8) |
Max BS |
|---|---|---|---|
MHA |
89 |
~540 |
~100 |
GQA (×4) |
90 |
~600 |
~400 |
MLA |
70 |
~650 |
~1500 |
GQA and MHA have similar single-sequence throughput; MLA pays a per-step overhead (~20%) but excels when many sequences are batched together because more of them fit in cache.
Disabling the Cache
Disable the KV cache for debugging or for short sequences where re-allocation overhead is negligible:
# Python
tokens = generate(model, prompt_ids, use_cache=False)
# CLI
dantinox generate --run_dir runs/my_run --no_cache
Cache Warmup
JAX JIT-compiles the decode step on first call. The second call (and all subsequent) use the compiled kernel. DantinoX’s Generator class does one warmup forward automatically:
gen = Generator("runs/my_run")
# First call triggers JIT compilation (slow)
_ = gen.generate("warmup", max_new_tokens=1)
# Second call uses cached compilation
text = gen.generate("Real prompt", max_new_tokens=200)
The XLA compilation cache (stored in ~/.cache/jax_xla/dantinox) persists across processes, so the overhead is only paid once per unique model architecture.