Skip to main content

rlx_runtime/
quantized_kv.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Block-quantized K/V cache — store decode-time history as `q8_0`,
6//! `q4_0`, or `q5_0` GGUF-encoded blocks instead of f32/f16. Memory
7//! saving vs f16 is roughly:
8//!
9//! | scheme | bits/elem | ratio vs f16 |
10//! |--------|-----------|--------------|
11//! | f16    | 16        | 1.0×         |
12//! | q8_0   | 8.5       | 0.53×        |
13//! | q5_0   | 5.5       | 0.34×        |
14//! | q4_0   | 4.5       | 0.28×        |
15//!
16//! Trade-off: quantization adds noise to attention scores. q8_0 is
17//! near-lossless for most decoder LMs; q4_0 typically costs ~0.3 ppl
18//! at 4× memory savings.
19//!
20//! Layout per layer
21//! ----------------
22//!
23//! Each layer's K and V buffer is a flat `Vec<u8>` of `past_len`
24//! quantized rows. Every "row" is `kv_dim` f32 elements when
25//! dequantized; rows are stored back-to-back. `kv_dim` must be a
26//! multiple of the scheme's block size (32 for all three schemes).
27//!
28//! On read, callers materialize a window of rows to f32 via
29//! `dequant_rows`. On write, freshly produced f32 K/V is quantized
30//! one row at a time via `quant_rows` before being appended. The
31//! quantization wrappers route to the `rlx_gguf::quantize` /
32//! `dequant_*` kernels for parity with on-disk GGUF blocks.
33
34use anyhow::{Result, anyhow, bail};
35use rlx_gguf::{GgmlType, quantize};
36
37macro_rules! define_kv_quant {
38    ($( $variant:ident, $dtype:path, $dequant:path, $qk:expr, [$($alias:literal),+]; )+) => {
39        /// Quantization scheme for cache rows.
40        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
41        pub enum KvQuant {
42            /// `f16` — lossless storage of f32→f16 (no quantization). Kept as a
43            /// useful baseline; 2 bytes per element.
44            F16,
45            $( $variant, )+
46        }
47
48        impl KvQuant {
49            /// Parse a quantization name (case-insensitive aliases accepted).
50            pub fn from_name(name: &str) -> Option<Self> {
51                let n = name.trim().to_ascii_lowercase();
52                if n == "f16" {
53                    return Some(Self::F16);
54                }
55                match n.as_str() {
56                    $( $( $alias => Some(Self::$variant), )+ )+
57                    _ => None,
58                }
59            }
60
61            /// On-disk block size in elements.
62            pub const fn block_elements(self) -> usize {
63                match self {
64                    Self::F16 => 1,
65                    $( Self::$variant => $qk, )+
66                }
67            }
68
69            /// On-disk block size in bytes.
70            pub fn block_bytes(self) -> usize {
71                match self {
72                    Self::F16 => 2,
73                    _ => {
74                        let gg = self.ggml_type().expect("non-F16 scheme must map to ggml type");
75                        rlx_gguf::bytes_for_public(gg, self.block_elements())
76                            .expect("ggml block size must be valid for kv quant scheme")
77                    }
78                }
79            }
80
81            fn ggml_type(self) -> Option<GgmlType> {
82                match self {
83                    Self::F16 => None,
84                    $( Self::$variant => Some($dtype), )+
85                }
86            }
87
88            fn dequant_dispatch(self, bytes: &[u8], n: usize) -> Result<Vec<f32>> {
89                match self {
90                    Self::F16 => unreachable!("F16 dequant is handled in dequant_rows"),
91                    $( Self::$variant => {
92                        let expected = self.bytes_for(n)?;
93                        Ok($dequant(&bytes[..expected], n)?)
94                    } )+
95                }
96            }
97
98            /// Bytes required to store `n_elements` quantized.
99            pub fn bytes_for(self, n_elements: usize) -> Result<usize> {
100                let blk = self.block_elements();
101                if !n_elements.is_multiple_of(blk) {
102                    bail!("{self:?}: element count {n_elements} not aligned to block size {blk}");
103                }
104                Ok((n_elements / blk) * self.block_bytes())
105            }
106        }
107    };
108}
109
110define_kv_quant! {
111    Q8_0, GgmlType::Q8_0, rlx_gguf::dequant_q8_0, 32, ["q8", "q8_0"];
112    Q4_0, GgmlType::Q4_0, rlx_gguf::dequant_q4_0, 32, ["q4", "q4_0"];
113    Q5_0, GgmlType::Q5_0, rlx_gguf::dequant_q5_0, 32, ["q5", "q5_0"];
114    Q4_1, GgmlType::Q4_1, rlx_gguf::dequant_q4_1, 32, ["q4_1"];
115    Q5_1, GgmlType::Q5_1, rlx_gguf::dequant_q5_1, 32, ["q5_1"];
116    Q1_0, GgmlType::Q1_0, rlx_gguf::q1_dequant::dequant_q1_0, 128, ["q1", "q1_0"];
117    Q2_0, GgmlType::Q2_0, rlx_gguf::q2_dequant::dequant_q2_0, 128, ["q2", "q2_0"];
118    Q2K, GgmlType::Q2K, rlx_gguf::dequant_q2_k, 256, ["q2k", "q2_k"];
119    Q3K, GgmlType::Q3K, rlx_gguf::dequant_q3_k, 256, ["q3k", "q3_k"];
120    Q4K, GgmlType::Q4K, rlx_gguf::dequant_q4_k, 256, ["q4k", "q4_k"];
121    Q5K, GgmlType::Q5K, rlx_gguf::dequant_q5_k, 256, ["q5k", "q5_k"];
122    Q6K, GgmlType::Q6K, rlx_gguf::dequant_q6_k, 256, ["q6k", "q6_k"];
123    Q8K, GgmlType::Q8K, rlx_gguf::dequant_q8_k, 256, ["q8k", "q8_k"];
124    IQ4NL, GgmlType::IQ4NL, rlx_gguf::iq_dequant::dequant_iq4_nl, 32, ["iq4_nl", "iq4nl"];
125    IQ4XS, GgmlType::IQ4XS, rlx_gguf::iq_dequant::dequant_iq4_xs, 256, ["iq4_xs", "iq4xs"];
126    IQ2XXS, GgmlType::IQ2XXS, rlx_gguf::iq_dequant::dequant_iq2_xxs, 256, ["iq2_xxs", "iq2xxs"];
127    IQ2XS, GgmlType::IQ2XS, rlx_gguf::iq_dequant::dequant_iq2_xs, 256, ["iq2_xs", "iq2xs"];
128    IQ2S, GgmlType::IQ2S, rlx_gguf::iq_dequant::dequant_iq2_s, 256, ["iq2_s", "iq2s"];
129    IQ3XXS, GgmlType::IQ3XXS, rlx_gguf::iq_dequant::dequant_iq3_xxs, 256, ["iq3_xxs", "iq3xxs"];
130    IQ3S, GgmlType::IQ3S, rlx_gguf::iq_dequant::dequant_iq3_s, 256, ["iq3_s", "iq3s"];
131    IQ1S, GgmlType::IQ1S, rlx_gguf::iq_dequant::dequant_iq1_s, 256, ["iq1_s", "iq1s"];
132    IQ1M, GgmlType::IQ1M, rlx_gguf::iq_dequant::dequant_iq1_m, 256, ["iq1_m", "iq1m"];
133    TQ1_0, GgmlType::TQ1_0, rlx_gguf::tq_dequant::dequant_tq1_0, 256, ["tq1_0", "tq1"];
134    TQ2_0, GgmlType::TQ2_0, rlx_gguf::tq_dequant::dequant_tq2_0, 256, ["tq2_0", "tq2"];
135}
136
137/// One layer's quantized K/V buffers.
138///
139/// Rows are appended back-to-back. `past_len` is the number of *logical*
140/// rows currently stored; the byte buffers carry `past_len × kv_dim`
141/// elements' worth of quantized bytes.
142#[derive(Debug, Clone)]
143pub struct QuantizedKvLayer {
144    pub k: Vec<u8>,
145    pub v: Vec<u8>,
146    pub past_len: usize,
147    pub kv_dim: usize,
148    pub scheme: KvQuant,
149    pub bytes_per_row: usize,
150}
151
152impl QuantizedKvLayer {
153    pub fn new(kv_dim: usize, scheme: KvQuant) -> Result<Self> {
154        let blk = scheme.block_elements();
155        if !kv_dim.is_multiple_of(blk) {
156            bail!("kv_dim ({kv_dim}) must be a multiple of {scheme:?} block size ({blk})");
157        }
158        let bytes_per_row = (kv_dim / blk) * scheme.block_bytes();
159        Ok(Self {
160            k: Vec::new(),
161            v: Vec::new(),
162            past_len: 0,
163            kv_dim,
164            scheme,
165            bytes_per_row,
166        })
167    }
168
169    /// Append `rows` worth of K and V (f32, row-major) — quantizing
170    /// each row independently. Caller passes interleaved K then V
171    /// blocks via separate slices.
172    pub fn append_rows(&mut self, k_rows: &[f32], v_rows: &[f32]) -> Result<()> {
173        if k_rows.len() != v_rows.len() {
174            bail!(
175                "append_rows: k len {} != v len {}",
176                k_rows.len(),
177                v_rows.len()
178            );
179        }
180        if !k_rows.len().is_multiple_of(self.kv_dim) {
181            bail!(
182                "append_rows: byte count {} not aligned to kv_dim {}",
183                k_rows.len(),
184                self.kv_dim
185            );
186        }
187        let n_rows = k_rows.len() / self.kv_dim;
188        let k_bytes = quant_rows(k_rows, self.scheme)?;
189        let v_bytes = quant_rows(v_rows, self.scheme)?;
190        self.k.extend_from_slice(&k_bytes);
191        self.v.extend_from_slice(&v_bytes);
192        self.past_len += n_rows;
193        Ok(())
194    }
195
196    /// Dequantize all stored rows back to f32 (K, V).
197    pub fn read_all(&self) -> Result<(Vec<f32>, Vec<f32>)> {
198        let k = dequant_rows(&self.k, self.scheme, self.past_len * self.kv_dim)?;
199        let v = dequant_rows(&self.v, self.scheme, self.past_len * self.kv_dim)?;
200        Ok((k, v))
201    }
202
203    /// Dequantize `count` rows starting at row `start` (K, V). Lets a caller
204    /// stream the cache a row (or small block) at a time — peak extra memory is
205    /// `count × kv_dim` floats, not the whole past.
206    pub fn read_rows(&self, start: usize, count: usize) -> Result<(Vec<f32>, Vec<f32>)> {
207        if start + count > self.past_len {
208            bail!(
209                "read_rows: [{start}, {}) out of range (past_len {})",
210                start + count,
211                self.past_len
212            );
213        }
214        let start_byte = start * self.bytes_per_row;
215        let n = count * self.kv_dim;
216        let k = dequant_rows(&self.k[start_byte..], self.scheme, n)?;
217        let v = dequant_rows(&self.v[start_byte..], self.scheme, n)?;
218        Ok((k, v))
219    }
220
221    /// Dequantize the last `window` rows (or all rows if past_len ≤ window).
222    pub fn read_window(&self, window: usize) -> Result<(Vec<f32>, Vec<f32>)> {
223        if window >= self.past_len {
224            return self.read_all();
225        }
226        self.read_rows(self.past_len - window, window)
227    }
228
229    /// Drop the oldest `n_rows` from this layer (sliding window).
230    pub fn drop_front(&mut self, n_rows: usize) -> Result<()> {
231        let n_rows = n_rows.min(self.past_len);
232        if n_rows == 0 {
233            return Ok(());
234        }
235        let drop_bytes = n_rows * self.bytes_per_row;
236        self.k.drain(..drop_bytes);
237        self.v.drain(..drop_bytes);
238        self.past_len -= n_rows;
239        Ok(())
240    }
241
242    /// Memory used by both buffers (bytes).
243    pub fn bytes(&self) -> usize {
244        self.k.len() + self.v.len()
245    }
246}
247
248/// Single-query attention directly over a **quantized** K/V layer, dequantizing
249/// one row at a time so peak extra memory is `O(kv_dim)`, not `O(past_len ×
250/// kv_dim)` — the point of quantized-KV attention vs the dequantize-the-whole-
251/// cache path. GQA-aware: `n_heads` query heads share `kv_heads` K/V heads
252/// (`group = n_heads / kv_heads`).
253///
254/// - `q`: the new token's query, `[n_heads × head_dim]`.
255/// - returns the attention output `[n_heads × head_dim]`.
256///
257/// This is the host reference for a future first-class `QuantizedAttention` op;
258/// it lets long-context decode keep the cache small without ever materializing
259/// the full f32 history.
260pub fn attend_quantized(
261    q: &[f32],
262    layer: &QuantizedKvLayer,
263    n_heads: usize,
264    kv_heads: usize,
265    head_dim: usize,
266    scale: f32,
267) -> Result<Vec<f32>> {
268    let kv_dim = kv_heads * head_dim;
269    if layer.kv_dim != kv_dim {
270        bail!(
271            "attend_quantized: layer kv_dim {} != {kv_heads}×{head_dim}",
272            layer.kv_dim
273        );
274    }
275    if q.len() != n_heads * head_dim {
276        bail!(
277            "attend_quantized: q len {} != {n_heads}×{head_dim}",
278            q.len()
279        );
280    }
281    let mut out = vec![0f32; n_heads * head_dim];
282    let past = layer.past_len;
283    if past == 0 {
284        return Ok(out);
285    }
286    let group = (n_heads / kv_heads.max(1)).max(1);
287
288    // Pass 1: stream K row-by-row, accumulate per-head scores.
289    let mut scores = vec![0f32; n_heads * past];
290    for p in 0..past {
291        let (k_row, _) = layer.read_rows(p, 1)?;
292        for h in 0..n_heads {
293            let kvh = h / group;
294            let q_h = &q[h * head_dim..(h + 1) * head_dim];
295            let k_h = &k_row[kvh * head_dim..(kvh + 1) * head_dim];
296            let dot: f32 = q_h.iter().zip(k_h).map(|(a, b)| a * b).sum();
297            scores[h * past + p] = dot * scale;
298        }
299    }
300
301    // Softmax per head over the `past` keys (numerically stable).
302    for h in 0..n_heads {
303        let row = &mut scores[h * past..(h + 1) * past];
304        let max = row.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
305        let mut sum = 0f32;
306        for v in row.iter_mut() {
307            *v = (*v - max).exp();
308            sum += *v;
309        }
310        let inv = 1.0 / sum.max(1e-20);
311        for v in row.iter_mut() {
312            *v *= inv;
313        }
314    }
315
316    // Pass 2: stream V row-by-row, accumulate weighted output.
317    for p in 0..past {
318        let (_, v_row) = layer.read_rows(p, 1)?;
319        for h in 0..n_heads {
320            let kvh = h / group;
321            let w = scores[h * past + p];
322            let v_h = &v_row[kvh * head_dim..(kvh + 1) * head_dim];
323            let o_h = &mut out[h * head_dim..(h + 1) * head_dim];
324            for (o, &vv) in o_h.iter_mut().zip(v_h) {
325                *o += w * vv;
326            }
327        }
328    }
329    Ok(out)
330}
331
332/// All layers of a quantized KV cache.
333#[derive(Debug, Clone)]
334pub struct QuantizedKvCache {
335    pub layers: Vec<QuantizedKvLayer>,
336}
337
338impl QuantizedKvCache {
339    pub fn new(n_layers: usize, kv_dim: usize, scheme: KvQuant) -> Result<Self> {
340        let layers = (0..n_layers)
341            .map(|_| QuantizedKvLayer::new(kv_dim, scheme))
342            .collect::<Result<Vec<_>>>()?;
343        Ok(Self { layers })
344    }
345
346    pub fn n_layers(&self) -> usize {
347        self.layers.len()
348    }
349
350    pub fn past_len(&self) -> usize {
351        self.layers.first().map(|l| l.past_len).unwrap_or(0)
352    }
353
354    /// Total bytes across all layers.
355    pub fn bytes(&self) -> usize {
356        self.layers.iter().map(|l| l.bytes()).sum()
357    }
358}
359
360// ─── quant / dequant entry points ────────────────────────────────────
361
362fn quant_rows(values: &[f32], scheme: KvQuant) -> Result<Vec<u8>> {
363    match scheme {
364        KvQuant::F16 => {
365            let mut out = Vec::with_capacity(values.len() * 2);
366            for &v in values {
367                let h = half::f16::from_f32(v);
368                out.extend_from_slice(&h.to_le_bytes());
369            }
370            Ok(out)
371        }
372        scheme => {
373            let ty = scheme
374                .ggml_type()
375                .ok_or_else(|| anyhow!("internal: missing ggml type for {scheme:?}"))?;
376            Ok(quantize(values, ty)?)
377        }
378    }
379}
380
381fn dequant_rows(bytes: &[u8], scheme: KvQuant, n: usize) -> Result<Vec<f32>> {
382    match scheme {
383        KvQuant::F16 => {
384            if bytes.len() < n * 2 {
385                bail!("F16 dequant: {} bytes < {} expected", bytes.len(), n * 2);
386            }
387            let mut out = Vec::with_capacity(n);
388            for chunk in bytes[..n * 2].chunks_exact(2) {
389                let h = half::f16::from_le_bytes([chunk[0], chunk[1]]);
390                out.push(h.to_f32());
391            }
392            Ok(out)
393        }
394        _ => scheme.dequant_dispatch(bytes, n),
395    }
396}
397
398// ─── mmap-backed storage (feature = "mmap-kv") ───────────────────────
399//
400// Memory-mapped storage trades a Vec<u8> for a file-backed (or anonymous)
401// `MmapMut` so the OS pages quantized blocks in/out on demand. Two
402// use cases:
403//
404// 1. **Long contexts** — KV history for 100k-token decode runs can
405//    exceed RAM. With mmap, the kernel evicts cold pages to swap or
406//    the backing file; reactivating them is a page fault, not a
407//    user-space read().
408//
409// 2. **Zero-copy GPU upload** — paged memory is friendlier to
410//    `cudaHostRegister` / Metal `MTLBuffer::newBufferWithBytesNoCopy`,
411//    which can pin and DMA without an extra staging copy.
412//
413// Append semantics are emulated: we pre-allocate `capacity_rows`
414// worth of blocks and track a write head, then `set_len` on the file
415// when finalizing. For "grow as you go" the caller passes
416// `capacity_rows = max_seq_len`.
417
418#[cfg(feature = "mmap-kv")]
419pub mod mmap {
420    use super::*;
421    use memmap2::{MmapMut, MmapOptions};
422    use std::fs::OpenOptions;
423    use std::path::{Path, PathBuf};
424
425    /// File-backed quantized K/V buffer. K and V share one mapping —
426    /// V follows K in the file. Disk layout: `[K bytes][V bytes]`.
427    pub struct MmapKvLayer {
428        pub mmap: MmapMut,
429        pub past_len: usize,
430        pub capacity_rows: usize,
431        pub kv_dim: usize,
432        pub scheme: KvQuant,
433        pub bytes_per_row: usize,
434        pub k_offset: usize,
435        pub v_offset: usize,
436        pub path: Option<PathBuf>,
437        /// Retained fd for the `pread` read path (file-backed maps only).
438        file: Option<std::fs::File>,
439        /// Use positional `read_at` (pread) instead of mmap-slice for `read_rows`
440        /// (opt-in via `RLX_KVSTORE_PREAD`; falls back to mmap for anonymous maps or
441        /// non-Unix). Reads become explicit bounded syscalls into a fresh buffer —
442        /// predictable latency, no reliance on the mapping, and the seam an
443        /// `io_uring`-batched cold-read backend would plug into on Linux. Writes
444        /// still go through the mmap; the shared page cache keeps the two coherent.
445        pread: bool,
446    }
447
448    impl MmapKvLayer {
449        /// File-backed mapping. Creates / truncates `path` to
450        /// `2 × capacity_rows × bytes_per_row` and maps it RW.
451        pub fn open<P: AsRef<Path>>(
452            path: P,
453            kv_dim: usize,
454            scheme: KvQuant,
455            capacity_rows: usize,
456        ) -> Result<Self> {
457            let blk = scheme.block_elements();
458            if !kv_dim.is_multiple_of(blk) {
459                bail!("kv_dim ({kv_dim}) must be a multiple of {scheme:?} block size ({blk})");
460            }
461            let bytes_per_row = (kv_dim / blk) * scheme.block_bytes();
462            let total = 2 * capacity_rows * bytes_per_row;
463            let file = OpenOptions::new()
464                .read(true)
465                .write(true)
466                .create(true)
467                .truncate(true)
468                .open(&path)?;
469            file.set_len(total as u64)?;
470            let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
471            Ok(Self {
472                mmap,
473                past_len: 0,
474                capacity_rows,
475                kv_dim,
476                scheme,
477                bytes_per_row,
478                k_offset: 0,
479                v_offset: capacity_rows * bytes_per_row,
480                path: Some(path.as_ref().to_path_buf()),
481                file: Some(file),
482                pread: rlx_ir::env::var_os("RLX_KVSTORE_PREAD").is_some(),
483            })
484        }
485
486        /// File-backed mapping that preserves existing contents. Opens `path`
487        /// read/write without truncating and maps `2 × capacity_rows × bytes_per_row`.
488        ///
489        /// This is used by KV-store reuse flows that rebuild retrieval metadata
490        /// and indexes without rewriting persisted K/V rows.
491        pub fn open_existing<P: AsRef<Path>>(
492            path: P,
493            kv_dim: usize,
494            scheme: KvQuant,
495            capacity_rows: usize,
496        ) -> Result<Self> {
497            let blk = scheme.block_elements();
498            if !kv_dim.is_multiple_of(blk) {
499                bail!("kv_dim ({kv_dim}) must be a multiple of {scheme:?} block size ({blk})");
500            }
501            let bytes_per_row = (kv_dim / blk) * scheme.block_bytes();
502            let total = 2 * capacity_rows * bytes_per_row;
503            let file = OpenOptions::new().read(true).write(true).open(&path)?;
504            let cur_len = file.metadata()?.len() as usize;
505            if cur_len < total {
506                bail!(
507                    "open_existing: file too small ({} < expected {}): {}",
508                    cur_len,
509                    total,
510                    path.as_ref().display()
511                );
512            }
513            let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
514            Ok(Self {
515                mmap,
516                past_len: 0,
517                capacity_rows,
518                kv_dim,
519                scheme,
520                bytes_per_row,
521                k_offset: 0,
522                v_offset: capacity_rows * bytes_per_row,
523                path: Some(path.as_ref().to_path_buf()),
524                file: Some(file),
525                pread: rlx_ir::env::var_os("RLX_KVSTORE_PREAD").is_some(),
526            })
527        }
528
529        /// Anonymous (private) mapping. Lives in swap-backed pages;
530        /// not persisted. Use when you want OS-level paging without
531        /// keeping a file around.
532        pub fn anonymous(kv_dim: usize, scheme: KvQuant, capacity_rows: usize) -> Result<Self> {
533            let blk = scheme.block_elements();
534            if !kv_dim.is_multiple_of(blk) {
535                bail!("kv_dim ({kv_dim}) must be a multiple of {scheme:?} block size ({blk})");
536            }
537            let bytes_per_row = (kv_dim / blk) * scheme.block_bytes();
538            let total = 2 * capacity_rows * bytes_per_row;
539            let mmap = MmapOptions::new().len(total).map_anon()?;
540            Ok(Self {
541                mmap,
542                past_len: 0,
543                capacity_rows,
544                kv_dim,
545                scheme,
546                bytes_per_row,
547                k_offset: 0,
548                v_offset: capacity_rows * bytes_per_row,
549                path: None,
550                file: None,
551                pread: false, // no fd for anonymous maps → always mmap-slice reads
552            })
553        }
554
555        /// Append `n_rows` × `kv_dim` worth of K and V floats (one row
556        /// at a time, quantized in place).
557        pub fn append_rows(&mut self, k_rows: &[f32], v_rows: &[f32]) -> Result<()> {
558            if k_rows.len() != v_rows.len() {
559                bail!("append_rows: k/v length mismatch");
560            }
561            if !k_rows.len().is_multiple_of(self.kv_dim) {
562                bail!("append_rows: byte count not aligned to kv_dim");
563            }
564            let n_rows = k_rows.len() / self.kv_dim;
565            if self.past_len + n_rows > self.capacity_rows {
566                bail!(
567                    "append_rows: would exceed capacity ({} + {} > {})",
568                    self.past_len,
569                    n_rows,
570                    self.capacity_rows
571                );
572            }
573            let kb = quant_rows(k_rows, self.scheme)?;
574            let vb = quant_rows(v_rows, self.scheme)?;
575            let k_start = self.k_offset + self.past_len * self.bytes_per_row;
576            let v_start = self.v_offset + self.past_len * self.bytes_per_row;
577            self.mmap[k_start..k_start + kb.len()].copy_from_slice(&kb);
578            self.mmap[v_start..v_start + vb.len()].copy_from_slice(&vb);
579            self.past_len += n_rows;
580            Ok(())
581        }
582
583        /// Dequantize all stored rows. Reads zero-copy from the page
584        /// cache — only touched pages are faulted in.
585        pub fn read_all(&self) -> Result<(Vec<f32>, Vec<f32>)> {
586            let n = self.past_len * self.kv_dim;
587            let k_end = self.k_offset + self.past_len * self.bytes_per_row;
588            let v_end = self.v_offset + self.past_len * self.bytes_per_row;
589            let k = dequant_rows(&self.mmap[self.k_offset..k_end], self.scheme, n)?;
590            let v = dequant_rows(&self.mmap[self.v_offset..v_end], self.scheme, n)?;
591            Ok((k, v))
592        }
593
594        /// Read the last `window` rows (page-fault-lazy on dequant).
595        pub fn read_window(&self, window: usize) -> Result<(Vec<f32>, Vec<f32>)> {
596            let window = window.min(self.past_len);
597            let start_row = self.past_len - window;
598            let n = window * self.kv_dim;
599            let k_start = self.k_offset + start_row * self.bytes_per_row;
600            let v_start = self.v_offset + start_row * self.bytes_per_row;
601            let k_end = k_start + window * self.bytes_per_row;
602            let v_end = v_start + window * self.bytes_per_row;
603            let k = dequant_rows(&self.mmap[k_start..k_end], self.scheme, n)?;
604            let v = dequant_rows(&self.mmap[v_start..v_end], self.scheme, n)?;
605            Ok((k, v))
606        }
607
608        /// Read an arbitrary row range `[start_row, start_row+n_rows)` — the
609        /// random-access primitive for block retrieval (only the touched pages
610        /// fault in from disk). Rows outside `past_len` are clamped away.
611        pub fn read_rows(&self, start_row: usize, n_rows: usize) -> Result<(Vec<f32>, Vec<f32>)> {
612            let start_row = start_row.min(self.past_len);
613            let n_rows = n_rows.min(self.past_len - start_row);
614            let n = n_rows * self.kv_dim;
615            let nbytes = n_rows * self.bytes_per_row;
616            let k_start = self.k_offset + start_row * self.bytes_per_row;
617            let v_start = self.v_offset + start_row * self.bytes_per_row;
618            // Positional-read path (opt-in): bounded `pread` into fresh buffers
619            // rather than slicing the mapping. Writes go through the (MAP_SHARED)
620            // mmap, so the shared page cache keeps pread coherent with them.
621            #[cfg(unix)]
622            if self.pread {
623                if let Some(f) = self.file.as_ref() {
624                    use std::os::unix::fs::FileExt;
625                    let mut kb = vec![0u8; nbytes];
626                    let mut vb = vec![0u8; nbytes];
627                    f.read_exact_at(&mut kb, k_start as u64)?;
628                    f.read_exact_at(&mut vb, v_start as u64)?;
629                    let k = dequant_rows(&kb, self.scheme, n)?;
630                    let v = dequant_rows(&vb, self.scheme, n)?;
631                    return Ok((k, v));
632                }
633            }
634            let k_end = k_start + nbytes;
635            let v_end = v_start + nbytes;
636            let k = dequant_rows(&self.mmap[k_start..k_end], self.scheme, n)?;
637            let v = dequant_rows(&self.mmap[v_start..v_end], self.scheme, n)?;
638            Ok((k, v))
639        }
640
641        /// Prefetch an arbitrary row range (madvise WILLNEED) so the retrieved
642        /// blocks' pages are warm before the synchronous read. Best-effort.
643        pub fn prefetch_rows(&self, start_row: usize, n_rows: usize) {
644            let start_row = start_row.min(self.past_len);
645            let n_rows = n_rows.min(self.past_len - start_row);
646            if n_rows == 0 {
647                return;
648            }
649            let k_start = self.k_offset + start_row * self.bytes_per_row;
650            let v_start = self.v_offset + start_row * self.bytes_per_row;
651            #[cfg(unix)]
652            {
653                let len = n_rows * self.bytes_per_row;
654                let _ = self
655                    .mmap
656                    .advise_range(memmap2::Advice::WillNeed, k_start, len);
657                let _ = self
658                    .mmap
659                    .advise_range(memmap2::Advice::WillNeed, v_start, len);
660            }
661            #[cfg(not(unix))]
662            {
663                let _ = (k_start, v_start);
664            }
665        }
666
667        /// Hint the kernel that we're about to read `window` rows
668        /// linearly — prefetches pages into the page cache (madvise
669        /// WILLNEED on supported platforms). Best-effort: failures are
670        /// logged but don't propagate.
671        pub fn prefetch_window(&self, window: usize) {
672            let window = window.min(self.past_len);
673            if window == 0 {
674                return;
675            }
676            let start_row = self.past_len - window;
677            let k_start = self.k_offset + start_row * self.bytes_per_row;
678            let v_start = self.v_offset + start_row * self.bytes_per_row;
679            // memmap2::Advice / advise_range are Unix-only; no-op on Windows.
680            #[cfg(unix)]
681            {
682                let _ = self.mmap.advise_range(
683                    memmap2::Advice::WillNeed,
684                    k_start,
685                    window * self.bytes_per_row,
686                );
687                let _ = self.mmap.advise_range(
688                    memmap2::Advice::WillNeed,
689                    v_start,
690                    window * self.bytes_per_row,
691                );
692            }
693            #[cfg(not(unix))]
694            {
695                let _ = (k_start, v_start);
696            }
697        }
698
699        /// Advise the kernel this mapping is accessed **randomly**, disabling the
700        /// sequential read-ahead that otherwise faults in neighbor pages a
701        /// scattered block-retrieval read never uses. Opt-in (not baked into
702        /// `open`) so sequential users — `read_window` / `read_all` on the decode
703        /// KV path — keep their read-ahead. Best-effort, Unix-only.
704        pub fn advise_random(&self) {
705            #[cfg(unix)]
706            {
707                let _ = self.mmap.advise(memmap2::Advice::Random);
708            }
709        }
710
711        /// Force the positional-read (`pread`) path on/off, overriding the
712        /// `RLX_KVSTORE_PREAD` env default. No-op → mmap for anonymous maps (no fd).
713        pub fn set_pread(&mut self, on: bool) {
714            self.pread = on && self.file.is_some();
715        }
716
717        /// Persist any dirty pages to the backing file. No-op for
718        /// anonymous mappings.
719        pub fn flush(&self) -> Result<()> {
720            self.mmap.flush()?;
721            Ok(())
722        }
723
724        pub fn bytes(&self) -> usize {
725            2 * self.past_len * self.bytes_per_row
726        }
727    }
728
729    /// All-layer mmap-backed KV cache.
730    pub struct MmapKvCache {
731        pub layers: Vec<MmapKvLayer>,
732    }
733
734    impl MmapKvCache {
735        /// One file per layer under `dir`, named `kv_{i}.bin`.
736        pub fn open_dir<P: AsRef<Path>>(
737            dir: P,
738            n_layers: usize,
739            kv_dim: usize,
740            scheme: KvQuant,
741            capacity_rows: usize,
742        ) -> Result<Self> {
743            let dir = dir.as_ref();
744            std::fs::create_dir_all(dir)?;
745            let layers = (0..n_layers)
746                .map(|i| {
747                    MmapKvLayer::open(
748                        dir.join(format!("kv_{i}.bin")),
749                        kv_dim,
750                        scheme,
751                        capacity_rows,
752                    )
753                })
754                .collect::<Result<Vec<_>>>()?;
755            Ok(Self { layers })
756        }
757
758        pub fn anonymous(
759            n_layers: usize,
760            kv_dim: usize,
761            scheme: KvQuant,
762            capacity_rows: usize,
763        ) -> Result<Self> {
764            let layers = (0..n_layers)
765                .map(|_| MmapKvLayer::anonymous(kv_dim, scheme, capacity_rows))
766                .collect::<Result<Vec<_>>>()?;
767            Ok(Self { layers })
768        }
769
770        pub fn n_layers(&self) -> usize {
771            self.layers.len()
772        }
773
774        pub fn past_len(&self) -> usize {
775            self.layers.first().map(|l| l.past_len).unwrap_or(0)
776        }
777
778        /// Total bytes currently in use across all layers.
779        pub fn bytes(&self) -> usize {
780            self.layers.iter().map(|l| l.bytes()).sum()
781        }
782    }
783
784    #[cfg(test)]
785    mod tests {
786        use super::*;
787
788        #[test]
789        fn anonymous_q8_0_roundtrip() {
790            let kv_dim = 64;
791            let mut layer = MmapKvLayer::anonymous(kv_dim, KvQuant::Q8_0, 4).unwrap();
792            let data: Vec<f32> = (0..kv_dim).map(|i| (i as f32).sin()).collect();
793            layer.append_rows(&data, &data).unwrap();
794            let (k, v) = layer.read_all().unwrap();
795            assert_eq!(k.len(), kv_dim);
796            assert_eq!(v.len(), kv_dim);
797            // Q8_0 is high-fidelity.
798            for (a, b) in k.iter().zip(data.iter()) {
799                assert!((a - b).abs() < 0.02);
800            }
801        }
802
803        #[test]
804        fn file_backed_persists_and_reopens() {
805            let dir = tempfile::tempdir().unwrap();
806            let kv_dim = 32;
807            let path = dir.path().join("layer.bin");
808            {
809                let mut layer = MmapKvLayer::open(&path, kv_dim, KvQuant::F16, 8).unwrap();
810                let data: Vec<f32> = (0..kv_dim).map(|i| i as f32 * 0.5).collect();
811                layer.append_rows(&data, &data).unwrap();
812                layer.flush().unwrap();
813            }
814            // Re-open and verify K bytes are present (we know offset 0..len).
815            let bytes = std::fs::read(&path).unwrap();
816            assert!(!bytes.is_empty());
817            assert!(bytes.iter().any(|&b| b != 0));
818        }
819
820        #[test]
821        fn append_past_capacity_errors() {
822            let mut l = MmapKvLayer::anonymous(32, KvQuant::Q8_0, 2).unwrap();
823            let row = vec![0.5f32; 32];
824            l.append_rows(&row, &row).unwrap();
825            l.append_rows(&row, &row).unwrap();
826            assert!(l.append_rows(&row, &row).is_err());
827        }
828
829        // Item 3: the opt-in positional-read (pread) path must return bit-identical
830        // rows to the mmap-slice path (writes go through the mmap; the shared page
831        // cache keeps pread coherent).
832        #[cfg(unix)]
833        #[test]
834        fn pread_matches_mmap_read() {
835            let dir = tempfile::tempdir().unwrap();
836            let kv_dim = 32;
837            let path = dir.path().join("layer.bin");
838            let mut layer = MmapKvLayer::open(&path, kv_dim, KvQuant::Q8_0, 64).unwrap();
839            let k: Vec<f32> = (0..kv_dim * 4).map(|i| (i as f32) * 0.01 - 1.0).collect();
840            let v: Vec<f32> = (0..kv_dim * 4).map(|i| (i as f32) * -0.02 + 0.5).collect();
841            layer.append_rows(&k, &v).unwrap();
842            // mmap-slice read (default).
843            layer.set_pread(false);
844            let (km, vm) = layer.read_rows(1, 2).unwrap();
845            // pread read of the same range.
846            layer.set_pread(true);
847            let (kp, vp) = layer.read_rows(1, 2).unwrap();
848            assert_eq!(km.len(), kp.len());
849            assert_eq!(vm.len(), vp.len());
850            assert!(
851                km.iter().zip(&kp).all(|(a, b)| (a - b).abs() < 1e-6),
852                "K mmap != pread"
853            );
854            assert!(
855                vm.iter().zip(&vp).all(|(a, b)| (a - b).abs() < 1e-6),
856                "V mmap != pread"
857            );
858        }
859    }
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    fn cosine(a: &[f32], b: &[f32]) -> f32 {
867        let mut dot = 0.0f32;
868        let mut na = 0.0f32;
869        let mut nb = 0.0f32;
870        for (x, y) in a.iter().zip(b.iter()) {
871            dot += x * y;
872            na += x * x;
873            nb += y * y;
874        }
875        dot / (na.sqrt() * nb.sqrt() + 1e-12)
876    }
877
878    fn mae(a: &[f32], b: &[f32]) -> f32 {
879        let n = a.len().min(b.len()).max(1) as f32;
880        a.iter()
881            .zip(b.iter())
882            .map(|(x, y)| (x - y).abs())
883            .sum::<f32>()
884            / n
885    }
886
887    fn rmse(a: &[f32], b: &[f32]) -> f32 {
888        let n = a.len().min(b.len()).max(1) as f32;
889        (a.iter()
890            .zip(b.iter())
891            .map(|(x, y)| {
892                let d = x - y;
893                d * d
894            })
895            .sum::<f32>()
896            / n)
897            .sqrt()
898    }
899
900    fn attention_reference(
901        q: &[f32],
902        k_all: &[f32],
903        v_all: &[f32],
904        n_heads: usize,
905        kv_heads: usize,
906        head_dim: usize,
907        past: usize,
908        scale: f32,
909    ) -> Vec<f32> {
910        let kv_dim = kv_heads * head_dim;
911        let group = n_heads / kv_heads;
912        let mut out = vec![0f32; n_heads * head_dim];
913        for h in 0..n_heads {
914            let kvh = h / group;
915            let mut sc = vec![0f32; past];
916            for p in 0..past {
917                let qh = &q[h * head_dim..(h + 1) * head_dim];
918                let base = p * kv_dim + kvh * head_dim;
919                let kh = &k_all[base..base + head_dim];
920                let dot: f32 = qh.iter().zip(kh).map(|(a, b)| a * b).sum();
921                sc[p] = dot * scale;
922            }
923            let max = sc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
924            let mut sum = 0.0;
925            for x in sc.iter_mut() {
926                *x = (*x - max).exp();
927                sum += *x;
928            }
929            for x in sc.iter_mut() {
930                *x /= sum;
931            }
932            for p in 0..past {
933                let base = p * kv_dim + kvh * head_dim;
934                let vh = &v_all[base..base + head_dim];
935                for d in 0..head_dim {
936                    out[h * head_dim + d] += sc[p] * vh[d];
937                }
938            }
939        }
940        out
941    }
942
943    #[test]
944    fn block_size_invariants() {
945        assert_eq!(KvQuant::F16.block_bytes(), 2);
946        assert_eq!(KvQuant::Q8_0.block_bytes(), 34);
947        assert_eq!(KvQuant::Q4_0.block_bytes(), 18);
948        assert_eq!(KvQuant::Q5_0.block_bytes(), 22);
949    }
950
951    #[test]
952    fn f16_roundtrip_exact() {
953        let kv_dim = 64;
954        let mut layer = QuantizedKvLayer::new(kv_dim, KvQuant::F16).unwrap();
955        let k_row: Vec<f32> = (0..kv_dim).map(|i| (i as f32) * 0.1).collect();
956        let v_row: Vec<f32> = (0..kv_dim).map(|i| (i as f32) * 0.2).collect();
957        layer.append_rows(&k_row, &v_row).unwrap();
958        let (k, v) = layer.read_all().unwrap();
959        for i in 0..kv_dim {
960            // f16 round-trip is bounded ~1e-3 relative for small magnitudes.
961            assert!((k[i] - k_row[i]).abs() < 0.01);
962            assert!((v[i] - v_row[i]).abs() < 0.01);
963        }
964    }
965
966    #[test]
967    fn q8_0_roundtrip_high_fidelity() {
968        let kv_dim = 64;
969        let n_rows = 4;
970        let mut layer = QuantizedKvLayer::new(kv_dim, KvQuant::Q8_0).unwrap();
971        let total = n_rows * kv_dim;
972        let k_data: Vec<f32> = (0..total).map(|i| (i as f32).sin()).collect();
973        let v_data: Vec<f32> = (0..total).map(|i| (i as f32).cos()).collect();
974        layer.append_rows(&k_data, &v_data).unwrap();
975        assert_eq!(layer.past_len, n_rows);
976        let (k, v) = layer.read_all().unwrap();
977        assert!(cosine(&k, &k_data) > 0.999, "Q8_0 K cosine too low");
978        assert!(cosine(&v, &v_data) > 0.999, "Q8_0 V cosine too low");
979    }
980
981    #[test]
982    fn q4_0_roundtrip_lossy_but_close() {
983        let kv_dim = 64;
984        let mut layer = QuantizedKvLayer::new(kv_dim, KvQuant::Q4_0).unwrap();
985        let k: Vec<f32> = (0..kv_dim).map(|i| (i as f32 * 0.05).tanh()).collect();
986        let v: Vec<f32> = (0..kv_dim).map(|i| (i as f32 * 0.07).tanh()).collect();
987        layer.append_rows(&k, &v).unwrap();
988        let (kr, vr) = layer.read_all().unwrap();
989        assert!(cosine(&kr, &k) > 0.99);
990        assert!(cosine(&vr, &v) > 0.99);
991    }
992
993    #[test]
994    fn q5_0_roundtrip_better_than_q4() {
995        let kv_dim = 64;
996        let mut q4 = QuantizedKvLayer::new(kv_dim, KvQuant::Q4_0).unwrap();
997        let mut q5 = QuantizedKvLayer::new(kv_dim, KvQuant::Q5_0).unwrap();
998        let k: Vec<f32> = (0..kv_dim).map(|i| (i as f32 * 0.1).sin() * 3.0).collect();
999        let v: Vec<f32> = (0..kv_dim).map(|i| (i as f32 * 0.13).cos() * 3.0).collect();
1000        q4.append_rows(&k, &v).unwrap();
1001        q5.append_rows(&k, &v).unwrap();
1002        let (k4, _) = q4.read_all().unwrap();
1003        let (k5, _) = q5.read_all().unwrap();
1004        let cos4 = cosine(&k4, &k);
1005        let cos5 = cosine(&k5, &k);
1006        assert!(cos5 >= cos4 - 1e-3, "Q5_0 should not be worse than Q4_0");
1007    }
1008
1009    #[test]
1010    fn sliding_window_drops_oldest() {
1011        let kv_dim = 32;
1012        let mut layer = QuantizedKvLayer::new(kv_dim, KvQuant::Q8_0).unwrap();
1013        for r in 0..5 {
1014            let v: Vec<f32> = (0..kv_dim).map(|i| (i + r * 100) as f32).collect();
1015            layer.append_rows(&v, &v).unwrap();
1016        }
1017        assert_eq!(layer.past_len, 5);
1018        layer.drop_front(2).unwrap();
1019        assert_eq!(layer.past_len, 3);
1020        let (k, _v) = layer.read_window(3).unwrap();
1021        // First kept row is original row 2 → starts with value 200.
1022        assert!((k[0] - 200.0).abs() < 1.0);
1023    }
1024
1025    #[test]
1026    fn kv_dim_must_align_to_block_size() {
1027        // kv_dim=24 < 32 → not aligned for Q8_0/Q4_0/Q5_0.
1028        assert!(QuantizedKvLayer::new(24, KvQuant::Q8_0).is_err());
1029        assert!(QuantizedKvLayer::new(24, KvQuant::Q4_0).is_err());
1030        // f16 has block 1 so any dim works.
1031        assert!(QuantizedKvLayer::new(24, KvQuant::F16).is_ok());
1032    }
1033
1034    #[test]
1035    fn streaming_quantized_attention_matches_full_dequant() {
1036        let (n_heads, kv_heads, head_dim) = (4usize, 2usize, 32usize);
1037        let kv_dim = kv_heads * head_dim; // 64 — multiple of 32 for Q8_0
1038        let past = 5usize;
1039        let scale = 1.0 / (head_dim as f32).sqrt();
1040
1041        let mut layer = QuantizedKvLayer::new(kv_dim, KvQuant::Q8_0).unwrap();
1042        for p in 0..past {
1043            let k: Vec<f32> = (0..kv_dim)
1044                .map(|i| ((i + p * 7) as f32 * 0.05).sin())
1045                .collect();
1046            let v: Vec<f32> = (0..kv_dim)
1047                .map(|i| ((i + p * 3) as f32 * 0.04).cos())
1048                .collect();
1049            layer.append_rows(&k, &v).unwrap();
1050        }
1051        let q: Vec<f32> = (0..n_heads * head_dim)
1052            .map(|i| (i as f32 * 0.02).sin())
1053            .collect();
1054
1055        let out = attend_quantized(&q, &layer, n_heads, kv_heads, head_dim, scale).unwrap();
1056
1057        // Reference: dequantize the whole cache, run standard attention.
1058        let (k_all, v_all) = layer.read_all().unwrap();
1059        let group = n_heads / kv_heads;
1060        let mut reference = vec![0f32; n_heads * head_dim];
1061        for h in 0..n_heads {
1062            let kvh = h / group;
1063            let mut sc = vec![0f32; past];
1064            for p in 0..past {
1065                let qh = &q[h * head_dim..(h + 1) * head_dim];
1066                let base = p * kv_dim + kvh * head_dim;
1067                let kh = &k_all[base..base + head_dim];
1068                let dot: f32 = qh.iter().zip(kh).map(|(a, b)| a * b).sum();
1069                sc[p] = dot * scale;
1070            }
1071            let max = sc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1072            let mut sum = 0.0;
1073            for x in sc.iter_mut() {
1074                *x = (*x - max).exp();
1075                sum += *x;
1076            }
1077            for x in sc.iter_mut() {
1078                *x /= sum;
1079            }
1080            for p in 0..past {
1081                let base = p * kv_dim + kvh * head_dim;
1082                let vh = &v_all[base..base + head_dim];
1083                for d in 0..head_dim {
1084                    reference[h * head_dim + d] += sc[p] * vh[d];
1085                }
1086            }
1087        }
1088        for (a, b) in out.iter().zip(&reference) {
1089            assert!((a - b).abs() < 1e-4, "streaming {a} vs full-dequant {b}");
1090        }
1091    }
1092
1093    #[test]
1094    fn cache_memory_decreases_with_quantization() {
1095        let kv_dim = 128;
1096        let n_layers = 4;
1097        let n_rows = 16;
1098        let data: Vec<f32> = (0..kv_dim).map(|i| (i as f32) * 0.01).collect();
1099        let mut f16 = QuantizedKvCache::new(n_layers, kv_dim, KvQuant::F16).unwrap();
1100        let mut q8 = QuantizedKvCache::new(n_layers, kv_dim, KvQuant::Q8_0).unwrap();
1101        let mut q4 = QuantizedKvCache::new(n_layers, kv_dim, KvQuant::Q4_0).unwrap();
1102        for _ in 0..n_rows {
1103            for l in 0..n_layers {
1104                f16.layers[l].append_rows(&data, &data).unwrap();
1105                q8.layers[l].append_rows(&data, &data).unwrap();
1106                q4.layers[l].append_rows(&data, &data).unwrap();
1107            }
1108        }
1109        assert!(q8.bytes() < f16.bytes());
1110        assert!(q4.bytes() < q8.bytes());
1111    }
1112
1113    #[test]
1114    fn q_variant_precision_report() {
1115        // kv_dim must satisfy the largest block requirement (256 for K/IQ/TQ).
1116        let (n_heads, kv_heads, head_dim) = (4usize, 2usize, 128usize);
1117        let kv_dim = kv_heads * head_dim;
1118        let past = 8usize;
1119        let scale = 1.0 / (head_dim as f32).sqrt();
1120
1121        let k_data: Vec<f32> = (0..past * kv_dim)
1122            .map(|i| {
1123                let x = i as f32;
1124                (x * 0.017).sin() * 2.3 + (x * 0.003).cos() * 0.7
1125            })
1126            .collect();
1127        let v_data: Vec<f32> = (0..past * kv_dim)
1128            .map(|i| {
1129                let x = i as f32;
1130                (x * 0.013).cos() * 1.9 + (x * 0.005).sin() * 0.6
1131            })
1132            .collect();
1133        let q: Vec<f32> = (0..n_heads * head_dim)
1134            .map(|i| (i as f32 * 0.021).sin() * 1.1)
1135            .collect();
1136        let ref_attn = attention_reference(
1137            &q, &k_data, &v_data, n_heads, kv_heads, head_dim, past, scale,
1138        );
1139
1140        let schemes = vec![
1141            ("f16", KvQuant::F16),
1142            ("q8_0", KvQuant::Q8_0),
1143            ("q8k", KvQuant::Q8K),
1144            ("q6k", KvQuant::Q6K),
1145            ("q5_1", KvQuant::Q5_1),
1146            ("q5_0", KvQuant::Q5_0),
1147            ("q5k", KvQuant::Q5K),
1148            ("q4_1", KvQuant::Q4_1),
1149            ("q4_0", KvQuant::Q4_0),
1150            ("q4k", KvQuant::Q4K),
1151            ("q3k", KvQuant::Q3K),
1152            ("q2k", KvQuant::Q2K),
1153            ("q2_0", KvQuant::Q2_0),
1154            ("q1_0", KvQuant::Q1_0),
1155            ("iq4_nl", KvQuant::IQ4NL),
1156            ("iq4_xs", KvQuant::IQ4XS),
1157            ("iq3_s", KvQuant::IQ3S),
1158            ("iq2_s", KvQuant::IQ2S),
1159            ("iq1_s", KvQuant::IQ1S),
1160            ("tq2_0", KvQuant::TQ2_0),
1161            ("tq1_0", KvQuant::TQ1_0),
1162        ];
1163
1164        println!("scheme,k_cos,v_cos,k_mae,v_mae,attn_cos,attn_rmse,bytes_per_row");
1165        for (name, scheme) in schemes {
1166            let mut layer = QuantizedKvLayer::new(kv_dim, scheme).unwrap();
1167            layer.append_rows(&k_data, &v_data).unwrap();
1168            let (kr, vr) = layer.read_all().unwrap();
1169            let attn = attend_quantized(&q, &layer, n_heads, kv_heads, head_dim, scale).unwrap();
1170
1171            let k_cos = cosine(&kr, &k_data);
1172            let v_cos = cosine(&vr, &v_data);
1173            let k_mae = mae(&kr, &k_data);
1174            let v_mae = mae(&vr, &v_data);
1175            let a_cos = cosine(&attn, &ref_attn);
1176            let a_rmse = rmse(&attn, &ref_attn);
1177
1178            assert!(k_cos.is_finite() && v_cos.is_finite() && a_cos.is_finite());
1179            println!(
1180                "{name},{k_cos:.6},{v_cos:.6},{k_mae:.6},{v_mae:.6},{a_cos:.6},{a_rmse:.6},{}",
1181                layer.bytes_per_row
1182            );
1183        }
1184    }
1185}