Veloxquant-mlx

Latest version: v0.94.6

Safety actively analyzes 1035885 Python packages for vulnerabilities to keep your Python projects secure.

Scan your dependencies

Page 1 of 37

16.307

points. The budget-responsive curve confirms the offset fix end-to-end —
pre-174 position drift grows without bound with sequence length and would
flatten every arm regardless of budget. Documented in
`docs-site/docs/algorithms/knorm.md`, replacing its "no model-level
benchmark has been run" note. **Scope**: one model, two budgets — larger
models and a wider sweep remain open under
[181](https://github.com/rajveer43/VeloxQuant-MLX/issues/181) /
[180](https://github.com/rajveer43/VeloxQuant-MLX/issues/180); L2Norm
TTFT/throughput is still unmeasured. Docs only, no code changes.

**Reviewed StreamingLLM-adapted's RoPE position semantics against the paper**
([189](https://github.com/rajveer43/VeloxQuant-MLX/issues/189)) — the
implementation already preserved original absolute token positions after
eviction rather than the paper's cache-slot renumbering (a deliberate
divergence noted briefly in the docs since the 171 offset fix), but this
had never been reviewed against the paper explicitly, and unlike every
other eviction cache in the library, `StreamingLLMKVCache` had **no
regression test** guarding the `_true_offset`/`171` fix. Added three
offset-tracking tests to `test_streaming_llm_cache.py` (mirroring the
KNorm/SnapKV/TOVA/Q-Filters coverage: true position through sustained
eviction, block-size advance on prefill, and correctness across a
prefill-then-decode mix), and expanded `docs-site/docs/algorithms/streaming_llm.md`
with a full review section explaining why absolute-position preservation
was kept over the paper's renumbering (RoPE relativity makes it exact;
consistency with every sibling cache; avoids a per-step re-rotation cost;
the cache-wrapper boundary can't implement the paper's scheme cleanly
anyway). **Not measured**: whether the paper's renumbering scheme would
produce different generation quality — that needs real model inference
and remains open, same as [187](https://github.com/rajveer43/VeloxQuant-MLX/issues/187).

**Reviewed SnapKV-adapted's RoPE position semantics against the paper**
([188](https://github.com/rajveer43/VeloxQuant-MLX/issues/188)) — unlike
StreamingLLM, the SnapKV paper doesn't define a position-renumbering scheme
at all (it compresses the cache once, at the end of prefill, before
generation starts); common HF-`DynamicCache`-based reference
implementations instead fall back to the retained-row-count convention this
repo's `171` fix moved away from. Reviewed why absolute-position
preservation isn't a stylistic choice here but the behavior mathematically
required by how the eviction mechanism works: SnapKV selects from K rows
the model already rotated with RoPE during its own prefill forward pass, so
survivors' rotations are permanently baked in before eviction happens —
reporting anything but their true position would break RoPE's
relative-distance identity. Unlike StreamingLLM (continuous per-step
eviction), a hypothetical renumbering scheme here would only cost a
one-time `O(budget)` re-rotation right after prefill compression — still not
worth implementing, since exact positions already require none. Test
coverage already existed
(`test_offset_tracks_true_position_not_retained_rows` plus the
chunked-prefill offset assertions) — no code or test changes needed, this
was purely a documentation review. Added to
`docs-site/docs/algorithms/snapkv.md`.

Fixed

**RoPE positions after eviction in TOVA-adapted**
([175](https://github.com/rajveer43/VeloxQuant-MLX/issues/175)) — audited
H2O-adapted and TOVA-adapted for the same class of defect fixed for
Q-Filters, L2Norm, SnapKV, and StreamingLLM. Findings:

- **H2O-adapted already fixed** (shipped in v0.44.4, predating this audit):
`self.offset` already tracks the true absolute step count directly, and
because H2O renumbers positions to close gaps when an interior eviction
happens (e.g. with `h2o_n_sink > 0`), it additionally re-rotates the
shifted survivors — a stronger fix than the other caches need, since they
never renumber. No change required.
- **TOVA-adapted carried the defect**: `self.offset` reported the retained
row count rather than the true absolute token position once eviction
pinned the kept set at `tova_budget`. `tova_update` drops exactly the
evicted row and keeps the rest in temporal order (never renumbers), so
the same `_true_offset` counter that sufficed for Q-Filters/L2Norm applies
directly — no `offset` property split like SnapKV's was needed, since
`TOVAKVCache.update_and_fetch` fully resets `self.keys`/`self.values`/
`self.offset` on every call, prefill and decode alike.

Regression tests added to `test_tova_cache.py` mirroring the Q-Filters/L2Norm
coverage: offset tracks true position through sustained eviction, advances
by block size on prefill, and stays correct across a prefill-then-decode mix.

**RoPE positions after eviction in L2Norm-adapted**
([174](https://github.com/rajveer43/VeloxQuant-MLX/issues/174)) — carried
the same defect fixed earlier for Q-Filters, SnapKV, and StreamingLLM:
`self.offset` reported the **retained row count** rather than the true
absolute token position once eviction pinned the kept set at
`knorm_budget`. `mlx_lm` rotates both the query and the incoming key at
`offset=cache.offset` *before* `update_and_fetch` runs, so subsequent
tokens were rotated at a stale, non-advancing position — the offset drift
that excluded L2Norm as a fair baseline in Q-Filters benchmarks.

L2Norm's `knorm_update` already restores kept rows to temporal order after
top-k selection (never renumbers survivors), so the same fix that sufficed
for Q-Filters applies directly: a `_true_offset` counter incremented by the
incoming block size `S`, reported as `self.offset` after every
`update_and_fetch` call. Unlike SnapKV, no `offset` property split was
needed — `L2NormKVCache.update_and_fetch` fully resets
`self.keys`/`self.values`/`self.offset` on *every* call (prefill and decode
alike), so the base class's cursor arithmetic never observes the true
position as a stale row count between calls.

Regression tests added mirroring the Q-Filters/SnapKV coverage: offset
tracks true position through sustained eviction, advances by block size
(not retained rows) on prefill, and stays correct across a prefill-then-decode
mix. L2Norm can now be re-enabled as a fair comparison arm in Q-Filters
benchmarks.

**RoPE positions after eviction in SnapKV-adapted and StreamingLLM-adapted**
([171](https://github.com/rajveer43/VeloxQuant-MLX/issues/171)) — both
carried the same defect fixed earlier for Q-Filters: `self.offset` reported
the **retained row count** rather than the true absolute token position.
`mlx_lm` rotates both the query and the incoming key at
`offset=cache.offset` *before* `update_and_fetch` runs, so once eviction
started every subsequent token was rotated at the wrong position. Measured
over 200 decode steps after a 64-token prefill at budget 32:

- **StreamingLLM** — offset froze at **32** while the true position reached
**263** (drift **+231**, growing without bound once the window saturated).
- **SnapKV** — offset advanced but stayed **exactly 32 behind**, the constant
deficit being the tokens dropped during prefill compression.

Both preserve original positions (`snap_select_indices` returns kept indices
sorted ascending; StreamingLLM's window drops rows without renumbering) and
RoPE is relative, so reporting the true position is sufficient — survivors
need no re-rotation, unlike H2O/Keyformer which renumber.

SnapKV needed more than the `_true_offset` counter that sufficed for
StreamingLLM: its decode path appends deltas, so the base class's cursor
arithmetic and return slice (`self.keys[..., :self.offset, :]`) genuinely
require the row count. `offset` is now a property yielding the row count
while the base class is on the stack and the true position outside it.

Note this diverges from the StreamingLLM paper, which assigns positions by
index *within* the cache; matching that would require re-rotating every
survivor each step. Recorded in the cache docstring.

Two existing tests asserted `offset == retained rows` — the old meaning —
and were updated, with a dedicated regression test added.

**A2ATS-adapted paper-fidelity fixes** ([29](https://github.com/rajveer43/VeloxQuant-MLX/issues/29)) —
four deviations from the source paper (He et al., ACL 2025 Findings), three
of which contradicted its equations rather than knowingly adapting them:

- **Far keys are no longer rotated.** `a2ats_apply_windowed_rope` applied a
fixed `R_window` rotation to out-of-window keys. Paper Eq. (12) leaves them
**unrotated** (`k̃_i = k_i`), with the constant `R_b` belonging on the
*query* (Eq. 11, `u_ij = q_i R_b k_j^T`). The old form computed
`q_i R_w^T k_j^T` — wrong operand, wrong direction — and left far keys in a
rotated frame, defeating the position-decoupling that makes a shared
codebook viable (§3.1, Observation 2). Adds `a2ats_apply_far_query_rope`
for the query-side half, exposed as `A2ATSKVCache.far_query_rope`.
- **`b` is now independent of `w`.** The far-token offset was derived from
`a2ats_window`, hardcoding `b == w`; the paper's §5.1 uses `w=64`,
`b=2048`. New `a2ats_b` config field (default `2048`).
- **Distance gating tracks the decode position.** Rotated keys were written
into the parent `KVCache`, which never revisits them — freezing each
token's near/far class at write time, so a token classified "near" during
prefill kept exact RoPE forever. The cache now stores *pre-RoPE*
reconstructions and re-applies windowed RoPE to the accumulated cache each
step, against the current query position. Costs an `O(total_tokens)` pass
per step, inherent to honest gating under this protocol.
- **The paper's Eq. (14) assignment is now implemented.** Adds
`a2ats_query_second_moment`, `a2ats_cholesky_factor`, and
`a2ats_h_weighted_assignment` — the `H`-weighted quadratic form, computed
exactly via the Eq. (15)–(18) Cholesky identity. Enabled by the new
`a2ats_query_h` config field. The previous cosine blend is retained as a
documented *substitute* for the decode path (no calibrated `H` available),
not relabeled as the paper's estimator.

Benchmark now measures **attention-score** error rather than key-vector
error — under Eq. (12) far keys are intentionally unrotated, so the old
target scored the method's design as error. Windowed RoPE still loses to
always-exact in both geometries (2.9x / 8.3x), and this **survived** the
fixes: the near bucket is now numerically identical to always-exact (max gap
`5e-07`), so the entire penalty is far tokens (~92–96% of the sequence).
Sweeping `b` does not remove it. Tests: 51 → 67, including the strengthened
Eq. (12) assertion that the old `R_window` bug slipped past.

**AdaKV-proxy: the default configuration was not adaptive** ([31](https://github.com/rajveer43/VeloxQuant-MLX/issues/31)) —
`adakv_target_avg_bits` defaulted to `2.0` while `adakv_lo_bit` defaulted to
`2`. Because per-head adaptation requires headroom on both sides of the target
(raising one head must be payable by lowering another), a target sitting exactly
on the floor forced *every* head to `lo_bit` for every possible importance
vector. The shipped default was therefore bit-identical to plain KIVI while the
docs advertised per-head adaptation. The default is now `2.5`, and
`allocate_head_bits` emits a `UserWarning` when the target sits at or outside an
endpoint of the allowed set rather than silently flattening.

**AdaKV-proxy: clamp-before-normalize saturated the allocation** — the
real-valued per-head budget was computed as
`clip(importance_share × H × target, lo, hi)`. Since raw key-norm variances span
orders of magnitude, this pinned nearly every head to `lo` or `hi` and discarded
the interior ordering that the snap and greedy-correction passes exist to act
on: importance vectors differing by 10 000× produced byte-identical allocations.
Replaced with rank-normalisation to a bounded, mean-centred spread placed around
the target, so the mean lands on the budget by construction and no single
extreme head can flatten the result.

**AdaKV-proxy: allocation depended on head ordering** — greedy correction scanned
heads in index order and kept the first strict minimum, so `[10,1,1,1]` and
`[1,1,1,10]` starved different heads. Ties are now broken on importance, making
the allocation permutation-equivariant for distinct importances. (With exact ties
and an indivisible budget some tied head must still lose a bit — an
integer-allocation fact, now documented rather than incidental.)

**AdaKV-proxy: corrected an inverted claim about the importance signal** — the
docstring and docs described inter-token key-norm variance as "a proxy for high
attention entropy". It is the opposite: high norm-variance indicates a few
outlier-magnitude tokens dominating the logits, i.e. an attention-*sparse* head,
which Ada-KV (§3.3, Fig. 1b) would give *less* budget. The signal is sound for
allocating bits — it measures quantization sensitivity — but it is a *different*
criterion from the paper's, not an approximation of it. Now stated plainly and
pinned by a test.

Added

**`compute_head_attention_entropy`** — an AdaKV-proxy importance signal that
carries Ada-KV's own sign: dispersed heads score higher and receive more budget.
Estimates per-head attention entropy over an observation window using the
keys-as-proxy-queries substitution already established by SnapKV-adapted,
normalised by `ln(S)`. Select via `adakv_importance="attention_entropy"`;
window size via `adakv_obs_window` (default 32).

**`KVCacheConfig`** — new fields `adakv_importance`
(default `"norm_variance"`) and `adakv_obs_window` (default `32`).

Changed

- `adakv_target_avg_bits` default `2.0` → `2.5` (see above).
- `benchmark_scripts/benchmark_adakv.py` now sweeps targets inside the adaptive
range and adds an `attention_entropy` arm at matched budget.

<!-- version list -->

7.5

| Model | fp16 | RVQ 1-bit | RVQ 1-bit + Outlier | vs fp16 |
|---|---|---|---|---|
| Mistral 7B | 21.4 | 21.9 | **22.2** | **104%** |
| Phi-4 | 10.3 | 9.1 | **11.3** | **110%** |
| Qwen3 4B | 38.9 | 34.7 (187 tok) | **35.7 (196 tok)** | 92% + better completeness |
| Qwen3 8B | 19.6 | 17.1 | **20.3** | **104%** |
| Gemma3 4B | 35.9 | 34.7 | **36.5** | **102%** |
| Llama 3.1 8B | 18.8 | 17.5 | 17.9 | 95% |
| Falcon3 7B | 23.4 | 22.5 | 21.8 | 93% |

Qwen2.5-32B-Instruct-4bit could not complete any non-fp16 OTRQ config on
24 GB unified memory — see `docs/MEMORY_CONSTRAINT_FINDINGS.md`.

Engineering note
- **Watchdog for large-model runs**: a memory-pressure poller
(`/tmp/memory_watchdog.sh`) terminates the benchmark process tree if
free + inactive memory drops below 1 GB. Validated: the watchdog caught
the Qwen2.5-32B run at 891 MB free and killed cleanly before MLX could
fault the Metal heap.

2.0

([384](https://github.com/rajveer43/VeloxQuant-MLX/pull/384)) had already
applied the docs-only half of the fix (a `field_schema` help-text warning);
this closes the remaining shipped-default half. `xquant.md` updated to
stop stating "highly similar" as fact and to document measured, not
assumed, effective bit-widths.

Changed

**`enable_vectorized_attend` now defaults to `True`**
([255](https://github.com/rajveer43/VeloxQuant-MLX/issues/255)) —
investigation into zero-copy KV-cache access
(`docs/ZERO_COPY_KV_CACHE_FINDINGS.md`) found that `TurboQuantKVCache`'s
`attend()` spent 89% of its wall time (11.3 of 12.7 ms/call, 500-token
cache, d=128, b=2) in a per-token Python bit-unpacking loop rather than in
attention math or in the NumPy↔MLX array-boundary crossing itself. The
existing `enable_vectorized_attend` flag (`KVCacheConfig`,
`turboquant_cache.py`) already replaces that loop with a vectorized
NumPy unpack for bit-widths 1/2/4 (3 keeps the scalar fallback) and was
verified bit-for-bit identical to the scalar path
(`test_vectorized_attend_matches_baseline`); it measures 26x faster
end-to-end (0.48 ms/call) but previously required opting in. Flipping the
default surfaces this win to every `turboquant_prod`/`turboquant_mse`
cache without code changes. The remaining ~7% (34 µs/call) of `attend()`
is genuine NumPy↔MLX marshaling structural to these caches' NumPy-backed
ring-buffer storage — see the findings doc for why a full MLX-native
rewrite isn't justified by the data, and where a true zero-copy path
(the `fused_sdpa`/VecInfer Metal kernel already demonstrates the end
state) would be worth prototyping instead.

Added

**KV-cache-aware block pool allocator**
([249](https://github.com/rajveer43/VeloxQuant-MLX/issues/249)) — new
`veloxquant_mlx/memory/` module addressing the fragmentation and repeated
malloc/free overhead of allocating KV-cache buffers per request.
`BlockPoolAllocator` (`block_pool.py`, no MLX dependency) pre-allocates a
fixed pool of fixed-size blocks up front and hands them out per request,
returning freed blocks to a LIFO free list for reuse instead of releasing
them back to the OS; keys and values can be tracked on independent free
lists (`PoolConfig.separate_kv`, default on) so asymmetric K/V pressure
doesn't cause cross-contention. `AllocationStats` tracks allocation count,
reuse count, exhaustion count, peak concurrent blocks, and fragmentation
(fraction of allocated-but-not-full blocks) — the metrics the issue asked
for, apart from wall-clock allocation latency which the benchmark measures
directly instead. `MLXBlockStorage` (`mlx_storage.py`) pairs with a pool to
provide the actual `mx.array` buffers, one per block id, reused in place
across requests, and supports different blocks holding different
compression formats (`fp16`/`fp32`/`int8`/`int4`/`int2`/`int1`) within one
pool. `PooledKVCache` (`pooled_cache.py`) wraps any existing `KVCache` so
its `append_key`/`append_value` calls check out and return blocks from a
shared pool without changing the wrapped cache's own compression logic —
mirrors the existing `SlidingWindowKVCache` wrapper pattern. New
`BlockPoolExhaustedError` (`core/exceptions.py`). 37 tests: 23 pure-Python
allocator tests in `tests/non_metal/test_block_pool.py` (no MLX required,
runs on the cheap `ubuntu-latest` non-metal CI lane) plus 14 MLX-dependent
tests in `veloxquant_mlx/tests/memory/`. Synthetic benchmark comparing
against naive per-token allocation
(`benchmark_scripts/benchmark_block_pool.py`, results committed at
`figures/block_pool/results.json`) shows allocations-per-request dropping
by roughly `block_size`x and >99% of steady-state allocations satisfied
from reused blocks; **tokens/sec is not yet a clean win** on this
microbenchmark — `mx.zeros()` is lazy/cheap in MLX so the naive baseline's
raw throughput isn't penalized the way a real allocator under memory
pressure or an eager backend would be, so the benchmark's own `note` field
says to read `allocations_per_request` and `n_reused`, not `tokens_per_sec`,
as the honest signal. No integration into the existing ~40
`veloxquant_mlx/cache/*.py` classes' internal storage yet — those still
manage their own capacity-sized buffers directly; `PooledKVCache` adds
pool-backed *accounting* around any of them without touching their
internals. Docs at `docs-site/docs/api/memory-api.md`.

**Block pool allocator now drives real `mlx_lm.generate()` traffic**
(follow-up to [249](https://github.com/rajveer43/VeloxQuant-MLX/issues/249))
— the block pool allocator above shipped without a way to put it in a real
model's decode hot path: `PooledKVCache` only wraps VeloxQuant's own
standalone-method cache interface, which `mlx_lm.generate()` never drives.
New `PoolBackedKVCache` (`veloxquant_mlx/memory/pool_backed_cache.py`) is a
drop-in replacement for `mlx_lm.models.cache.KVCache` that implements
`update_and_fetch` directly — same contiguous-buffer growth strategy as the
stock class (attention needs one contiguous tensor per step; true
block-paged storage would need a per-step gather), but every growth step
now routes through `pool.allocate()`, and the growth chunk size is the
pool's configured `block_size` instead of a hardcoded `step=256`. New
`build_pooled_caches(model, pool, owner)` builds one per layer, all sharing
one pool/owner, as a drop-in for `make_prompt_cache()`. 15 new tests verify
bit-for-bit output parity against the stock `KVCache` (same random inputs,
same growth boundaries, same `trim`/`state` semantics) while also checking
pool accounting. Verified end-to-end against real Llama-3.2-3B-Instruct-4bit
generation
(`benchmark_scripts/benchmark_pool_backed_kvcache.py`, results committed at
`figures/block_pool/pool_backed_kvcache_results.json`): decode tokens/sec
and peak memory are within normal run-to-run noise between stock and pooled
caches on the identical prompt, and a second sequential request sharing the
same pool had **100% of its allocations satisfied by blocks the first
request had released** — real cross-request reuse on a real model, not a
synthetic replay. `PooledKVCache` is unchanged and still the entry point
for the 5 standalone methods; `PoolBackedKVCache` is the one to reach for
when the pool needs to actually drive live generation.

**Block pool allocator: exhaustion growth, concurrency safety, defrag,
owner protection, observability**
([266](https://github.com/rajveer43/VeloxQuant-MLX/issues/266)) — five
robustness gaps identified in the allocator design (see the issue), all
closed in `veloxquant_mlx/memory/block_pool.py`, backward-compatible with
existing `PoolConfig`/`BlockPoolAllocator` call sites:

- **Growth on exhaustion.** New `PoolConfig.grow_on_exhaustion` (default
`False`, so existing behavior is unchanged) — when set, an `allocate()`
call that would otherwise raise `BlockPoolExhaustedError` instead grows
the exhausted stream by just enough blocks to satisfy the request,
capped by the new `PoolConfig.max_blocks` (`None` = unbounded). Growth
past the cap still raises, and a failed allocation stays all-or-nothing
even when partial growth happened along the way. New `AllocationStats.n_grown`
counter.
- **Concurrency safety.** Every mutating and read method now acquires an
internal `threading.Lock`, so one pool can be shared across threads or
concurrent async request handlers without external synchronization.
Verified with a 500-thread concurrent-allocate stress test (no lost or
double-handed-out blocks) and an interleaved allocate/free stress test
across 16 threads.
- **Shrink / defragmentation.** New `BlockPoolAllocator.shrink(stream,
target_free)` permanently retires fully-free blocks down to a target
free-block count, for giving memory back when a pool was sized for peak
load that turned out much higher than steady state. In-use blocks are
never touched. New `AllocationStats.n_retired` counter.
- **Owner collision protection.** New `BlockPoolAllocator.register_owner()`
/ `release_owner()` and `OwnerAlreadyActiveError`
(`core/exceptions.py`). A second `register_owner()` call for an owner id
that's still checked out now raises immediately, catching two different
callers accidentally reusing the same id before one silently frees the
other's blocks via `free_all()`. Ordinary repeated `allocate()` calls for
a single request's own owner (e.g. `PoolBackedKVCache` growing over
multiple steps) are unaffected — that continues to work exactly as
before, since `allocate()` can't otherwise distinguish "same request
growing" from "id collision" without the caller opting into the explicit
check.
- **Observability.** New `BlockPoolAllocator.history`: a bounded
(256-entry) deque of independent `AllocationStats` snapshots, one
appended per mutating call, so recent fragmentation/exhaustion pressure
trends are visible instead of only the current instant. New
`AllocationStats.to_prometheus()` renders the live stats (or any history
entry) as Prometheus text-exposition format for a `/metrics` endpoint.

24 new tests in `tests/non_metal/test_block_pool.py` (pure-Python, no MLX
dependency, 47 total in that file now) — the full existing `veloxquant_mlx/`
and `tests/non_metal/` suites pass with no regressions.

**RocketKV-adapted: two-stage KV cache compression**
([239](https://github.com/rajveer43/VeloxQuant-MLX/issues/239)) — new
`method="rocketkv"`, inspired by "RocketKV: Accelerating Long-Context LLM
Inference via Two-Stage KV Cache Compression" (Behnam, Fu, Zhao, Tsai, Yu,
Tumanov; NVIDIA/Georgia Tech; ICML 2025, arXiv:2502.14051). Composes
coarse-grain permanent eviction (stage 1, directly reuses SnapKV-adapted)
with fine-grain dynamic per-decode-step top-k selection over the survivors
(stage 2, Hybrid Sparse Attention — a two-dimensional approximate-attention
reduction combining Quest-style paged sequence-dimension max/min summaries
with SparQ-style head-dimension top-k channel selection). An adaptive
formula (paper §3.6) splits a single `rocketkv_compression_ratio` knob
across both stages automatically. `MethodFamily.HYBRID`. No RocketKV-MT
(multi-turn) variant in this PR — see the issue for follow-up. New
`veloxquant_mlx/quantizers/rocketkv.py` (paged summaries, HSA scoring,
adaptive split) and `veloxquant_mlx/cache/rocketkv_cache.py`
(`RocketKVKVCache`), tests in
`veloxquant_mlx/tests/{quantizers,cache}/test_rocketkv*.py`, offline
benchmark in `benchmark_scripts/benchmark_rocketkv.py`, docs at
`docs-site/docs/algorithms/rocketkv.md`, adaptation rationale in
`paper/research/surveys/NEW_METHOD_SURVEY_V23.md`.

Performance

**Wired the existing Q-Filters Metal eviction kernels into the cache**
([179](https://github.com/rajveer43/VeloxQuant-MLX/issues/179)) — the
per-`(B, H)` Python loop on the calibrated path was already removed in
173 (`qfilters_update_batched`), but the fused Metal kernels written and
tested alongside it (`qfilters_fused_evict`,
`veloxquant_mlx/metal/_qfilters_evict.py`) were never called from
`QFiltersKVCache` — the batched path always ran the pure-MLX
`mx.argsort` / `mx.take_along_axis` selection even when Metal was
available. `QFiltersKVCache` now resolves a three-state
`use_metal_kernels` flag (`None` auto-detect, `True` require, `False`
force pure-MLX — the same convention `VecInferKVCache` already uses) and
dispatches the over-budget branch of the calibrated/batched path to the
fused kernel when eligible. Verified bit-for-bit interchangeable with the
pure-MLX path it replaces via a new parametrized parity test (Metal
hardware only); the key-SVD fallback's per-head loop and the under-budget
passthrough are untouched. **Not yet benchmarked** — no before/after
TTFT or decode-throughput numbers exist for this path; that measurement
needs real Apple Silicon and remains open.

Documentation

**Measured real-model perplexity for L2Norm after the RoPE offset fix**
([190](https://github.com/rajveer43/VeloxQuant-MLX/issues/190),
validating [174](https://github.com/rajveer43/VeloxQuant-MLX/issues/174))
— 174 generalized the 171 offset fix to `L2NormKVCache` and re-enabled
L2Norm as an arm in the Q-Filters generation-perplexity benchmark, but was
authored in a sandbox without MLX, so the fix shipped with unit-test
coverage and **no end-to-end numbers**. Ran
`benchmark_scripts/qfilters_real_model_perplexity.py` on
Llama-3.2-1B-Instruct-4bit (1024 tokens, budgets 128/256) on Apple
Silicon: against an fp16 baseline of ppl 4.050, L2Norm scores 20.469 at
budget 128 and 9.529 at budget 256, landing between calibrated Q-Filters

0.94.6

Bug Fixes

- **core**: EncodedVector.memory_bytes mis-sized dtypes missing from its table
([797](https://github.com/rajveer43/VeloxQuant-MLX/pull/797),
[`101a75c`](https://github.com/rajveer43/VeloxQuant-MLX/commit/101a75c8abf43461a6e0a67a81c4ee7b5f9bccfb))

- **integration**: Mistral template rejected an empty initial system message
([796](https://github.com/rajveer43/VeloxQuant-MLX/pull/796),
[`779fc81`](https://github.com/rajveer43/VeloxQuant-MLX/commit/779fc814ceccac9046476944b59608663d9c1492))

- **math**: Lloyd-Max left dead centroids at the support edges for narrow PDFs
([795](https://github.com/rajveer43/VeloxQuant-MLX/pull/795),
[`b992c65`](https://github.com/rajveer43/VeloxQuant-MLX/commit/b992c6513d5327f5211083d0d8d294663858a517))

0.94.5

Bug Fixes

- **ratequant**: Isolate calibration prompts so they don't share a KV cache
([794](https://github.com/rajveer43/VeloxQuant-MLX/pull/794),
[`bfd6df6`](https://github.com/rajveer43/VeloxQuant-MLX/commit/bfd6df61d31f0d6ae0142d8eda801aae844def01))

0.94.4

Bug Fixes

- **artifacts**: Saved .npy artifacts were created 0600 instead of honouring umask
([793](https://github.com/rajveer43/VeloxQuant-MLX/pull/793),
[`4f8a376`](https://github.com/rajveer43/VeloxQuant-MLX/commit/4f8a37604c871a794fe6ac74f76ff0a8e7b72e6a))

Page 1 of 37

Links

Releases

Has known vulnerabilities

© 2026 Safety CLI Cybersecurity Inc. All Rights Reserved.