1use std::sync::{Arc, RwLock};
30
31use fuel_dispatch::pipelined::{PipelinedExecutor, StorageCache};
32use fuel_graph::{Graph, Node, NodeId, Op};
33use fuel_ir::{DType, Error, Layout, Result, Shape};
34use fuel_memory::Storage;
35
36use crate::Device;
37use crate::decode_shape::KvAllocId;
38use crate::lazy::Tensor;
39use fuel_dispatch::residency::ResidencyReport;
40use fuel_kv_pool::{
41 Externalized, KvAllocError, KvBlockPool, KvBudget, KvGeometry, PhysBlockId, SessionHandle,
42 non_kv_resident_bytes,
43};
44
45pub fn fit_geometry_from_residency(
60 template: KvGeometry,
61 report: &ResidencyReport,
62 probe: &KvGeometry,
63 device_bytes: u64,
64 utilization: f64,
65) -> Result<KvGeometry> {
66 let budget = KvBudget {
67 device_bytes,
68 utilization,
69 non_kv_resident_bytes: non_kv_resident_bytes(report.peak_bytes as u64, probe),
70 };
71 template
72 .fit_to_budget(&budget)
73 .map_err(|e| msg_err(e.to_string()))
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum BlockKind {
79 K,
80 V,
81}
82
83fn msg_err(s: String) -> crate::Error {
84 fuel_ir::Error::Msg(s).bt()
85}
86
87fn alloc_err(e: KvAllocError) -> crate::Error {
88 msg_err(format!("KV allocator: {e:?}"))
89}
90
91pub struct DeviceEvicted {
99 core_handle: Externalized,
100 saved: Vec<SavedBlock>,
101}
102
103struct SavedBlock {
107 logical_slot: usize,
108 k: Vec<Vec<u8>>,
109 v: Vec<Vec<u8>>,
110}
111
112impl DeviceEvicted {
113 pub fn covers(&self) -> &[fuel_kv_pool::StateKind] {
116 self.core_handle.covers()
117 }
118 pub fn fidelity(&self) -> fuel_kv_pool::Fidelity {
120 self.core_handle.fidelity()
121 }
122 pub fn saved_block_count(&self) -> usize {
125 self.saved.len()
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct PageTableHost {
135 pub block_table: Vec<u32>,
141 pub context_lens: Vec<u32>,
143 pub batch: usize,
145 pub max_blocks: usize,
148}
149
150impl PageTableHost {
151 pub fn block_table_shape(&self) -> Shape {
153 Shape::from_dims(&[self.batch, self.max_blocks])
154 }
155 pub fn context_lens_shape(&self) -> Shape {
157 Shape::from_dims(&[self.batch])
158 }
159}
160
161pub struct DeviceKvPool {
167 core: KvBlockPool,
168 device: Device,
169 dtype: DType,
170 pool_shape: Shape,
172 k_pools: Vec<Arc<RwLock<Storage>>>,
174 v_pools: Vec<Arc<RwLock<Storage>>>,
176 alloc_id: KvAllocId,
186}
187
188impl DeviceKvPool {
189 pub fn new(geom: KvGeometry, dtype: DType, device: &Device) -> Result<Self> {
197 if geom.elem_size != dtype.size_in_bytes() {
198 return Err(Error::Msg(format!(
199 "DeviceKvPool::new: geometry elem_size {} ≠ dtype {:?} size {} — the \
200 pool's byte accounting would disagree with its physical buffers",
201 geom.elem_size,
202 dtype,
203 dtype.size_in_bytes(),
204 ))
205 .bt());
206 }
207 let n_layers = geom.n_layers;
208 let pool_shape = Shape::from_dims(&[
209 geom.num_blocks,
210 geom.block_size,
211 geom.n_kv_heads,
212 geom.head_dim,
213 ]);
214 let (k_pools, v_pools) = alloc_layer_buffers(&pool_shape, dtype, device, n_layers)?;
215 Ok(Self {
216 core: KvBlockPool::new(geom),
217 device: device.clone(),
218 dtype,
219 pool_shape,
220 k_pools,
221 v_pools,
222 alloc_id: KvAllocId::next(),
223 })
224 }
225
226 pub fn alloc_id(&self) -> KvAllocId {
235 self.alloc_id
236 }
237
238 pub fn core(&self) -> &KvBlockPool {
242 &self.core
243 }
244 pub fn core_mut(&mut self) -> &mut KvBlockPool {
246 &mut self.core
247 }
248 pub fn geometry(&self) -> KvGeometry {
250 self.core.geometry()
251 }
252
253 pub fn n_layers(&self) -> usize {
258 self.k_pools.len()
259 }
260 pub fn pool_shape(&self) -> &Shape {
262 &self.pool_shape
263 }
264 pub fn pool_shape_flat(&self) -> Shape {
275 let g = self.geometry();
276 Shape::from_dims(&[g.num_blocks * g.block_size, g.n_kv_heads, g.head_dim])
277 }
278 pub fn dtype(&self) -> DType {
280 self.dtype
281 }
282 pub fn device(&self) -> &Device {
285 &self.device
286 }
287 pub fn k_pool(&self, layer: usize) -> Option<&Arc<RwLock<Storage>>> {
289 self.k_pools.get(layer)
290 }
291 pub fn v_pool(&self, layer: usize) -> Option<&Arc<RwLock<Storage>>> {
293 self.v_pools.get(layer)
294 }
295
296 fn buffer(&self, layer: usize, kind: BlockKind) -> crate::Result<&Arc<RwLock<Storage>>> {
301 let v = match kind {
302 BlockKind::K => &self.k_pools,
303 BlockKind::V => &self.v_pools,
304 };
305 v.get(layer).ok_or_else(|| {
306 msg_err(format!(
307 "DeviceKvPool: no layer {layer} (n_layers={})",
308 self.n_layers()
309 ))
310 })
311 }
312
313 fn block_elems(&self) -> usize {
315 let g = self.geometry();
316 g.block_size * g.n_kv_heads * g.head_dim
317 }
318
319 pub fn block_write_ranges(&self, phys: PhysBlockId) -> Vec<(usize, usize)> {
325 let g = self.geometry();
326 let p = phys as usize;
327 vec![
328 (p, p + 1),
329 (0, g.block_size),
330 (0, g.n_kv_heads),
331 (0, g.head_dim),
332 ]
333 }
334
335 pub fn write_block(
344 &self,
345 layer: usize,
346 kind: BlockKind,
347 phys: PhysBlockId,
348 data: &[f32],
349 ) -> crate::Result<()> {
350 if self.dtype != DType::F32 {
351 return Err(msg_err(format!(
352 "DeviceKvPool::write_block: f32-typed API, pool dtype is {:?} — use write_block_bytes",
353 self.dtype,
354 )));
355 }
356 if data.len() != self.block_elems() {
357 return Err(msg_err(format!(
358 "DeviceKvPool::write_block: data len {} ≠ block elems {}",
359 data.len(),
360 self.block_elems(),
361 )));
362 }
363 self.write_block_bytes(layer, kind, phys, bytemuck::cast_slice::<f32, u8>(data))
365 }
366
367 pub fn read_block(
373 &self,
374 layer: usize,
375 kind: BlockKind,
376 phys: PhysBlockId,
377 ) -> crate::Result<Vec<f32>> {
378 if self.dtype != DType::F32 {
379 return Err(msg_err(format!(
380 "DeviceKvPool::read_block: f32-typed API, pool dtype is {:?} — use read_block_bytes",
381 self.dtype,
382 )));
383 }
384 let bytes = self.read_block_bytes(layer, kind, phys)?;
385 let v: &[f32] = bytemuck::try_cast_slice(&bytes)
386 .map_err(|e| msg_err(format!("DeviceKvPool::read_block: f32 cast: {e:?}")))?;
387 Ok(v.to_vec())
388 }
389
390 pub fn splice_prefix_shifted(
403 &mut self,
404 prefix: fuel_kv_pool::PrefixId,
405 dst: fuel_kv_pool::SessionHandle,
406 rope_base: f64,
407 ) -> crate::Result<usize> {
408 let g = self.geometry();
409 let (m, pairs) = self
410 .core_mut()
411 .alloc_shifted_prefix_slots(prefix, dst)
412 .map_err(|e| msg_err(format!("DeviceKvPool::splice_prefix_shifted: {e:?}")))?;
413 for (src, fresh) in &pairs {
414 for l in 0..g.n_layers {
415 let k = self.read_block(l, BlockKind::K, *src)?;
417 let rot = crate::lazy::Tensor::rope_delta_rotate_block_f32(
418 self.device(),
419 &k,
420 rope_base,
421 m,
422 g.block_size,
423 g.n_kv_heads,
424 g.head_dim,
425 );
426 self.write_block(l, BlockKind::K, *fresh, &rot)?;
427 let v = self.read_block_bytes(l, BlockKind::V, *src)?;
429 self.write_block_bytes(l, BlockKind::V, *fresh, &v)?;
430 }
431 }
432 Ok(pairs.len() * g.block_size)
433 }
434
435 pub fn write_block_bytes(
444 &self,
445 layer: usize,
446 kind: BlockKind,
447 phys: PhysBlockId,
448 bytes: &[u8],
449 ) -> crate::Result<()> {
450 let g = self.geometry();
451 if phys as usize >= g.num_blocks {
452 return Err(msg_err(format!(
453 "DeviceKvPool::write_block_bytes: physical block {phys} out of range (num_blocks={})",
454 g.num_blocks,
455 )));
456 }
457 let want = self.block_elems() * self.dtype_elem_bytes()?;
458 if bytes.len() != want {
459 return Err(msg_err(format!(
460 "DeviceKvPool::write_block_bytes: {} bytes ≠ block bytes {} \
461 ([block_size {}, Hkv {}, D {}] for {:?})",
462 bytes.len(),
463 want,
464 g.block_size,
465 g.n_kv_heads,
466 g.head_dim,
467 self.dtype,
468 )));
469 }
470 let buf = Arc::clone(self.buffer(layer, kind)?);
471 let src = self.const_from_bytes(
474 bytes,
475 Shape::from_dims(&[1, g.block_size, g.n_kv_heads, g.head_dim]),
476 )?;
477 let dest = src.const_placeholder_like(self.pool_shape.clone(), self.dtype);
478 let post = dest.write_slice(&src, self.block_write_ranges(phys))?;
479 let mut cache = StorageCache::new();
480 cache.insert(dest.node_id(), buf);
481 let _ = crate::pipelined_bridge::realize_one_bytes_with_initial(
482 post.graph_handle(),
483 post.node_id(),
484 &self.device,
485 cache,
486 )?;
487 Ok(())
488 }
489
490 pub fn read_block_bytes(
495 &self,
496 layer: usize,
497 kind: BlockKind,
498 phys: PhysBlockId,
499 ) -> crate::Result<Vec<u8>> {
500 let g = self.geometry();
501 if phys as usize >= g.num_blocks {
502 return Err(msg_err(format!(
503 "DeviceKvPool::read_block_bytes: physical block {phys} out of range (num_blocks={})",
504 g.num_blocks,
505 )));
506 }
507 let buf = Arc::clone(self.buffer(layer, kind)?);
508 let anchor = Tensor::from_f32(vec![0.0], Shape::from_dims(&[1]), &self.device)?;
512 let dest = anchor.const_placeholder_like(self.pool_shape.clone(), self.dtype);
513 let block = dest
514 .slice(0, phys as usize, 1)?
515 .reshape(Shape::from_dims(&[self.block_elems()]))?;
516 let mut cache = StorageCache::new();
517 cache.insert(dest.node_id(), buf);
518 crate::pipelined_bridge::realize_one_bytes_with_initial(
519 block.graph_handle(),
520 block.node_id(),
521 &self.device,
522 cache,
523 )
524 }
525
526 fn dtype_elem_bytes(&self) -> crate::Result<usize> {
530 match self.dtype {
531 DType::F32 => Ok(4),
532 DType::BF16 => Ok(2),
533 other => Err(msg_err(format!(
534 "DeviceKvPool: byte movement unsupported for dtype {other:?} (F32/BF16)",
535 ))),
536 }
537 }
538
539 fn const_from_bytes(&self, bytes: &[u8], shape: Shape) -> crate::Result<Tensor> {
546 let dev = &self.device;
547 match self.dtype {
548 DType::F32 => {
549 let v: &[f32] = bytemuck::try_cast_slice(bytes)
550 .map_err(|e| msg_err(format!("const_from_bytes: f32 cast: {e:?}")))?;
551 Ok(Tensor::from_f32(v.to_vec(), shape, dev)?)
552 }
553 DType::BF16 => {
554 let v: &[half::bf16] = bytemuck::try_cast_slice(bytes)
555 .map_err(|e| msg_err(format!("const_from_bytes: bf16 cast: {e:?}")))?;
556 let anchor = Tensor::from_f32(vec![0.0f32], Shape::from_dims(&[1]), dev)?;
557 Ok(Tensor::from_bf16_on(
558 anchor.graph(),
559 v.to_vec(),
560 shape,
561 dev,
562 )?)
563 }
564 other => Err(msg_err(format!(
565 "DeviceKvPool::const_from_bytes: unsupported dtype {other:?} (F32/BF16)",
566 ))),
567 }
568 }
569
570 pub fn materialize_block_table(
582 &self,
583 sessions: &[SessionHandle],
584 ) -> std::result::Result<PageTableHost, KvAllocError> {
585 let batch = sessions.len();
586 let mut rows: Vec<Vec<PhysBlockId>> = Vec::with_capacity(batch);
587 let mut context_lens: Vec<u32> = Vec::with_capacity(batch);
588 for &s in sessions {
589 rows.push(self.core.session_block_table(s)?);
590 let filled = self
591 .core
592 .filled_tokens(s)
593 .ok_or(KvAllocError::UnknownSession)?;
594 context_lens.push(filled as u32);
595 }
596 let max_blocks = rows.iter().map(|r| r.len()).max().unwrap_or(0).max(1);
598 let mut block_table = vec![0u32; batch * max_blocks];
599 for (bi, row) in rows.iter().enumerate() {
600 for (i, &p) in row.iter().enumerate() {
601 block_table[bi * max_blocks + i] = p;
602 }
603 }
604 Ok(PageTableHost {
605 block_table,
606 context_lens,
607 batch,
608 max_blocks,
609 })
610 }
611
612 pub fn materialize_block_table_padded(
620 &self,
621 sessions: &[SessionHandle],
622 max_blocks_cap: usize,
623 ) -> std::result::Result<PageTableHost, KvAllocError> {
624 let batch = sessions.len();
625 let cap = max_blocks_cap.max(1); let mut block_table = vec![0u32; batch * cap];
627 let mut context_lens: Vec<u32> = Vec::with_capacity(batch);
628 for (bi, &s) in sessions.iter().enumerate() {
629 let row = self.core.session_block_table(s)?;
630 if row.len() > cap {
631 return Err(KvAllocError::OutOfBlocks {
634 need: row.len(),
635 have: cap,
636 });
637 }
638 for (i, &p) in row.iter().enumerate() {
639 block_table[bi * cap + i] = p;
640 }
641 let filled = self
642 .core
643 .filled_tokens(s)
644 .ok_or(KvAllocError::UnknownSession)?;
645 context_lens.push(filled as u32);
646 }
647 Ok(PageTableHost {
648 block_table,
649 context_lens,
650 batch,
651 max_blocks: cap,
652 })
653 }
654
655 pub fn build_decode_attn(
679 &self,
680 k_pool_ph: &Tensor,
681 v_pool_ph: &Tensor,
682 q: &Tensor,
683 k_new: &Tensor,
684 v_new: &Tensor,
685 block_table: &Tensor,
686 context_lens: &Tensor,
687 phys_block: PhysBlockId,
688 slot: usize,
689 scale: f32,
690 ) -> crate::Result<Tensor> {
691 let g = self.geometry();
692 let p = phys_block as usize;
693 let slot_ranges = vec![
695 (p, p + 1),
696 (slot, slot + 1),
697 (0, g.n_kv_heads),
698 (0, g.head_dim),
699 ];
700 let slot_shape = Shape::from_dims(&[1, 1, g.n_kv_heads, g.head_dim]);
701 let k_slot = k_new.reshape(slot_shape.clone())?;
704 let v_slot = v_new.reshape(slot_shape)?;
705 let post_k = k_pool_ph.write_slice(&k_slot, slot_ranges.clone())?;
706 let post_v = v_pool_ph.write_slice(&v_slot, slot_ranges)?;
707 q.paged_attn(
708 &post_k,
709 &post_v,
710 block_table,
711 context_lens,
712 None,
713 scale,
714 g.block_size,
715 None,
716 )
717 }
718
719 pub fn build_decode_attn_batched(
732 &self,
733 k_pool_ph: &Tensor,
734 v_pool_ph: &Tensor,
735 q: &Tensor,
736 k_new: &Tensor,
737 v_new: &Tensor,
738 block_table: &Tensor,
739 context_lens: &Tensor,
740 writes: &[(PhysBlockId, usize)],
741 scale: f32,
742 ) -> crate::Result<Tensor> {
743 let g = self.geometry();
744 let slot_shape = Shape::from_dims(&[1, 1, g.n_kv_heads, g.head_dim]);
745 let mut post_k: Option<Tensor> = None;
746 let mut post_v: Option<Tensor> = None;
747 for (i, &(phys, slot)) in writes.iter().enumerate() {
748 let p = phys as usize;
749 let ranges = vec![
750 (p, p + 1),
751 (slot, slot + 1),
752 (0, g.n_kv_heads),
753 (0, g.head_dim),
754 ];
755 let k_row = k_new.slice(0, i, 1)?.reshape(slot_shape.clone())?;
758 let v_row = v_new.slice(0, i, 1)?.reshape(slot_shape.clone())?;
759 let dest_k = post_k.as_ref().unwrap_or(k_pool_ph);
760 let dest_v = post_v.as_ref().unwrap_or(v_pool_ph);
761 post_k = Some(dest_k.write_slice(&k_row, ranges.clone())?);
762 post_v = Some(dest_v.write_slice(&v_row, ranges)?);
763 }
764 let post_k = post_k.ok_or_else(|| {
765 msg_err("build_decode_attn_batched: writes is empty (need ≥ 1 session)".into())
766 })?;
767 let post_v = post_v.ok_or_else(|| {
771 msg_err("build_decode_attn_batched: post_v unset while post_k is set".into())
772 })?;
773 q.paged_attn(
774 &post_k,
775 &post_v,
776 block_table,
777 context_lens,
778 None,
779 scale,
780 g.block_size,
781 None,
782 )
783 }
784
785 pub fn build_decode_attn_off(
807 &self,
808 k_pool_ph: &Tensor,
809 v_pool_ph: &Tensor,
810 q: &Tensor,
811 k_new: &Tensor,
812 v_new: &Tensor,
813 block_table: &Tensor,
814 context_lens: &Tensor,
815 write_off: Option<&Tensor>,
816 write_sym: fuel_ir::SymId,
817 scale: f32,
818 ) -> crate::Result<Tensor> {
819 let g = self.geometry();
820 let flat_row = Shape::from_dims(&[1, g.n_kv_heads, g.head_dim]);
823 let k_row = k_new.reshape(flat_row.clone())?;
824 let v_row = v_new.reshape(flat_row)?;
825 let ranges = vec![(0usize, 1usize), (0, g.n_kv_heads), (0, g.head_dim)];
829 let (post_flat_k, post_flat_v) = match write_off {
833 Some(off) => (
834 k_pool_ph.write_slice_doff(&k_row, off, 0, ranges.clone())?,
835 v_pool_ph.write_slice_doff(&v_row, off, 0, ranges)?,
836 ),
837 None => {
838 let dyn_off = fuel_ir::DynScalar::Sym(write_sym);
839 (
840 k_pool_ph.write_slice_dyn(&k_row, ranges.clone(), 0, dyn_off)?,
841 v_pool_ph.write_slice_dyn(&v_row, ranges, 0, dyn_off)?,
842 )
843 }
844 };
845 let pool4 = Shape::from_dims(&[g.num_blocks, g.block_size, g.n_kv_heads, g.head_dim]);
849 let post_k = post_flat_k.reshape(pool4.clone())?;
850 let post_v = post_flat_v.reshape(pool4)?;
851 q.paged_attn(
852 &post_k,
853 &post_v,
854 block_table,
855 context_lens,
856 None,
857 scale,
858 g.block_size,
859 None,
860 )
861 }
862
863 pub fn ensure_writable_block(
874 &mut self,
875 session: SessionHandle,
876 logical_block: usize,
877 ) -> crate::Result<PhysBlockId> {
878 let old = self
879 .core
880 .resident_block(session, logical_block)
881 .ok_or_else(|| {
882 msg_err(format!(
883 "ensure_writable_block: logical block {logical_block} not resident"
884 ))
885 })?;
886 if self.core.block_refcount(old) <= 1 {
887 return Ok(old); }
889 let new = self
892 .core
893 .cow_break(session, logical_block)
894 .map_err(alloc_err)?;
895 for l in 0..self.n_layers() {
896 let k = self.read_block_bytes(l, BlockKind::K, old)?;
897 self.write_block_bytes(l, BlockKind::K, new, &k)?;
898 let v = self.read_block_bytes(l, BlockKind::V, old)?;
899 self.write_block_bytes(l, BlockKind::V, new, &v)?;
900 }
901 Ok(new)
902 }
903
904 pub fn evict(&mut self, s: SessionHandle) -> crate::Result<DeviceEvicted> {
912 let n = self
913 .core
914 .session_blocks(s)
915 .ok_or_else(|| alloc_err(KvAllocError::UnknownSession))?;
916 let all: Vec<usize> = (0..n).collect();
917 self.evict_blocks(s, &all)
918 }
919
920 pub fn evict_blocks(
926 &mut self,
927 s: SessionHandle,
928 indices: &[usize],
929 ) -> crate::Result<DeviceEvicted> {
930 let n_layers = self.n_layers();
931 let to_save: Vec<usize> = indices
936 .iter()
937 .copied()
938 .filter(|&i| match self.core.resident_block(s, i) {
939 Some(p) => self.core.block_refcount(p) == 1,
940 None => false,
941 })
942 .collect();
943 let mut saved = Vec::with_capacity(to_save.len());
944 for &i in &to_save {
945 let p = self.core.resident_block(s, i).ok_or_else(|| {
946 msg_err(format!(
947 "evict: slot {i} stopped being resident between the \
948 residency filter and the save loop"
949 ))
950 })?;
951 let mut k = Vec::with_capacity(n_layers);
952 let mut v = Vec::with_capacity(n_layers);
953 for l in 0..n_layers {
954 k.push(self.read_block_bytes(l, BlockKind::K, p)?);
955 v.push(self.read_block_bytes(l, BlockKind::V, p)?);
956 }
957 saved.push(SavedBlock {
958 logical_slot: i,
959 k,
960 v,
961 });
962 }
963 let report = self.core.evict_blocks(s, indices).map_err(alloc_err)?;
964 Ok(DeviceEvicted {
965 core_handle: report.handle,
966 saved,
967 })
968 }
969
970 pub fn restore(&mut self, s: SessionHandle, handle: DeviceEvicted) -> crate::Result<()> {
976 let DeviceEvicted { core_handle, saved } = handle;
977 self.core.restore(s, core_handle).map_err(alloc_err)?;
978 for sb in &saved {
979 let p = self
980 .core
981 .resident_block(s, sb.logical_slot)
982 .ok_or_else(|| {
983 msg_err(format!(
984 "DeviceKvPool::restore: slot {} not resident after core restore — \
985 the core and device handle disagree (internal bug)",
986 sb.logical_slot,
987 ))
988 })?;
989 for l in 0..self.n_layers() {
990 self.write_block_bytes(l, BlockKind::K, p, &sb.k[l])?;
991 self.write_block_bytes(l, BlockKind::V, p, &sb.v[l])?;
992 }
993 }
994 Ok(())
995 }
996}
997
998type KvStorageColumns = (Vec<Arc<RwLock<Storage>>>, Vec<Arc<RwLock<Storage>>>);
1002
1003fn alloc_layer_buffers(
1009 shape: &Shape,
1010 dtype: DType,
1011 device: &Device,
1012 n_layers: usize,
1013) -> Result<KvStorageColumns> {
1014 let target_loc = device.location();
1015 let graph = Arc::new(RwLock::new(Graph::new()));
1016 let mut cache = StorageCache::new();
1017
1018 if let Some(seed) = crate::pipelined_bridge::device_seed_storage(device)? {
1021 let anchor_id = graph
1022 .write()
1023 .map_err(|_| Error::Msg("graph lock poisoned during DeviceKvPool build".into()).bt())?
1024 .push(Node {
1025 op: Op::Const,
1026 inputs: vec![],
1027 shape: Shape::from_dims(&[4]),
1028 dtype: DType::U8,
1029 });
1030 cache.insert(anchor_id, Arc::new(RwLock::new(seed)));
1031 }
1032
1033 let zero_fill_ids: Vec<NodeId> = {
1035 let mut g = graph
1036 .write()
1037 .map_err(|_| Error::Msg("graph lock poisoned during DeviceKvPool build".into()).bt())?;
1038 (0..(2 * n_layers))
1039 .map(|_| {
1040 let alloc_id = g.push(Node {
1041 op: Op::Alloc { target: target_loc },
1042 inputs: vec![],
1043 shape: shape.clone(),
1044 dtype,
1045 });
1046 g.push(Node {
1047 op: Op::ZeroFill,
1048 inputs: vec![alloc_id],
1049 shape: shape.clone(),
1050 dtype,
1051 })
1052 })
1053 .collect()
1054 };
1055
1056 let realized = PipelinedExecutor::realize_many(Arc::clone(&graph), &zero_fill_ids, cache)?;
1057 if realized.len() != 2 * n_layers {
1058 return Err(Error::Msg(format!(
1059 "DeviceKvPool::new: realize_many returned {} storages for {} Op::ZeroFill \
1060 targets — internal bug",
1061 realized.len(),
1062 2 * n_layers,
1063 ))
1064 .bt());
1065 }
1066
1067 let mut it = realized.into_iter();
1068 let mut k_pools = Vec::with_capacity(n_layers);
1069 let mut v_pools = Vec::with_capacity(n_layers);
1070 let short = |what: &str, l: usize| {
1075 crate::Error::Msg(format!(
1076 "kv pool realize: iterator ran short at layer {l} fetching {what}, \
1077 despite a checked length of {} — internal bug",
1078 2 * n_layers
1079 ))
1080 .bt()
1081 };
1082 for l in 0..n_layers {
1083 let (k_arc, _) = it.next().ok_or_else(|| short("K", l))?;
1084 let (v_arc, _) = it.next().ok_or_else(|| short("V", l))?;
1085 k_pools.push(k_arc);
1086 v_pools.push(v_arc);
1087 }
1088 let _ = Layout::contiguous(shape.clone()); Ok((k_pools, v_pools))
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094 use super::*;
1095
1096 fn geom(n_layers: usize, num_blocks: usize, block_size: usize) -> KvGeometry {
1097 KvGeometry {
1098 n_layers,
1099 num_blocks,
1100 block_size,
1101 n_kv_heads: 2,
1102 head_dim: 8,
1103 elem_size: 4, }
1105 }
1106
1107 #[test]
1111 fn allocates_two_pool_buffers_per_layer_with_the_paged_cache_shape() {
1112 let g = geom(
1113 3, 8, 4,
1114 );
1115 let pool = DeviceKvPool::new(g, DType::F32, &Device::cpu()).unwrap();
1116
1117 assert_eq!(pool.n_layers(), 3, "one K + one V buffer per layer");
1118 assert_eq!(
1119 pool.pool_shape().dims(),
1120 &[8, 4, 2, 8],
1121 "[num_blocks, block_size, Hkv, D]"
1122 );
1123 let want_elems = 8 * 4 * 2 * 8;
1124 for l in 0..3 {
1125 for (name, buf) in [("k", pool.k_pool(l)), ("v", pool.v_pool(l))] {
1126 let s = buf
1127 .unwrap_or_else(|| panic!("{name}_pool({l}) exists"))
1128 .read()
1129 .unwrap();
1130 assert_eq!(s.dtype(), DType::F32, "{name}[{l}] dtype");
1131 assert_eq!(s.elem_count(), want_elems, "{name}[{l}] element count");
1132 }
1133 }
1134 assert!(pool.k_pool(3).is_none(), "no layer 3 in a 3-layer pool");
1135 }
1136
1137 #[test]
1140 fn dtype_vs_elem_size_mismatch_is_a_build_time_error() {
1141 let mut g = geom(1, 4, 4);
1142 g.elem_size = 2; let err = DeviceKvPool::new(g, DType::F32, &Device::cpu()); assert!(
1145 err.is_err(),
1146 "elem_size 2 vs f32 (4 bytes) must be rejected"
1147 );
1148 }
1149
1150 #[test]
1156 fn materialize_block_table_projects_the_cores_permuted_layout() {
1157 let mut pool = DeviceKvPool::new(geom(1, 16, 4), DType::F32, &Device::cpu()).unwrap();
1158
1159 let filler = pool.core_mut().open();
1163 pool.core_mut().append(filler, 8).unwrap(); let sa = pool.core_mut().open();
1165 pool.core_mut().append(sa, 12).unwrap(); pool.core_mut().discard(filler); let sb = pool.core_mut().open();
1168 pool.core_mut().append(sb, 6).unwrap(); let a_blocks = pool.core().session_block_table(sa).unwrap();
1172 let b_blocks = pool.core().session_block_table(sb).unwrap();
1173 assert_eq!(a_blocks.len(), 3);
1174 assert_eq!(b_blocks.len(), 2);
1175
1176 let pt = pool.materialize_block_table(&[sa, sb]).unwrap();
1177 assert_eq!(pt.batch, 2);
1178 assert_eq!(pt.max_blocks, 3, "widest session (sa) sets the row width");
1179 assert_eq!(
1180 pt.context_lens,
1181 vec![12u32, 6u32],
1182 "context_len = filled_tokens per session"
1183 );
1184
1185 let row_a = &pt.block_table[0..3];
1187 let row_b = &pt.block_table[3..6];
1188 assert_eq!(
1189 row_a,
1190 &[a_blocks[0], a_blocks[1], a_blocks[2]],
1191 "sa row = its real physical ids"
1192 );
1193 assert_eq!(
1194 row_b[0..2],
1195 [b_blocks[0], b_blocks[1]],
1196 "sb row = its real physical ids"
1197 );
1198 assert_eq!(row_b[2], 0, "sb's short row is zero-padded");
1199 assert_eq!(pt.block_table_shape().dims(), &[2, 3]);
1200 assert_eq!(pt.context_lens_shape().dims(), &[2]);
1201 }
1202
1203 #[test]
1209 fn write_block_then_read_block_round_trips_in_place() {
1210 let pool = DeviceKvPool::new(geom(2, 8, 4), DType::F32, &Device::cpu()).unwrap();
1211 let block_elems = 4 * 2 * 8; let data: Vec<f32> = (0..block_elems).map(|i| i as f32 + 0.5).collect();
1213
1214 pool.write_block(1, BlockKind::K, 5, &data).unwrap();
1215 let back = pool.read_block(1, BlockKind::K, 5).unwrap();
1216 assert_eq!(
1217 back, data,
1218 "block content survives the device write→read round trip"
1219 );
1220 }
1221
1222 #[test]
1228 fn splice_prefix_shifted_rotates_k_copies_v() {
1229 let g = KvGeometry {
1230 n_layers: 2,
1231 n_kv_heads: 2,
1232 head_dim: 4,
1233 num_blocks: 64,
1234 block_size: 4,
1235 elem_size: DType::F32.size_in_bytes(),
1236 };
1237 let mut pool = DeviceKvPool::new(g, DType::F32, &Device::cpu()).unwrap();
1238 let rope_base = 10000.0f64;
1239 let block_elems = 4 * 2 * 4; let donor = pool.core_mut().open();
1243 pool.core_mut().append(donor, 8).unwrap();
1244 let d0 = pool.core().resident_block(donor, 0).unwrap();
1245 let d1 = pool.core().resident_block(donor, 1).unwrap();
1246 for (bi, phys) in [(0usize, d0), (1usize, d1)] {
1247 for l in 0..2usize {
1248 let base = (bi * 100 + l * 10) as f32;
1249 let k: Vec<f32> = (0..block_elems).map(|i| (base + i as f32) * 0.01).collect();
1250 let v: Vec<f32> = (0..block_elems)
1251 .map(|i| (base + i as f32) * 0.02 + 1.0)
1252 .collect();
1253 pool.write_block(l, BlockKind::K, phys, &k).unwrap();
1254 pool.write_block(l, BlockKind::V, phys, &v).unwrap();
1255 }
1256 }
1257 let pid = pool.core_mut().register_prefix(donor, 2).unwrap();
1258
1259 let dst = pool.core_mut().open();
1261 pool.core_mut().append(dst, 8).unwrap();
1262 let n = pool.splice_prefix_shifted(pid, dst, rope_base).unwrap();
1263 assert_eq!(n, 8, "shared token count = 2 blocks * block_size");
1264
1265 for (dst_slot, src_phys) in [(2usize, d0), (3usize, d1)] {
1267 let fresh = pool.core().resident_block(dst, dst_slot).unwrap();
1268 for l in 0..2usize {
1269 let src_k = pool.read_block(l, BlockKind::K, src_phys).unwrap();
1270 let want = crate::lazy::Tensor::rope_delta_rotate_block_f32(
1271 &Device::cpu(),
1272 &src_k,
1273 rope_base,
1274 8,
1275 4,
1276 2,
1277 4,
1278 );
1279 let got = pool.read_block(l, BlockKind::K, fresh).unwrap();
1280 let md = want
1281 .iter()
1282 .zip(&got)
1283 .map(|(a, b)| (a - b).abs())
1284 .fold(0.0f32, f32::max);
1285 assert!(
1286 md < 1e-6,
1287 "K delta-rotated by θ·8 (layer {l}, slot {dst_slot}, maxdiff {md})"
1288 );
1289 assert_eq!(
1290 pool.read_block(l, BlockKind::V, fresh).unwrap(),
1291 pool.read_block(l, BlockKind::V, src_phys).unwrap(),
1292 "V copied verbatim (layer {l}, slot {dst_slot})",
1293 );
1294 }
1295 }
1296 }
1297
1298 #[test]
1302 fn write_block_is_isolated_to_its_layer_kind_and_physical_block() {
1303 let pool = DeviceKvPool::new(geom(2, 8, 4), DType::F32, &Device::cpu()).unwrap();
1304 let block_elems = 4 * 2 * 8;
1305 let ones = vec![1.0_f32; block_elems];
1306 let zeros = vec![0.0_f32; block_elems];
1307
1308 pool.write_block(0, BlockKind::K, 3, &ones).unwrap();
1309
1310 assert_eq!(
1311 pool.read_block(0, BlockKind::K, 3).unwrap(),
1312 ones,
1313 "target block written"
1314 );
1315 assert_eq!(
1316 pool.read_block(0, BlockKind::K, 2).unwrap(),
1317 zeros,
1318 "neighbor block untouched"
1319 );
1320 assert_eq!(
1321 pool.read_block(0, BlockKind::K, 4).unwrap(),
1322 zeros,
1323 "neighbor block untouched"
1324 );
1325 assert_eq!(
1326 pool.read_block(0, BlockKind::V, 3).unwrap(),
1327 zeros,
1328 "same-layer V untouched"
1329 );
1330 assert_eq!(
1331 pool.read_block(1, BlockKind::K, 3).unwrap(),
1332 zeros,
1333 "other-layer K untouched"
1334 );
1335 }
1336
1337 #[test]
1340 fn block_io_bounds_are_typed_errors() {
1341 let pool = DeviceKvPool::new(geom(1, 4, 4), DType::F32, &Device::cpu()).unwrap();
1342 let block_elems = 4 * 2 * 8;
1343 assert!(
1344 pool.write_block(0, BlockKind::K, 4, &vec![0.0; block_elems])
1345 .is_err(),
1346 "phys 4 ≥ num_blocks 4"
1347 );
1348 assert!(
1349 pool.read_block(0, BlockKind::K, 9).is_err(),
1350 "read phys 9 out of range"
1351 );
1352 assert!(
1353 pool.write_block(0, BlockKind::K, 0, &[1.0, 2.0]).is_err(),
1354 "short data rejected"
1355 );
1356 assert!(
1357 pool.write_block(3, BlockKind::K, 0, &vec![0.0; block_elems])
1358 .is_err(),
1359 "no layer 3"
1360 );
1361 }
1362
1363 #[test]
1367 fn write_read_block_bytes_round_trips_f32() {
1368 let pool = DeviceKvPool::new(geom(2, 8, 4), DType::F32, &Device::cpu()).unwrap();
1369 let block_elems = 4 * 2 * 8; let vals: Vec<f32> = (0..block_elems).map(|i| i as f32 + 0.5).collect();
1371 let bytes: Vec<u8> = bytemuck::cast_slice::<f32, u8>(&vals).to_vec();
1372
1373 pool.write_block_bytes(1, BlockKind::K, 5, &bytes).unwrap();
1374 let back = pool.read_block_bytes(1, BlockKind::K, 5).unwrap();
1375 assert_eq!(
1376 back, bytes,
1377 "f32 block bytes survive the byte-level round trip"
1378 );
1379 }
1380
1381 #[test]
1387 fn write_read_block_bytes_round_trips_bf16() {
1388 let mut g = geom(2, 8, 4);
1389 g.elem_size = 2; let pool = DeviceKvPool::new(g, DType::BF16, &Device::cpu()).unwrap();
1391 let block_elems = 4 * 2 * 8;
1392 let vals: Vec<half::bf16> = (0..block_elems)
1394 .map(|i| half::bf16::from_f32(i as f32 * 0.25 - 3.0))
1395 .collect();
1396 let bytes: Vec<u8> = bytemuck::cast_slice::<half::bf16, u8>(&vals).to_vec();
1397 assert_eq!(bytes.len(), block_elems * 2, "bf16 block = 2 bytes/elem");
1398
1399 pool.write_block_bytes(1, BlockKind::K, 5, &bytes).unwrap();
1400 let back = pool.read_block_bytes(1, BlockKind::K, 5).unwrap();
1401 assert_eq!(
1402 back, bytes,
1403 "bf16 block bytes survive the byte-level round trip"
1404 );
1405 }
1406
1407 #[test]
1412 fn bf16_pool_evict_restore_is_byte_exact() {
1413 let mut g = geom(2, 8, 4);
1414 g.elem_size = 2;
1415 let mut pool = DeviceKvPool::new(g, DType::BF16, &Device::cpu()).unwrap();
1416 let block_elems = 4 * 2 * 8;
1417 let vals: Vec<half::bf16> = (0..block_elems)
1418 .map(|i| half::bf16::from_f32(i as f32 * 0.5 - 5.0))
1419 .collect();
1420 let bytes: Vec<u8> = bytemuck::cast_slice::<half::bf16, u8>(&vals).to_vec();
1421
1422 let s = pool.core_mut().open();
1423 pool.core_mut().append(s, 1).unwrap(); let phys = pool.core().resident_block(s, 0).unwrap();
1425 for l in 0..pool.n_layers() {
1426 pool.write_block_bytes(l, BlockKind::K, phys, &bytes)
1427 .unwrap();
1428 pool.write_block_bytes(l, BlockKind::V, phys, &bytes)
1429 .unwrap();
1430 }
1431
1432 let handle = pool.evict(s).unwrap();
1433 pool.restore(s, handle).unwrap();
1434
1435 let phys2 = pool.core().resident_block(s, 0).unwrap();
1436 for l in 0..pool.n_layers() {
1437 assert_eq!(
1438 pool.read_block_bytes(l, BlockKind::K, phys2).unwrap(),
1439 bytes,
1440 "K byte-exact after bf16 evict/restore (layer {l})",
1441 );
1442 assert_eq!(
1443 pool.read_block_bytes(l, BlockKind::V, phys2).unwrap(),
1444 bytes,
1445 "V byte-exact after bf16 evict/restore (layer {l})",
1446 );
1447 }
1448 }
1449
1450 fn rand_f32(n: usize, seed: u32) -> Vec<f32> {
1452 let mut s = seed;
1453 (0..n)
1454 .map(|_| {
1455 s = s.wrapping_mul(1103515245).wrapping_add(12345);
1456 ((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.5
1457 })
1458 .collect()
1459 }
1460
1461 #[test]
1469 fn padded_block_table_paged_attn_matches_unpadded() {
1470 let (hq, hkv, d, block_size, sk) = (2usize, 2usize, 4usize, 4usize, 12usize);
1471 let num_blocks = 8;
1472 let cap = 6; let scale = 1.0f32 / (d as f32).sqrt();
1474 let dev = Device::cpu();
1475
1476 let g = KvGeometry {
1477 n_layers: 1,
1478 num_blocks,
1479 block_size,
1480 n_kv_heads: hkv,
1481 head_dim: d,
1482 elem_size: 4,
1483 };
1484 let mut pool = DeviceKvPool::new(g, DType::F32, &dev).unwrap();
1485 let s = pool.core_mut().open();
1486 pool.core_mut().append(s, sk).unwrap(); let phys = pool.core().session_block_table(s).unwrap();
1488
1489 let k_all = rand_f32(sk * hkv * d, 2);
1490 let v_all = rand_f32(sk * hkv * d, 3);
1491 let q_data = rand_f32(hq * d, 1);
1492 let per_block = block_size * hkv * d;
1493 for (b, &p) in phys.iter().enumerate() {
1494 pool.write_block(
1495 0,
1496 BlockKind::K,
1497 p,
1498 &k_all[b * per_block..(b + 1) * per_block],
1499 )
1500 .unwrap();
1501 pool.write_block(
1502 0,
1503 BlockKind::V,
1504 p,
1505 &v_all[b * per_block..(b + 1) * per_block],
1506 )
1507 .unwrap();
1508 }
1509
1510 let pt_fit = pool.materialize_block_table(&[s]).unwrap();
1511 let pt_pad = pool.materialize_block_table_padded(&[s], cap).unwrap();
1512 assert_eq!(
1513 pt_fit.block_table_shape().dims(),
1514 &[1, phys.len()],
1515 "L-fitted width"
1516 );
1517 assert_eq!(
1518 pt_pad.block_table_shape().dims(),
1519 &[1, cap],
1520 "padded width is the fixed capacity"
1521 );
1522 assert_eq!(
1523 pt_pad.context_lens, pt_fit.context_lens,
1524 "same context_lens"
1525 );
1526
1527 let run = |pt: &PageTableHost| -> Vec<f32> {
1528 let q =
1529 Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, 1, d]), &dev).unwrap();
1530 let kc = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
1531 let vc = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
1532 let bt = q
1533 .const_u32_like(pt.block_table.clone(), pt.block_table_shape())
1534 .unwrap();
1535 let cl = q
1536 .const_u32_like(pt.context_lens.clone(), pt.context_lens_shape())
1537 .unwrap();
1538 let out = q
1539 .paged_attn(&kc, &vc, &bt, &cl, None, scale, block_size, None)
1540 .unwrap();
1541 let mut cache = StorageCache::new();
1542 cache.insert(kc.node_id(), Arc::clone(pool.k_pool(0).unwrap()));
1543 cache.insert(vc.node_id(), Arc::clone(pool.v_pool(0).unwrap()));
1544 crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
1545 out.graph_handle(),
1546 out.node_id(),
1547 &dev,
1548 cache,
1549 )
1550 .unwrap()
1551 };
1552
1553 assert_eq!(
1554 run(&pt_pad),
1555 run(&pt_fit),
1556 "capacity-padded block_table (mask-neutralized pad) == L-fitted, byte-identical",
1557 );
1558 }
1559
1560 #[test]
1574 fn symbolic_offset_write_matches_concrete_write() {
1575 let (hq, hkv, d, block_size) = (2usize, 2usize, 8usize, 4usize);
1576 let num_blocks = 8;
1577 let sk = 6usize; let scale = 1.0f32 / (d as f32).sqrt();
1579 let dev = Device::cpu();
1580 let g = KvGeometry {
1581 n_layers: 1,
1582 num_blocks,
1583 block_size,
1584 n_kv_heads: hkv,
1585 head_dim: d,
1586 elem_size: 4,
1587 };
1588
1589 let q_data = rand_f32(hq * d, 1);
1591 let k_new_data = rand_f32(hkv * d, 4);
1592 let v_new_data = rand_f32(hkv * d, 5);
1593 let q_shape = Shape::from_dims(&[1, hq, 1, d]);
1594 let kv_new_shape = Shape::from_dims(&[1, hkv, 1, d]);
1595
1596 let setup = || -> (DeviceKvPool, PhysBlockId, usize, PageTableHost) {
1599 let mut pool = DeviceKvPool::new(g, DType::F32, &dev).unwrap();
1600 let filler = pool.core_mut().open();
1601 pool.core_mut().append(filler, block_size).unwrap(); let s = pool.core_mut().open();
1603 pool.core_mut().append(s, sk).unwrap(); let tok_pos = sk; pool.core_mut().append(s, 1).unwrap(); let phys = pool.core().resident_block(s, tok_pos / block_size).unwrap();
1607 let slot = tok_pos % block_size;
1608 let pt = pool.materialize_block_table(&[s]).unwrap();
1609 (pool, phys, slot, pt)
1610 };
1611
1612 let (pool_ref, phys, slot, pt) = setup();
1614 assert_eq!(
1615 phys, 2,
1616 "permuted layout — new token's physical block is 2, not 0"
1617 );
1618 assert_eq!(slot, 2, "new token lands at slot 2");
1619 let linear = phys as usize * block_size + slot; let attn_ref = {
1621 let q = Tensor::from_f32(q_data.clone(), q_shape.clone(), &dev).unwrap();
1622 let k_new = q
1625 .const_f32_like(k_new_data.clone(), kv_new_shape.clone())
1626 .unwrap();
1627 let v_new = q
1628 .const_f32_like(v_new_data.clone(), kv_new_shape.clone())
1629 .unwrap();
1630 let k_ph = q.const_placeholder_like(pool_ref.pool_shape().clone(), DType::F32);
1631 let v_ph = q.const_placeholder_like(pool_ref.pool_shape().clone(), DType::F32);
1632 let bt = q
1633 .const_u32_like(pt.block_table.clone(), pt.block_table_shape())
1634 .unwrap();
1635 let cl = q
1636 .const_u32_like(pt.context_lens.clone(), pt.context_lens_shape())
1637 .unwrap();
1638 let out = pool_ref
1639 .build_decode_attn(
1640 &k_ph, &v_ph, &q, &k_new, &v_new, &bt, &cl, phys, slot, scale,
1641 )
1642 .unwrap();
1643 let mut cache = StorageCache::new();
1644 cache.insert(k_ph.node_id(), Arc::clone(pool_ref.k_pool(0).unwrap()));
1645 cache.insert(v_ph.node_id(), Arc::clone(pool_ref.v_pool(0).unwrap()));
1646 crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
1647 out.graph_handle(),
1648 out.node_id(),
1649 &dev,
1650 cache,
1651 )
1652 .unwrap()
1653 };
1654 let ref_k_bytes = pool_ref.read_block_bytes(0, BlockKind::K, phys).unwrap();
1655 let ref_v_bytes = pool_ref.read_block_bytes(0, BlockKind::V, phys).unwrap();
1656
1657 let sym = fuel_ir::SymId(0);
1659 let (pool_dyn, _p, _s, pt_dyn) = setup();
1660 let attn_dyn = {
1661 let q = Tensor::from_f32(q_data.clone(), q_shape.clone(), &dev).unwrap();
1662 let k_new = q
1665 .const_f32_like(k_new_data.clone(), kv_new_shape.clone())
1666 .unwrap();
1667 let v_new = q
1668 .const_f32_like(v_new_data.clone(), kv_new_shape.clone())
1669 .unwrap();
1670 let k_ph = q.const_placeholder_like(pool_dyn.pool_shape_flat(), DType::F32);
1671 let v_ph = q.const_placeholder_like(pool_dyn.pool_shape_flat(), DType::F32);
1672 let bt = q
1673 .const_u32_like(pt_dyn.block_table.clone(), pt_dyn.block_table_shape())
1674 .unwrap();
1675 let cl = q
1676 .const_u32_like(pt_dyn.context_lens.clone(), pt_dyn.context_lens_shape())
1677 .unwrap();
1678 let out = pool_dyn
1679 .build_decode_attn_off(&k_ph, &v_ph, &q, &k_new, &v_new, &bt, &cl, None, sym, scale)
1680 .unwrap();
1681 let mut cache = StorageCache::new();
1682 cache.insert(k_ph.node_id(), Arc::clone(pool_dyn.k_pool(0).unwrap()));
1683 cache.insert(v_ph.node_id(), Arc::clone(pool_dyn.v_pool(0).unwrap()));
1684 let mut env = fuel_ir::SymEnv::new();
1685 env.bind(sym, linear).unwrap();
1686 crate::pipelined_bridge::realize_one_as_with_initial_env::<f32>(
1687 out.graph_handle(),
1688 out.node_id(),
1689 &dev,
1690 cache,
1691 &env,
1692 )
1693 .unwrap()
1694 };
1695 assert_eq!(
1696 pool_dyn.read_block_bytes(0, BlockKind::K, phys).unwrap(),
1697 ref_k_bytes,
1698 "dyn write: K block bytes match the concrete write",
1699 );
1700 assert_eq!(
1701 pool_dyn.read_block_bytes(0, BlockKind::V, phys).unwrap(),
1702 ref_v_bytes,
1703 "dyn write: V block bytes match the concrete write",
1704 );
1705 assert_eq!(
1706 attn_dyn, attn_ref,
1707 "dyn arm paged_attn output is bit-identical to concrete"
1708 );
1709
1710 let (pool_doff, _p, _s, pt_doff) = setup();
1712 let attn_doff = {
1713 let q = Tensor::from_f32(q_data.clone(), q_shape.clone(), &dev).unwrap();
1714 let k_new = q
1717 .const_f32_like(k_new_data.clone(), kv_new_shape.clone())
1718 .unwrap();
1719 let v_new = q
1720 .const_f32_like(v_new_data.clone(), kv_new_shape.clone())
1721 .unwrap();
1722 let k_ph = q.const_placeholder_like(pool_doff.pool_shape_flat(), DType::F32);
1723 let v_ph = q.const_placeholder_like(pool_doff.pool_shape_flat(), DType::F32);
1724 let bt = q
1725 .const_u32_like(pt_doff.block_table.clone(), pt_doff.block_table_shape())
1726 .unwrap();
1727 let cl = q
1728 .const_u32_like(pt_doff.context_lens.clone(), pt_doff.context_lens_shape())
1729 .unwrap();
1730 let off = q
1731 .const_i64_like(vec![linear as i64], Shape::from_dims(&[]))
1732 .unwrap();
1733 let out = pool_doff
1734 .build_decode_attn_off(
1735 &k_ph,
1736 &v_ph,
1737 &q,
1738 &k_new,
1739 &v_new,
1740 &bt,
1741 &cl,
1742 Some(&off),
1743 sym,
1744 scale,
1745 )
1746 .unwrap();
1747 let mut cache = StorageCache::new();
1748 cache.insert(k_ph.node_id(), Arc::clone(pool_doff.k_pool(0).unwrap()));
1749 cache.insert(v_ph.node_id(), Arc::clone(pool_doff.v_pool(0).unwrap()));
1750 crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
1751 out.graph_handle(),
1752 out.node_id(),
1753 &dev,
1754 cache,
1755 )
1756 .unwrap()
1757 };
1758 assert_eq!(
1759 pool_doff.read_block_bytes(0, BlockKind::K, phys).unwrap(),
1760 ref_k_bytes,
1761 "doff write: K block bytes match the concrete write",
1762 );
1763 assert_eq!(
1764 pool_doff.read_block_bytes(0, BlockKind::V, phys).unwrap(),
1765 ref_v_bytes,
1766 "doff write: V block bytes match the concrete write",
1767 );
1768 assert_eq!(
1769 attn_doff, attn_ref,
1770 "doff arm paged_attn output is bit-identical to concrete"
1771 );
1772 }
1773
1774 #[test]
1784 fn pool_routed_paged_attn_matches_dense_reference_over_a_permuted_layout() {
1785 let (hq, hkv, d, block_size, sk) = (2usize, 2usize, 4usize, 4usize, 12usize);
1786 let num_blocks = 8;
1787 let scale = 1.0f32 / (d as f32).sqrt();
1788 let dev = Device::cpu();
1789
1790 let g = KvGeometry {
1791 n_layers: 1,
1792 num_blocks,
1793 block_size,
1794 n_kv_heads: hkv,
1795 head_dim: d,
1796 elem_size: 4,
1797 };
1798 let mut pool = DeviceKvPool::new(g, DType::F32, &dev).unwrap();
1799
1800 let filler = pool.core_mut().open();
1803 pool.core_mut().append(filler, 8).unwrap();
1804 let s = pool.core_mut().open();
1805 pool.core_mut().append(s, sk).unwrap(); let phys = pool.core().session_block_table(s).unwrap();
1807 assert_eq!(phys, vec![2, 3, 4], "permuted layout — not identity 0,1,2");
1808
1809 let k_all = rand_f32(sk * hkv * d, 2);
1811 let v_all = rand_f32(sk * hkv * d, 3);
1812 let q_data = rand_f32(hq * d, 1);
1813
1814 let per_block = block_size * hkv * d;
1816 for (b, &p) in phys.iter().enumerate() {
1817 let kb = &k_all[b * per_block..(b + 1) * per_block];
1818 let vb = &v_all[b * per_block..(b + 1) * per_block];
1819 pool.write_block(0, BlockKind::K, p, kb).unwrap();
1820 pool.write_block(0, BlockKind::V, p, vb).unwrap();
1821 }
1822
1823 let pt = pool.materialize_block_table(&[s]).unwrap();
1825 assert_eq!(pt.context_lens, vec![sk as u32]);
1826
1827 let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, 1, d]), &dev).unwrap();
1829 let kc = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
1830 let vc = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
1831 let bt = q
1832 .const_u32_like(pt.block_table.clone(), pt.block_table_shape())
1833 .unwrap();
1834 let cl = q
1835 .const_u32_like(pt.context_lens.clone(), pt.context_lens_shape())
1836 .unwrap();
1837 let out = q
1838 .paged_attn(&kc, &vc, &bt, &cl, None, scale, block_size, None)
1839 .unwrap();
1840
1841 let mut cache = StorageCache::new();
1842 cache.insert(kc.node_id(), Arc::clone(pool.k_pool(0).unwrap()));
1843 cache.insert(vc.node_id(), Arc::clone(pool.v_pool(0).unwrap()));
1844 let got = crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
1845 out.graph_handle(),
1846 out.node_id(),
1847 &dev,
1848 cache,
1849 )
1850 .unwrap();
1851
1852 let mut want = vec![0.0f32; hq * d];
1854 for h in 0..hq {
1855 let mut scores = vec![0.0f32; sk];
1856 for t in 0..sk {
1857 let mut dot = 0.0f32;
1858 for dd in 0..d {
1859 dot += q_data[h * d + dd] * k_all[(t * hkv + h) * d + dd];
1860 }
1861 scores[t] = dot * scale;
1862 }
1863 let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1864 let mut denom = 0.0f32;
1865 for sc in scores.iter_mut() {
1866 *sc = (*sc - m).exp();
1867 denom += *sc;
1868 }
1869 for t in 0..sk {
1870 let p = scores[t] / denom;
1871 for dd in 0..d {
1872 want[h * d + dd] += p * v_all[(t * hkv + h) * d + dd];
1873 }
1874 }
1875 }
1876
1877 assert_eq!(got.len(), want.len());
1878 for (i, (&a, &b)) in got.iter().zip(want.iter()).enumerate() {
1879 let diff = (a - b).abs();
1880 let denom = a.abs().max(b.abs()).max(f32::MIN_POSITIVE);
1881 assert!(
1882 diff < 1e-5 || diff / denom < 1e-5,
1883 "[{i}]: pool={a} dense={b} (abs={diff})"
1884 );
1885 }
1886 }
1887
1888 #[test]
1894 fn device_evict_then_restore_is_a_byte_exact_round_trip_across_block_reuse() {
1895 let (n_layers, num_blocks, block_size) = (2usize, 8usize, 4usize);
1896 let mut pool = DeviceKvPool::new(
1897 geom(n_layers, num_blocks, block_size),
1898 DType::F32,
1899 &Device::cpu(),
1900 )
1901 .unwrap();
1902 let block_elems = block_size * 2 * 8; let content = |slot: usize, layer: usize, kind: BlockKind| -> Vec<f32> {
1906 let base =
1907 (slot * 1000 + layer * 100 + if kind == BlockKind::K { 0 } else { 50 }) as f32;
1908 (0..block_elems).map(|e| base + e as f32 * 0.001).collect()
1909 };
1910
1911 let s = pool.core_mut().open();
1912 pool.core_mut().append(s, 8).unwrap(); for slot in 0..2 {
1914 let p = pool.core().resident_block(s, slot).unwrap();
1915 for l in 0..n_layers {
1916 pool.write_block(l, BlockKind::K, p, &content(slot, l, BlockKind::K))
1917 .unwrap();
1918 pool.write_block(l, BlockKind::V, p, &content(slot, l, BlockKind::V))
1919 .unwrap();
1920 }
1921 }
1922
1923 let free_before = pool.core().free_blocks();
1924 let handle = pool.evict(s).unwrap();
1925 assert_eq!(
1926 handle.saved_block_count(),
1927 2,
1928 "both exclusive blocks captured"
1929 );
1930 assert_eq!(handle.fidelity(), fuel_kv_pool::Fidelity::Lossy);
1931 assert_eq!(
1932 pool.core().free_blocks(),
1933 free_before + 2,
1934 "2 blocks returned to the pool"
1935 );
1936
1937 for p in 0..num_blocks as PhysBlockId {
1940 for l in 0..n_layers {
1941 pool.write_block(l, BlockKind::K, p, &vec![-9.0; block_elems])
1942 .unwrap();
1943 pool.write_block(l, BlockKind::V, p, &vec![-9.0; block_elems])
1944 .unwrap();
1945 }
1946 }
1947
1948 pool.restore(s, handle).unwrap();
1949 assert_eq!(
1950 pool.core().session_blocks(s),
1951 Some(2),
1952 "session resident again"
1953 );
1954
1955 for slot in 0..2 {
1957 let p = pool.core().resident_block(s, slot).unwrap();
1958 for l in 0..n_layers {
1959 assert_eq!(
1960 pool.read_block(l, BlockKind::K, p).unwrap(),
1961 content(slot, l, BlockKind::K),
1962 "K slot {slot} layer {l} restored byte-exact",
1963 );
1964 assert_eq!(
1965 pool.read_block(l, BlockKind::V, p).unwrap(),
1966 content(slot, l, BlockKind::V),
1967 "V slot {slot} layer {l} restored byte-exact",
1968 );
1969 }
1970 }
1971 }
1972
1973 #[test]
1977 fn device_partial_evict_captures_only_exclusive_blocks() {
1978 let mut pool = DeviceKvPool::new(geom(1, 16, 4), DType::F32, &Device::cpu()).unwrap();
1979 let a = pool.core_mut().open();
1980 pool.core_mut().append(a, 9).unwrap(); let b = pool.core_mut().open();
1982 pool.core_mut().splice(a, b, 0, 2).unwrap(); let handle = pool.evict(a).unwrap();
1986 assert_eq!(
1987 handle.saved_block_count(),
1988 1,
1989 "only the 1 exclusive block's bytes are captured; the 2 shared stay resident",
1990 );
1991 assert_eq!(pool.core().session_blocks(b), Some(2));
1993 }
1994
1995 #[test]
2005 fn paged_decode_multistep_matches_dense_reference_across_block_growth() {
2006 let (hq, hkv, d, block_size) = (2usize, 2usize, 4usize, 4usize);
2007 let (num_blocks, n_steps) = (8usize, 10usize);
2008 let scale = 1.0f32 / (d as f32).sqrt();
2009 let dev = Device::cpu();
2010 let g = KvGeometry {
2011 n_layers: 1,
2012 num_blocks,
2013 block_size,
2014 n_kv_heads: hkv,
2015 head_dim: d,
2016 elem_size: 4,
2017 };
2018 let mut pool = DeviceKvPool::new(g, DType::F32, &dev).unwrap();
2019 let session = pool.core_mut().open();
2020
2021 let mut k_hist: Vec<Vec<f32>> = Vec::new();
2023 let mut v_hist: Vec<Vec<f32>> = Vec::new();
2024
2025 for t in 0..n_steps {
2026 let q_data = rand_f32(hq * d, 100 + t as u32);
2027 let k_data = rand_f32(hkv * d, 200 + t as u32);
2028 let v_data = rand_f32(hkv * d, 300 + t as u32);
2029 k_hist.push(k_data.clone());
2030 v_hist.push(v_data.clone());
2031
2032 pool.core_mut().append(session, 1).unwrap();
2034 let phys = pool.core().resident_block(session, t / block_size).unwrap();
2035 let slot = t % block_size;
2036 let pt = pool.materialize_block_table(&[session]).unwrap();
2037 assert_eq!(
2038 pt.context_lens,
2039 vec![(t + 1) as u32],
2040 "context_len tracks tokens so far"
2041 );
2042
2043 let q =
2045 Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, 1, d]), &dev).unwrap();
2046 let kph = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
2047 let vph = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
2048 let knew = q
2049 .const_f32_like(k_data, Shape::from_dims(&[1, hkv, 1, d]))
2050 .unwrap();
2051 let vnew = q
2052 .const_f32_like(v_data, Shape::from_dims(&[1, hkv, 1, d]))
2053 .unwrap();
2054 let bt = q
2055 .const_u32_like(pt.block_table.clone(), pt.block_table_shape())
2056 .unwrap();
2057 let cl = q
2058 .const_u32_like(pt.context_lens.clone(), pt.context_lens_shape())
2059 .unwrap();
2060 let attn = pool
2061 .build_decode_attn(&kph, &vph, &q, &knew, &vnew, &bt, &cl, phys, slot, scale)
2062 .unwrap();
2063
2064 let mut cache = StorageCache::new();
2065 cache.insert(kph.node_id(), Arc::clone(pool.k_pool(0).unwrap()));
2066 cache.insert(vph.node_id(), Arc::clone(pool.v_pool(0).unwrap()));
2067 let got = crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
2068 attn.graph_handle(),
2069 attn.node_id(),
2070 &dev,
2071 cache,
2072 )
2073 .unwrap();
2074
2075 let mut want = vec![0.0f32; hq * d];
2077 for h in 0..hq {
2078 let mut scores = vec![0.0f32; t + 1];
2079 for (j, sc) in scores.iter_mut().enumerate() {
2080 let mut dot = 0.0f32;
2081 for dd in 0..d {
2082 dot += q_data[h * d + dd] * k_hist[j][h * d + dd];
2083 }
2084 *sc = dot * scale;
2085 }
2086 let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2087 let mut denom = 0.0f32;
2088 for sc in scores.iter_mut() {
2089 *sc = (*sc - m).exp();
2090 denom += *sc;
2091 }
2092 for (j, &p) in scores.iter().enumerate() {
2093 let w = p / denom;
2094 for dd in 0..d {
2095 want[h * d + dd] += w * v_hist[j][h * d + dd];
2096 }
2097 }
2098 }
2099
2100 assert_eq!(got.len(), want.len());
2101 for (i, (&a, &b)) in got.iter().zip(want.iter()).enumerate() {
2102 let diff = (a - b).abs();
2103 let den = a.abs().max(b.abs()).max(f32::MIN_POSITIVE);
2104 assert!(
2105 diff < 1e-5 || diff / den < 1e-5,
2106 "step {t} [{i}]: paged={a} dense={b} (abs={diff})",
2107 );
2108 }
2109 }
2110 assert_eq!(
2111 pool.core().session_blocks(session),
2112 Some(3),
2113 "10 tokens / block_size 4 = 3 blocks"
2114 );
2115 }
2116
2117 #[test]
2124 fn spliced_shared_blocks_are_read_by_paged_attention() {
2125 let (hq, hkv, d, block_size) = (2usize, 2usize, 4usize, 4usize);
2126 let (num_blocks, sk) = (8usize, 8usize); let scale = 1.0f32 / (d as f32).sqrt();
2128 let dev = Device::cpu();
2129 let g = KvGeometry {
2130 n_layers: 1,
2131 num_blocks,
2132 block_size,
2133 n_kv_heads: hkv,
2134 head_dim: d,
2135 elem_size: 4,
2136 };
2137 let mut pool = DeviceKvPool::new(g, DType::F32, &dev).unwrap();
2138
2139 let a = pool.core_mut().open();
2141 pool.core_mut().append(a, sk).unwrap();
2142 let k_all = rand_f32(sk * hkv * d, 2);
2143 let v_all = rand_f32(sk * hkv * d, 3);
2144 let per_block = block_size * hkv * d;
2145 let a_blocks = pool.core().session_block_table(a).unwrap();
2146 for (bi, &p) in a_blocks.iter().enumerate() {
2147 pool.write_block(
2148 0,
2149 BlockKind::K,
2150 p,
2151 &k_all[bi * per_block..(bi + 1) * per_block],
2152 )
2153 .unwrap();
2154 pool.write_block(
2155 0,
2156 BlockKind::V,
2157 p,
2158 &v_all[bi * per_block..(bi + 1) * per_block],
2159 )
2160 .unwrap();
2161 }
2162
2163 let b = pool.core_mut().open();
2165 pool.core_mut().splice(a, b, 0, 2).unwrap();
2166 assert_eq!(
2167 pool.core().session_block_table(b).unwrap(),
2168 a_blocks,
2169 "B shares A's exact blocks"
2170 );
2171 assert_eq!(
2172 pool.core().block_refcount(a_blocks[0]),
2173 2,
2174 "shared → refcount 2"
2175 );
2176
2177 let pt = pool.materialize_block_table(&[b]).unwrap();
2179 assert_eq!(
2180 pt.context_lens,
2181 vec![sk as u32],
2182 "spliced prefix length carried to B"
2183 );
2184 let q_data = rand_f32(hq * d, 1);
2185 let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, 1, d]), &dev).unwrap();
2186 let kph = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
2187 let vph = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
2188 let bt = q
2189 .const_u32_like(pt.block_table.clone(), pt.block_table_shape())
2190 .unwrap();
2191 let cl = q
2192 .const_u32_like(pt.context_lens.clone(), pt.context_lens_shape())
2193 .unwrap();
2194 let out = q
2195 .paged_attn(&kph, &vph, &bt, &cl, None, scale, block_size, None)
2196 .unwrap();
2197 let mut cache = StorageCache::new();
2198 cache.insert(kph.node_id(), Arc::clone(pool.k_pool(0).unwrap()));
2199 cache.insert(vph.node_id(), Arc::clone(pool.v_pool(0).unwrap()));
2200 let got = crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
2201 out.graph_handle(),
2202 out.node_id(),
2203 &dev,
2204 cache,
2205 )
2206 .unwrap();
2207
2208 let mut want = vec![0.0f32; hq * d];
2210 for h in 0..hq {
2211 let mut scores = vec![0.0f32; sk];
2212 for (t, sc) in scores.iter_mut().enumerate() {
2213 let mut dot = 0.0f32;
2214 for dd in 0..d {
2215 dot += q_data[h * d + dd] * k_all[(t * hkv + h) * d + dd];
2216 }
2217 *sc = dot * scale;
2218 }
2219 let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2220 let mut denom = 0.0f32;
2221 for sc in scores.iter_mut() {
2222 *sc = (*sc - m).exp();
2223 denom += *sc;
2224 }
2225 for (t, &p) in scores.iter().enumerate() {
2226 let w = p / denom;
2227 for dd in 0..d {
2228 want[h * d + dd] += w * v_all[(t * hkv + h) * d + dd];
2229 }
2230 }
2231 }
2232 for (i, (&x, &y)) in got.iter().zip(want.iter()).enumerate() {
2233 let diff = (x - y).abs();
2234 let den = x.abs().max(y.abs()).max(f32::MIN_POSITIVE);
2235 assert!(
2236 diff < 1e-5 || diff / den < 1e-5,
2237 "[{i}]: spliced={x} dense={y} (abs={diff})"
2238 );
2239 }
2240 }
2241
2242 #[test]
2249 fn ensure_writable_block_copy_on_writes_a_shared_block() {
2250 let (hkv, d, block_size) = (2usize, 4usize, 4usize);
2251 let g = KvGeometry {
2252 n_layers: 1,
2253 num_blocks: 8,
2254 block_size,
2255 n_kv_heads: hkv,
2256 head_dim: d,
2257 elem_size: 4,
2258 };
2259 let mut pool = DeviceKvPool::new(g, DType::F32, &Device::cpu()).unwrap();
2260 let block_elems = block_size * hkv * d;
2261
2262 let a = pool.core_mut().open();
2264 pool.core_mut().append(a, block_size).unwrap();
2265 let a_phys = pool.core().resident_block(a, 0).unwrap();
2266 let a_data: Vec<f32> = (0..block_elems).map(|i| i as f32 + 1.0).collect();
2267 pool.write_block(0, BlockKind::K, a_phys, &a_data).unwrap();
2268
2269 let b = pool.core_mut().open();
2271 pool.core_mut().splice(a, b, 0, 1).unwrap();
2272 assert_eq!(
2273 pool.core().resident_block(b, 0),
2274 Some(a_phys),
2275 "B shares A's block"
2276 );
2277 assert_eq!(pool.core().block_refcount(a_phys), 2);
2278
2279 let b_phys = pool.ensure_writable_block(b, 0).unwrap();
2281 assert_ne!(b_phys, a_phys, "B got a fresh private block");
2282 assert_eq!(
2283 pool.core().block_refcount(a_phys),
2284 1,
2285 "A's block no longer shared"
2286 );
2287 assert_eq!(
2288 pool.read_block(0, BlockKind::K, b_phys).unwrap(),
2289 a_data,
2290 "B's block is a copy of A's"
2291 );
2292
2293 pool.write_block(0, BlockKind::K, b_phys, &vec![-1.0; block_elems])
2295 .unwrap();
2296 assert_eq!(
2297 pool.read_block(0, BlockKind::K, a_phys).unwrap(),
2298 a_data,
2299 "A's block intact after B writes its own copy",
2300 );
2301
2302 assert_eq!(
2304 pool.ensure_writable_block(a, 0).unwrap(),
2305 a_phys,
2306 "exclusive block: no CoW"
2307 );
2308 }
2309
2310 #[test]
2313 fn materialize_rejects_a_non_resident_session() {
2314 let mut pool = DeviceKvPool::new(geom(1, 16, 4), DType::F32, &Device::cpu()).unwrap();
2315 let s = pool.core_mut().open();
2316 pool.core_mut().append(s, 9).unwrap();
2317 pool.core_mut().evict(s).unwrap(); let err = pool.materialize_block_table(&[s]);
2319 assert_eq!(err, Err(KvAllocError::SessionNotResident { slot: 0 }));
2320 }
2321
2322 fn probe_report(weight_elems: usize, probe_blocks: usize) -> (ResidencyReport, KvGeometry) {
2326 use fuel_dispatch::residency::ResidencyPlanner;
2327 let probe = geom(2, probe_blocks, 4);
2328 let mut g = Graph::new();
2329 let mut push = |op: Op, inputs: Vec<NodeId>, dims: &[usize]| {
2330 g.push(Node {
2331 op,
2332 inputs,
2333 shape: Shape::from_dims(dims),
2334 dtype: DType::F32,
2335 })
2336 };
2337 let w = push(Op::Const, vec![], &[weight_elems]);
2338 let mut roots = vec![push(Op::Neg, vec![w], &[weight_elems])];
2339 for _ in 0..(2 * probe.n_layers) {
2340 let dims = [
2341 probe_blocks,
2342 probe.block_size,
2343 probe.n_kv_heads,
2344 probe.head_dim,
2345 ];
2346 let leaf = push(Op::Const, vec![], &dims);
2347 roots.push(push(Op::Neg, vec![leaf], &dims));
2348 }
2349 (ResidencyPlanner::analyze(&g, &roots), probe)
2350 }
2351
2352 #[test]
2356 fn fit_geometry_from_residency_matches_the_arithmetic_and_allocates() {
2357 let (report, probe) = probe_report(4096, 2);
2358 let non_kv = report.peak_bytes as u64 - probe.block_bytes() * probe.num_blocks as u64;
2359 let template = geom(2, 1, 4);
2360 let usable = non_kv + 10 * template.block_bytes() + 123; let g = fit_geometry_from_residency(template, &report, &probe, usable, 1.0).unwrap();
2362 assert_eq!(
2363 g.num_blocks, 10,
2364 "10 whole blocks fit, the 123 B slack is not a block"
2365 );
2366 let pool = DeviceKvPool::new(g, DType::F32, &Device::cpu()).unwrap();
2367 assert_eq!(pool.core().free_blocks(), 10);
2368 }
2369
2370 #[test]
2374 fn forgetting_the_probe_pool_undersizes_the_pool() {
2375 let (report, probe) = probe_report(4096, 2);
2376 let template = geom(2, 1, 4);
2377 let usable =
2378 (report.peak_bytes as u64 - probe.block_bytes() * 2) + 10 * template.block_bytes();
2379 let right = fit_geometry_from_residency(template, &report, &probe, usable, 1.0).unwrap();
2380 let naive = template
2381 .fit_to_budget(&KvBudget {
2382 device_bytes: usable,
2383 utilization: 1.0,
2384 non_kv_resident_bytes: report.peak_bytes as u64,
2385 })
2386 .unwrap();
2387 assert!(
2388 naive.num_blocks < right.num_blocks,
2389 "{naive:?} vs {right:?}"
2390 );
2391 }
2392
2393 #[test]
2396 fn fit_geometry_from_residency_errors_when_the_graph_alone_does_not_fit() {
2397 let (report, probe) = probe_report(1 << 20, 2);
2398 let r = fit_geometry_from_residency(geom(2, 1, 4), &report, &probe, 1024, 1.0);
2399 assert!(r.is_err());
2400 }
2401}