@@ -103,7 +103,18 @@ def estimate_kv_cache_memory(cache: list[Any]) -> int:
103103
104104 total_bytes = 0
105105
106+ try :
107+ from mlx_lm .models .turboquant_cache import TurboQuantKVCache as _TQ
108+ except ImportError :
109+ _TQ = None
110+
106111 for layer_cache in cache :
112+ # Handle TurboQuantKVCache — estimate from shape+dtype (avoid lazy eval)
113+ if _TQ is not None and isinstance (layer_cache , _TQ ):
114+ if layer_cache .k_packed is not None :
115+ for arr in layer_cache .state :
116+ total_bytes += _array_memory (arr )
117+ continue
107118 # Handle different cache object types
108119 # Check dict first since dicts have .keys() method that would match below
109120 if isinstance (layer_cache , dict ) and "state" in layer_cache :
@@ -165,6 +176,7 @@ class MemoryCacheConfig:
165176 kv_bits : int = 8
166177 kv_group_size : int = 64
167178 kv_min_quantize_tokens : int = 256
179+ turbo_kv_bits : int | None = None # TurboQuant: 1-4 bit, 4.6x compression at 3-bit
168180
169181 def __post_init__ (self ) -> None :
170182 if not 0.0 < self .max_memory_percent <= 1.0 :
@@ -177,6 +189,15 @@ def __post_init__(self) -> None:
177189 raise ValueError (
178190 f"kv_min_quantize_tokens must be >= 0, got { self .kv_min_quantize_tokens } "
179191 )
192+ if self .turbo_kv_bits is not None and self .turbo_kv_bits not in (1 , 2 , 3 , 4 ):
193+ raise ValueError (
194+ f"turbo_kv_bits must be 1-4, got { self .turbo_kv_bits } "
195+ )
196+
197+ @property
198+ def needs_dequantize (self ) -> bool :
199+ """Whether stored caches need dequantization on fetch."""
200+ return self .kv_quantize or self .turbo_kv_bits is not None
180201
181202 def compute_memory_limit (self ) -> int :
182203 """
@@ -255,14 +276,14 @@ def create(cls, tokens: list[int], cache: list[Any]) -> _CacheEntry:
255276
256277
257278def _trim_cache_offset (cache : list [Any ], trim_by : int ) -> list [Any ]:
258- """Create shallow copies of KVCache/QuantizedKVCache layers with offset reduced.
279+ """Create shallow copies of KVCache/QuantizedKVCache/TurboQuantKVCache layers
280+ with offset reduced.
259281
260282 This is used when returning a cached KV state to the scheduler so that
261283 the last N positions are "freed" and the model will recompute them on the
262284 next forward pass (preventing duplicate KV entries).
263285
264- Supports both KVCache (keys/values are arrays) and QuantizedKVCache
265- (keys/values are 3-tuples of arrays).
286+ Supports KVCache, QuantizedKVCache, and TurboQuantKVCache.
266287 """
267288 from mlx_lm .models .cache import KVCache
268289
@@ -271,6 +292,11 @@ def _trim_cache_offset(cache: list[Any], trim_by: int) -> list[Any]:
271292 except ImportError :
272293 QuantizedKVCache = None # noqa: N806
273294
295+ try :
296+ from mlx_lm .models .turboquant_cache import TurboQuantKVCache
297+ except ImportError :
298+ TurboQuantKVCache = None # noqa: N806
299+
274300 trimmed : list [Any ] = []
275301 for layer_cache in cache :
276302 if QuantizedKVCache is not None and isinstance (layer_cache , QuantizedKVCache ):
@@ -281,6 +307,18 @@ def _trim_cache_offset(cache: list[Any], trim_by: int) -> list[Any]:
281307 tc .group_size = layer_cache .group_size
282308 tc .bits = layer_cache .bits
283309 trimmed .append (tc )
310+ elif TurboQuantKVCache is not None and isinstance (
311+ layer_cache , TurboQuantKVCache
312+ ):
313+ # Shallow copy with adjusted offset (do NOT mutate original)
314+ tc = TurboQuantKVCache .__new__ (TurboQuantKVCache )
315+ tc .__dict__ .update (layer_cache .__dict__ )
316+ tc .offset = max (layer_cache .offset - trim_by , 0 )
317+ tc ._k_deq_buf = None # invalidate decode buffer
318+ tc ._v_deq_buf = None
319+ tc ._deq_offset = 0
320+ tc ._deq_alloc = 0
321+ trimmed .append (tc )
284322 elif (
285323 hasattr (layer_cache , "offset" )
286324 and hasattr (layer_cache , "keys" )
@@ -366,11 +404,33 @@ def _quantize_cache(cache: list[Any], bits: int = 8, group_size: int = 64) -> li
366404 return quantized
367405
368406
407+ def _turbo_quantize_cache (cache : list [Any ], bits : int = 3 ) -> list [Any ]:
408+ """Compress KVCache layers with TurboQuant (4.6x at 3-bit).
409+
410+ Uses PolarQuant: randomized Hadamard rotation + Lloyd-Max codebook
411+ quantization with fused Metal kernels. See arXiv 2504.19874.
412+ """
413+ from mlx_lm .models .cache import KVCache
414+
415+ compressed = []
416+ for layer in cache :
417+ if isinstance (layer , KVCache ) and layer .keys is not None :
418+ compressed .append (layer .to_turbo_quantized (bits = bits ))
419+ else :
420+ compressed .append (layer )
421+ return compressed
422+
423+
369424def _dequantize_cache (cache : list [Any ]) -> list [Any ]:
370- """Dequantize QuantizedKVCache layers back to regular KVCache."""
425+ """Dequantize QuantizedKVCache or TurboQuantKVCache layers back to KVCache."""
371426 import mlx .core as mx
372427 from mlx_lm .models .cache import KVCache , QuantizedKVCache
373428
429+ try :
430+ from mlx_lm .models .turboquant_cache import TurboQuantKVCache
431+ except ImportError :
432+ TurboQuantKVCache = None # noqa: N806
433+
374434 result = []
375435 for layer in cache :
376436 if isinstance (layer , QuantizedKVCache ) and layer .keys is not None :
@@ -383,6 +443,23 @@ def _dequantize_cache(cache: list[Any]) -> list[Any]:
383443 )
384444 kv .offset = layer .offset
385445 result .append (kv )
446+ elif TurboQuantKVCache is not None and isinstance (layer , TurboQuantKVCache ) and not layer .empty ():
447+ # Ensure quantizer is initialized (needed after from_state)
448+ if layer ._k_q is None :
449+ layer ._ensure_quantizer (layer ._k_dim , layer ._v_dim )
450+ B , H = layer .k_packed .shape [:2 ]
451+ dtype = layer ._dtype if layer ._dtype is not None else mx .float16
452+ k_all = layer ._full_dequant (
453+ layer .k_packed , layer .k_norms , layer ._k_q ,
454+ layer ._k_dim , B , H , layer .offset , dtype ,
455+ )
456+ v_all = layer ._full_dequant (
457+ layer .v_packed , layer .v_norms , layer ._v_q ,
458+ layer ._v_dim , B , H , layer .offset , dtype ,
459+ )
460+ kv = KVCache ()
461+ kv .update_and_fetch (k_all , v_all )
462+ result .append (kv )
386463 else :
387464 result .append (layer )
388465 return result
@@ -481,7 +558,7 @@ def fetch(self, tokens: list[int]) -> tuple[list[Any] | None, list[int]]:
481558 self ._last_match_type = "exact"
482559 cache_out = (
483560 _dequantize_cache (entry .cache )
484- if self ._config .kv_quantize
561+ if self ._config .needs_dequantize
485562 else entry .cache
486563 )
487564 return cache_out , []
@@ -539,7 +616,7 @@ def fetch(self, tokens: list[int]) -> tuple[list[Any] | None, list[int]]:
539616 excess = n_cached - n_requested
540617
541618 has_non_trimmable = any (
542- not (hasattr (lc , "offset " ) and hasattr ( lc , "keys" ))
619+ not (hasattr (lc , "is_trimmable " ) and lc . is_trimmable ( ))
543620 for lc in best_super .cache
544621 )
545622
@@ -556,7 +633,7 @@ def fetch(self, tokens: list[int]) -> tuple[list[Any] | None, list[int]]:
556633 self ._last_match_type = "supersequence"
557634 trimmed_cache = (
558635 _dequantize_cache (trimmed_cache )
559- if self ._config .kv_quantize
636+ if self ._config .needs_dequantize
560637 else trimmed_cache
561638 )
562639 return trimmed_cache , []
@@ -567,7 +644,7 @@ def fetch(self, tokens: list[int]) -> tuple[list[Any] | None, list[int]]:
567644 self ._last_match_type = "supersequence"
568645 cache_out = (
569646 _dequantize_cache (best_super .cache )
570- if self ._config .kv_quantize
647+ if self ._config .needs_dequantize
571648 else best_super .cache
572649 )
573650 return cache_out , []
@@ -581,7 +658,7 @@ def fetch(self, tokens: list[int]) -> tuple[list[Any] | None, list[int]]:
581658 self ._last_match_type = "prefix"
582659 cache_out = (
583660 _dequantize_cache (best_match .cache )
584- if self ._config .kv_quantize
661+ if self ._config .needs_dequantize
585662 else best_match .cache
586663 )
587664 return cache_out , remaining
@@ -624,7 +701,7 @@ def fetch(self, tokens: list[int]) -> tuple[list[Any] | None, list[int]]:
624701 excess = len (best_lcp_entry .tokens ) - best_lcp_length
625702
626703 has_non_trimmable = any (
627- not (hasattr (lc , "offset " ) and hasattr ( lc , "keys" ))
704+ not (hasattr (lc , "is_trimmable " ) and lc . is_trimmable ( ))
628705 for lc in best_lcp_entry .cache
629706 )
630707 logger .debug (
@@ -648,7 +725,7 @@ def fetch(self, tokens: list[int]) -> tuple[list[Any] | None, list[int]]:
648725 self ._last_match_type = "lcp"
649726 trimmed_cache = (
650727 _dequantize_cache (trimmed_cache )
651- if self ._config .kv_quantize
728+ if self ._config .needs_dequantize
652729 else trimmed_cache
653730 )
654731 return trimmed_cache , remaining
@@ -693,8 +770,13 @@ def store(
693770 # Trim oversized KV arrays to actual used size
694771 cache = _trim_to_offset (cache )
695772
696- # Quantize if enabled and sequence is long enough
773+ # Compress KV cache for storage: TurboQuant (4.6x) or standard quantization (2x)
697774 if (
775+ self ._config .turbo_kv_bits is not None
776+ and len (tokens ) >= self ._config .kv_min_quantize_tokens
777+ ):
778+ cache = _turbo_quantize_cache (cache , self ._config .turbo_kv_bits )
779+ elif (
698780 self ._config .kv_quantize
699781 and len (tokens ) >= self ._config .kv_min_quantize_tokens
700782 ):
0 commit comments