veloxquant_mlx.quantizers.knorm module¶
L2-norm KV eviction primitives — intrinsic key-norm token importance.
Inspired by “A Simple and Effective L2 Norm-Based Strategy for KV Cache Compression” (Devoto, Zhao, Scardapane, Minervini — EMNLP 2024; arXiv:2406.11430, code at https://github.com/alessiodevoto/l2compress). Documented as “L2Norm-adapted (VeloxQuant-MLX implementation)” — not a faithful port.
The paper’s finding (in trained decoder LMs): a low L2 norm of a key
embedding usually leads to a high attention score during decoding — a KV
pair’s influence is largely determined by the key itself, before it is ever
queried. Eviction follows directly: rank cached tokens by key norm, keep the
lowest-norm ones. This is the repo’s first intrinsic eviction scorer:
every other eviction method scores with attention / a key-as-query proxy
(SnapKV, H2O, TOVA, PyramidKV, SqueezeAttention, ChunkKV, CaM) or pure
structure (StreamingLLM, sink, sliding-window). Note the sign inversion vs
ChunkKV’s key_norm option and ZipCache’s saliency proxy, which treat
high norm as important — the inversion is the paper’s empirical content.
Path-independence invariant (recent=0): the norm is computed once at
insertion and never updated, so evicting the worst-scoring non-sink token
whenever over budget is the classic “keep k best with a heap” algorithm —
the final kept set equals the global budget-best over all tokens seen,
regardless of how arrivals were grouped. Prefill-in-one-block and
token-by-token decode produce bit-for-bit identical kept sets. No
accumulating-score method (H2O, TOVA) has this property. (recent > 0
breaks it: the protected window moves with time.)
- Adaptation limitations (stated plainly):
The low-norm ⇒ high-attention correlation is the paper’s empirical claim about trained models; nothing here validates it on synthetic data (see the benchmark’s isotropic control, where the method shows no advantage).
No RoPE position-ID renumbering after eviction — surviving tokens keep their original absolute positions (
knorm_updaterestores temporal order after top-k selection), so the cache layer reports the true token position for RoPE rather than the retained row count (seecache/knorm_cache.pyand :issue:`171`, :issue:`174`).Uniform budget and n_sink across all heads.
recent(trailing protected window) is an extension, off by default.
Public API (mirrors quantizers/h2o.py)¶
KnormState — immutable per-head state dataclass init_knorm_state — construct empty state (validates budget vs guards) knorm_update — absorb a whole [S, D] block, evict if over budget knorm_get_kv — extract current (keys, values) arrays knorm_fp16_bytes — bytes stored in current state full_knorm_fp16_bytes — hypothetical cost without eviction
- class veloxquant_mlx.quantizers.knorm.KnormState(keys, values, norms, n_sink, budget, recent, keep)[source]¶
Bases:
objectPer-head L2-norm eviction state.
- Variables:
keys – [n_kept, D] fp16 stored key rows, or None before first update.
values – [n_kept, D] fp16 stored value rows, or None before first update.
norms – [n_kept] float32 L2 norm of each kept key row — computed once at insertion, never updated (the intrinsic-score property).
n_sink – Number of leading sink positions — never evicted.
budget – Maximum tokens kept at any time (including sinks).
recent – Trailing protected window (0 = off, paper-faithful).
keep – “low” = keep lowest-norm tokens (the paper’s finding); “high” = inverted selection (the benchmark’s ablation arm).
- Parameters:
- veloxquant_mlx.quantizers.knorm.init_knorm_state(n_sink, budget, head_dim, recent=0, keep='low')[source]¶
Create an empty KnormState before any tokens arrive.
- Raises:
ValueError – if
keepis invalid or the protected positions (sinks + recent) leave no evictable room within the budget.- Return type:
- Parameters:
- veloxquant_mlx.quantizers.knorm.knorm_update(state, new_keys, new_values)[source]¶
Absorb a whole block of S tokens, then evict down to budget in one shot.
Vectorized — no per-token loop: norms are intrinsic (never updated), so the over-budget case is a single protected top-k selection. Kept tokens are returned in original temporal order.
- Return type:
- Parameters:
state (KnormState)
new_keys (mlx.core.array)
new_values (mlx.core.array)
- veloxquant_mlx.quantizers.knorm.knorm_update_batched(prior_keys, prior_values, prior_norms, new_keys, new_values, budget, n_sink, recent, keep, *, return_indices=False)[source]¶
Batched
knorm_updateacross N = B*H independent rows.Requires
budget/n_sink/recent/keepto be uniform across all N rows (true for everyL2NormKVCacheinstance — one config per cache, not per head), so every row’sn_totalafter concatenation is identical and the top-k selection is a single batchedargsortalong the token axis instead of N independent Python-level calls.prior_keys/prior_values/prior_normsmust be pre-materialized with a uniformn_prior(0 before any tokens have arrived — the caller passes empty[N, 0, D]/[N, 0]arrays rather thanNone, since batching needs uniform shapes; unlikeappend_paged_summarythere is no partial-page carry to fold in, just concatenation followed by an ordinary top-k).With
return_indices=True, append the selected input-row indices.Returns
(keys, values, norms), each[N, min(n_total, budget), D]/[N, min(n_total, budget)], kept rows in original temporal order — bit-for-bit identical to runningknorm_updateper row in a loop (exact reindex, no lossy floating-point reduction involved).
- veloxquant_mlx.quantizers.knorm.knorm_get_kv(state)[source]¶
Return
(keys, values)arrays from state.Returns
([0, 1], [0, 1])zero-row placeholders before the first update (same contract ash2o_get_kv).- Return type:
tuple[array,array]- Parameters:
state (KnormState)
- veloxquant_mlx.quantizers.knorm.knorm_fp16_bytes(state)[source]¶
Bytes currently stored for K + V in fp16.
- Return type:
- Parameters:
state (KnormState)