1use anyhow::{Result, anyhow, bail};
46use rlx_gguf::{GgmlType, quantize};
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum KvQuant {
55 F16,
58 Q8_0,
59 Q4_0,
60 Q5_0,
61}
62
63impl KvQuant {
64 pub const fn block_elements(self) -> usize {
66 match self {
67 Self::F16 => 1,
68 Self::Q8_0 | Self::Q4_0 | Self::Q5_0 => 32,
69 }
70 }
71
72 pub const fn block_bytes(self) -> usize {
74 match self {
75 Self::F16 => 2,
76 Self::Q8_0 => 2 + 32,
77 Self::Q4_0 => 2 + 32 / 2,
78 Self::Q5_0 => 2 + 4 + 32 / 2,
79 }
80 }
81
82 fn ggml_type(self) -> Option<GgmlType> {
83 match self {
84 Self::F16 => None, Self::Q8_0 => Some(GgmlType::Q8_0),
86 Self::Q4_0 => Some(GgmlType::Q4_0),
87 Self::Q5_0 => Some(GgmlType::Q5_0),
88 }
89 }
90
91 pub fn bytes_for(self, n_elements: usize) -> Result<usize> {
93 let blk = self.block_elements();
94 if !n_elements.is_multiple_of(blk) {
95 bail!("{self:?}: element count {n_elements} not aligned to block size {blk}");
96 }
97 Ok((n_elements / blk) * self.block_bytes())
98 }
99}
100
101#[derive(Debug, Clone)]
107pub struct QuantizedKvLayer {
108 pub k: Vec<u8>,
109 pub v: Vec<u8>,
110 pub past_len: usize,
111 pub kv_dim: usize,
112 pub scheme: KvQuant,
113}
114
115impl QuantizedKvLayer {
116 pub fn new(kv_dim: usize, scheme: KvQuant) -> Result<Self> {
117 let blk = scheme.block_elements();
118 if !kv_dim.is_multiple_of(blk) {
119 bail!("kv_dim ({kv_dim}) must be a multiple of {scheme:?} block size ({blk})");
120 }
121 Ok(Self {
122 k: Vec::new(),
123 v: Vec::new(),
124 past_len: 0,
125 kv_dim,
126 scheme,
127 })
128 }
129
130 pub fn append_rows(&mut self, k_rows: &[f32], v_rows: &[f32]) -> Result<()> {
134 if k_rows.len() != v_rows.len() {
135 bail!(
136 "append_rows: k len {} != v len {}",
137 k_rows.len(),
138 v_rows.len()
139 );
140 }
141 if !k_rows.len().is_multiple_of(self.kv_dim) {
142 bail!(
143 "append_rows: byte count {} not aligned to kv_dim {}",
144 k_rows.len(),
145 self.kv_dim
146 );
147 }
148 let n_rows = k_rows.len() / self.kv_dim;
149 let k_bytes = quant_rows(k_rows, self.scheme)?;
150 let v_bytes = quant_rows(v_rows, self.scheme)?;
151 self.k.extend_from_slice(&k_bytes);
152 self.v.extend_from_slice(&v_bytes);
153 self.past_len += n_rows;
154 Ok(())
155 }
156
157 pub fn read_all(&self) -> Result<(Vec<f32>, Vec<f32>)> {
159 let k = dequant_rows(&self.k, self.scheme, self.past_len * self.kv_dim)?;
160 let v = dequant_rows(&self.v, self.scheme, self.past_len * self.kv_dim)?;
161 Ok((k, v))
162 }
163
164 pub fn read_rows(&self, start: usize, count: usize) -> Result<(Vec<f32>, Vec<f32>)> {
168 if start + count > self.past_len {
169 bail!(
170 "read_rows: [{start}, {}) out of range (past_len {})",
171 start + count,
172 self.past_len
173 );
174 }
175 let blk = self.scheme.block_elements();
176 let bytes_per_row = (self.kv_dim / blk) * self.scheme.block_bytes();
177 let start_byte = start * bytes_per_row;
178 let n = count * self.kv_dim;
179 let k = dequant_rows(&self.k[start_byte..], self.scheme, n)?;
180 let v = dequant_rows(&self.v[start_byte..], self.scheme, n)?;
181 Ok((k, v))
182 }
183
184 pub fn read_window(&self, window: usize) -> Result<(Vec<f32>, Vec<f32>)> {
186 if window >= self.past_len {
187 return self.read_all();
188 }
189 self.read_rows(self.past_len - window, window)
190 }
191
192 pub fn drop_front(&mut self, n_rows: usize) -> Result<()> {
194 let n_rows = n_rows.min(self.past_len);
195 if n_rows == 0 {
196 return Ok(());
197 }
198 let blk = self.scheme.block_elements();
199 let blocks_per_row = self.kv_dim / blk;
200 let drop_bytes = n_rows * blocks_per_row * self.scheme.block_bytes();
201 self.k.drain(..drop_bytes);
202 self.v.drain(..drop_bytes);
203 self.past_len -= n_rows;
204 Ok(())
205 }
206
207 pub fn bytes(&self) -> usize {
209 self.k.len() + self.v.len()
210 }
211}
212
213pub fn attend_quantized(
226 q: &[f32],
227 layer: &QuantizedKvLayer,
228 n_heads: usize,
229 kv_heads: usize,
230 head_dim: usize,
231 scale: f32,
232) -> Result<Vec<f32>> {
233 let kv_dim = kv_heads * head_dim;
234 if layer.kv_dim != kv_dim {
235 bail!(
236 "attend_quantized: layer kv_dim {} != {kv_heads}×{head_dim}",
237 layer.kv_dim
238 );
239 }
240 if q.len() != n_heads * head_dim {
241 bail!(
242 "attend_quantized: q len {} != {n_heads}×{head_dim}",
243 q.len()
244 );
245 }
246 let mut out = vec![0f32; n_heads * head_dim];
247 let past = layer.past_len;
248 if past == 0 {
249 return Ok(out);
250 }
251 let group = (n_heads / kv_heads.max(1)).max(1);
252
253 let mut scores = vec![0f32; n_heads * past];
255 for p in 0..past {
256 let (k_row, _) = layer.read_rows(p, 1)?;
257 for h in 0..n_heads {
258 let kvh = h / group;
259 let q_h = &q[h * head_dim..(h + 1) * head_dim];
260 let k_h = &k_row[kvh * head_dim..(kvh + 1) * head_dim];
261 let dot: f32 = q_h.iter().zip(k_h).map(|(a, b)| a * b).sum();
262 scores[h * past + p] = dot * scale;
263 }
264 }
265
266 for h in 0..n_heads {
268 let row = &mut scores[h * past..(h + 1) * past];
269 let max = row.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
270 let mut sum = 0f32;
271 for v in row.iter_mut() {
272 *v = (*v - max).exp();
273 sum += *v;
274 }
275 let inv = 1.0 / sum.max(1e-20);
276 for v in row.iter_mut() {
277 *v *= inv;
278 }
279 }
280
281 for p in 0..past {
283 let (_, v_row) = layer.read_rows(p, 1)?;
284 for h in 0..n_heads {
285 let kvh = h / group;
286 let w = scores[h * past + p];
287 let v_h = &v_row[kvh * head_dim..(kvh + 1) * head_dim];
288 let o_h = &mut out[h * head_dim..(h + 1) * head_dim];
289 for (o, &vv) in o_h.iter_mut().zip(v_h) {
290 *o += w * vv;
291 }
292 }
293 }
294 Ok(out)
295}
296
297#[derive(Debug, Clone)]
299pub struct QuantizedKvCache {
300 pub layers: Vec<QuantizedKvLayer>,
301}
302
303impl QuantizedKvCache {
304 pub fn new(n_layers: usize, kv_dim: usize, scheme: KvQuant) -> Result<Self> {
305 let layers = (0..n_layers)
306 .map(|_| QuantizedKvLayer::new(kv_dim, scheme))
307 .collect::<Result<Vec<_>>>()?;
308 Ok(Self { layers })
309 }
310
311 pub fn n_layers(&self) -> usize {
312 self.layers.len()
313 }
314
315 pub fn past_len(&self) -> usize {
316 self.layers.first().map(|l| l.past_len).unwrap_or(0)
317 }
318
319 pub fn bytes(&self) -> usize {
321 self.layers.iter().map(|l| l.bytes()).sum()
322 }
323}
324
325fn quant_rows(values: &[f32], scheme: KvQuant) -> Result<Vec<u8>> {
328 match scheme {
329 KvQuant::F16 => {
330 let mut out = Vec::with_capacity(values.len() * 2);
331 for &v in values {
332 let h = half::f16::from_f32(v);
333 out.extend_from_slice(&h.to_le_bytes());
334 }
335 Ok(out)
336 }
337 scheme => {
338 let ty = scheme
339 .ggml_type()
340 .ok_or_else(|| anyhow!("internal: missing ggml type for {scheme:?}"))?;
341 Ok(quantize(values, ty)?)
342 }
343 }
344}
345
346fn dequant_rows(bytes: &[u8], scheme: KvQuant, n: usize) -> Result<Vec<f32>> {
347 match scheme {
348 KvQuant::F16 => {
349 if bytes.len() < n * 2 {
350 bail!("F16 dequant: {} bytes < {} expected", bytes.len(), n * 2);
351 }
352 let mut out = Vec::with_capacity(n);
353 for chunk in bytes[..n * 2].chunks_exact(2) {
354 let h = half::f16::from_le_bytes([chunk[0], chunk[1]]);
355 out.push(h.to_f32());
356 }
357 Ok(out)
358 }
359 KvQuant::Q8_0 => {
360 let expected = scheme.bytes_for(n)?;
361 Ok(rlx_gguf::dequant_q8_0(&bytes[..expected], n)?)
362 }
363 KvQuant::Q4_0 => {
364 let expected = scheme.bytes_for(n)?;
365 Ok(rlx_gguf::dequant_q4_0(&bytes[..expected], n)?)
366 }
367 KvQuant::Q5_0 => {
368 decode_q5_0(bytes, n)
377 }
378 }
379}
380
381fn decode_q5_0(bytes: &[u8], n: usize) -> Result<Vec<f32>> {
382 const QK5_0: usize = 32;
383 let blk_bytes = 2 + 4 + QK5_0 / 2;
384 if !n.is_multiple_of(QK5_0) {
385 bail!("Q5_0: n={n} not divisible by {QK5_0}");
386 }
387 let nb = n / QK5_0;
388 if bytes.len() < nb * blk_bytes {
389 bail!(
390 "Q5_0: expected {} bytes, got {}",
391 nb * blk_bytes,
392 bytes.len()
393 );
394 }
395 let mut out = Vec::with_capacity(n);
396 for i in 0..nb {
397 let off = i * blk_bytes;
398 let d = half::f16::from_le_bytes([bytes[off], bytes[off + 1]]).to_f32();
399 let qh = u32::from_le_bytes([
400 bytes[off + 2],
401 bytes[off + 3],
402 bytes[off + 4],
403 bytes[off + 5],
404 ]);
405 let qs = &bytes[off + 6..off + 6 + QK5_0 / 2];
406 for j in 0..QK5_0 / 2 {
407 let xh0 = (((qh >> j) & 1) as u8) << 4;
408 let v0 = ((qs[j] & 0x0F) | xh0) as i32 - 16;
409 out.push(d * v0 as f32);
410 }
411 for j in 0..QK5_0 / 2 {
412 let xh1 = (((qh >> (j + 16)) & 1) as u8) << 4;
413 let v1 = ((qs[j] >> 4) | xh1) as i32 - 16;
414 out.push(d * v1 as f32);
415 }
416 }
417 Ok(out)
418}
419
420#[cfg(feature = "mmap-kv")]
441pub mod mmap {
442 use super::*;
443 use memmap2::{MmapMut, MmapOptions};
444 use std::fs::OpenOptions;
445 use std::path::{Path, PathBuf};
446
447 pub struct MmapKvLayer {
450 pub mmap: MmapMut,
451 pub past_len: usize,
452 pub capacity_rows: usize,
453 pub kv_dim: usize,
454 pub scheme: KvQuant,
455 pub bytes_per_row: usize,
456 pub k_offset: usize,
457 pub v_offset: usize,
458 pub path: Option<PathBuf>,
459 }
460
461 impl MmapKvLayer {
462 pub fn open<P: AsRef<Path>>(
465 path: P,
466 kv_dim: usize,
467 scheme: KvQuant,
468 capacity_rows: usize,
469 ) -> Result<Self> {
470 let blk = scheme.block_elements();
471 if !kv_dim.is_multiple_of(blk) {
472 bail!("kv_dim ({kv_dim}) must be a multiple of {scheme:?} block size ({blk})");
473 }
474 let bytes_per_row = (kv_dim / blk) * scheme.block_bytes();
475 let total = 2 * capacity_rows * bytes_per_row;
476 let file = OpenOptions::new()
477 .read(true)
478 .write(true)
479 .create(true)
480 .truncate(true)
481 .open(&path)?;
482 file.set_len(total as u64)?;
483 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
484 Ok(Self {
485 mmap,
486 past_len: 0,
487 capacity_rows,
488 kv_dim,
489 scheme,
490 bytes_per_row,
491 k_offset: 0,
492 v_offset: capacity_rows * bytes_per_row,
493 path: Some(path.as_ref().to_path_buf()),
494 })
495 }
496
497 pub fn anonymous(kv_dim: usize, scheme: KvQuant, capacity_rows: usize) -> Result<Self> {
501 let blk = scheme.block_elements();
502 if !kv_dim.is_multiple_of(blk) {
503 bail!("kv_dim ({kv_dim}) must be a multiple of {scheme:?} block size ({blk})");
504 }
505 let bytes_per_row = (kv_dim / blk) * scheme.block_bytes();
506 let total = 2 * capacity_rows * bytes_per_row;
507 let mmap = MmapOptions::new().len(total).map_anon()?;
508 Ok(Self {
509 mmap,
510 past_len: 0,
511 capacity_rows,
512 kv_dim,
513 scheme,
514 bytes_per_row,
515 k_offset: 0,
516 v_offset: capacity_rows * bytes_per_row,
517 path: None,
518 })
519 }
520
521 pub fn append_rows(&mut self, k_rows: &[f32], v_rows: &[f32]) -> Result<()> {
524 if k_rows.len() != v_rows.len() {
525 bail!("append_rows: k/v length mismatch");
526 }
527 if !k_rows.len().is_multiple_of(self.kv_dim) {
528 bail!("append_rows: byte count not aligned to kv_dim");
529 }
530 let n_rows = k_rows.len() / self.kv_dim;
531 if self.past_len + n_rows > self.capacity_rows {
532 bail!(
533 "append_rows: would exceed capacity ({} + {} > {})",
534 self.past_len,
535 n_rows,
536 self.capacity_rows
537 );
538 }
539 let kb = quant_rows(k_rows, self.scheme)?;
540 let vb = quant_rows(v_rows, self.scheme)?;
541 let k_start = self.k_offset + self.past_len * self.bytes_per_row;
542 let v_start = self.v_offset + self.past_len * self.bytes_per_row;
543 self.mmap[k_start..k_start + kb.len()].copy_from_slice(&kb);
544 self.mmap[v_start..v_start + vb.len()].copy_from_slice(&vb);
545 self.past_len += n_rows;
546 Ok(())
547 }
548
549 pub fn read_all(&self) -> Result<(Vec<f32>, Vec<f32>)> {
552 let n = self.past_len * self.kv_dim;
553 let k_end = self.k_offset + self.past_len * self.bytes_per_row;
554 let v_end = self.v_offset + self.past_len * self.bytes_per_row;
555 let k = dequant_rows(&self.mmap[self.k_offset..k_end], self.scheme, n)?;
556 let v = dequant_rows(&self.mmap[self.v_offset..v_end], self.scheme, n)?;
557 Ok((k, v))
558 }
559
560 pub fn read_window(&self, window: usize) -> Result<(Vec<f32>, Vec<f32>)> {
562 let window = window.min(self.past_len);
563 let start_row = self.past_len - window;
564 let n = window * self.kv_dim;
565 let k_start = self.k_offset + start_row * self.bytes_per_row;
566 let v_start = self.v_offset + start_row * self.bytes_per_row;
567 let k_end = k_start + window * self.bytes_per_row;
568 let v_end = v_start + window * self.bytes_per_row;
569 let k = dequant_rows(&self.mmap[k_start..k_end], self.scheme, n)?;
570 let v = dequant_rows(&self.mmap[v_start..v_end], self.scheme, n)?;
571 Ok((k, v))
572 }
573
574 pub fn prefetch_window(&self, window: usize) {
579 let window = window.min(self.past_len);
580 if window == 0 {
581 return;
582 }
583 let start_row = self.past_len - window;
584 let k_start = self.k_offset + start_row * self.bytes_per_row;
585 let v_start = self.v_offset + start_row * self.bytes_per_row;
586 let _ = self.mmap.advise_range(
587 memmap2::Advice::WillNeed,
588 k_start,
589 window * self.bytes_per_row,
590 );
591 let _ = self.mmap.advise_range(
592 memmap2::Advice::WillNeed,
593 v_start,
594 window * self.bytes_per_row,
595 );
596 }
597
598 pub fn flush(&self) -> Result<()> {
601 self.mmap.flush()?;
602 Ok(())
603 }
604
605 pub fn bytes(&self) -> usize {
606 2 * self.past_len * self.bytes_per_row
607 }
608 }
609
610 pub struct MmapKvCache {
612 pub layers: Vec<MmapKvLayer>,
613 }
614
615 impl MmapKvCache {
616 pub fn open_dir<P: AsRef<Path>>(
618 dir: P,
619 n_layers: usize,
620 kv_dim: usize,
621 scheme: KvQuant,
622 capacity_rows: usize,
623 ) -> Result<Self> {
624 let dir = dir.as_ref();
625 std::fs::create_dir_all(dir)?;
626 let layers = (0..n_layers)
627 .map(|i| {
628 MmapKvLayer::open(
629 dir.join(format!("kv_{i}.bin")),
630 kv_dim,
631 scheme,
632 capacity_rows,
633 )
634 })
635 .collect::<Result<Vec<_>>>()?;
636 Ok(Self { layers })
637 }
638
639 pub fn anonymous(
640 n_layers: usize,
641 kv_dim: usize,
642 scheme: KvQuant,
643 capacity_rows: usize,
644 ) -> Result<Self> {
645 let layers = (0..n_layers)
646 .map(|_| MmapKvLayer::anonymous(kv_dim, scheme, capacity_rows))
647 .collect::<Result<Vec<_>>>()?;
648 Ok(Self { layers })
649 }
650
651 pub fn n_layers(&self) -> usize {
652 self.layers.len()
653 }
654
655 pub fn past_len(&self) -> usize {
656 self.layers.first().map(|l| l.past_len).unwrap_or(0)
657 }
658
659 pub fn bytes(&self) -> usize {
661 self.layers.iter().map(|l| l.bytes()).sum()
662 }
663 }
664
665 #[cfg(test)]
666 mod tests {
667 use super::*;
668
669 #[test]
670 fn anonymous_q8_0_roundtrip() {
671 let kv_dim = 64;
672 let mut layer = MmapKvLayer::anonymous(kv_dim, KvQuant::Q8_0, 4).unwrap();
673 let data: Vec<f32> = (0..kv_dim).map(|i| (i as f32).sin()).collect();
674 layer.append_rows(&data, &data).unwrap();
675 let (k, v) = layer.read_all().unwrap();
676 assert_eq!(k.len(), kv_dim);
677 assert_eq!(v.len(), kv_dim);
678 for (a, b) in k.iter().zip(data.iter()) {
680 assert!((a - b).abs() < 0.02);
681 }
682 }
683
684 #[test]
685 fn file_backed_persists_and_reopens() {
686 let dir = tempfile::tempdir().unwrap();
687 let kv_dim = 32;
688 let path = dir.path().join("layer.bin");
689 {
690 let mut layer = MmapKvLayer::open(&path, kv_dim, KvQuant::F16, 8).unwrap();
691 let data: Vec<f32> = (0..kv_dim).map(|i| i as f32 * 0.5).collect();
692 layer.append_rows(&data, &data).unwrap();
693 layer.flush().unwrap();
694 }
695 let bytes = std::fs::read(&path).unwrap();
697 assert!(!bytes.is_empty());
698 assert!(bytes.iter().any(|&b| b != 0));
699 }
700
701 #[test]
702 fn append_past_capacity_errors() {
703 let mut l = MmapKvLayer::anonymous(32, KvQuant::Q8_0, 2).unwrap();
704 let row = vec![0.5f32; 32];
705 l.append_rows(&row, &row).unwrap();
706 l.append_rows(&row, &row).unwrap();
707 assert!(l.append_rows(&row, &row).is_err());
708 }
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715
716 fn cosine(a: &[f32], b: &[f32]) -> f32 {
717 let mut dot = 0.0f32;
718 let mut na = 0.0f32;
719 let mut nb = 0.0f32;
720 for (x, y) in a.iter().zip(b.iter()) {
721 dot += x * y;
722 na += x * x;
723 nb += y * y;
724 }
725 dot / (na.sqrt() * nb.sqrt() + 1e-12)
726 }
727
728 #[test]
729 fn block_size_invariants() {
730 assert_eq!(KvQuant::F16.block_bytes(), 2);
731 assert_eq!(KvQuant::Q8_0.block_bytes(), 34);
732 assert_eq!(KvQuant::Q4_0.block_bytes(), 18);
733 assert_eq!(KvQuant::Q5_0.block_bytes(), 22);
734 }
735
736 #[test]
737 fn f16_roundtrip_exact() {
738 let kv_dim = 64;
739 let mut layer = QuantizedKvLayer::new(kv_dim, KvQuant::F16).unwrap();
740 let k_row: Vec<f32> = (0..kv_dim).map(|i| (i as f32) * 0.1).collect();
741 let v_row: Vec<f32> = (0..kv_dim).map(|i| (i as f32) * 0.2).collect();
742 layer.append_rows(&k_row, &v_row).unwrap();
743 let (k, v) = layer.read_all().unwrap();
744 for i in 0..kv_dim {
745 assert!((k[i] - k_row[i]).abs() < 0.01);
747 assert!((v[i] - v_row[i]).abs() < 0.01);
748 }
749 }
750
751 #[test]
752 fn q8_0_roundtrip_high_fidelity() {
753 let kv_dim = 64;
754 let n_rows = 4;
755 let mut layer = QuantizedKvLayer::new(kv_dim, KvQuant::Q8_0).unwrap();
756 let total = n_rows * kv_dim;
757 let k_data: Vec<f32> = (0..total).map(|i| (i as f32).sin()).collect();
758 let v_data: Vec<f32> = (0..total).map(|i| (i as f32).cos()).collect();
759 layer.append_rows(&k_data, &v_data).unwrap();
760 assert_eq!(layer.past_len, n_rows);
761 let (k, v) = layer.read_all().unwrap();
762 assert!(cosine(&k, &k_data) > 0.999, "Q8_0 K cosine too low");
763 assert!(cosine(&v, &v_data) > 0.999, "Q8_0 V cosine too low");
764 }
765
766 #[test]
767 fn q4_0_roundtrip_lossy_but_close() {
768 let kv_dim = 64;
769 let mut layer = QuantizedKvLayer::new(kv_dim, KvQuant::Q4_0).unwrap();
770 let k: Vec<f32> = (0..kv_dim).map(|i| (i as f32 * 0.05).tanh()).collect();
771 let v: Vec<f32> = (0..kv_dim).map(|i| (i as f32 * 0.07).tanh()).collect();
772 layer.append_rows(&k, &v).unwrap();
773 let (kr, vr) = layer.read_all().unwrap();
774 assert!(cosine(&kr, &k) > 0.99);
775 assert!(cosine(&vr, &v) > 0.99);
776 }
777
778 #[test]
779 fn q5_0_roundtrip_better_than_q4() {
780 let kv_dim = 64;
781 let mut q4 = QuantizedKvLayer::new(kv_dim, KvQuant::Q4_0).unwrap();
782 let mut q5 = QuantizedKvLayer::new(kv_dim, KvQuant::Q5_0).unwrap();
783 let k: Vec<f32> = (0..kv_dim).map(|i| (i as f32 * 0.1).sin() * 3.0).collect();
784 let v: Vec<f32> = (0..kv_dim).map(|i| (i as f32 * 0.13).cos() * 3.0).collect();
785 q4.append_rows(&k, &v).unwrap();
786 q5.append_rows(&k, &v).unwrap();
787 let (k4, _) = q4.read_all().unwrap();
788 let (k5, _) = q5.read_all().unwrap();
789 let cos4 = cosine(&k4, &k);
790 let cos5 = cosine(&k5, &k);
791 assert!(cos5 >= cos4 - 1e-3, "Q5_0 should not be worse than Q4_0");
792 }
793
794 #[test]
795 fn sliding_window_drops_oldest() {
796 let kv_dim = 32;
797 let mut layer = QuantizedKvLayer::new(kv_dim, KvQuant::Q8_0).unwrap();
798 for r in 0..5 {
799 let v: Vec<f32> = (0..kv_dim).map(|i| (i + r * 100) as f32).collect();
800 layer.append_rows(&v, &v).unwrap();
801 }
802 assert_eq!(layer.past_len, 5);
803 layer.drop_front(2).unwrap();
804 assert_eq!(layer.past_len, 3);
805 let (k, _v) = layer.read_window(3).unwrap();
806 assert!((k[0] - 200.0).abs() < 1.0);
808 }
809
810 #[test]
811 fn kv_dim_must_align_to_block_size() {
812 assert!(QuantizedKvLayer::new(24, KvQuant::Q8_0).is_err());
814 assert!(QuantizedKvLayer::new(24, KvQuant::Q4_0).is_err());
815 assert!(QuantizedKvLayer::new(24, KvQuant::F16).is_ok());
817 }
818
819 #[test]
820 fn streaming_quantized_attention_matches_full_dequant() {
821 let (n_heads, kv_heads, head_dim) = (4usize, 2usize, 32usize);
822 let kv_dim = kv_heads * head_dim; let past = 5usize;
824 let scale = 1.0 / (head_dim as f32).sqrt();
825
826 let mut layer = QuantizedKvLayer::new(kv_dim, KvQuant::Q8_0).unwrap();
827 for p in 0..past {
828 let k: Vec<f32> = (0..kv_dim)
829 .map(|i| ((i + p * 7) as f32 * 0.05).sin())
830 .collect();
831 let v: Vec<f32> = (0..kv_dim)
832 .map(|i| ((i + p * 3) as f32 * 0.04).cos())
833 .collect();
834 layer.append_rows(&k, &v).unwrap();
835 }
836 let q: Vec<f32> = (0..n_heads * head_dim)
837 .map(|i| (i as f32 * 0.02).sin())
838 .collect();
839
840 let out = attend_quantized(&q, &layer, n_heads, kv_heads, head_dim, scale).unwrap();
841
842 let (k_all, v_all) = layer.read_all().unwrap();
844 let group = n_heads / kv_heads;
845 let mut reference = vec![0f32; n_heads * head_dim];
846 for h in 0..n_heads {
847 let kvh = h / group;
848 let mut sc = vec![0f32; past];
849 for p in 0..past {
850 let qh = &q[h * head_dim..(h + 1) * head_dim];
851 let base = p * kv_dim + kvh * head_dim;
852 let kh = &k_all[base..base + head_dim];
853 let dot: f32 = qh.iter().zip(kh).map(|(a, b)| a * b).sum();
854 sc[p] = dot * scale;
855 }
856 let max = sc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
857 let mut sum = 0.0;
858 for x in sc.iter_mut() {
859 *x = (*x - max).exp();
860 sum += *x;
861 }
862 for x in sc.iter_mut() {
863 *x /= sum;
864 }
865 for p in 0..past {
866 let base = p * kv_dim + kvh * head_dim;
867 let vh = &v_all[base..base + head_dim];
868 for d in 0..head_dim {
869 reference[h * head_dim + d] += sc[p] * vh[d];
870 }
871 }
872 }
873 for (a, b) in out.iter().zip(&reference) {
874 assert!((a - b).abs() < 1e-4, "streaming {a} vs full-dequant {b}");
875 }
876 }
877
878 #[test]
879 fn cache_memory_decreases_with_quantization() {
880 let kv_dim = 128;
881 let n_layers = 4;
882 let n_rows = 16;
883 let data: Vec<f32> = (0..kv_dim).map(|i| (i as f32) * 0.01).collect();
884 let mut f16 = QuantizedKvCache::new(n_layers, kv_dim, KvQuant::F16).unwrap();
885 let mut q8 = QuantizedKvCache::new(n_layers, kv_dim, KvQuant::Q8_0).unwrap();
886 let mut q4 = QuantizedKvCache::new(n_layers, kv_dim, KvQuant::Q4_0).unwrap();
887 for _ in 0..n_rows {
888 for l in 0..n_layers {
889 f16.layers[l].append_rows(&data, &data).unwrap();
890 q8.layers[l].append_rows(&data, &data).unwrap();
891 q4.layers[l].append_rows(&data, &data).unwrap();
892 }
893 }
894 assert!(q8.bytes() < f16.bytes());
895 assert!(q4.bytes() < q8.bytes());
896 }
897}