1extern crate alloc;
31use alloc::vec;
32use alloc::vec::Vec;
33
34use crate::math::round;
35use crate::v3::attention::KvStore;
36use crate::v3::config::V3Config;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum KvPrecision {
46 F32,
49 Int8,
55}
56
57pub fn fake_quant_vec(x: &mut [f32], bits: u32) {
69 let (scale, inv, qmax) = quant_scale(x, bits);
70 for v in x.iter_mut() {
71 *v = round(*v * inv).clamp(-qmax - 1.0, qmax) * scale;
72 }
73}
74
75fn quant_scale(x: &[f32], bits: u32) -> (f32, f32, f32) {
81 let qmax = ((1u32 << (bits - 1)) - 1) as f32;
82 let mut absmax = 0.0f32;
83 for v in x.iter() {
84 let a = if *v < 0.0 { -*v } else { *v };
85 if a > absmax {
86 absmax = a;
87 }
88 }
89 let scale = if absmax > 0.0 { absmax / qmax } else { 1.0 };
90 (scale, 1.0 / scale, qmax)
91}
92
93fn quant_into(src: &[f32], dst: &mut [i8]) -> f32 {
99 let (scale, inv, qmax) = quant_scale(src, 8);
100 for (d, v) in dst.iter_mut().zip(src) {
101 *d = round(*v * inv).clamp(-qmax - 1.0, qmax) as i8;
102 }
103 scale
104}
105
106pub fn quantize_rows(x: &[f32], head_dim: usize) -> (Vec<i8>, Vec<f32>) {
114 let rows = x.len() / head_dim;
115 let mut ints = vec![0i8; x.len()];
116 let mut scales = vec![1.0f32; rows];
117 for (r, scale) in scales.iter_mut().enumerate() {
118 let o = r * head_dim;
119 *scale = quant_into(&x[o..o + head_dim], &mut ints[o..o + head_dim]);
120 }
121 (ints, scales)
122}
123
124struct LayerCache {
126 k: Vec<f32>,
128 v: Vec<f32>,
129 kq: Vec<i8>,
132 vq: Vec<i8>,
133 k_scale: Vec<f32>,
134 v_scale: Vec<f32>,
135 slots: usize,
137 cap: usize,
140 q_tail: Vec<f32>,
143 k_tail: Vec<f32>,
144 v_tail: Vec<f32>,
145}
146
147pub struct V3Cache {
149 cfg: V3Config,
150 precision: KvPrecision,
151 layers: Vec<LayerCache>,
152 tokens: Vec<u32>,
154 engram_tail: Vec<Vec<f32>>,
157 pos: usize,
159}
160
161impl V3Cache {
162 pub fn new(cfg: &V3Config, hint: usize) -> Self {
167 Self::with_precision(cfg, hint, KvPrecision::F32)
168 }
169
170 pub fn with_precision(cfg: &V3Config, hint: usize, precision: KvPrecision) -> Self {
178 let hint = hint.clamp(1, cfg.max_seq_len);
179 let taps_tail = cfg.qkv_conv_taps.saturating_sub(1);
180 let layers = (0..cfg.num_layers)
181 .map(|li| {
182 let cap = match cfg.attention_span(li) {
187 Some(w) => w.min(cfg.max_seq_len),
188 None => cfg.max_seq_len,
189 };
190 let slots = hint.min(cap);
191 let int8 = precision == KvPrecision::Int8;
192 let heads = slots * cfg.num_kv_heads;
193 LayerCache {
194 k: if int8 {
195 Vec::new()
196 } else {
197 vec![0.0; slots * cfg.k_dim()]
198 },
199 v: if int8 {
200 Vec::new()
201 } else {
202 vec![0.0; slots * cfg.v_dim()]
203 },
204 kq: if int8 {
205 vec![0; slots * cfg.k_dim()]
206 } else {
207 Vec::new()
208 },
209 vq: if int8 {
210 vec![0; slots * cfg.v_dim()]
211 } else {
212 Vec::new()
213 },
214 k_scale: if int8 { vec![1.0; heads] } else { Vec::new() },
215 v_scale: if int8 { vec![1.0; heads] } else { Vec::new() },
216 slots,
217 cap,
218 q_tail: vec![0.0; taps_tail * cfg.q_dim()],
219 k_tail: vec![0.0; taps_tail * cfg.k_dim()],
220 v_tail: vec![0.0; taps_tail * cfg.v_dim()],
221 }
222 })
223 .collect();
224
225 let reach = cfg.engram.conv_taps.saturating_sub(1) * cfg.engram.conv_dilation;
226 let engram_tail = (0..cfg.engram.sites.len())
227 .map(|_| vec![0.0; reach * cfg.d_model])
228 .collect();
229
230 Self {
231 cfg: cfg.clone(),
232 precision,
233 layers,
234 tokens: Vec::with_capacity(hint),
235 engram_tail,
236 pos: 0,
237 }
238 }
239
240 pub fn precision(&self) -> KvPrecision {
241 self.precision
242 }
243
244 pub fn len(&self) -> usize {
246 self.pos
247 }
248
249 pub fn is_empty(&self) -> bool {
250 self.pos == 0
251 }
252
253 pub fn bytes(&self) -> usize {
256 let per_layer: usize = self
257 .layers
258 .iter()
259 .map(|l| {
260 (l.k.len() + l.v.len() + l.k_scale.len() + l.v_scale.len()) * 4
261 + l.kq.len()
262 + l.vq.len()
263 + (l.q_tail.len() + l.k_tail.len() + l.v_tail.len()) * 4
264 })
265 .sum();
266 let engram: usize = self.engram_tail.iter().map(|t| t.len() * 4).sum();
267 per_layer + engram + self.tokens.len() * 4
268 }
269
270 pub fn token_history(&self) -> &[u32] {
272 &self.tokens
273 }
274
275 pub fn push_token(&mut self, tok: u32) {
277 let keep = self.cfg.engram.orders.iter().copied().max().unwrap_or(1);
278 self.tokens.push(tok);
279 if self.tokens.len() > keep {
280 let drop = self.tokens.len() - keep;
281 self.tokens.drain(..drop);
282 }
283 }
284
285 fn ensure(&mut self, li: usize, pos: usize) {
292 let (k_dim, v_dim) = (self.cfg.k_dim(), self.cfg.v_dim());
293 let heads = self.cfg.num_kv_heads;
294 let layer = &mut self.layers[li];
295 if pos < layer.slots || layer.slots >= layer.cap {
296 return;
297 }
298 let want = (layer.slots * 2).max(pos + 1).min(layer.cap);
299 if layer.kq.is_empty() && layer.vq.is_empty() {
300 layer.k.resize(want * k_dim, 0.0);
301 layer.v.resize(want * v_dim, 0.0);
302 } else {
303 layer.kq.resize(want * k_dim, 0);
304 layer.vq.resize(want * v_dim, 0);
305 layer.k_scale.resize(want * heads, 1.0);
306 layer.v_scale.resize(want * heads, 1.0);
307 }
308 layer.slots = want;
309 }
310
311 pub fn span(&self, li: usize, pos: usize) -> (usize, usize) {
323 let slots = self.layers[li].slots;
324 let lo = match self.cfg.attention_span(li) {
325 Some(w) if pos + 1 > w => pos + 1 - w,
326 _ => 0,
327 };
328 let lo = lo.max((pos + 1).saturating_sub(slots));
330 (lo, pos)
331 }
332
333 pub fn is_saturated(&self) -> bool {
336 (0..self.cfg.num_layers)
337 .any(|li| self.cfg.attention_span(li).is_none() && self.pos > self.layers[li].slots)
338 }
339
340 pub fn slots(&self, li: usize) -> usize {
342 self.layers[li].slots
343 }
344
345 pub fn write_kv(&mut self, li: usize, pos: usize, k: &[f32], v: &[f32]) {
347 self.ensure(li, pos);
348 let (k_dim, v_dim) = (self.cfg.k_dim(), self.cfg.v_dim());
349 let precision = self.precision;
350 let num_kv_heads = self.cfg.num_kv_heads;
351 let (qk, vh) = (self.cfg.qk_head_dim, self.cfg.v_head_dim);
352 let layer = &mut self.layers[li];
353 let slot = pos % layer.slots;
354 match precision {
355 KvPrecision::F32 => {
356 layer.k[slot * k_dim..(slot + 1) * k_dim].copy_from_slice(k);
357 layer.v[slot * v_dim..(slot + 1) * v_dim].copy_from_slice(v);
358 }
359 KvPrecision::Int8 => {
360 let base = slot * num_kv_heads;
365 for h in 0..num_kv_heads {
366 let sk = quant_into(
367 &k[h * qk..(h + 1) * qk],
368 &mut layer.kq[slot * k_dim + h * qk..slot * k_dim + (h + 1) * qk],
369 );
370 layer.k_scale[base + h] = sk;
371 let sv = quant_into(
372 &v[h * vh..(h + 1) * vh],
373 &mut layer.vq[slot * v_dim + h * vh..slot * v_dim + (h + 1) * vh],
374 );
375 layer.v_scale[base + h] = sv;
376 }
377 }
378 }
379 }
380
381 pub fn kv(&self, li: usize) -> KvStore<'_> {
383 let l = &self.layers[li];
384 match self.precision {
385 KvPrecision::F32 => KvStore::F32 { k: &l.k, v: &l.v },
386 KvPrecision::Int8 => KvStore::Int8 {
387 k: &l.kq,
388 k_scale: &l.k_scale,
389 v: &l.vq,
390 v_scale: &l.v_scale,
391 },
392 }
393 }
394
395 pub fn conv_step(&mut self, li: usize, which: Qkv, taps: &[f32], raw: &mut [f32]) {
402 let n_taps = self.cfg.qkv_conv_taps;
403 if n_taps == 0 {
404 return;
405 }
406 let dim = raw.len();
407 let pos = self.pos;
408 let layer = &mut self.layers[li];
409 let tail = match which {
410 Qkv::Q => &mut layer.q_tail,
411 Qkv::K => &mut layer.k_tail,
412 Qkv::V => &mut layer.v_tail,
413 };
414 debug_assert_eq!(tail.len(), (n_taps - 1) * dim);
415
416 let mut out = vec![0.0f32; dim];
417 for c in 0..dim {
418 let mut acc = taps[c] * raw[c];
419 for j in 1..n_taps {
420 if j > pos {
421 break;
422 }
423 let slot = n_taps - 1 - j;
426 acc += taps[j * dim + c] * tail[slot * dim + c];
427 }
428 out[c] = acc;
429 }
430
431 if n_taps > 1 {
433 tail.copy_within(dim.., 0);
434 let last = (n_taps - 2) * dim;
435 tail[last..last + dim].copy_from_slice(raw);
436 }
437 raw.copy_from_slice(&out);
438 }
439
440 pub fn engram_conv_step(&mut self, site: usize, taps: &[f32], raw: &mut [f32]) {
445 let e = &self.cfg.engram;
446 let (n_taps, dil) = (e.conv_taps, e.conv_dilation);
447 let max_order = e.orders.iter().copied().max().unwrap_or(1);
448 let d = self.cfg.d_model;
449 let pos = self.pos;
450 let reach = (n_taps - 1) * dil;
451 let tail = &mut self.engram_tail[site];
452
453 let mut out = vec![0.0f32; d];
454 for c in 0..d {
455 let mut acc = 0.0f32;
456 for j in 0..n_taps {
457 let back = j * dil;
458 if back > pos || pos < j * max_order {
459 continue;
460 }
461 let val = if j == 0 {
462 raw[c]
463 } else {
464 tail[(reach - back) * d + c]
467 };
468 acc += taps[j * d + c] * val;
469 }
470 out[c] = acc;
471 }
472
473 if reach > 0 {
474 tail.copy_within(d.., 0);
475 let last = (reach - 1) * d;
476 tail[last..last + d].copy_from_slice(raw);
477 }
478 raw.copy_from_slice(&out);
479 }
480
481 pub fn seed_conv_tail(&mut self, li: usize, which: Qkv, raw: &[f32]) {
487 let layer = &mut self.layers[li];
488 let tail = match which {
489 Qkv::Q => &mut layer.q_tail,
490 Qkv::K => &mut layer.k_tail,
491 Qkv::V => &mut layer.v_tail,
492 };
493 debug_assert_eq!(raw.len(), tail.len());
494 tail.copy_from_slice(raw);
495 }
496
497 pub fn seed_engram_tail(&mut self, site: usize, raw: &[f32]) {
499 let tail = &mut self.engram_tail[site];
500 debug_assert_eq!(raw.len(), tail.len());
501 tail.copy_from_slice(raw);
502 }
503
504 pub fn seed_tokens(&mut self, tokens: &[u32]) {
506 let keep = self.cfg.engram.orders.iter().copied().max().unwrap_or(1);
507 self.tokens.clear();
508 let start = tokens.len().saturating_sub(keep);
509 self.tokens.extend_from_slice(&tokens[start..]);
510 }
511
512 pub fn set_pos(&mut self, pos: usize) {
514 self.pos = pos;
515 }
516
517 pub fn advance(&mut self) {
520 self.pos += 1;
521 }
522
523 pub fn pos(&self) -> usize {
525 self.pos
526 }
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq)]
531pub enum Qkv {
532 Q,
533 K,
534 V,
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use crate::v3::config::V3Engram;
541
542 fn cfg() -> V3Config {
543 V3Config {
544 vocab_size: 8192,
545 out_vocab: 8192,
546 d_model: 8,
547 num_heads: 2,
548 num_kv_heads: 1,
549 num_layers: 4,
550 qk_head_dim: 4,
551 v_head_dim: 4,
552 max_seq_len: 64,
553 hada_n: 8,
554 mhc_lanes: 2,
555 rope_theta: 10000.0,
556 sliding_window: 4,
557 global_layers: vec![3],
558 qkv_conv_taps: 3,
559 engram: V3Engram {
560 orders: vec![2, 3],
561 heads: 1,
562 slots: 16,
563 sub_dim: 4,
564 sites: vec![1],
565 conv_taps: 4,
566 conv_dilation: 3,
567 seed_heads: 0,
568 },
569 }
570 }
571
572 #[test]
579 fn an_int8_ring_wraps_and_grows_like_the_f32_one() {
580 let c = cfg();
581 let mut cache = V3Cache::with_precision(&c, 2, KvPrecision::Int8);
582 let heads = c.num_kv_heads;
583
584 let write = |cache: &mut V3Cache, pos: usize| {
587 let m = (pos + 1) as f32;
588 let k: Vec<f32> = (0..c.k_dim()).map(|i| m * (i as f32 + 1.0)).collect();
589 let v: Vec<f32> = (0..c.v_dim()).map(|i| -m * (i as f32 + 1.0)).collect();
590 cache.write_kv(0, pos, &k, &v);
591 };
592
593 for pos in 0..10 {
595 write(&mut cache, pos);
596 }
597 assert_eq!(cache.slots(0), 4, "local layer is capped at its window");
598
599 let KvStore::Int8 {
600 k,
601 k_scale,
602 v_scale,
603 ..
604 } = cache.kv(0)
605 else {
606 panic!("an int8 cache must hand out an int8 store");
607 };
608 assert_eq!(k.len(), 4 * c.k_dim());
609 assert_eq!(k_scale.len(), 4 * heads);
610 assert_eq!(v_scale.len(), 4 * heads);
611
612 let slot = 9 % 4;
616 let want = {
617 let m = 10.0f32;
618 let mut x: Vec<f32> = (0..c.k_dim()).map(|i| m * (i as f32 + 1.0)).collect();
619 for ch in x.chunks_mut(c.qk_head_dim) {
620 fake_quant_vec(ch, 8);
621 }
622 x
623 };
624 for h in 0..heads {
625 let sc = k_scale[slot * heads + h];
626 for i in 0..c.qk_head_dim {
627 let idx = slot * c.k_dim() + h * c.qk_head_dim + i;
628 let got = k[idx] as f32 * sc;
629 let expect = want[h * c.qk_head_dim + i];
630 assert!(
631 (got - expect).abs() <= 1e-4 * expect.abs().max(1.0),
632 "slot {slot} head {h} element {i}: {got} against {expect}"
633 );
634 }
635 }
636 }
637
638 #[test]
639 fn layers_grow_to_their_cap_and_no_further() {
640 let c = cfg();
641 let mut cache = V3Cache::new(&c, 2);
642 assert_eq!(
643 cache.slots(0),
644 2,
645 "local layer starts at the hint, not its window"
646 );
647 assert_eq!(cache.slots(3), 2, "global layer starts at the hint");
648
649 let k = vec![1.0f32; c.k_dim()];
650 let v = vec![1.0f32; c.v_dim()];
651
652 cache.write_kv(0, 3, &k, &v);
654 assert_eq!(cache.slots(0), 4);
655 cache.write_kv(0, 99, &k, &v);
656 assert_eq!(
657 cache.slots(0),
658 4,
659 "a local layer must never exceed its window"
660 );
661
662 cache.write_kv(3, 9, &k, &v);
664 assert!(cache.slots(3) >= 10, "global layer should have grown");
665 }
666
667 fn shipped() -> V3Config {
670 V3Config {
671 vocab_size: 8192,
672 out_vocab: 8192,
673 d_model: 768,
674 num_heads: 12,
675 num_kv_heads: 2,
676 num_layers: 20,
677 qk_head_dim: 48,
678 v_head_dim: 64,
679 max_seq_len: 8192,
680 hada_n: 1024,
681 mhc_lanes: 4,
682 rope_theta: 100_000.0,
683 sliding_window: 1024,
684 global_layers: vec![4, 9, 14, 19],
685 qkv_conv_taps: 3,
686 engram: V3Engram {
687 orders: vec![2, 3],
688 heads: 3,
689 slots: 18432,
690 sub_dim: 128,
691 sites: vec![3, 7, 11, 15, 19],
692 conv_taps: 4,
693 conv_dilation: 3,
694 seed_heads: 0,
695 },
696 }
697 }
698
699 #[test]
700 fn a_global_layer_never_asks_for_more_than_it_stores() {
701 let mut c = shipped();
705 c.max_seq_len = 8;
706 c.sliding_window = 4;
707 c.num_layers = 2;
708 c.global_layers = vec![1];
709 let mut cache = V3Cache::new(&c, 8);
710 let k = vec![0.0f32; c.k_dim()];
711 let v = vec![0.0f32; c.v_dim()];
712
713 for pos in 0..12 {
714 cache.write_kv(1, pos, &k, &v);
715 let (lo, hi) = cache.span(1, pos);
716 let slots = cache.slots(1);
717 assert!(
718 hi - lo < slots,
719 "span {lo}..={hi} exceeds {slots} slots at position {pos}"
720 );
721 }
722 for _ in 0..12 {
724 cache.advance();
725 }
726 assert!(
727 cache.is_saturated(),
728 "running past the context must be visible"
729 );
730 }
731
732 #[test]
733 fn a_short_session_pays_for_what_it_uses() {
734 let c = shipped();
735 let k = vec![0.0f32; c.k_dim()];
736 let v = vec![0.0f32; c.v_dim()];
737
738 let mut short = V3Cache::new(&c, 512);
739 for li in 0..c.num_layers {
740 short.write_kv(li, 511, &k, &v);
741 }
742 let short_mb = short.bytes() as f64 / 1024.0 / 1024.0;
743
744 let reserved = (16 * 1024 + 4 * 8192) * (c.k_dim() + c.v_dim()) * 4;
747 let reserved_mb = reserved as f64 / 1024.0 / 1024.0;
748
749 assert!(
750 short_mb < reserved_mb / 4.0,
751 "a 512-token session should cost far less than reserving the \
752 windows: {short_mb:.1} MB vs {reserved_mb:.1} MB"
753 );
754 assert!(
765 short_mb < 9.5,
766 "expected under 9.5 MB at f32, got {short_mb:.1} MB"
767 );
768 assert!(
769 short_mb > 8.0,
770 "unexpectedly small — has the KV representation changed? {short_mb:.1} MB"
771 );
772 }
773
774 #[test]
775 fn a_long_session_caps_the_local_layers() {
776 let c = shipped();
777 let k = vec![0.0f32; c.k_dim()];
778 let v = vec![0.0f32; c.v_dim()];
779 let mut cache = V3Cache::new(&c, 64);
780 for li in 0..c.num_layers {
781 cache.write_kv(li, 4095, &k, &v);
782 }
783 for li in 0..c.num_layers {
784 if c.is_global(li) {
785 assert!(
786 cache.slots(li) >= 4096,
787 "global layer {li} must hold all of it"
788 );
789 } else {
790 assert_eq!(cache.slots(li), 1024, "local layer {li} caps at its window");
791 }
792 }
793 }
794
795 #[test]
796 fn spans_bound_local_layers_only() {
797 let c = cfg();
798 let mut cache = V3Cache::new(&c, 8);
799 let k = vec![0.0f32; c.k_dim()];
800 let v = vec![0.0f32; c.v_dim()];
801 for pos in 0..=10 {
804 cache.write_kv(0, pos, &k, &v);
805 cache.write_kv(3, pos, &k, &v);
806 }
807 assert_eq!(cache.span(0, 2), (0, 2));
809 assert_eq!(cache.span(0, 10), (7, 10));
810 assert_eq!(cache.span(3, 10), (0, 10));
812 }
813
814 #[test]
815 fn conv_step_matches_the_sequence_conv() {
816 use crate::v3::attention::causal_depthwise_conv;
817 let c = cfg();
818 let dim = c.q_dim();
819 let taps: Vec<f32> = (0..3 * dim).map(|i| (i as f32 * 0.37).sin()).collect();
820
821 let seq = 6;
823 let raw: Vec<f32> = (0..seq * dim).map(|i| (i as f32 * 0.11).cos()).collect();
824 let mut want = raw.clone();
825 causal_depthwise_conv(&mut want, &taps, seq, dim, 3);
826
827 let mut cache = V3Cache::new(&c, seq);
829 for t in 0..seq {
830 let mut cur = raw[t * dim..(t + 1) * dim].to_vec();
831 cache.conv_step(0, Qkv::Q, &taps, &mut cur);
832 for (i, &g) in cur.iter().enumerate() {
833 let e = want[t * dim + i];
834 assert!(
835 (g - e).abs() < 1e-5,
836 "position {t} dim {i}: step {g} vs sequence {e}"
837 );
838 }
839 cache.advance();
840 }
841 }
842
843 #[test]
844 fn engram_conv_step_matches_the_sequence_conv() {
845 use crate::v3::engram::{value_conv, EngramDims};
846 let c = cfg();
847 let d = c.d_model;
848 let dims = EngramDims {
849 num_tables: 2,
850 slots: 16,
851 sub_dim: 4,
852 d_model: d,
853 conv_taps: 4,
854 conv_dilation: 3,
855 max_order: 3,
856 };
857 let taps: Vec<f32> = (0..4 * d).map(|i| (i as f32 * 0.23).cos()).collect();
858 let seq = 12;
859 let raw: Vec<f32> = (0..seq * d).map(|i| (i as f32 * 0.19).sin()).collect();
860 let mut want = raw.clone();
861 value_conv(&mut want, &taps, seq, &dims);
862
863 let mut cache = V3Cache::new(&c, seq);
864 for t in 0..seq {
865 let mut cur = raw[t * d..(t + 1) * d].to_vec();
866 cache.engram_conv_step(0, &taps, &mut cur);
867 for (i, &g) in cur.iter().enumerate() {
868 let e = want[t * d + i];
869 assert!(
870 (g - e).abs() < 1e-5,
871 "position {t} dim {i}: step {g} vs sequence {e}"
872 );
873 }
874 cache.advance();
875 }
876 }
877}