-
Notifications
You must be signed in to change notification settings - Fork 0
Add RotorQuant KV cache backend with deferred prefill on Metal #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
4557598
53a0907
9c93b6e
ff21598
f40e9d4
aba98c2
8545421
032593b
f587d2b
a43ed82
7ba35fc
f41b97d
3d526b7
3100207
bae6455
c5ec472
13a5f47
04893db
6c4ba6f
d612d14
a50a537
856b30f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -449,16 +449,20 @@ export function SettingsPanel({ open, onClose }: SettingsPanelProps) { | |
| filled | ||
| content={ | ||
| `• Default — No cache quantization. Best baseline quality, highest memory use.\n` + | ||
| `• OptiQ — Rotation-based quantization via mlx-optiq. Best long-context quality.\n` + | ||
| `• RotorQuant Adaptive — IsoQuant 3-bit with deferred prefill, FP16 edge layers. Recommended.\n` + | ||
| `• RotorQuant — IsoQuant 3-bit on all KV layers. Most aggressive compression with deferred prefill.\n` + | ||
| `• OptiQ — Rotation-based quantization via mlx-optiq. Good long-context quality, no GQA support.\n` + | ||
| `• TurboQuant Adaptive — Quantizes middle KV layers, keeps edge layers in FP16. Proven stable.\n` + | ||
| `• TurboQuant — Quantizes all KV layers. Most aggressive compression, higher quality risk.\n` + | ||
| `• TurboQuant — Quantizes all KV layers. Most aggressive non-rotorquant compression.\n` + | ||
| `• MLX Quantized — MLX's built-in cache quantization.\n\n` + | ||
| `Takes effect on next model launch. Incompatible models fall back to Default automatically.` | ||
| `Takes effect on next model launch. OptiQ falls back to Default for unsupported architectures; other backends will error on incompatible models.` | ||
| } | ||
|
Comment on lines
451
to
457
|
||
| /> | ||
| </FieldLabel> | ||
| <Select value={kvBackend} onChange={(e) => setKvBackend(e.target.value)} disabled={!!envOverride}> | ||
| <option value="default">Default (no quantization)</option> | ||
| <option value="rotorquant_adaptive">RotorQuant Adaptive (recommended)</option> | ||
| <option value="rotorquant">RotorQuant</option> | ||
| <option value="optiq">OptiQ (rotation-based)</option> | ||
| <option value="turboquant_adaptive">TurboQuant Adaptive</option> | ||
| <option value="turboquant">TurboQuant</option> | ||
|
|
@@ -467,7 +471,7 @@ export function SettingsPanel({ open, onClose }: SettingsPanelProps) { | |
| {envOverride ? ( | ||
| <HintText>Overridden by SKULK_KV_CACHE_BACKEND environment variable. Remove the env var to configure here.</HintText> | ||
| ) : ( | ||
| <HintText>Changes take effect on the next model launch. Models with incompatible architectures (GQA, non-power-of-two head_dim) will automatically fall back to default.</HintText> | ||
| <HintText>Changes take effect on the next model launch. OptiQ falls back to Default for unsupported architectures; other backends will error on incompatible models.</HintText> | ||
|
||
| )} | ||
|
Comment on lines
474
to
477
|
||
| </Fieldset> | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,12 +23,19 @@ | |
| KV_CACHE_BITS, | ||
| OPTIQ_BITS, | ||
| OPTIQ_FP16_LAYERS, | ||
| ROTORQUANT_DEFER_PREFILL, | ||
| ROTORQUANT_FP16_LAYERS, | ||
| TURBOQUANT_FP16_LAYERS, | ||
| TURBOQUANT_K_BITS, | ||
| TURBOQUANT_V_BITS, | ||
| VALID_KV_CACHE_BACKENDS, | ||
| KVCacheBackend, | ||
| ) | ||
| from exo.worker.engines.mlx.rotorquant import ( | ||
| make_rotorquant_adaptive_cache, | ||
| make_rotorquant_cache_from_template, | ||
| ) | ||
| from exo.worker.engines.mlx.rotorquant.tables import ISO3_BLOCK_SIZE | ||
| from exo.worker.engines.mlx.turboquant import ( | ||
| make_turboquant_adaptive_cache, | ||
| make_turboquant_cache_from_template, | ||
|
|
@@ -417,6 +424,13 @@ def get_memory_used_percentage() -> float: | |
| return float(mem.percent / 100) | ||
|
|
||
|
|
||
| def _make_default_cache(model: Model) -> KVCacheType: | ||
| """Build the default (unquantized) KV cache for a model.""" | ||
| if hasattr(model, "make_cache"): | ||
| return model.make_cache() # type: ignore | ||
| return [KVCache() for _ in model.layers] | ||
|
|
||
|
|
||
| def make_kv_cache( | ||
| model: Model, max_kv_size: int | None = None, keep: int = 0 | ||
| ) -> KVCacheType: | ||
|
|
@@ -579,12 +593,46 @@ def _is_power_of_two(n: int) -> bool: | |
| for i, _ in enumerate(model.layers) | ||
| ] | ||
|
|
||
| if hasattr(model, "make_cache"): | ||
| logger.info("Using MLX LM's make cache") | ||
| return model.make_cache() # type: ignore | ||
| if backend in ("rotorquant", "rotorquant_adaptive"): | ||
| # Preflight: RotorQuant requires head_dim divisible by the | ||
| # IsoQuant block size (128). Models with smaller heads (e.g. 64-d) | ||
| # would crash at first token; fall back to default instead. | ||
| if len(model.layers) > 0: | ||
| first_layer = model.layers[0] | ||
| attn = getattr(first_layer, "self_attn", first_layer) | ||
| model_head_dim: int = getattr(attn, "head_dim", ISO3_BLOCK_SIZE) | ||
| else: | ||
|
Comment on lines
+615
to
+618
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The rotorquant preflight reads Useful? React with 👍 / 👎. |
||
| model_head_dim = ISO3_BLOCK_SIZE | ||
| if model_head_dim % ISO3_BLOCK_SIZE != 0: | ||
| logger.warning( | ||
| f"RotorQuant requires head_dim divisible by {ISO3_BLOCK_SIZE}, " | ||
| f"but this model has head_dim={model_head_dim}; " | ||
| f"falling back to default KV cache" | ||
| ) | ||
| return _make_default_cache(model) | ||
|
|
||
| if backend == "rotorquant": | ||
| logger.info( | ||
| f"Using rotorquant KV cache (defer_prefill={ROTORQUANT_DEFER_PREFILL})" | ||
| ) | ||
| return make_rotorquant_cache_from_template( | ||
| model, | ||
| defer_prefill=ROTORQUANT_DEFER_PREFILL, | ||
| ) | ||
|
|
||
| logger.info( | ||
| f"Using rotorquant adaptive KV cache " | ||
| f"(fp16_layers={ROTORQUANT_FP16_LAYERS}, " | ||
| f"defer_prefill={ROTORQUANT_DEFER_PREFILL})" | ||
| ) | ||
| return make_rotorquant_adaptive_cache( | ||
| model, | ||
| fp16_layers=ROTORQUANT_FP16_LAYERS, | ||
| defer_prefill=ROTORQUANT_DEFER_PREFILL, | ||
| ) | ||
|
|
||
| logger.info("Using default KV cache") | ||
| return [KVCache() for _ in model.layers] | ||
| return _make_default_cache(model) | ||
|
|
||
|
|
||
| def get_kv_cache_backend() -> KVCacheBackend: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| """RotorQuant KV cache backend (IsoQuant variant). | ||
|
|
||
| Pure-MLX port of the IsoQuant 3-bit KV cache compression from | ||
| johndpope/llama-cpp-turboquant (MIT) and scrya-com/rotorquant (MIT). | ||
|
|
||
| Key properties vs the older TurboQuant native backend: | ||
| - Block-diagonal 4D quaternion rotations instead of randomized Hadamard | ||
| - Norm-correction trick for unbiased magnitudes after centroid quantization | ||
| - Optional deferred prefill: K/V stay in fp16 during prompt processing, | ||
| flushed to compressed storage on the first decode-shaped call | ||
| - GQA-native (compression is per-(kv_head, token), Q heads fan out at SDPA) | ||
|
|
||
| The backend stores indices and norms; ``update_and_fetch`` returns fully | ||
| dequantized fp16 K/V to standard ``mx.fast.scaled_dot_product_attention``. | ||
| The centroid-space attention optimization from OptiQ is intentionally | ||
| deferred to a follow-up; v1 prioritizes correctness and the deferred-prefill | ||
| quality win over the rotated-space SDPA perf win. | ||
| """ | ||
|
|
||
| from exo.worker.engines.mlx.rotorquant.cache import ( | ||
| RotorQuantKVCache, | ||
| make_rotorquant_adaptive_cache, | ||
| make_rotorquant_cache_from_template, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "RotorQuantKVCache", | ||
| "make_rotorquant_adaptive_cache", | ||
| "make_rotorquant_cache_from_template", | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bullet links to
docs/kv-cache-backends.md, but that page currently doesn’t mention RotorQuant (it still lists only default/mlx_quantized/turboquant/optiq). Either updatedocs/kv-cache-backends.mdin this PR to include RotorQuant, or change the link to the up-to-date page underwebsite/docs/kv-cache-backends.md/ the published docs URL so readers don’t land on stale information.