Skip to content

Commit d4ef67a

Browse files
committed
add TurboQuant KV cache compression
WHT rotation + Lloyd-Max codebook quantization for the KV cache, with asymmetric K/V bit-widths, routed through the kvScheme parameter. Two-phase design: raw FP16 during prefill (zero overhead), batch-compress on the first decode step, fused Metal kernels for encode and compressed-domain attention afterwards. Stacked on the kvScheme plumbing branch; resolveTurboScheme + maybeTurboQuantizeKVCache convert eligible KVCacheSimple layers when a turbo scheme is selected. Changes from the earlier draft of this branch: - f32 rotation matrices (bf16 rounding in the rotation compounds across layers on larger models) - norm correction applied on the WHT path too, matching the dense path and the fused encode kernel (quantized unit vectors land ~5-10% short without it) - the codec setup crash this branch used to hit is fixed by computing tokenCount from the leading dims instead of an out-of-range dim index, so the external array-metadata guard is no longer needed - raw-key schemes (turbo0v2/turbo0v4) are not exposed via kvScheme yet: the rawKeyMode decode path produces NaN from the second decode step on real checkpoints while staying unit-clean at every probed shape, dtype, and magnitude — tracked for a dedicated debugging pass; the mode stays reachable through the TurboQuantKVCache initializer - CI-runnable test suite: rotation orthogonality, codec round trip, cache offsets/trim, compressed-vs-FP16 attention parity (incl. GQA head-mapping with head-distinguishable values and the kvScheme conversion route), and an end-to-end synthetic-model generation test
1 parent 4e799a4 commit d4ef67a

6 files changed

Lines changed: 2783 additions & 6 deletions

File tree

Libraries/MLXLMCommon/AttentionUtils.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,23 @@ public func attentionWithCacheUpdate(
5151
mask: mask
5252
)
5353
}
54-
if let quantizedKVCache = cache as? QuantizedKVCacheProtocol {
54+
if let turboCache = cache as? TurboQuantKVCache {
55+
let L = queries.dim(2)
56+
if L > 1 {
57+
// Prefill (L>1): raw update + standard SDPA. Zero overhead.
58+
let (cachedKeys, cachedValues) = turboCache.update(keys: keys, values: values)
59+
return MLXFast.scaledDotProductAttention(
60+
queries: queries, keys: cachedKeys, values: cachedValues,
61+
scale: scale, mask: mask
62+
)
63+
}
64+
// Decode (L=1): compressed cache path. First call triggers
65+
// compressRawCache() inside compressedAttention.
66+
return turboCache.compressedAttention(
67+
queries: queries, keys: keys, values: values,
68+
scale: scale, mask: mask
69+
)
70+
} else if let quantizedKVCache = cache as? QuantizedKVCacheProtocol {
5571
let (quantizedKeys, quantizedValues) = quantizedKVCache.updateQuantized(
5672
keys: keys, values: values)
5773
return quantizedScaledDotProductAttention(

Libraries/MLXLMCommon/Evaluate.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ public struct GenerateParameters: Sendable {
7373
public var quantizedKVStart: Int
7474

7575
/// KV cache compression scheme. Overrides kvBits when set.
76-
/// Built-in: "affine4", "affine8" (equivalent to kvBits 4/8).
77-
/// Extensible for custom schemes (e.g. WHT-based compression).
76+
/// Built-in: "affine4", "affine8" (equivalent to kvBits 4/8), plus the
77+
/// TurboQuant family ("turbo4", "turbo3", "turbo2", "turbo4v2", "turbo4v3").
7878
public var kvScheme: String?
7979

8080
/// Sampling temperature
@@ -810,7 +810,8 @@ public struct SpeculativeTokenIterator: TokenIteratorProtocol {
810810
cache: &cache,
811811
kvBits: parameters.kvBits,
812812
kvGroupSize: parameters.kvGroupSize,
813-
quantizedKVStart: parameters.quantizedKVStart
813+
quantizedKVStart: parameters.quantizedKVStart,
814+
kvScheme: parameters.kvScheme
814815
)
815816
}
816817

Libraries/MLXLMCommon/KVCache.swift

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,7 @@ private func cacheClassName(_ cache: KVCache) -> String {
15291529
case is ArraysCache: return "ArraysCache"
15301530
case is RotatingKVCache: return "RotatingKVCache"
15311531
case is QuantizedKVCache: return "QuantizedKVCache"
1532+
case is TurboQuantKVCache: return "TurboQuantKVCache"
15321533
case is KVCacheSimple: return "KVCache"
15331534
case is CacheList: return "CacheList"
15341535
default: return "KVCache"
@@ -1682,6 +1683,22 @@ private func restoreCacheFromMetaState(
16821683
cache.restoreFromMetaState(state: state, savedMetaState: metaState)
16831684
return cache
16841685

1686+
case "TurboQuantKVCache":
1687+
guard metaState.count >= 5,
1688+
let bits = Int(metaState[1]),
1689+
let keyBits = Int(metaState[2]),
1690+
let valueBits = Int(metaState[3]),
1691+
let seed = UInt64(metaState[4])
1692+
else {
1693+
throw KVCacheError(
1694+
message: "Invalid TurboQuantKVCache metaState")
1695+
}
1696+
let cache = TurboQuantKVCache(
1697+
bits: bits, keyBits: keyBits, valueBits: valueBits, seed: seed)
1698+
cache.state = state
1699+
cache.metaState = metaState
1700+
return cache
1701+
16851702
case "CacheList":
16861703
return try CacheList.fromState(state: state, metaState: metaState)
16871704

@@ -1948,15 +1965,28 @@ public func resolveAffineScheme(_ scheme: String?) -> (bits: Int, groupSize: Int
19481965
/// - kvGroupSize: Group size for quantization
19491966
/// - quantizedKVStart: Token count threshold to begin quantizing
19501967
/// - kvScheme: Scheme selector; overrides kvBits when it names a built-in
1951-
/// affine scheme ("affine4", "affine8"). Unrecognized schemes are left to
1952-
/// custom cache implementations and do not quantize here.
1968+
/// affine scheme ("affine4", "affine8") or a TurboQuant scheme
1969+
/// ("turbo4", "turbo4v2", ...). Unrecognized schemes are left to custom
1970+
/// cache implementations and do not quantize here.
19531971
public func maybeQuantizeKVCache(
19541972
cache: inout [KVCache],
19551973
kvBits: Int?,
19561974
kvGroupSize: Int = 64,
19571975
quantizedKVStart: Int = 0,
19581976
kvScheme: String? = nil
19591977
) {
1978+
// TurboQuant schemes convert eligible layers to TurboQuantKVCache
1979+
// (handled in TurboQuantKVCache.swift to keep this file scheme-agnostic).
1980+
if let scheme = kvScheme, let turbo = resolveTurboScheme(scheme) {
1981+
maybeTurboQuantizeKVCache(
1982+
cache: &cache,
1983+
keyBits: turbo.keyBits,
1984+
valueBits: turbo.valueBits,
1985+
quantizedKVStart: quantizedKVStart
1986+
)
1987+
return
1988+
}
1989+
19601990
// Resolve effective bits: kvScheme overrides kvBits.
19611991
let effectiveBits: Int
19621992
let effectiveGroupSize: Int

0 commit comments

Comments
 (0)