1use std::collections::hash_map::Entry;
34use std::sync::Arc;
35#[cfg(feature = "kvbm-offload")]
36use std::sync::Mutex;
37
38use dynamo_kv_router::protocols::{
39 ExternalSequenceBlockHash, KvCacheEvent, KvCacheEventData, KvCacheRemoveData, KvCacheStoreData,
40 KvCacheStoredBlockData, LocalBlockHash, StorageTier,
41};
42use dynamo_tokens::blocks::UniqueBlock;
43use dynamo_tokens::{BlockHash, PositionalLineageHash, SequenceHash};
44use kvbm_logical::blocks::BlockDuplicationPolicy;
45use kvbm_logical::registry::BlockRegistry;
46use kvbm_logical::tinylfu::TinyLFUTracker;
47use kvbm_logical::{BlockManager, ImmutableBlock, MutableBlock};
48use rustc_hash::FxHashMap;
49use uuid::Uuid;
50
51use crate::common::kv_cache_trace;
52use crate::common::protocols::{
53 G1, KvEventPublishers, MockerEvictionBackend, MoveBlock, PrefillCost,
54};
55use crate::common::sequence::ActiveSequence;
56use crate::kv_manager::{G1Acquire, OffloadDependency};
57#[cfg(feature = "kvbm-offload")]
58use crate::kvbm_offload::{
59 G1EvictionOutcome, G2BlockEventMetadata, G2OffloadBlock, G2RouterEvent, MockOffloadEngine,
60 OffloadId, SwapInHandle,
61};
62
63#[cfg(feature = "kvbm-offload")]
67pub enum BatchSwapInOutcome {
68 NoHits,
71 Scheduled { handle: SwapInHandle },
76 BlockedOnG1Offload(OffloadDependency),
79}
80
81#[cfg(feature = "kvbm-offload")]
82pub struct SwapInRegistrationOutcome {
83 pub consumed_entries: usize,
84}
85
86#[cfg(feature = "kvbm-offload")]
87pub(crate) struct SwapInRegistrationBlock {
88 pub(crate) seq_hash: SequenceHash,
89 pub(crate) plh: PositionalLineageHash,
90 pub(crate) local_hash: Option<BlockHash>,
91 pub(crate) token_ids: Option<Vec<u32>>,
92}
93
94#[cfg(feature = "kvbm-offload")]
95enum SwapInSlotReservation {
96 Reserved(Vec<MutableBlock<G1>>),
97 BlockedOnG1Offload(OffloadDependency),
98 NoCapacity,
99}
100
101#[cfg(not(feature = "kvbm-offload"))]
114enum G1EvictionOutcome {}
115
116enum PreparedUseBlock {
117 ExistingActiveFull {
120 seq_hash: SequenceHash,
121 },
122 ExistingMatchedFull {
125 seq_hash: SequenceHash,
126 handle: ImmutableBlock<G1>,
127 },
128 PendingNonLocalFull {
131 seq_hash: SequenceHash,
132 full_idx: usize,
133 },
134 ExistingPartial,
135 FreshFull {
136 seq_hash: SequenceHash,
137 full_idx: usize,
138 mutable: Option<MutableBlock<G1>>,
139 },
140 FreshPartial {
141 uuid: Uuid,
142 mutable: Option<MutableBlock<G1>>,
143 },
144}
145
146struct UseSignalRef<'a> {
147 local_hashes: &'a [BlockHash],
148 plhs: &'a [PositionalLineageHash],
149 token_ids: Option<&'a [Vec<u32>]>,
150 parent: Option<&'a UniqueBlock>,
151}
152
153struct UseTransaction<'a> {
154 signal: UseSignalRef<'a>,
155 prepared: Vec<PreparedUseBlock>,
156 fresh_full_blocks: usize,
157 evicted_plhs: Vec<PositionalLineageHash>,
158}
159
160struct G1SlotReservation {
161 blocks: Vec<MutableBlock<G1>>,
162 evicted_plhs: Vec<PositionalLineageHash>,
163}
164
165pub struct DecodeBlockReservation {
166 blocks: Vec<MutableBlock<G1>>,
167}
168
169pub struct VllmDestinationReservation {
170 cached_prefix: Vec<(SequenceHash, ImmutableBlock<G1>)>,
171 unpublished_blocks: Vec<MutableBlock<G1>>,
172 layout: Option<MoveBlock>,
173}
174
175impl VllmDestinationReservation {
176 pub(crate) fn transferable_prompt_tokens(&self, block_size: usize) -> usize {
177 self.unpublished_blocks.len().saturating_mul(block_size)
178 }
179
180 #[cfg(test)]
181 pub(crate) fn len(&self) -> usize {
182 self.cached_prefix.len() + self.unpublished_blocks.len()
183 }
184
185 #[cfg(test)]
186 pub(crate) fn block_ids(&self) -> Vec<usize> {
187 self.cached_prefix
188 .iter()
189 .map(|(_, block)| block.block_id())
190 .chain(self.unpublished_blocks.iter().map(MutableBlock::block_id))
191 .collect()
192 }
193}
194
195impl DecodeBlockReservation {
196 fn take(&mut self) -> Option<MutableBlock<G1>> {
197 self.blocks.pop()
198 }
199
200 pub(crate) fn len(&self) -> usize {
201 self.blocks.len()
202 }
203}
204
205#[derive(Clone)]
206struct RegisteredBlockInfo {
207 seq_hash: SequenceHash,
208 #[cfg_attr(not(feature = "kvbm-offload"), allow(dead_code))]
209 block_id: usize,
210 #[cfg_attr(not(feature = "kvbm-offload"), allow(dead_code))]
211 parent_hash: Option<SequenceHash>,
212 #[cfg_attr(not(feature = "kvbm-offload"), allow(dead_code))]
213 local_hash: Option<BlockHash>,
214 #[cfg_attr(not(feature = "kvbm-offload"), allow(dead_code))]
215 token_ids: Option<Vec<u32>>,
216}
217
218struct FullBlockMetadata {
219 seq_hash: SequenceHash,
220 plh: PositionalLineageHash,
221 parent_hash: Option<SequenceHash>,
222 local_hash: Option<BlockHash>,
223 token_ids: Option<Vec<u32>>,
224}
225
226struct ActiveFullBlock {
234 handle: ImmutableBlock<G1>,
235 logical_refs: usize,
236}
237
238impl ActiveFullBlock {
239 fn new(handle: ImmutableBlock<G1>) -> Self {
240 Self {
241 handle,
242 logical_refs: 1,
243 }
244 }
245
246 fn retain(&mut self) {
247 self.logical_refs = self
248 .logical_refs
249 .checked_add(1)
250 .expect("active full-block logical reference count overflowed");
251 }
252}
253
254#[derive(Clone, Copy, Debug, Eq, PartialEq)]
255enum FullBlockCommit {
256 Reused,
257 Stored,
258}
259
260pub struct KvManager {
262 block_manager: BlockManager<G1>,
263 max_capacity: usize,
264 block_size: usize,
265 kv_event_publishers: KvEventPublishers,
266 dp_rank: u32,
267 next_event_id: u64,
268
269 active_partial: FxHashMap<Uuid, MutableBlock<G1>>,
272
273 active_full: FxHashMap<SequenceHash, ActiveFullBlock>,
278
279 registered_blocks: FxHashMap<PositionalLineageHash, RegisteredBlockInfo>,
285
286 #[cfg(feature = "kvbm-offload")]
299 offload_engine: Option<Arc<Mutex<MockOffloadEngine>>>,
300
301 capacity_generation: u64,
304}
305
306impl KvManager {
307 pub fn new_with_event_sink(
308 max_capacity: usize,
309 block_size: usize,
310 kv_event_publishers: KvEventPublishers,
311 dp_rank: u32,
312 ) -> Self {
313 Self::new_with_eviction_backend(
314 max_capacity,
315 block_size,
316 kv_event_publishers,
317 dp_rank,
318 MockerEvictionBackend::default(),
319 )
320 }
321
322 pub fn new_with_eviction_backend(
323 max_capacity: usize,
324 block_size: usize,
325 kv_event_publishers: KvEventPublishers,
326 dp_rank: u32,
327 eviction_backend: MockerEvictionBackend,
328 ) -> Self {
329 debug_assert!(max_capacity > 0, "max_capacity must be > 0");
330
331 let mut registry_builder = BlockRegistry::builder();
332 if matches!(eviction_backend, MockerEvictionBackend::MultiLru) {
333 let tracker = Arc::new(TinyLFUTracker::new(max_capacity));
334 registry_builder = registry_builder.frequency_tracker(tracker);
335 }
336 let registry = registry_builder.build();
337
338 let mut mgr_builder = BlockManager::builder()
339 .block_count(max_capacity)
340 .block_size(block_size)
341 .registry(registry)
342 .duplication_policy(BlockDuplicationPolicy::Reject);
343
344 mgr_builder = match eviction_backend {
353 MockerEvictionBackend::Lineage => mgr_builder.with_lineage_backend(),
354 MockerEvictionBackend::Lru => mgr_builder.with_lru_backend(),
355 MockerEvictionBackend::MultiLru => mgr_builder.with_multi_lru_backend(),
356 };
357 let block_manager = mgr_builder.build().expect("BlockManager build failed");
358
359 if !kv_event_publishers.is_empty() {
360 tracing::info!(
361 "KvManager initialized with event sink for DP rank {dp_rank} with block_size {block_size}, eviction={eviction_backend:?}"
362 );
363 }
364
365 Self {
366 block_manager,
367 max_capacity,
368 block_size,
369 kv_event_publishers,
370 dp_rank,
371 next_event_id: 0,
372 active_partial: FxHashMap::default(),
373 active_full: FxHashMap::default(),
374 registered_blocks: FxHashMap::default(),
375 #[cfg(feature = "kvbm-offload")]
376 offload_engine: None,
377 capacity_generation: 0,
378 }
379 }
380
381 fn insert_or_retain_active_full(&mut self, seq_hash: SequenceHash, handle: ImmutableBlock<G1>) {
384 match self.active_full.entry(seq_hash) {
385 Entry::Vacant(entry) => {
386 entry.insert(ActiveFullBlock::new(handle));
387 }
388 Entry::Occupied(mut entry) => {
389 assert_eq!(
390 entry.get().handle.block_id(),
391 handle.block_id(),
392 "active full-block hash resolved to a different physical block"
393 );
394 entry.get_mut().retain();
395 drop(handle);
399 }
400 }
401 }
402
403 fn retain_active_full(&mut self, seq_hash: SequenceHash) {
407 self.active_full
408 .get_mut(&seq_hash)
409 .unwrap_or_else(|| panic!("active full block {seq_hash:?} disappeared before commit"))
410 .retain();
411 }
412
413 fn release_active_full(&mut self, seq_hash: SequenceHash) {
416 let Entry::Occupied(mut entry) = self.active_full.entry(seq_hash) else {
417 panic!("Deref: full block not in active pool");
418 };
419 assert!(
420 entry.get().logical_refs > 0,
421 "active full block must retain at least one logical owner"
422 );
423 if entry.get().logical_refs == 1 {
424 entry.remove();
425 } else {
426 entry.get_mut().logical_refs -= 1;
427 }
428 }
429
430 #[cfg(feature = "kvbm-offload")]
435 pub fn attach_new_offload_engine(
436 &mut self,
437 engine: MockOffloadEngine,
438 ) -> Arc<Mutex<MockOffloadEngine>> {
439 let shared = Arc::new(Mutex::new(engine));
440 self.offload_engine = Some(shared.clone());
441 shared
442 }
443
444 #[cfg(feature = "kvbm-offload")]
446 pub fn has_offload_engine(&self) -> bool {
447 self.offload_engine.is_some()
448 }
449
450 #[cfg(feature = "kvbm-offload")]
457 pub fn tick_offload_engine(&mut self, now_ms: f64) {
458 let Some(engine_arc) = self.offload_engine.clone() else {
459 return;
460 };
461 let prepared = {
462 let engine = engine_arc.lock().expect("offload engine mutex poisoned");
463 engine.prepare_tick_for_kv_manager(now_ms)
464 };
465 self.publish_g2_router_events(prepared.router_events);
466 let released_g1_slots = engine_arc
467 .lock()
468 .expect("offload engine mutex poisoned")
469 .acknowledge_tick_for_kv_manager(prepared.acknowledgement)
470 .expect("freshly prepared offload advance must acknowledge");
471 if released_g1_slots > 0 {
472 self.bump_capacity_generation(released_g1_slots);
473 }
474 }
475
476 #[cfg(feature = "kvbm-offload")]
480 pub fn earliest_offload_deadline(&self) -> Option<f64> {
481 let engine_arc = self.offload_engine.as_ref()?;
482 let engine = engine_arc.lock().expect("offload engine mutex poisoned");
483 engine.earliest_pending_deadline()
484 }
485
486 #[cfg(feature = "kvbm-offload")]
487 pub(crate) fn refresh_offload_dependency(
488 &self,
489 dependency: OffloadDependency,
490 ) -> Option<OffloadDependency> {
491 let engine_arc = self.offload_engine.as_ref()?;
492 let engine = engine_arc.lock().expect("offload engine mutex poisoned");
493 engine
494 .g1_offload_dependency(dependency.offload_id)
495 .map(|(offload_id, deadline_ms)| OffloadDependency {
496 offload_id,
497 deadline_ms,
498 })
499 }
500
501 #[cfg(not(feature = "kvbm-offload"))]
502 pub(crate) fn refresh_offload_dependency(
503 &self,
504 _dependency: OffloadDependency,
505 ) -> Option<OffloadDependency> {
506 None
507 }
508
509 #[cfg(feature = "kvbm-offload")]
515 fn enqueue_evictions_to_g2(
516 &mut self,
517 evicted: &[G2OffloadBlock],
518 source_slots: Vec<MutableBlock<G1>>,
519 now_ms: Option<f64>,
520 ) -> (Vec<G2RouterEvent>, Option<G1EvictionOutcome>) {
521 let Some(engine_arc) = self.offload_engine.as_ref() else {
522 drop(source_slots);
523 return (Vec::new(), None);
524 };
525 if evicted.is_empty() {
526 drop(source_slots);
527 return (Vec::new(), None);
528 }
529 let mut engine = engine_arc.lock().expect("offload engine mutex poisoned");
530 let outcome = engine.enqueue_g1_evictions_with_metadata(evicted, source_slots, now_ms);
531 (engine.drain_g2_router_events(), outcome)
532 }
533
534 #[cfg(feature = "kvbm-offload")]
542 pub(crate) fn register_swapped_in_blocks(
543 &mut self,
544 entries: Vec<SwapInRegistrationBlock>,
545 initial_parent_hash: Option<SequenceHash>,
546 destination_slots: Vec<MutableBlock<G1>>,
547 ) -> SwapInRegistrationOutcome {
548 let total_entries = entries.len();
549 let mut stored_seq_hashes = Vec::with_capacity(total_entries);
550 let mut stored_local_hashes = Vec::with_capacity(total_entries);
551 let mut stored_token_ids = Vec::with_capacity(total_entries);
552 let mut stored_parent_hash = initial_parent_hash;
553 let mut metadata_parent_hash = initial_parent_hash;
554 let mut consumed_entries = 0usize;
555 let mut destination_slots = destination_slots.into_iter();
556
557 for entry in entries {
558 let Some(mutable) = destination_slots.next() else {
559 tracing::warn!(
560 consumed_entries,
561 entries = total_entries,
562 "kvbm-offload: swap-in registration ran out of reserved G1 slots"
563 );
564 break;
565 };
566 if self.active_full.contains_key(&entry.seq_hash) {
567 drop(mutable);
568 if !stored_seq_hashes.is_empty() {
569 self.publish_swap_in_stored_batch(
570 &mut stored_seq_hashes,
571 &mut stored_local_hashes,
572 &mut stored_token_ids,
573 stored_parent_hash,
574 );
575 }
576 stored_parent_hash = Some(entry.seq_hash);
577 metadata_parent_hash = Some(entry.seq_hash);
578 consumed_entries += 1;
579 continue;
580 }
581 let presence = self
582 .block_manager
583 .block_registry()
584 .check_presence::<G1>(&[entry.plh]);
585 if presence.first().is_some_and(|(_, p)| *p) {
586 drop(mutable);
587 if !stored_seq_hashes.is_empty() {
588 self.publish_swap_in_stored_batch(
589 &mut stored_seq_hashes,
590 &mut stored_local_hashes,
591 &mut stored_token_ids,
592 stored_parent_hash,
593 );
594 }
595 stored_parent_hash = Some(entry.seq_hash);
596 metadata_parent_hash = Some(entry.seq_hash);
597 consumed_entries += 1;
598 continue;
599 }
600 let complete = mutable
601 .stage(entry.plh, self.block_size)
602 .expect("stage failed during swap-in registration");
603 let immutable = self.block_manager.register_block(complete);
604 let block_id = immutable.block_id();
605 drop(immutable);
609 let registry_token_ids = entry.token_ids.clone();
612 if let Some(token_ids) = entry.token_ids {
613 stored_token_ids.push(token_ids);
614 }
615 self.registered_blocks.insert(
616 entry.plh,
617 RegisteredBlockInfo {
618 seq_hash: entry.seq_hash,
619 block_id,
620 parent_hash: metadata_parent_hash,
621 local_hash: entry.local_hash,
622 token_ids: registry_token_ids,
623 },
624 );
625 stored_seq_hashes.push(entry.seq_hash);
626 if let Some(local_hash) = entry.local_hash {
627 stored_local_hashes.push(local_hash);
628 }
629 metadata_parent_hash = Some(entry.seq_hash);
630 consumed_entries += 1;
631 }
632
633 if !stored_seq_hashes.is_empty() {
634 self.publish_swap_in_stored_batch(
635 &mut stored_seq_hashes,
636 &mut stored_local_hashes,
637 &mut stored_token_ids,
638 stored_parent_hash,
639 );
640 }
641
642 SwapInRegistrationOutcome { consumed_entries }
643 }
644
645 #[cfg(feature = "kvbm-offload")]
646 fn publish_swap_in_stored_batch(
647 &mut self,
648 stored_seq_hashes: &mut Vec<SequenceHash>,
649 stored_local_hashes: &mut Vec<BlockHash>,
650 stored_token_ids: &mut Vec<Vec<u32>>,
651 parent_hash: Option<SequenceHash>,
652 ) {
653 if stored_seq_hashes.is_empty() {
654 return;
655 }
656
657 let full_blocks = std::mem::take(stored_seq_hashes);
658 let local_hashes = if stored_local_hashes.len() == full_blocks.len() {
659 std::mem::take(stored_local_hashes)
660 } else {
661 stored_local_hashes.clear();
662 Vec::new()
663 };
664 let token_ids = if stored_token_ids.len() == full_blocks.len() {
665 Some(std::mem::take(stored_token_ids))
666 } else {
667 stored_token_ids.clear();
668 None
669 };
670
671 self.publish_kv_event(full_blocks, &local_hashes, parent_hash, true, token_ids);
672 }
673
674 #[cfg(feature = "kvbm-offload")]
688 pub fn try_batch_swap_in(
689 &mut self,
690 remaining_plhs: &[PositionalLineageHash],
691 prefix_pins: Vec<ImmutableBlock<G1>>,
692 now_ms: Option<f64>,
693 ) -> BatchSwapInOutcome {
694 let Some(engine_arc) = self.offload_engine.clone() else {
695 return BatchSwapInOutcome::NoHits;
696 };
697 let Some(prepared) = ({
698 let mut engine = engine_arc.lock().expect("offload engine mutex poisoned");
699 engine.prepare_onboard_prefix(remaining_plhs)
700 }) else {
701 return BatchSwapInOutcome::NoHits;
702 };
703 let block_count = prepared.block_count();
704 let destination_slots = match self.reserve_swap_in_destination_slots(block_count) {
710 SwapInSlotReservation::Reserved(slots) => slots,
711 SwapInSlotReservation::BlockedOnG1Offload(dependency) => {
712 return BatchSwapInOutcome::BlockedOnG1Offload(dependency);
713 }
714 SwapInSlotReservation::NoCapacity => return BatchSwapInOutcome::NoHits,
715 };
716 let handle = {
717 let mut engine = engine_arc.lock().expect("offload engine mutex poisoned");
718 engine.start_onboard_prefix(prepared, destination_slots, prefix_pins, now_ms)
719 };
720 BatchSwapInOutcome::Scheduled { handle }
721 }
722
723 #[cfg(feature = "kvbm-offload")]
724 pub(crate) fn cancel_swap_in(&mut self, id: OffloadId) -> bool {
725 self.offload_engine.as_ref().is_some_and(|engine| {
726 engine
727 .lock()
728 .expect("offload engine mutex poisoned")
729 .cancel_swap_in(id)
730 })
731 }
732
733 #[cfg(feature = "kvbm-offload")]
734 pub(crate) fn register_completed_swap_in(
735 &mut self,
736 id: OffloadId,
737 entries: Vec<SwapInRegistrationBlock>,
738 parent_hash: Option<SequenceHash>,
739 ) -> SwapInRegistrationOutcome {
740 let (destination_slots, prefix_pins) = self
741 .offload_engine
742 .as_ref()
743 .and_then(|engine| {
744 engine
745 .lock()
746 .expect("offload engine mutex poisoned")
747 .take_completed_swap_in(id)
748 })
749 .expect("completed swap-in lease must retain its G1 resources");
750 let outcome = self.register_swapped_in_blocks(entries, parent_hash, destination_slots);
751 drop(prefix_pins);
752 outcome
753 }
754
755 #[cfg(feature = "kvbm-offload")]
759 pub(crate) fn try_pin_g1_prefix(
760 &mut self,
761 prefix_plhs: &[PositionalLineageHash],
762 ) -> Option<Vec<ImmutableBlock<G1>>> {
763 if prefix_plhs.is_empty() {
764 return Some(Vec::new());
765 }
766
767 let pins = self.block_manager.match_blocks(prefix_plhs);
768 if pins.len() == prefix_plhs.len() {
769 Some(pins)
770 } else {
771 None
772 }
773 }
774
775 fn publish_kv_event(
779 &mut self,
780 full_blocks: Vec<SequenceHash>,
781 local_hashes: &[BlockHash],
782 parent_hash: Option<u64>,
783 is_store: bool,
784 token_ids: Option<Vec<Vec<u32>>>,
785 ) {
786 self.publish_kv_event_for_tier(
787 full_blocks,
788 local_hashes,
789 parent_hash,
790 is_store,
791 token_ids,
792 StorageTier::Device,
793 );
794 }
795
796 fn publish_kv_event_for_tier(
797 &mut self,
798 full_blocks: Vec<SequenceHash>,
799 local_hashes: &[BlockHash],
800 parent_hash: Option<u64>,
801 is_store: bool,
802 token_ids: Option<Vec<Vec<u32>>>,
803 storage_tier: StorageTier,
804 ) {
805 if full_blocks.is_empty() {
806 return;
807 }
808
809 kv_cache_trace::log_vllm_trace(
810 if is_store { "allocation" } else { "eviction" },
811 self.dp_rank,
812 self.block_size,
813 self.num_active_blocks(),
814 self.num_inactive_blocks(),
815 self.max_capacity,
816 );
817
818 if self.kv_event_publishers.is_empty() {
819 return;
820 }
821
822 let event_data = if is_store {
823 debug_assert!(
827 local_hashes.is_empty() || local_hashes.len() == full_blocks.len(),
828 "publish_kv_event: local_hashes must be empty or 1:1 with full_blocks ({} vs {})",
829 local_hashes.len(),
830 full_blocks.len(),
831 );
832
833 KvCacheEventData::Stored(KvCacheStoreData {
834 parent_hash: parent_hash.map(ExternalSequenceBlockHash),
835 start_position: None,
836 blocks: full_blocks
837 .into_iter()
838 .enumerate()
839 .map(|(i, global_hash)| KvCacheStoredBlockData {
840 block_hash: ExternalSequenceBlockHash(global_hash),
841 tokens_hash: LocalBlockHash(
842 local_hashes.get(i).copied().unwrap_or_default(),
843 ),
844 mm_extra_info: None,
845 })
846 .collect(),
847 })
848 } else {
849 KvCacheEventData::Removed(KvCacheRemoveData {
850 block_hashes: full_blocks
851 .into_iter()
852 .map(ExternalSequenceBlockHash)
853 .collect(),
854 })
855 };
856
857 let event_id = self.next_event_id;
858 self.next_event_id += 1;
859
860 let event = KvCacheEvent {
861 event_id,
862 data: event_data,
863 dp_rank: self.dp_rank,
864 };
865
866 if let Err(e) = self.kv_event_publishers.publish_with_storage_tier(
867 event,
868 token_ids.as_deref(),
869 storage_tier,
870 ) {
871 tracing::warn!("Failed to publish KV event: {e}");
872 }
873 }
874
875 #[cfg(feature = "kvbm-offload")]
876 fn publish_g2_router_events(&mut self, events: Vec<G2RouterEvent>) {
877 for event in events {
878 match event {
879 G2RouterEvent::Stored(meta) => {
880 let local_hashes = meta.local_hash.into_iter().collect::<Vec<_>>();
881 self.publish_kv_event_for_tier(
882 vec![meta.seq_hash],
883 &local_hashes,
884 meta.parent_hash,
885 true,
886 meta.token_ids.map(|ids| vec![ids]),
887 StorageTier::HostPinned,
888 );
889 }
890 G2RouterEvent::Removed { seq_hash } => {
891 self.publish_kv_event_for_tier(
892 vec![seq_hash],
893 &[],
894 None,
895 false,
896 None,
897 StorageTier::HostPinned,
898 );
899 }
900 }
901 }
902 }
903
904 #[cfg_attr(feature = "profile", inline(never))]
910 pub(crate) fn process(&mut self, event: &MoveBlock) -> G1Acquire<usize> {
911 match event {
912 MoveBlock::Use(blocks, local_hashes, plhs, token_ids, parent) => self.process_use(
913 blocks,
914 local_hashes,
915 plhs,
916 token_ids.as_deref(),
917 parent.as_ref(),
918 None,
919 ),
920 MoveBlock::Deref(hashes) => {
921 self.process_deref(hashes);
922 G1Acquire::Ready(1)
923 }
924 MoveBlock::Promote(uuid, seq_hash, parent_hash, local_hash, plh, token_ids) => {
925 self.process_promote(
926 *uuid,
927 *seq_hash,
928 *parent_hash,
929 *local_hash,
930 *plh,
931 token_ids.clone(),
932 );
933 G1Acquire::Ready(1)
934 }
935 }
936 }
937
938 pub(crate) fn reserve_decode_blocks(
939 &mut self,
940 count: usize,
941 ) -> G1Acquire<DecodeBlockReservation> {
942 let mut attempted_generation = self.capacity_generation;
943 let mut retried = false;
944 loop {
945 match self.allocate_use_slots(count, None) {
946 G1Acquire::Ready(blocks) => {
947 return G1Acquire::Ready(DecodeBlockReservation { blocks });
948 }
949 G1Acquire::CapacityExhausted => return G1Acquire::CapacityExhausted,
950 G1Acquire::BlockedOnOffload {
951 offload_id,
952 deadline_ms,
953 } => {
954 return G1Acquire::BlockedOnOffload {
955 offload_id,
956 deadline_ms,
957 };
958 }
959 G1Acquire::RetryNow {
960 capacity_generation,
961 released_slots,
962 } => {
963 self.validate_retry_witness(
964 attempted_generation,
965 retried,
966 capacity_generation,
967 released_slots,
968 );
969 attempted_generation = capacity_generation;
970 retried = true;
971 }
972 }
973 }
974 }
975
976 fn bump_capacity_generation(&mut self, released_slots: usize) -> u64 {
977 assert!(released_slots > 0, "capacity increase must release slots");
978 let released_slots =
979 u64::try_from(released_slots).expect("released G1 slot count does not fit in u64");
980 self.capacity_generation = self
981 .capacity_generation
982 .checked_add(released_slots)
983 .expect("G1 capacity generation exhausted");
984 self.capacity_generation
985 }
986
987 fn allocate_use_slots(
988 &mut self,
989 count: usize,
990 eviction_now_ms: Option<f64>,
991 ) -> G1Acquire<Vec<MutableBlock<G1>>> {
992 match self.reserve_g1_slots(count, eviction_now_ms) {
993 G1Acquire::Ready(reservation) => {
994 self.handle_evictions(reservation.evicted_plhs);
995 G1Acquire::Ready(reservation.blocks)
996 }
997 G1Acquire::CapacityExhausted => G1Acquire::CapacityExhausted,
998 G1Acquire::BlockedOnOffload {
999 offload_id,
1000 deadline_ms,
1001 } => G1Acquire::BlockedOnOffload {
1002 offload_id,
1003 deadline_ms,
1004 },
1005 G1Acquire::RetryNow {
1006 capacity_generation,
1007 released_slots,
1008 } => G1Acquire::RetryNow {
1009 capacity_generation,
1010 released_slots,
1011 },
1012 }
1013 }
1014
1015 fn reserve_g1_slots(
1016 &mut self,
1017 count: usize,
1018 eviction_now_ms: Option<f64>,
1019 ) -> G1Acquire<G1SlotReservation> {
1020 #[cfg(not(feature = "kvbm-offload"))]
1021 let _ = eviction_now_ms;
1022 if count == 0 {
1023 return G1Acquire::Ready(G1SlotReservation {
1024 blocks: Vec::new(),
1025 evicted_plhs: Vec::new(),
1026 });
1027 }
1028 let Some((blocks, evicted_plhs)) = self.block_manager.allocate_blocks_with_evictions(count)
1029 else {
1030 return G1Acquire::CapacityExhausted;
1031 };
1032 if !self.should_block_on_g1_offload(&evicted_plhs) {
1033 return G1Acquire::Ready(G1SlotReservation {
1034 blocks,
1035 evicted_plhs,
1036 });
1037 }
1038
1039 #[cfg(feature = "kvbm-offload")]
1040 {
1041 let outcome = self
1042 .handle_evictions_with_source_slots_at(evicted_plhs, blocks, eviction_now_ms)
1043 .expect("G1 offload-enabled eviction must return a dependency outcome");
1044 match outcome {
1045 G1EvictionOutcome::BlockedOnOffload {
1046 offload_id,
1047 deadline_ms,
1048 } => G1Acquire::BlockedOnOffload {
1049 offload_id,
1050 deadline_ms,
1051 },
1052 G1EvictionOutcome::RetryNow { released_slots } => {
1053 let capacity_generation = self.bump_capacity_generation(released_slots);
1054 G1Acquire::RetryNow {
1055 capacity_generation,
1056 released_slots,
1057 }
1058 }
1059 }
1060 }
1061
1062 #[cfg(not(feature = "kvbm-offload"))]
1063 unreachable!("G1 offload blocking is disabled without kvbm-offload")
1064 }
1065
1066 fn allocate_unpublished_blocks(
1067 &mut self,
1068 count: usize,
1069 eviction_now_ms: Option<f64>,
1070 ) -> G1Acquire<Vec<MutableBlock<G1>>> {
1071 self.allocate_use_slots(count, eviction_now_ms)
1072 }
1073
1074 fn acquire_existing_full(
1075 &mut self,
1076 seq_hash: SequenceHash,
1077 plh: PositionalLineageHash,
1078 ) -> Option<ImmutableBlock<G1>> {
1079 if let Some(active) = self.active_full.get(&seq_hash) {
1080 return Some(active.handle.clone());
1081 }
1082 self.block_manager.match_blocks(&[plh]).into_iter().next()
1083 }
1084
1085 fn commit_active_full(
1086 &mut self,
1087 candidate: MutableBlock<G1>,
1088 metadata: FullBlockMetadata,
1089 ) -> FullBlockCommit {
1090 let FullBlockMetadata {
1091 seq_hash,
1092 plh,
1093 parent_hash,
1094 local_hash,
1095 token_ids,
1096 } = metadata;
1097
1098 if let Some(canonical) = self.acquire_existing_full(seq_hash, plh) {
1099 drop(candidate);
1100 self.insert_or_retain_active_full(seq_hash, canonical);
1101 return FullBlockCommit::Reused;
1102 }
1103
1104 let candidate_block_id = candidate.block_id();
1105 let complete = candidate
1106 .stage(plh, self.block_size)
1107 .expect("full block stage failed");
1108 let canonical = self.block_manager.register_block(complete);
1109 let canonical_block_id = canonical.block_id();
1110 self.insert_or_retain_active_full(seq_hash, canonical);
1111
1112 if canonical_block_id != candidate_block_id {
1113 return FullBlockCommit::Reused;
1114 }
1115
1116 let previous = self.registered_blocks.insert(
1117 plh,
1118 RegisteredBlockInfo {
1119 seq_hash,
1120 block_id: canonical_block_id,
1121 parent_hash,
1122 local_hash,
1123 token_ids,
1124 },
1125 );
1126 debug_assert!(previous.is_none());
1127 FullBlockCommit::Stored
1128 }
1129
1130 pub(crate) fn reserve_destination_at(
1131 &mut self,
1132 sequence: &ActiveSequence,
1133 eviction_now_ms: Option<f64>,
1134 ) -> G1Acquire<VllmDestinationReservation> {
1135 let layout = sequence.prepare_allocation(sequence.num_input_tokens());
1136 let Some(MoveBlock::Use(blocks, _, plhs, _, _)) = layout.as_ref() else {
1137 return G1Acquire::Ready(VllmDestinationReservation {
1138 cached_prefix: Vec::new(),
1139 unpublished_blocks: Vec::new(),
1140 layout,
1141 });
1142 };
1143
1144 let mut cached_prefix = Vec::new();
1145 for (plh_idx, block) in blocks.iter().enumerate() {
1146 let UniqueBlock::FullBlock(seq_hash) = block else {
1147 break;
1148 };
1149 let plh = plhs[plh_idx];
1150 let Some(handle) = self.acquire_existing_full(*seq_hash, plh) else {
1151 break;
1152 };
1153 cached_prefix.push((*seq_hash, handle));
1154 }
1155
1156 let count = blocks.len() - cached_prefix.len();
1157 let mut attempted_generation = self.capacity_generation;
1158 let mut retried = false;
1159 loop {
1160 match self.allocate_unpublished_blocks(count, eviction_now_ms) {
1161 G1Acquire::Ready(unpublished_blocks) => {
1162 return G1Acquire::Ready(VllmDestinationReservation {
1163 cached_prefix,
1164 unpublished_blocks,
1165 layout,
1166 });
1167 }
1168 G1Acquire::CapacityExhausted => return G1Acquire::CapacityExhausted,
1169 G1Acquire::BlockedOnOffload {
1170 offload_id,
1171 deadline_ms,
1172 } => {
1173 return G1Acquire::BlockedOnOffload {
1174 offload_id,
1175 deadline_ms,
1176 };
1177 }
1178 G1Acquire::RetryNow {
1179 capacity_generation,
1180 released_slots,
1181 } => {
1182 self.validate_retry_witness(
1183 attempted_generation,
1184 retried,
1185 capacity_generation,
1186 released_slots,
1187 );
1188 attempted_generation = capacity_generation;
1189 retried = true;
1190 }
1191 }
1192 }
1193 }
1194
1195 pub(crate) fn activate_destination(&mut self, reservation: VllmDestinationReservation) {
1201 let VllmDestinationReservation {
1202 cached_prefix,
1203 unpublished_blocks,
1204 layout,
1205 } = reservation;
1206 let Some(MoveBlock::Use(blocks, local_hashes, plhs, token_ids, parent)) = layout else {
1207 debug_assert!(cached_prefix.is_empty());
1208 debug_assert!(unpublished_blocks.is_empty());
1209 return;
1210 };
1211
1212 let prefix_len = cached_prefix.len();
1213 let full_blocks = blocks
1214 .iter()
1215 .filter(|block| matches!(block, UniqueBlock::FullBlock(_)))
1216 .count();
1217 assert_eq!(
1218 plhs.len(),
1219 full_blocks,
1220 "destination PLH count must match full block count"
1221 );
1222 assert!(
1223 local_hashes.is_empty() || local_hashes.len() == full_blocks,
1224 "destination local hash count must be empty or match full block count"
1225 );
1226 assert!(
1227 token_ids
1228 .as_ref()
1229 .is_none_or(|all_ids| all_ids.len() == full_blocks),
1230 "destination token metadata count must match full block count"
1231 );
1232 assert!(
1233 prefix_len <= blocks.len(),
1234 "destination cached prefix exceeds block layout"
1235 );
1236 assert_eq!(
1237 unpublished_blocks.len(),
1238 blocks.len() - prefix_len,
1239 "destination unpublished block count must cover the uncached layout"
1240 );
1241 for (block, (reserved_hash, _)) in blocks.iter().zip(&cached_prefix) {
1242 let UniqueBlock::FullBlock(layout_hash) = block else {
1243 panic!("destination cached prefix cannot contain a partial block");
1244 };
1245 assert_eq!(
1246 layout_hash, reserved_hash,
1247 "destination cached prefix hash must match block layout"
1248 );
1249 }
1250
1251 let mut cached_prefix = cached_prefix.into_iter();
1252 let mut unpublished_blocks = unpublished_blocks.into_iter();
1253 let mut stored_seq_hashes = Vec::new();
1254 let mut stored_local_hashes = Vec::new();
1255 let mut stored_token_ids = token_ids.as_ref().map(|_| Vec::new());
1256 let mut first_store_parent = None;
1257 let mut metadata_parent_hash = match parent {
1258 Some(UniqueBlock::FullBlock(hash)) => Some(hash),
1259 Some(UniqueBlock::PartialBlock(_)) => panic!("parent block cannot be partial"),
1260 None => None,
1261 };
1262 let mut plh_idx = 0usize;
1263
1264 for (block_idx, block) in blocks.into_iter().enumerate() {
1265 match block {
1266 UniqueBlock::FullBlock(seq_hash) => {
1267 let full_idx = plh_idx;
1268 let plh = plhs[plh_idx];
1269 plh_idx += 1;
1270 if block_idx < prefix_len {
1271 let (_, handle) = cached_prefix
1272 .next()
1273 .expect("reserved prefix handle must exist");
1274 self.insert_or_retain_active_full(seq_hash, handle);
1275 metadata_parent_hash = Some(seq_hash);
1276 continue;
1277 }
1278
1279 let mutable = unpublished_blocks
1280 .next()
1281 .expect("reserved destination block must exist");
1282 let local_hash = local_hashes.get(full_idx).copied();
1283 let block_token_ids = token_ids
1284 .as_ref()
1285 .and_then(|all_ids| all_ids.get(full_idx).cloned());
1286 let commit = self.commit_active_full(
1287 mutable,
1288 FullBlockMetadata {
1289 seq_hash,
1290 plh,
1291 parent_hash: metadata_parent_hash,
1292 local_hash,
1293 token_ids: block_token_ids.clone(),
1294 },
1295 );
1296 if commit == FullBlockCommit::Reused {
1297 if !stored_seq_hashes.is_empty() {
1298 let local_hashes = std::mem::take(&mut stored_local_hashes);
1299 self.publish_kv_event(
1300 std::mem::take(&mut stored_seq_hashes),
1301 &local_hashes,
1302 first_store_parent,
1303 true,
1304 stored_token_ids.take(),
1305 );
1306 first_store_parent = None;
1307 stored_token_ids = token_ids.as_ref().map(|_| Vec::new());
1308 }
1309 metadata_parent_hash = Some(seq_hash);
1310 continue;
1311 }
1312 if stored_seq_hashes.is_empty() {
1313 first_store_parent = metadata_parent_hash;
1314 }
1315 stored_seq_hashes.push(seq_hash);
1316 if let Some(local_hash) = local_hash {
1317 stored_local_hashes.push(local_hash);
1318 }
1319 if let (Some(stored), Some(block_token_ids)) =
1320 (stored_token_ids.as_mut(), block_token_ids)
1321 {
1322 stored.push(block_token_ids);
1323 }
1324 metadata_parent_hash = Some(seq_hash);
1325 }
1326 UniqueBlock::PartialBlock(uuid) => {
1327 let mutable = unpublished_blocks
1328 .next()
1329 .expect("reserved destination partial block must exist");
1330 let previous = self.active_partial.insert(uuid, mutable);
1331 debug_assert!(previous.is_none());
1332 }
1333 }
1334 }
1335
1336 self.publish_kv_event(
1337 stored_seq_hashes,
1338 &stored_local_hashes,
1339 first_store_parent,
1340 true,
1341 stored_token_ids,
1342 );
1343 }
1344
1345 pub fn process_decode_signal(
1346 &mut self,
1347 event: &MoveBlock,
1348 reservation: &mut DecodeBlockReservation,
1349 ) {
1350 match event {
1351 MoveBlock::Use(blocks, local_hashes, plhs, token_ids, parent) => {
1352 let outcome = self.process_use(
1353 blocks,
1354 local_hashes,
1355 plhs,
1356 token_ids.as_deref(),
1357 parent.as_ref(),
1358 Some(reservation),
1359 );
1360 match outcome {
1361 G1Acquire::Ready(allocated) => assert_eq!(
1362 allocated,
1363 blocks.len(),
1364 "reserved decode allocation must commit every block"
1365 ),
1366 G1Acquire::CapacityExhausted
1367 | G1Acquire::BlockedOnOffload { .. }
1368 | G1Acquire::RetryNow { .. } => {
1369 panic!("reserved decode allocation must be infallible")
1370 }
1371 }
1372 }
1373 _ => {
1374 assert!(
1375 matches!(self.process(event), G1Acquire::Ready(_)),
1376 "non-Use decode signal must be infallible"
1377 );
1378 }
1379 }
1380 }
1381
1382 #[cfg(feature = "kvbm-offload")]
1383 fn reserve_swap_in_destination_slots(&mut self, count: usize) -> SwapInSlotReservation {
1384 let mut attempted_generation = self.capacity_generation;
1385 let mut retried = false;
1386 loop {
1387 match self.allocate_use_slots(count, None) {
1388 G1Acquire::Ready(slots) => return SwapInSlotReservation::Reserved(slots),
1389 G1Acquire::CapacityExhausted => return SwapInSlotReservation::NoCapacity,
1390 G1Acquire::BlockedOnOffload {
1391 offload_id,
1392 deadline_ms,
1393 } => {
1394 return SwapInSlotReservation::BlockedOnG1Offload(OffloadDependency {
1395 offload_id,
1396 deadline_ms,
1397 });
1398 }
1399 G1Acquire::RetryNow {
1400 capacity_generation,
1401 released_slots,
1402 } => {
1403 self.validate_retry_witness(
1404 attempted_generation,
1405 retried,
1406 capacity_generation,
1407 released_slots,
1408 );
1409 attempted_generation = capacity_generation;
1410 retried = true;
1411 }
1412 }
1413 }
1414 }
1415
1416 #[cfg(feature = "kvbm-offload")]
1417 fn should_block_on_g1_offload(&self, evicted_plhs: &[PositionalLineageHash]) -> bool {
1418 self.offload_engine.is_some()
1419 && evicted_plhs
1420 .iter()
1421 .any(|plh| self.registered_blocks.contains_key(plh))
1422 }
1423
1424 #[cfg(not(feature = "kvbm-offload"))]
1425 fn should_block_on_g1_offload(&self, _evicted_plhs: &[PositionalLineageHash]) -> bool {
1426 false
1427 }
1428
1429 fn process_use(
1430 &mut self,
1431 blocks: &[UniqueBlock],
1432 local_hashes: &[BlockHash],
1433 plhs: &[PositionalLineageHash],
1434 token_ids: Option<&[Vec<u32>]>,
1435 parent: Option<&UniqueBlock>,
1436 mut reservation: Option<&mut DecodeBlockReservation>,
1437 ) -> G1Acquire<usize> {
1438 let mut attempted_generation = self.capacity_generation;
1439 let mut retried = false;
1440
1441 loop {
1442 let outcome = self.prepare_use(
1443 blocks,
1444 local_hashes,
1445 plhs,
1446 token_ids,
1447 parent,
1448 reservation.as_deref_mut(),
1449 );
1450 match outcome {
1451 G1Acquire::Ready(transaction) => {
1452 self.commit_use(transaction);
1453 return G1Acquire::Ready(blocks.len());
1454 }
1455 G1Acquire::CapacityExhausted => return G1Acquire::CapacityExhausted,
1456 G1Acquire::BlockedOnOffload {
1457 offload_id,
1458 deadline_ms,
1459 } => {
1460 return G1Acquire::BlockedOnOffload {
1461 offload_id,
1462 deadline_ms,
1463 };
1464 }
1465 G1Acquire::RetryNow {
1466 capacity_generation,
1467 released_slots,
1468 } => {
1469 self.validate_retry_witness(
1470 attempted_generation,
1471 retried,
1472 capacity_generation,
1473 released_slots,
1474 );
1475 attempted_generation = capacity_generation;
1476 retried = true;
1477 }
1478 }
1479 }
1480 }
1481
1482 fn validate_retry_witness(
1483 &self,
1484 attempted_generation: u64,
1485 retried: bool,
1486 witness_generation: u64,
1487 released_slots: usize,
1488 ) {
1489 assert!(!retried, "one atomic G1 reservation retried more than once");
1490 assert!(released_slots > 0, "RetryNow released zero G1 slots");
1491 assert!(
1492 witness_generation > attempted_generation,
1493 "RetryNow generation {witness_generation} is not newer than attempted generation {attempted_generation}"
1494 );
1495 assert_eq!(
1496 witness_generation, self.capacity_generation,
1497 "RetryNow generation does not match current G1 capacity generation"
1498 );
1499 }
1500
1501 fn prepare_use<'a>(
1502 &mut self,
1503 blocks: &[UniqueBlock],
1504 local_hashes: &'a [BlockHash],
1505 plhs: &'a [PositionalLineageHash],
1506 token_ids: Option<&'a [Vec<u32>]>,
1507 parent: Option<&'a UniqueBlock>,
1508 mut reservation: Option<&mut DecodeBlockReservation>,
1509 ) -> G1Acquire<UseTransaction<'a>> {
1510 let expected_full_blocks = blocks
1511 .iter()
1512 .filter(|block| matches!(block, UniqueBlock::FullBlock(_)))
1513 .count();
1514 assert_eq!(
1515 plhs.len(),
1516 expected_full_blocks,
1517 "Use: plhs.len() must match FullBlock count in blocks"
1518 );
1519 assert!(
1520 local_hashes.is_empty() || local_hashes.len() == expected_full_blocks,
1521 "Use: local_hashes must be empty or match FullBlock count ({} vs {})",
1522 local_hashes.len(),
1523 expected_full_blocks,
1524 );
1525 assert!(
1526 token_ids.is_none_or(|ids| ids.len() == expected_full_blocks),
1527 "Use: token_ids must be absent or match FullBlock count ({} vs {})",
1528 token_ids.map_or(0, |ids| ids.len()),
1529 expected_full_blocks,
1530 );
1531
1532 let mut prepared = Vec::with_capacity(blocks.len());
1541 let mut nonlocal_plhs = Vec::new();
1542 let mut fresh_blocks = 0usize;
1543 let mut fresh_full_blocks = 0usize;
1544 let mut full_idx = 0usize;
1545 for block in blocks {
1546 match block {
1547 UniqueBlock::FullBlock(seq_hash) => {
1548 if self.active_full.contains_key(seq_hash) {
1549 prepared.push(PreparedUseBlock::ExistingActiveFull {
1550 seq_hash: *seq_hash,
1551 });
1552 } else {
1553 prepared.push(PreparedUseBlock::PendingNonLocalFull {
1554 seq_hash: *seq_hash,
1555 full_idx,
1556 });
1557 if nonlocal_plhs.is_empty() {
1561 nonlocal_plhs.reserve_exact(expected_full_blocks - full_idx);
1562 }
1563 nonlocal_plhs.push(plhs[full_idx]);
1564 }
1565 full_idx += 1;
1566 }
1567 UniqueBlock::PartialBlock(uuid) => {
1568 if self.active_partial.contains_key(uuid) {
1569 prepared.push(PreparedUseBlock::ExistingPartial);
1570 } else {
1571 fresh_blocks += 1;
1572 prepared.push(PreparedUseBlock::FreshPartial {
1573 uuid: *uuid,
1574 mutable: None,
1575 });
1576 }
1577 }
1578 }
1579 }
1580
1581 if !nonlocal_plhs.is_empty() {
1582 let mut nonlocal_matches = self
1583 .block_manager
1584 .match_blocks_scattered(&nonlocal_plhs)
1585 .into_iter();
1586 for entry in &mut prepared {
1587 let PreparedUseBlock::PendingNonLocalFull { seq_hash, full_idx } = entry else {
1588 continue;
1589 };
1590 let seq_hash = *seq_hash;
1591 let full_idx = *full_idx;
1592 *entry = if let Some(handle) = nonlocal_matches
1593 .next()
1594 .expect("scattered match result must align with non-local full blocks")
1595 {
1596 PreparedUseBlock::ExistingMatchedFull { seq_hash, handle }
1597 } else {
1598 fresh_blocks += 1;
1599 fresh_full_blocks += 1;
1600 PreparedUseBlock::FreshFull {
1601 seq_hash,
1602 full_idx,
1603 mutable: None,
1604 }
1605 };
1606 }
1607 assert!(
1608 nonlocal_matches.next().is_none(),
1609 "scattered match returned more entries than non-local full blocks"
1610 );
1611 }
1612
1613 let mut evicted_plhs = Vec::new();
1614
1615 if let Some(reservation) = reservation.as_mut() {
1616 if reservation.len() < fresh_blocks {
1617 return G1Acquire::CapacityExhausted;
1618 }
1619 for entry in &mut prepared {
1620 match entry {
1621 PreparedUseBlock::FreshFull { mutable, .. }
1622 | PreparedUseBlock::FreshPartial { mutable, .. } => {
1623 *mutable = Some(
1624 reservation
1625 .take()
1626 .expect("prechecked decode reservation must contain a slot"),
1627 );
1628 }
1629 PreparedUseBlock::ExistingActiveFull { .. }
1630 | PreparedUseBlock::ExistingMatchedFull { .. }
1631 | PreparedUseBlock::ExistingPartial => {}
1632 PreparedUseBlock::PendingNonLocalFull { .. } => {
1633 unreachable!("non-local full block must be resolved before reservation")
1634 }
1635 }
1636 }
1637 } else {
1638 let reservation = match self.reserve_g1_slots(fresh_blocks, None) {
1639 G1Acquire::Ready(reservation) => reservation,
1640 G1Acquire::CapacityExhausted => return G1Acquire::CapacityExhausted,
1641 G1Acquire::BlockedOnOffload {
1642 offload_id,
1643 deadline_ms,
1644 } => {
1645 return G1Acquire::BlockedOnOffload {
1646 offload_id,
1647 deadline_ms,
1648 };
1649 }
1650 G1Acquire::RetryNow {
1651 capacity_generation,
1652 released_slots,
1653 } => {
1654 return G1Acquire::RetryNow {
1655 capacity_generation,
1656 released_slots,
1657 };
1658 }
1659 };
1660 evicted_plhs = reservation.evicted_plhs;
1661 let mut slots = reservation.blocks.into_iter();
1662 for entry in &mut prepared {
1663 match entry {
1664 PreparedUseBlock::FreshFull { mutable, .. }
1665 | PreparedUseBlock::FreshPartial { mutable, .. } => {
1666 *mutable = Some(
1667 slots
1668 .next()
1669 .expect("atomic Use reservation returned too few slots"),
1670 );
1671 }
1672 PreparedUseBlock::ExistingActiveFull { .. }
1673 | PreparedUseBlock::ExistingMatchedFull { .. }
1674 | PreparedUseBlock::ExistingPartial => {}
1675 PreparedUseBlock::PendingNonLocalFull { .. } => {
1676 unreachable!("non-local full block must be resolved before reservation")
1677 }
1678 }
1679 }
1680 assert!(
1681 slots.next().is_none(),
1682 "atomic Use reservation returned too many slots"
1683 );
1684 }
1685
1686 G1Acquire::Ready(UseTransaction {
1687 signal: UseSignalRef {
1688 local_hashes,
1689 plhs,
1690 token_ids,
1691 parent,
1692 },
1693 prepared,
1694 fresh_full_blocks,
1695 evicted_plhs,
1696 })
1697 }
1698
1699 fn commit_use(&mut self, transaction: UseTransaction<'_>) {
1700 let UseTransaction {
1701 signal,
1702 mut prepared,
1703 fresh_full_blocks,
1704 evicted_plhs,
1705 } = transaction;
1706
1707 let mut completed_blocks = Vec::with_capacity(fresh_full_blocks);
1712 let mut candidate_block_ids = Vec::with_capacity(fresh_full_blocks);
1713 for entry in &mut prepared {
1714 if let PreparedUseBlock::FreshFull {
1715 full_idx, mutable, ..
1716 } = entry
1717 {
1718 let mutable = mutable
1719 .take()
1720 .expect("committing Use must own every fresh full slot");
1721 candidate_block_ids.push(mutable.block_id());
1722 let complete = mutable
1723 .stage(signal.plhs[*full_idx], self.block_size)
1724 .expect("Use full block stage failed");
1725 completed_blocks.push(complete);
1726 }
1727 }
1728 let registered_blocks = self.block_manager.register_blocks(completed_blocks);
1729 assert_eq!(
1730 candidate_block_ids.len(),
1731 fresh_full_blocks,
1732 "prepared fresh full count must match staged candidate IDs"
1733 );
1734 assert_eq!(
1735 candidate_block_ids.len(),
1736 registered_blocks.len(),
1737 "fresh candidate IDs must align with batch registration results"
1738 );
1739 let mut fresh_registrations = candidate_block_ids.into_iter().zip(registered_blocks);
1740
1741 let mut metadata_parent_hash = match signal.parent {
1742 None => None,
1743 Some(UniqueBlock::FullBlock(seq_hash)) => Some(*seq_hash),
1744 Some(UniqueBlock::PartialBlock(_)) => panic!("parent block cannot be partial"),
1745 };
1746 let mut first_store_parent = metadata_parent_hash;
1747 let mut blocks_stored = Vec::<SequenceHash>::new();
1748 let mut stored_local_hashes = Vec::<BlockHash>::new();
1749 let mut stored_token_ids = signal.token_ids.map(|_| Vec::<Vec<u32>>::new());
1750
1751 for entry in prepared {
1752 match entry {
1753 PreparedUseBlock::ExistingActiveFull { seq_hash } => {
1754 if !blocks_stored.is_empty() {
1755 let hashes = std::mem::take(&mut blocks_stored);
1756 let local_hashes = std::mem::take(&mut stored_local_hashes);
1757 let token_ids = stored_token_ids.as_mut().map(std::mem::take);
1758 self.publish_kv_event(
1759 hashes,
1760 &local_hashes,
1761 first_store_parent,
1762 true,
1763 token_ids,
1764 );
1765 }
1766 self.retain_active_full(seq_hash);
1767 metadata_parent_hash = Some(seq_hash);
1768 first_store_parent = metadata_parent_hash;
1769 }
1770 PreparedUseBlock::ExistingMatchedFull { seq_hash, handle } => {
1771 if !blocks_stored.is_empty() {
1772 let hashes = std::mem::take(&mut blocks_stored);
1773 let local_hashes = std::mem::take(&mut stored_local_hashes);
1774 let token_ids = stored_token_ids.as_mut().map(std::mem::take);
1775 self.publish_kv_event(
1776 hashes,
1777 &local_hashes,
1778 first_store_parent,
1779 true,
1780 token_ids,
1781 );
1782 }
1783 self.insert_or_retain_active_full(seq_hash, handle);
1784 metadata_parent_hash = Some(seq_hash);
1785 first_store_parent = metadata_parent_hash;
1786 }
1787 PreparedUseBlock::PendingNonLocalFull { .. } => {
1788 unreachable!("non-local full block must be resolved before commit")
1789 }
1790 PreparedUseBlock::ExistingPartial => {}
1791 PreparedUseBlock::FreshFull {
1792 seq_hash,
1793 full_idx,
1794 mutable,
1795 } => {
1796 if blocks_stored.is_empty() {
1797 first_store_parent = metadata_parent_hash;
1798 }
1799 let plh = signal.plhs[full_idx];
1800 assert!(
1801 mutable.is_none(),
1802 "fresh full slot must be consumed by batch staging"
1803 );
1804 let (candidate_block_id, immutable) = fresh_registrations
1805 .next()
1806 .expect("fresh full block must have a registration result");
1807 if immutable.block_id() != candidate_block_id {
1808 if !blocks_stored.is_empty() {
1815 let hashes = std::mem::take(&mut blocks_stored);
1816 let local_hashes = std::mem::take(&mut stored_local_hashes);
1817 let token_ids = stored_token_ids.as_mut().map(std::mem::take);
1818 self.publish_kv_event(
1819 hashes,
1820 &local_hashes,
1821 first_store_parent,
1822 true,
1823 token_ids,
1824 );
1825 }
1826 self.insert_or_retain_active_full(seq_hash, immutable);
1827 metadata_parent_hash = Some(seq_hash);
1828 first_store_parent = metadata_parent_hash;
1829 continue;
1830 }
1831 self.insert_or_retain_active_full(seq_hash, immutable);
1832
1833 let local_hash = signal.local_hashes.get(full_idx).copied();
1834 let registry_token_ids = signal
1835 .token_ids
1836 .and_then(|token_ids| token_ids.get(full_idx).cloned());
1837 let previous = self.registered_blocks.insert(
1838 plh,
1839 RegisteredBlockInfo {
1840 seq_hash,
1841 block_id: candidate_block_id,
1842 parent_hash: metadata_parent_hash,
1843 local_hash,
1844 token_ids: registry_token_ids,
1845 },
1846 );
1847 assert!(
1848 previous.is_none(),
1849 "fresh Use replaced registered block {plh:?}"
1850 );
1851 blocks_stored.push(seq_hash);
1852 if let Some(local_hash) = local_hash {
1853 stored_local_hashes.push(local_hash);
1854 }
1855 if let (Some(stored), Some(token_ids)) =
1856 (stored_token_ids.as_mut(), signal.token_ids)
1857 {
1858 stored.push(token_ids[full_idx].clone());
1859 }
1860 metadata_parent_hash = Some(seq_hash);
1861 }
1862 PreparedUseBlock::FreshPartial { uuid, mutable } => {
1863 let mutable =
1864 mutable.expect("committing Use must own every fresh partial slot");
1865 assert!(
1866 self.active_partial.insert(uuid, mutable).is_none(),
1867 "fresh Use replaced active partial block {uuid}"
1868 );
1869 }
1870 }
1871 }
1872 assert!(
1873 fresh_registrations.next().is_none(),
1874 "unused fresh full registration result"
1875 );
1876
1877 if !blocks_stored.is_empty() {
1878 self.publish_kv_event(
1879 blocks_stored,
1880 &stored_local_hashes,
1881 first_store_parent,
1882 true,
1883 stored_token_ids,
1884 );
1885 }
1886 self.handle_evictions(evicted_plhs);
1887 }
1888
1889 fn handle_evictions(
1894 &mut self,
1895 evicted_plhs: Vec<PositionalLineageHash>,
1896 ) -> Option<G1EvictionOutcome> {
1897 self.handle_evictions_with_source_slots(evicted_plhs, Vec::new())
1898 }
1899
1900 fn handle_evictions_with_source_slots(
1904 &mut self,
1905 evicted_plhs: Vec<PositionalLineageHash>,
1906 source_slots: Vec<MutableBlock<G1>>,
1907 ) -> Option<G1EvictionOutcome> {
1908 self.handle_evictions_with_source_slots_at(evicted_plhs, source_slots, None)
1909 }
1910
1911 fn handle_evictions_with_source_slots_at(
1912 &mut self,
1913 evicted_plhs: Vec<PositionalLineageHash>,
1914 source_slots: Vec<MutableBlock<G1>>,
1915 eviction_now_ms: Option<f64>,
1916 ) -> Option<G1EvictionOutcome> {
1917 #[cfg(not(feature = "kvbm-offload"))]
1918 let _ = eviction_now_ms;
1919 if evicted_plhs.is_empty() {
1920 drop(source_slots);
1921 return None;
1922 }
1923 let mut evicted_seq_hashes = Vec::with_capacity(evicted_plhs.len());
1924 #[cfg(feature = "kvbm-offload")]
1925 let mut offload_blocks = Vec::with_capacity(evicted_plhs.len());
1926
1927 for plh in evicted_plhs {
1928 let Some(info) = self.registered_blocks.remove(&plh) else {
1929 continue;
1930 };
1931 evicted_seq_hashes.push(info.seq_hash);
1932 #[cfg(feature = "kvbm-offload")]
1933 offload_blocks.push(G2OffloadBlock {
1934 block_id: info.block_id,
1935 plh,
1936 metadata: G2BlockEventMetadata {
1937 seq_hash: info.seq_hash,
1938 parent_hash: info.parent_hash,
1939 local_hash: info.local_hash,
1940 token_ids: info.token_ids,
1941 },
1942 });
1943 }
1944
1945 #[cfg(feature = "kvbm-offload")]
1946 let (g2_events, offload_outcome) = {
1947 let offload_source_slots = if source_slots.is_empty() {
1948 Vec::new()
1949 } else {
1950 let mut source_slots_by_id: FxHashMap<_, _> = source_slots
1951 .into_iter()
1952 .map(|slot| (slot.block_id(), slot))
1953 .collect();
1954 let mut matching_slots = Vec::with_capacity(offload_blocks.len());
1955 for block in &offload_blocks {
1956 let source_slot =
1957 source_slots_by_id
1958 .remove(&block.block_id)
1959 .unwrap_or_else(|| {
1960 panic!(
1961 "G1 offload block {} has no matching source slot",
1962 block.block_id
1963 )
1964 });
1965 matching_slots.push(source_slot);
1966 }
1967 drop(source_slots_by_id);
1968 matching_slots
1969 };
1970 self.enqueue_evictions_to_g2(&offload_blocks, offload_source_slots, eviction_now_ms)
1971 };
1972 #[cfg(not(feature = "kvbm-offload"))]
1973 drop(source_slots);
1974
1975 if !evicted_seq_hashes.is_empty() {
1976 self.publish_kv_event(evicted_seq_hashes, &[], None, false, None);
1977 }
1978
1979 #[cfg(feature = "kvbm-offload")]
1980 self.publish_g2_router_events(g2_events);
1981
1982 #[cfg(feature = "kvbm-offload")]
1983 return offload_outcome;
1984
1985 #[cfg(not(feature = "kvbm-offload"))]
1986 None
1987 }
1988
1989 fn process_deref(&mut self, blocks: &[UniqueBlock]) {
1990 let available_before = self.block_manager.available_blocks();
1991 for block in blocks {
1992 match block {
1993 UniqueBlock::PartialBlock(uuid) => {
1994 self.active_partial
1995 .remove(uuid)
1996 .expect("Deref: partial block not in active pool");
1997 }
1998 UniqueBlock::FullBlock(seq_hash) => {
1999 self.release_active_full(*seq_hash);
2000 }
2001 }
2002 }
2003 let released_slots = self
2004 .block_manager
2005 .available_blocks()
2006 .saturating_sub(available_before);
2007 if released_slots > 0 {
2008 self.bump_capacity_generation(released_slots);
2009 }
2010 }
2011
2012 fn process_promote(
2013 &mut self,
2014 uuid: Uuid,
2015 seq_hash: SequenceHash,
2016 parent_hash: Option<u64>,
2017 local_hash: Option<BlockHash>,
2018 plh: PositionalLineageHash,
2019 token_ids: Option<Vec<u32>>,
2020 ) {
2021 let mutable = self
2022 .active_partial
2023 .remove(&uuid)
2024 .expect("Promote: partial block not found");
2025
2026 let commit = self.commit_active_full(
2027 mutable,
2028 FullBlockMetadata {
2029 seq_hash,
2030 plh,
2031 parent_hash,
2032 local_hash,
2033 token_ids: token_ids.clone(),
2034 },
2035 );
2036
2037 if commit == FullBlockCommit::Stored {
2038 let local_hashes = local_hash.into_iter().collect::<Vec<_>>();
2039 self.publish_kv_event(
2040 vec![seq_hash],
2041 &local_hashes,
2042 parent_hash,
2043 true,
2044 token_ids.map(|t| vec![t]),
2045 );
2046 }
2047 }
2048
2049 pub fn num_active_blocks(&self) -> usize {
2052 self.block_manager.total_blocks() - self.block_manager.available_blocks()
2058 }
2059
2060 pub fn num_active_block_refs(&self) -> usize {
2065 self.active_partial.len()
2066 + self
2067 .active_full
2068 .values()
2069 .map(|active| active.logical_refs)
2070 .sum::<usize>()
2071 }
2072
2073 #[cfg(test)]
2074 pub(crate) fn active_block_ids(&self, sequence: &ActiveSequence) -> Vec<usize> {
2075 sequence
2076 .unique_blocks()
2077 .iter()
2078 .filter_map(|block| match block {
2079 UniqueBlock::FullBlock(hash) => self
2080 .active_full
2081 .get(hash)
2082 .map(|active| active.handle.block_id()),
2083 UniqueBlock::PartialBlock(uuid) => {
2084 self.active_partial.get(uuid).map(MutableBlock::block_id)
2085 }
2086 })
2087 .collect()
2088 }
2089
2090 pub fn get_active_perc(&self) -> f64 {
2091 self.num_active_blocks() as f64 / self.max_capacity as f64
2092 }
2093
2094 pub fn num_inactive_blocks(&self) -> usize {
2095 self.block_manager.metrics().snapshot().inactive_pool_size as usize
2096 }
2097
2098 pub fn max_capacity(&self) -> usize {
2099 self.max_capacity
2100 }
2101
2102 pub fn block_size(&self) -> usize {
2103 self.block_size
2104 }
2105
2106 pub fn dp_rank(&self) -> u32 {
2107 self.dp_rank
2108 }
2109
2110 pub fn get_prefill_cost(&self, sequence: &ActiveSequence) -> PrefillCost {
2115 let seq_blocks = sequence.unique_blocks();
2116
2117 let (overlap_blocks, active_overlap_blocks) = if sequence.enable_prefix_caching() {
2124 let plhs = sequence.positional_lineage_hashes();
2125 let mut overlap = 0;
2126 let mut active_overlap = 0;
2127 for (i, block) in seq_blocks.iter().enumerate() {
2128 match block {
2129 UniqueBlock::FullBlock(seq_hash) => {
2130 if self.active_full.contains_key(seq_hash) {
2131 overlap += 1;
2132 active_overlap += 1;
2133 continue;
2134 }
2135 let Some(plh) = plhs.get(i) else {
2136 break;
2137 };
2138 if self.registered_blocks.contains_key(plh) {
2139 overlap += 1;
2140 } else {
2141 break;
2142 }
2143 }
2144 UniqueBlock::PartialBlock(_) => break,
2145 }
2146 }
2147 (overlap, active_overlap)
2148 } else {
2149 (0, 0)
2150 };
2151
2152 let new_blocks = seq_blocks.len() - overlap_blocks;
2153 let cached_tokens = (overlap_blocks * self.block_size).min(sequence.len());
2157 let active_cached_tokens = (active_overlap_blocks * self.block_size).min(sequence.len());
2158 let new_tokens = sequence.len() - cached_tokens;
2159
2160 PrefillCost {
2161 new_blocks,
2162 new_tokens,
2163 cached_tokens,
2164 active_cached_tokens,
2165 }
2166 }
2167}
2168
2169#[cfg(test)]
2170mod tests {
2171 use std::sync::Mutex;
2172
2173 use super::*;
2174 use crate::common::protocols::{KvCacheEventSink, RawKvEvent, RawKvEventSink};
2175
2176 #[derive(Default)]
2178 struct CapturingSink {
2179 events: Mutex<Vec<KvCacheEvent>>,
2180 }
2181 impl KvCacheEventSink for CapturingSink {
2182 fn publish(&self, event: KvCacheEvent) -> anyhow::Result<()> {
2183 self.events.lock().unwrap().push(event);
2184 Ok(())
2185 }
2186 }
2187
2188 #[derive(Default)]
2189 struct CapturingRawSink {
2190 events: Mutex<Vec<RawKvEvent>>,
2191 }
2192
2193 impl RawKvEventSink for CapturingRawSink {
2194 fn publish(&self, event: RawKvEvent) -> anyhow::Result<()> {
2195 self.events.lock().unwrap().push(event);
2196 Ok(())
2197 }
2198 }
2199
2200 fn make_mgr(capacity: usize, block_size: usize) -> KvManager {
2201 KvManager::new_with_event_sink(capacity, block_size, KvEventPublishers::default(), 0)
2202 }
2203
2204 fn expect_ready<T>(outcome: G1Acquire<T>) -> T {
2205 match outcome {
2206 G1Acquire::Ready(value) => value,
2207 G1Acquire::CapacityExhausted => panic!("expected Ready, got CapacityExhausted"),
2208 G1Acquire::BlockedOnOffload { .. } => {
2209 panic!("expected Ready, got BlockedOnOffload")
2210 }
2211 G1Acquire::RetryNow { .. } => panic!("expected Ready, got RetryNow"),
2212 }
2213 }
2214
2215 fn make_mgr_capturing(capacity: usize, block_size: usize) -> (KvManager, Arc<CapturingSink>) {
2216 let sink = Arc::new(CapturingSink::default());
2217 let publishers = KvEventPublishers::new(Some(sink.clone() as _), None);
2218 (
2219 KvManager::new_with_event_sink(capacity, block_size, publishers, 0),
2220 sink,
2221 )
2222 }
2223
2224 fn make_mgr_capturing_with_raw(
2225 capacity: usize,
2226 block_size: usize,
2227 ) -> (KvManager, Arc<CapturingSink>, Arc<CapturingRawSink>) {
2228 let sink = Arc::new(CapturingSink::default());
2229 let raw_sink = Arc::new(CapturingRawSink::default());
2230 let publishers =
2231 KvEventPublishers::new(Some(sink.clone() as _), Some(raw_sink.clone() as _));
2232 (
2233 KvManager::new_with_event_sink(capacity, block_size, publishers, 0),
2234 sink,
2235 raw_sink,
2236 )
2237 }
2238
2239 fn make_mgr_capturing_with_backend(
2240 capacity: usize,
2241 block_size: usize,
2242 backend: MockerEvictionBackend,
2243 ) -> (KvManager, Arc<CapturingSink>) {
2244 let sink = Arc::new(CapturingSink::default());
2245 let publishers = KvEventPublishers::new(Some(sink.clone() as _), None);
2246 (
2247 KvManager::new_with_eviction_backend(capacity, block_size, publishers, 0, backend),
2248 sink,
2249 )
2250 }
2251
2252 #[test]
2253 #[should_panic(expected = "not newer than attempted generation")]
2254 fn retry_witness_rejects_same_generation() {
2255 make_mgr(1, 4).validate_retry_witness(0, false, 0, 1);
2256 }
2257
2258 #[test]
2259 #[should_panic(expected = "released zero G1 slots")]
2260 fn retry_witness_rejects_zero_released_slots() {
2261 let mut mgr = make_mgr(1, 4);
2262 mgr.capacity_generation = 1;
2263 mgr.validate_retry_witness(0, false, 1, 0);
2264 }
2265
2266 #[test]
2267 #[should_panic(expected = "retried more than once")]
2268 fn retry_witness_rejects_second_retry() {
2269 let mut mgr = make_mgr(1, 4);
2270 mgr.capacity_generation = 1;
2271 mgr.validate_retry_witness(0, true, 1, 1);
2272 }
2273
2274 fn plh(v: u64) -> PositionalLineageHash {
2275 PositionalLineageHash::new(v, None, 0)
2276 }
2277
2278 fn lineage_plh(id: u64) -> PositionalLineageHash {
2279 match id {
2280 0 => PositionalLineageHash::new(0, None, 0),
2281 1 => PositionalLineageHash::new(1, Some(0), 1),
2282 2 => PositionalLineageHash::new(2, Some(1), 2),
2283 3 => PositionalLineageHash::new(3, Some(2), 3),
2284 4 => PositionalLineageHash::new(4, Some(3), 4),
2285 5 => PositionalLineageHash::new(5, Some(1), 2),
2286 6 => PositionalLineageHash::new(6, Some(5), 3),
2287 7 => PositionalLineageHash::new(7, Some(2), 3),
2288 8 => PositionalLineageHash::new(8, Some(7), 4),
2289 9 => PositionalLineageHash::new(9, Some(8), 5),
2290 10 => PositionalLineageHash::new(10, None, 0),
2291 11 => PositionalLineageHash::new(11, Some(10), 1),
2292 12 => PositionalLineageHash::new(12, Some(11), 2),
2293 13 => PositionalLineageHash::new(13, None, 0),
2294 _ => plh(id),
2295 }
2296 }
2297
2298 fn use_full(mgr: &mut KvManager, seq_hash: u64, p: PositionalLineageHash) -> usize {
2299 expect_ready(mgr.process(&MoveBlock::Use(
2300 vec![UniqueBlock::FullBlock(seq_hash)],
2301 vec![],
2302 vec![p],
2303 None,
2304 None,
2305 )))
2306 }
2307
2308 fn use_partial(mgr: &mut KvManager, uuid: Uuid) -> usize {
2309 expect_ready(mgr.process(&MoveBlock::Use(
2310 vec![UniqueBlock::PartialBlock(uuid)],
2311 vec![],
2312 vec![],
2313 None,
2314 None,
2315 )))
2316 }
2317
2318 fn deref_full(mgr: &mut KvManager, seq_hash: u64) {
2319 mgr.process(&MoveBlock::Deref(vec![UniqueBlock::FullBlock(seq_hash)]));
2320 }
2321
2322 fn deref_partial(mgr: &mut KvManager, uuid: Uuid) {
2323 mgr.process(&MoveBlock::Deref(vec![UniqueBlock::PartialBlock(uuid)]));
2324 }
2325
2326 #[test]
2327 fn test_use_single_full_block() {
2328 let mut mgr = make_mgr(10, 16);
2329 assert_eq!(use_full(&mut mgr, 1, plh(100)), 1);
2330 assert_eq!(mgr.num_active_blocks(), 1);
2331 }
2332
2333 #[test]
2337 fn prefill_cost_splits_active_and_inactive_cached_reuse() {
2338 let mut mgr = make_mgr(10, 4);
2339 let seq = ActiveSequence::new((0u32..8).collect(), 4, Some(4), true, false);
2341 let blocks = seq.unique_blocks();
2342 let plhs = seq.positional_lineage_hashes();
2343 let h0 = match &blocks[0] {
2344 UniqueBlock::FullBlock(h) => *h,
2345 other => panic!("expected a full block, got {other:?}"),
2346 };
2347 use_full(&mut mgr, h0, plhs[0]);
2350 deref_full(&mut mgr, h0);
2351
2352 let cost = mgr.get_prefill_cost(&seq);
2353 assert!(
2354 cost.cached_tokens >= 4,
2355 "inactive prefix should count for compute reuse: {cost:?}"
2356 );
2357 assert_eq!(
2358 cost.active_cached_tokens, 0,
2359 "inactive reuse must not be discounted for capacity: {cost:?}"
2360 );
2361 }
2362
2363 #[test]
2364 fn use_rejects_short_token_ids_before_mutating_state() {
2365 let (mut mgr, sink) = make_mgr_capturing(10, 4);
2366
2367 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2368 mgr.process(&MoveBlock::Use(
2369 vec![UniqueBlock::FullBlock(1), UniqueBlock::FullBlock(2)],
2370 vec![101, 102],
2371 vec![plh(100), plh(200)],
2372 Some(vec![vec![1, 2, 3, 4]]),
2373 None,
2374 ));
2375 }));
2376
2377 assert!(result.is_err());
2378 assert_eq!(mgr.num_active_blocks(), 0);
2379 assert!(mgr.active_full.is_empty());
2380 assert!(sink.events.lock().unwrap().is_empty());
2381 }
2382
2383 #[test]
2384 fn test_duplicate_use_bumps_refcount() {
2385 let mut mgr = make_mgr(10, 16);
2386 use_full(&mut mgr, 1, plh(100));
2387 use_full(&mut mgr, 1, plh(100));
2388 assert_eq!(mgr.num_active_blocks(), 1);
2392 assert_eq!(mgr.num_active_block_refs(), 2);
2393 assert_eq!(mgr.block_manager.metrics().snapshot().inflight_immutable, 1);
2394
2395 deref_full(&mut mgr, 1);
2396 assert_eq!(mgr.num_active_blocks(), 1);
2397 assert_eq!(mgr.num_active_block_refs(), 1);
2398 assert_eq!(mgr.block_manager.metrics().snapshot().inflight_immutable, 1);
2399
2400 deref_full(&mut mgr, 1);
2401 assert_eq!(mgr.num_active_blocks(), 0);
2402 assert_eq!(mgr.num_active_block_refs(), 0);
2403 assert_eq!(mgr.block_manager.metrics().snapshot().inflight_immutable, 0);
2404 }
2405
2406 #[test]
2407 fn all_active_multi_block_use_only_retains_logical_owners() {
2408 let (mut mgr, sink) = make_mgr_capturing(4, 4);
2409 let blocks = vec![UniqueBlock::FullBlock(10), UniqueBlock::FullBlock(20)];
2410 let plhs = vec![plh(100), plh(200)];
2411
2412 assert_eq!(
2413 expect_ready(mgr.process(&MoveBlock::Use(
2414 blocks.clone(),
2415 vec![101, 201],
2416 plhs.clone(),
2417 None,
2418 None,
2419 ))),
2420 2
2421 );
2422 let available_before = mgr.block_manager.available_blocks();
2423 let first_block_id = mgr.active_full[&10].handle.block_id();
2424 let second_block_id = mgr.active_full[&20].handle.block_id();
2425 sink.events.lock().unwrap().clear();
2426
2427 assert_eq!(
2428 expect_ready(mgr.process(&MoveBlock::Use(blocks, vec![101, 201], plhs, None, None,))),
2429 2
2430 );
2431
2432 assert_eq!(mgr.block_manager.available_blocks(), available_before);
2433 assert_eq!(mgr.num_active_blocks(), 2);
2434 assert_eq!(mgr.num_active_block_refs(), 4);
2435 assert_eq!(mgr.active_full[&10].handle.block_id(), first_block_id);
2436 assert_eq!(mgr.active_full[&20].handle.block_id(), second_block_id);
2437 assert!(
2438 sink.events.lock().unwrap().is_empty(),
2439 "retaining active blocks must not publish another Stored event"
2440 );
2441
2442 for _ in 0..2 {
2443 deref_full(&mut mgr, 10);
2444 deref_full(&mut mgr, 20);
2445 }
2446 assert_eq!(mgr.num_active_blocks(), 0);
2447 assert_eq!(mgr.num_active_block_refs(), 0);
2448 }
2449
2450 #[test]
2451 fn capacity_exhaustion_returns_without_partial_commit() {
2452 let mut mgr = make_mgr(4, 16);
2453 for i in 0..4 {
2454 assert_eq!(use_full(&mut mgr, i, plh(i + 100)), 1);
2455 }
2456 let refs_before = mgr.num_active_block_refs();
2457 assert!(matches!(
2458 mgr.process(&MoveBlock::Use(
2459 vec![UniqueBlock::FullBlock(4)],
2460 vec![],
2461 vec![plh(500)],
2462 None,
2463 None,
2464 )),
2465 G1Acquire::CapacityExhausted
2466 ));
2467 assert_eq!(mgr.num_active_block_refs(), refs_before);
2468 }
2469
2470 #[test]
2471 fn failed_mixed_use_does_not_retain_existing_active_blocks() {
2472 let mut mgr = make_mgr(1, 16);
2473 use_full(&mut mgr, 1, plh(100));
2474
2475 assert!(matches!(
2476 mgr.process(&MoveBlock::Use(
2477 vec![UniqueBlock::FullBlock(1), UniqueBlock::FullBlock(2)],
2478 vec![],
2479 vec![plh(100), plh(200)],
2480 None,
2481 None,
2482 )),
2483 G1Acquire::CapacityExhausted
2484 ));
2485 assert_eq!(mgr.num_active_blocks(), 1);
2486 assert_eq!(mgr.num_active_block_refs(), 1);
2487 assert_eq!(mgr.active_full[&1].logical_refs, 1);
2488 assert!(!mgr.active_full.contains_key(&2));
2489 }
2490
2491 #[test]
2492 fn test_deref_returns_to_inactive() {
2493 let mut mgr = make_mgr(4, 16);
2494 use_full(&mut mgr, 1, plh(100));
2495 deref_full(&mut mgr, 1);
2496 assert_eq!(mgr.num_active_blocks(), 0);
2497 }
2498
2499 #[test]
2500 fn test_inactive_reuse_via_match_blocks() {
2501 let mut mgr = make_mgr(10, 16);
2502 let p = plh(100);
2503 use_full(&mut mgr, 1, p);
2504 deref_full(&mut mgr, 1);
2505 assert_eq!(use_full(&mut mgr, 2, p), 1);
2507 }
2508
2509 #[test]
2510 fn scattered_use_reuses_later_hit_after_a_miss() {
2511 let (mut mgr, sink) = make_mgr_capturing(10, 16);
2512 let first_plh = plh(100);
2513 let missing_plh = plh(200);
2514 let later_plh = plh(300);
2515
2516 use_full(&mut mgr, 10, first_plh);
2517 use_full(&mut mgr, 30, later_plh);
2518 let later_block_id = mgr.active_full[&30].handle.block_id();
2519 deref_full(&mut mgr, 10);
2520 deref_full(&mut mgr, 30);
2521 sink.events.lock().unwrap().clear();
2522
2523 assert_eq!(
2524 expect_ready(mgr.process(&MoveBlock::Use(
2525 vec![
2526 UniqueBlock::FullBlock(10),
2527 UniqueBlock::FullBlock(20),
2528 UniqueBlock::FullBlock(30),
2529 ],
2530 vec![],
2531 vec![first_plh, missing_plh, later_plh],
2532 None,
2533 None,
2534 ))),
2535 3
2536 );
2537
2538 assert_eq!(mgr.num_active_blocks(), 3);
2539 assert_eq!(mgr.num_active_block_refs(), 3);
2540 assert_eq!(
2541 mgr.active_full[&30].handle.block_id(),
2542 later_block_id,
2543 "the registered block after a miss must still be reused"
2544 );
2545
2546 let events = sink.events.lock().unwrap();
2547 assert_eq!(events.len(), 1, "only the missing middle block is stored");
2548 let KvCacheEventData::Stored(stored) = &events[0].data else {
2549 panic!("expected Stored event, got {:?}", events[0].data);
2550 };
2551 assert_eq!(stored.parent_hash.map(|hash| hash.0), Some(10));
2552 assert_eq!(stored.blocks.len(), 1);
2553 assert_eq!(stored.blocks[0].block_hash.0, 20);
2554 }
2555
2556 #[test]
2557 fn mixed_use_keeps_fresh_registration_and_event_order() {
2558 let (mut mgr, sink) = make_mgr_capturing(8, 4);
2559 let reused_plh = plh(200);
2560
2561 use_full(&mut mgr, 20, reused_plh);
2562 sink.events.lock().unwrap().clear();
2563
2564 let seq_hashes = [10, 11, 20, 30, 31];
2565 let plhs = [plh(100), plh(110), reused_plh, plh(300), plh(310)];
2566 let local_hashes = vec![1010, 1011, 1020, 1030, 1031];
2567 let token_ids = vec![
2568 vec![10, 10, 10, 10],
2569 vec![11, 11, 11, 11],
2570 vec![20, 20, 20, 20],
2571 vec![30, 30, 30, 30],
2572 vec![31, 31, 31, 31],
2573 ];
2574 let blocks = seq_hashes.into_iter().map(UniqueBlock::FullBlock).collect();
2575
2576 assert_eq!(
2577 expect_ready(mgr.process(&MoveBlock::Use(
2578 blocks,
2579 local_hashes.clone(),
2580 plhs.to_vec(),
2581 Some(token_ids.clone()),
2582 Some(UniqueBlock::FullBlock(5)),
2583 ))),
2584 seq_hashes.len()
2585 );
2586
2587 for (idx, (seq_hash, plh)) in [(10, plhs[0]), (11, plhs[1]), (30, plhs[3]), (31, plhs[4])]
2588 .into_iter()
2589 .enumerate()
2590 {
2591 let signal_idx = [0, 1, 3, 4][idx];
2592 let info = mgr
2593 .registered_blocks
2594 .get(&plh)
2595 .expect("fresh block must retain registration metadata");
2596 assert_eq!(info.seq_hash, seq_hash);
2597 assert_eq!(info.block_id, mgr.active_full[&seq_hash].handle.block_id());
2598 assert_eq!(info.local_hash, Some(local_hashes[signal_idx]));
2599 assert_eq!(info.token_ids.as_ref(), Some(&token_ids[signal_idx]));
2600 }
2601 assert_eq!(mgr.registered_blocks[&plhs[0]].parent_hash, Some(5));
2602 assert_eq!(mgr.registered_blocks[&plhs[1]].parent_hash, Some(10));
2603 assert_eq!(mgr.registered_blocks[&plhs[3]].parent_hash, Some(20));
2604 assert_eq!(mgr.registered_blocks[&plhs[4]].parent_hash, Some(30));
2605
2606 let events = sink.events.lock().unwrap();
2607 assert_eq!(events.len(), 2, "the reused middle block splits stores");
2608 for (event, expected_hashes, expected_local_hashes, expected_parent) in [
2609 (&events[0], &[10, 11][..], &[1010, 1011][..], Some(5)),
2610 (&events[1], &[30, 31][..], &[1030, 1031][..], Some(20)),
2611 ] {
2612 let KvCacheEventData::Stored(stored) = &event.data else {
2613 panic!("expected Stored event, got {:?}", event.data);
2614 };
2615 assert_eq!(stored.parent_hash.map(|hash| hash.0), expected_parent);
2616 assert_eq!(
2617 stored
2618 .blocks
2619 .iter()
2620 .map(|block| block.block_hash.0)
2621 .collect::<Vec<_>>(),
2622 expected_hashes
2623 );
2624 assert_eq!(
2625 stored
2626 .blocks
2627 .iter()
2628 .map(|block| block.tokens_hash.0)
2629 .collect::<Vec<_>>(),
2630 expected_local_hashes
2631 );
2632 }
2633 }
2634
2635 #[test]
2636 fn duplicate_fresh_registration_reuses_canonical_without_duplicate_event() {
2637 const CAPACITY: usize = 6;
2638 let (mut mgr, sink, raw_sink) = make_mgr_capturing_with_raw(CAPACITY, 4);
2639 let a_plh = plh(100);
2640 let b_plh = plh(200);
2641 let local_hashes = vec![101, 102, 201];
2642 let token_ids = vec![vec![1; 4], vec![2; 4], vec![3; 4]];
2643 let dedup_before = mgr.block_manager.metrics().snapshot().registration_dedup;
2644
2645 assert_eq!(
2646 expect_ready(mgr.process(&MoveBlock::Use(
2647 vec![
2648 UniqueBlock::FullBlock(10),
2649 UniqueBlock::FullBlock(10),
2650 UniqueBlock::FullBlock(20),
2651 ],
2652 local_hashes.clone(),
2653 vec![a_plh, a_plh, b_plh],
2654 Some(token_ids.clone()),
2655 Some(UniqueBlock::FullBlock(5)),
2656 ))),
2657 3
2658 );
2659
2660 assert_eq!(mgr.num_active_blocks(), 2);
2661 assert_eq!(mgr.num_active_block_refs(), 3);
2662 assert_eq!(mgr.block_manager.available_blocks(), CAPACITY - 2);
2663 assert_eq!(
2664 mgr.block_manager.metrics().snapshot().registration_dedup,
2665 dedup_before + 1
2666 );
2667 assert_eq!(mgr.registered_blocks.len(), 2);
2668
2669 let a_info = &mgr.registered_blocks[&a_plh];
2670 assert_eq!(a_info.seq_hash, 10);
2671 assert_eq!(a_info.block_id, mgr.active_full[&10].handle.block_id());
2672 assert_eq!(a_info.parent_hash, Some(5));
2673 assert_eq!(a_info.local_hash, Some(local_hashes[0]));
2674 assert_eq!(a_info.token_ids.as_ref(), Some(&token_ids[0]));
2675
2676 let b_info = &mgr.registered_blocks[&b_plh];
2677 assert_eq!(b_info.seq_hash, 20);
2678 assert_eq!(b_info.block_id, mgr.active_full[&20].handle.block_id());
2679 assert_eq!(b_info.parent_hash, Some(10));
2680 assert_eq!(b_info.local_hash, Some(local_hashes[2]));
2681 assert_eq!(b_info.token_ids.as_ref(), Some(&token_ids[2]));
2682
2683 let events = sink.events.lock().unwrap();
2684 assert_eq!(events.len(), 2, "the duplicate must split Stored groups");
2685 for (event, expected_hash, expected_local_hash, expected_parent) in [
2686 (&events[0], 10, local_hashes[0], Some(5)),
2687 (&events[1], 20, local_hashes[2], Some(10)),
2688 ] {
2689 let KvCacheEventData::Stored(stored) = &event.data else {
2690 panic!("expected Stored event, got {:?}", event.data);
2691 };
2692 assert_eq!(stored.parent_hash.map(|hash| hash.0), expected_parent);
2693 assert_eq!(stored.blocks.len(), 1);
2694 assert_eq!(stored.blocks[0].block_hash.0, expected_hash);
2695 assert_eq!(stored.blocks[0].tokens_hash.0, expected_local_hash);
2696 }
2697 drop(events);
2698
2699 let raw_events = raw_sink.events.lock().unwrap();
2700 assert_eq!(
2701 raw_events.len(),
2702 2,
2703 "the duplicate must split raw Stored groups"
2704 );
2705 assert_eq!(
2706 raw_events[0].block_token_ids.as_deref(),
2707 Some(std::slice::from_ref(&token_ids[0]))
2708 );
2709 assert_eq!(
2710 raw_events[1].block_token_ids.as_deref(),
2711 Some(std::slice::from_ref(&token_ids[2]))
2712 );
2713 drop(raw_events);
2714
2715 deref_full(&mut mgr, 10);
2716 deref_full(&mut mgr, 10);
2717 deref_full(&mut mgr, 20);
2718 assert_eq!(mgr.num_active_blocks(), 0);
2719 assert_eq!(mgr.num_active_block_refs(), 0);
2720 assert!(mgr.active_full.is_empty());
2721 assert_eq!(mgr.block_manager.available_blocks(), CAPACITY);
2722 }
2723
2724 #[test]
2725 fn failed_decode_reservation_preserves_inactive_cache() {
2726 let (mut mgr, sink) = make_mgr_capturing(2, 16);
2727 let first = plh(100);
2728 let second = plh(200);
2729 use_full(&mut mgr, 1, first);
2730 use_full(&mut mgr, 2, second);
2731 deref_full(&mut mgr, 1);
2732 deref_full(&mut mgr, 2);
2733 assert_eq!(mgr.num_inactive_blocks(), 2);
2734 sink.events.lock().unwrap().clear();
2735
2736 assert!(matches!(
2737 mgr.reserve_decode_blocks(3),
2738 G1Acquire::CapacityExhausted
2739 ));
2740 assert_eq!(mgr.num_inactive_blocks(), 2);
2741 assert!(sink.events.lock().unwrap().is_empty());
2742 assert_eq!(use_full(&mut mgr, 3, first), 1);
2743 assert_eq!(use_full(&mut mgr, 4, second), 1);
2744 }
2745
2746 #[test]
2747 fn test_eviction_frees_inactive_for_new_allocation() {
2748 let mut mgr = make_mgr(4, 16);
2749 for i in 0..4 {
2750 use_full(&mut mgr, i, plh(i + 100));
2751 }
2752 for i in 0..4 {
2753 deref_full(&mut mgr, i);
2754 }
2755 for i in 10..14 {
2756 assert_eq!(use_full(&mut mgr, i, plh(i + 1000)), 1);
2757 }
2758 assert_eq!(mgr.num_active_blocks(), 4);
2759 }
2760
2761 #[test]
2762 fn test_promote_basic() {
2763 let mut mgr = make_mgr(10, 16);
2764 let uuid = Uuid::new_v4();
2765 use_partial(&mut mgr, uuid);
2766 mgr.process(&MoveBlock::Promote(uuid, 42, None, Some(0), plh(500), None));
2767 assert_eq!(mgr.num_active_blocks(), 1);
2768 assert!(mgr.active_partial.is_empty());
2769 assert!(mgr.active_full.contains_key(&42));
2770 }
2771
2772 #[test]
2773 #[should_panic(expected = "Promote: partial block not found")]
2774 fn test_promote_nonexistent_panics() {
2775 let mut mgr = make_mgr(10, 16);
2776 mgr.process(&MoveBlock::Promote(
2777 Uuid::new_v4(),
2778 42,
2779 None,
2780 Some(0),
2781 plh(500),
2782 None,
2783 ));
2784 }
2785
2786 #[test]
2787 fn test_deref_partial_returns_to_reset() {
2788 let mut mgr = make_mgr(10, 16);
2789 let uuid = Uuid::new_v4();
2790 use_partial(&mut mgr, uuid);
2791 assert_eq!(mgr.active_partial.len(), 1);
2792 deref_partial(&mut mgr, uuid);
2793 assert!(mgr.active_partial.is_empty());
2794 assert_eq!(mgr.num_active_block_refs(), 0);
2795 }
2796
2797 #[test]
2798 fn test_prefill_cost_no_overlap() {
2799 let mgr = make_mgr(10, 16);
2800 let tokens: Vec<u32> = (0..35).collect();
2801 let seq = ActiveSequence::new(tokens, 10, Some(16), true, false);
2802 let cost = mgr.get_prefill_cost(&seq);
2803 assert_eq!(cost.new_blocks, seq.unique_blocks().len());
2804 assert_eq!(cost.new_tokens, 35);
2805 }
2806
2807 #[test]
2808 fn test_eviction_backend_lru_and_multi_lru() {
2809 for backend in [MockerEvictionBackend::Lru, MockerEvictionBackend::MultiLru] {
2810 let mut mgr = KvManager::new_with_eviction_backend(
2811 4,
2812 16,
2813 KvEventPublishers::default(),
2814 0,
2815 backend,
2816 );
2817 for i in 0..4u64 {
2818 assert_eq!(use_full(&mut mgr, i, plh(i + 100)), 1);
2819 }
2820 for i in 0..4u64 {
2821 deref_full(&mut mgr, i);
2822 }
2823 for i in 10..14u64 {
2824 assert_eq!(
2825 use_full(&mut mgr, i, plh(i + 1000)),
2826 1,
2827 "backend={backend:?}"
2828 );
2829 }
2830 assert_eq!(mgr.num_active_blocks(), 4);
2831 }
2832 }
2833
2834 #[test]
2835 fn test_failure_on_max_capacity() {
2836 fn use_batch(mgr: &mut KvManager, ids: &[u64]) -> G1Acquire<usize> {
2837 let blocks: Vec<_> = ids.iter().map(|&id| UniqueBlock::FullBlock(id)).collect();
2838 let plhs: Vec<_> = ids.iter().map(|&id| plh(id)).collect();
2839 mgr.process(&MoveBlock::Use(blocks, vec![], plhs, None, None))
2840 }
2841
2842 let mut mgr = make_mgr(10, 16);
2843
2844 let ids: Vec<u64> = (0..10).collect();
2846 assert!(matches!(use_batch(&mut mgr, &ids), G1Acquire::Ready(10)));
2847 assert_eq!(mgr.num_active_blocks(), 10);
2848
2849 assert!(matches!(
2850 use_batch(&mut mgr, &[10]),
2851 G1Acquire::CapacityExhausted
2852 ));
2853 }
2854
2855 #[test]
2856 fn test_block_lifecycle_stringent() {
2857 fn use_blocks(mgr: &mut KvManager, ids: &[u64]) -> usize {
2858 let blocks: Vec<_> = ids.iter().map(|&id| UniqueBlock::FullBlock(id)).collect();
2859 let plhs: Vec<_> = ids.iter().map(|&id| lineage_plh(id)).collect();
2860 expect_ready(mgr.process(&MoveBlock::Use(blocks, vec![], plhs, None, None)))
2861 }
2862 fn deref_blocks(mgr: &mut KvManager, ids: &[u64]) {
2863 let blocks = ids.iter().map(|&id| UniqueBlock::FullBlock(id)).collect();
2864 mgr.process(&MoveBlock::Deref(blocks));
2865 }
2866 fn refcount(mgr: &KvManager, id: u64) -> usize {
2867 mgr.active_full
2868 .get(&id)
2869 .map(|active| active.logical_refs)
2870 .unwrap_or(0)
2871 }
2872 fn assert_active(mgr: &KvManager, expected: &[(u64, usize)]) {
2873 let distinct = expected.len();
2874 let total_refs: usize = expected.iter().map(|&(_, r)| r).sum();
2875 assert_eq!(
2876 mgr.num_active_blocks(),
2877 distinct,
2878 "distinct active-block count mismatch; expected={expected:?}"
2879 );
2880 assert_eq!(
2881 mgr.num_active_block_refs(),
2882 total_refs,
2883 "active handle-refcount mismatch; expected={expected:?}"
2884 );
2885 for &(id, r) in expected {
2886 assert_eq!(refcount(mgr, id), r, "block {id} refcount mismatch");
2887 }
2888 }
2889 fn assert_inactive_blocks(mgr: &KvManager, expected_ids: &[u64]) {
2898 assert_eq!(
2899 mgr.num_inactive_blocks(),
2900 expected_ids.len(),
2901 "inactive count mismatch; expected={expected_ids:?}"
2902 );
2903 let plhs: Vec<_> = expected_ids.iter().map(|&id| lineage_plh(id)).collect();
2904 let presence = mgr
2905 .block_manager
2906 .block_registry()
2907 .check_presence::<G1>(&plhs);
2908 for ((_, present), &id) in presence.iter().zip(expected_ids.iter()) {
2909 assert!(
2910 *present,
2911 "block {id} expected in inactive pool, not found in registry"
2912 );
2913 assert!(
2914 !mgr.active_full.contains_key(&id),
2915 "block {id} expected inactive but is in active pool"
2916 );
2917 }
2918 }
2919 fn drain_events(sink: &Arc<CapturingSink>) -> Vec<KvCacheEvent> {
2920 std::mem::take(&mut *sink.events.lock().unwrap())
2921 }
2922 fn assert_stored_event(
2923 event: &KvCacheEvent,
2924 expected_blocks: &[u64],
2925 expected_parent: Option<u64>,
2926 ) {
2927 let KvCacheEventData::Stored(data) = &event.data else {
2928 panic!("expected Stored event, got {:?}", event.data);
2929 };
2930 let actual_blocks: Vec<u64> =
2931 data.blocks.iter().map(|block| block.block_hash.0).collect();
2932 assert_eq!(actual_blocks, expected_blocks, "stored blocks mismatch");
2933 assert_eq!(
2934 data.parent_hash.map(|hash| hash.0),
2935 expected_parent,
2936 "stored parent_hash mismatch"
2937 );
2938 }
2939 fn assert_removed_event(event: &KvCacheEvent, expected_blocks: &[u64]) {
2940 let KvCacheEventData::Removed(data) = &event.data else {
2941 panic!("expected Removed event, got {:?}", event.data);
2942 };
2943 let actual_blocks: Vec<u64> = data.block_hashes.iter().map(|hash| hash.0).collect();
2944 assert_eq!(actual_blocks, expected_blocks, "removed blocks mismatch");
2945 }
2946
2947 let (mut mgr, sink) =
2948 make_mgr_capturing_with_backend(10, 16, MockerEvictionBackend::Lineage);
2949
2950 assert_eq!(use_blocks(&mut mgr, &[0, 1, 2, 3, 4]), 5);
2952 let events = drain_events(&sink);
2953 assert_eq!(events.len(), 1, "expected one Stored event for [0..=4]");
2954 assert_stored_event(&events[0], &[0, 1, 2, 3, 4], None);
2955
2956 assert_eq!(use_blocks(&mut mgr, &[0, 1, 5, 6]), 4);
2957 let events = drain_events(&sink);
2958 assert_eq!(events.len(), 1, "expected one Stored event for [5, 6]");
2959 assert_stored_event(&events[0], &[5, 6], Some(1));
2960 assert_active(
2961 &mgr,
2962 &[(0, 2), (1, 2), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1)],
2963 );
2964
2965 deref_blocks(&mut mgr, &[4, 3, 2, 1, 0]);
2968 let events = drain_events(&sink);
2969 assert!(events.is_empty(), "Deref should not emit KV events");
2970 assert_active(&mgr, &[(0, 1), (1, 1), (5, 1), (6, 1)]);
2971 assert_inactive_blocks(&mgr, &[2, 3, 4]);
2972
2973 deref_blocks(&mut mgr, &[6, 5, 1, 0]);
2975 let events = drain_events(&sink);
2976 assert!(events.is_empty(), "Deref should not emit KV events");
2977 assert_active(&mgr, &[]);
2978 assert_inactive_blocks(&mgr, &[0, 1, 2, 3, 4, 5, 6]);
2979
2980 assert_eq!(use_blocks(&mut mgr, &[0, 1, 2, 7, 8, 9]), 6);
2983 let events = drain_events(&sink);
2984 assert_eq!(events.len(), 1, "expected one Stored event for [7, 8, 9]");
2985 assert_stored_event(&events[0], &[7, 8, 9], Some(2));
2986 assert_active(&mgr, &[(0, 1), (1, 1), (2, 1), (7, 1), (8, 1), (9, 1)]);
2987 assert_inactive_blocks(&mgr, &[3, 4, 5, 6]);
2988
2989 assert_eq!(use_blocks(&mut mgr, &[10, 11, 12]), 3);
2992 let events = drain_events(&sink);
2993 assert_eq!(
2994 events.len(),
2995 2,
2996 "expected Stored + Removed for [10, 11, 12]"
2997 );
2998 assert_stored_event(&events[0], &[10, 11, 12], None);
2999 assert_removed_event(&events[1], &[4, 3, 6]);
3000 assert_active(
3001 &mgr,
3002 &[
3003 (0, 1),
3004 (1, 1),
3005 (2, 1),
3006 (7, 1),
3007 (8, 1),
3008 (9, 1),
3009 (10, 1),
3010 (11, 1),
3011 (12, 1),
3012 ],
3013 );
3014 assert_inactive_blocks(&mgr, &[5]);
3015
3016 assert_eq!(use_blocks(&mut mgr, &[13]), 1);
3017 let events = drain_events(&sink);
3018 assert_eq!(events.len(), 2, "expected Stored + Removed for [13]");
3019 assert_stored_event(&events[0], &[13], None);
3020 assert_removed_event(&events[1], &[5]);
3021 assert_active(
3022 &mgr,
3023 &[
3024 (0, 1),
3025 (1, 1),
3026 (2, 1),
3027 (7, 1),
3028 (8, 1),
3029 (9, 1),
3030 (10, 1),
3031 (11, 1),
3032 (12, 1),
3033 (13, 1),
3034 ],
3035 );
3036 assert_eq!(mgr.num_inactive_blocks(), 0);
3037 }
3038
3039 #[test]
3040 fn test_chunked_prefill_parent_hash() {
3041 let block_size = 64;
3042 let tokens: Vec<u32> = (0..512).collect(); let mut seq = ActiveSequence::new(tokens, 100, Some(block_size), true, false);
3044
3045 let (mut mgr, sink) = make_mgr_capturing(256, block_size);
3046
3047 let signal = seq.prepare_allocation(256).unwrap();
3049 mgr.process(&signal);
3050 seq.commit_allocation(256);
3051
3052 let signal = seq.prepare_allocation(512).unwrap();
3054 mgr.process(&signal);
3055 seq.commit_allocation(512);
3056
3057 let events = sink.events.lock().unwrap();
3058 assert_eq!(events.len(), 2, "expected two Stored events");
3059
3060 let KvCacheEventData::Stored(ref store1) = events[0].data else {
3061 panic!("expected Stored event");
3062 };
3063 assert!(
3064 store1.parent_hash.is_none(),
3065 "first chunk should have no parent_hash"
3066 );
3067
3068 let KvCacheEventData::Stored(ref store2) = events[1].data else {
3069 panic!("expected Stored event");
3070 };
3071 let UniqueBlock::FullBlock(expected_hash) = seq.unique_blocks()[3].clone() else {
3072 panic!("expected FullBlock at index 3");
3073 };
3074 assert_eq!(
3075 store2.parent_hash,
3076 Some(ExternalSequenceBlockHash(expected_hash)),
3077 "second chunk's parent_hash should be block 3's seq_hash"
3078 );
3079 }
3080
3081 #[test]
3082 fn test_repreempt_after_partial_recompute_only_frees_reallocated_blocks() {
3083 let mut seq = ActiveSequence::new((0..6).collect(), 16, Some(4), true, false);
3084 let mut mgr = make_mgr(16, 4);
3085
3086 let signal = seq.take_creation_signal().unwrap();
3087 assert_eq!(expect_ready(mgr.process(&signal)), 2);
3088
3089 for _ in 0..3 {
3090 let signals = seq.generate();
3091 for signal in &signals {
3092 mgr.process(signal);
3093 }
3094 if seq.generated_tokens() < seq.max_output_tokens() {
3095 seq.commit_allocation(seq.len());
3096 }
3097 }
3098 assert_eq!(mgr.num_active_blocks(), 3);
3099
3100 let first_reset = seq.reset_with_signal();
3101 for signal in &first_reset {
3102 mgr.process(signal);
3103 }
3104 assert_eq!(mgr.num_active_blocks(), 0);
3105
3106 let prompt_only = seq.prepare_allocation(seq.num_input_tokens()).unwrap();
3107 assert_eq!(expect_ready(mgr.process(&prompt_only)), 2);
3108 seq.commit_allocation(seq.num_input_tokens());
3109 assert_eq!(mgr.num_active_blocks(), 2);
3110
3111 let second_reset = seq.reset_with_signal();
3112 for signal in &second_reset {
3113 mgr.process(signal);
3114 }
3115 assert_eq!(mgr.num_active_blocks(), 0);
3116 }
3117
3118 #[test]
3122 fn test_inactive_hit_does_not_republish_stored() {
3123 let (mut mgr, sink) = make_mgr_capturing(4, 16);
3124
3125 use_full(&mut mgr, 1, plh(100));
3127 deref_full(&mut mgr, 1);
3130 use_full(&mut mgr, 1, plh(100));
3133
3134 let events = sink.events.lock().unwrap();
3135 let stored_count = events
3136 .iter()
3137 .filter(|e| matches!(e.data, KvCacheEventData::Stored(_)))
3138 .count();
3139 let removed_count = events
3140 .iter()
3141 .filter(|e| matches!(e.data, KvCacheEventData::Removed(_)))
3142 .count();
3143 assert_eq!(stored_count, 1, "reactivation must not re-emit Stored");
3144 assert_eq!(removed_count, 0, "Deref must not emit Removed");
3145 }
3146
3147 #[test]
3148 fn destination_activation_collision_reuses_canonical_block_and_offload_metadata() {
3149 let (mut mgr, sink) = make_mgr_capturing(2, 4);
3150 assert_eq!(
3151 mgr.block_manager.duplication_policy(),
3152 &BlockDuplicationPolicy::Reject
3153 );
3154 let sequence = ActiveSequence::new(vec![1, 2, 3, 4], 1, Some(4), true, true);
3155 let reservation = expect_ready(mgr.reserve_destination_at(&sequence, None));
3156 let reserved_block_id = reservation.block_ids()[0];
3157 assert_eq!(mgr.num_active_blocks(), 1);
3158
3159 let signal = sequence
3160 .prepare_allocation(sequence.num_input_tokens())
3161 .expect("full prompt should require allocation");
3162 let MoveBlock::Use(blocks, local_hashes, plhs, token_ids, _) = &signal else {
3163 panic!("expected full prompt allocation");
3164 };
3165 let UniqueBlock::FullBlock(seq_hash) = blocks[0] else {
3166 panic!("expected a full prompt block");
3167 };
3168 let plh = plhs[0];
3169 let local_hash = local_hashes[0];
3170 let token_ids = token_ids.as_ref().expect("token metadata enabled")[0].clone();
3171
3172 assert_eq!(expect_ready(mgr.process(&signal)), 1);
3173 let canonical_block_id = mgr.active_block_ids(&sequence)[0];
3174 assert_ne!(reserved_block_id, canonical_block_id);
3175 assert_eq!(mgr.num_active_blocks(), 2);
3176 sink.events.lock().unwrap().clear();
3177
3178 mgr.activate_destination(reservation);
3179
3180 assert_eq!(mgr.num_active_blocks(), 1);
3181 assert_eq!(mgr.active_block_ids(&sequence), vec![canonical_block_id]);
3182 assert!(
3183 sink.events.lock().unwrap().is_empty(),
3184 "collision must not emit another Stored"
3185 );
3186
3187 let metadata = mgr
3189 .registered_blocks
3190 .get(&plh)
3191 .expect("canonical registry metadata must remain available for offload");
3192 assert_eq!(metadata.seq_hash, seq_hash);
3193 assert_eq!(metadata.block_id, canonical_block_id);
3194 assert_eq!(metadata.parent_hash, None);
3195 assert_eq!(metadata.local_hash, Some(local_hash));
3196 assert_eq!(metadata.token_ids.as_deref(), Some(token_ids.as_slice()));
3197 }
3198
3199 #[test]
3200 fn destination_transfer_footprint_uses_missing_physical_blocks() {
3201 let (mut mgr, _) = make_mgr_capturing(16, 4);
3202
3203 let cold = ActiveSequence::new((0..10).collect(), 1, Some(4), true, true);
3204 let cold_reservation = expect_ready(mgr.reserve_destination_at(&cold, None));
3205 assert_eq!(cold_reservation.transferable_prompt_tokens(4), 12);
3206 drop(cold_reservation);
3207
3208 let mut prefix = ActiveSequence::new((0..4).collect(), 1, Some(4), true, true);
3209 let prefix_allocation = prefix
3210 .prepare_allocation(prefix.num_input_tokens())
3211 .expect("prefix should require one full block");
3212 assert_eq!(expect_ready(mgr.process(&prefix_allocation)), 1);
3213 prefix.commit_allocation(prefix.num_input_tokens());
3214
3215 let partial = expect_ready(mgr.reserve_destination_at(&cold, None));
3216 assert_eq!(partial.transferable_prompt_tokens(4), 8);
3217 drop(partial);
3218
3219 let mut aligned = ActiveSequence::new((20..28).collect(), 1, Some(4), true, true);
3220 let aligned_allocation = aligned
3221 .prepare_allocation(aligned.num_input_tokens())
3222 .expect("aligned prompt should require two full blocks");
3223 assert_eq!(expect_ready(mgr.process(&aligned_allocation)), 2);
3224 aligned.commit_allocation(aligned.num_input_tokens());
3225 let full_hit = expect_ready(mgr.reserve_destination_at(&aligned, None));
3226 assert_eq!(full_hit.transferable_prompt_tokens(4), 0);
3227 }
3228
3229 #[test]
3230 fn destination_activation_splits_stores_across_reused_middle_block() {
3231 let (mut mgr, sink) = make_mgr_capturing(8, 4);
3232 let sequence = ActiveSequence::new((0..12).collect(), 1, Some(4), true, true);
3233 let reservation = expect_ready(mgr.reserve_destination_at(&sequence, None));
3234 let signal = sequence
3235 .prepare_allocation(sequence.num_input_tokens())
3236 .expect("full prompt should require allocation");
3237 let MoveBlock::Use(blocks, _, plhs, _, _) = &signal else {
3238 panic!("expected full prompt allocation");
3239 };
3240 let [
3241 UniqueBlock::FullBlock(first),
3242 UniqueBlock::FullBlock(middle),
3243 UniqueBlock::FullBlock(last),
3244 ] = blocks.as_slice()
3245 else {
3246 panic!("expected three full prompt blocks");
3247 };
3248
3249 use_full(&mut mgr, *middle, plhs[1]);
3250 sink.events.lock().unwrap().clear();
3251
3252 mgr.activate_destination(reservation);
3253
3254 let events = sink.events.lock().unwrap();
3255 assert_eq!(events.len(), 2);
3256 let KvCacheEventData::Stored(first_store) = &events[0].data else {
3257 panic!("expected first Stored event");
3258 };
3259 assert_eq!(first_store.blocks.len(), 1);
3260 assert_eq!(
3261 first_store.blocks[0].block_hash,
3262 ExternalSequenceBlockHash(*first)
3263 );
3264 let KvCacheEventData::Stored(last_store) = &events[1].data else {
3265 panic!("expected second Stored event");
3266 };
3267 assert_eq!(last_store.blocks.len(), 1);
3268 assert_eq!(
3269 last_store.blocks[0].block_hash,
3270 ExternalSequenceBlockHash(*last)
3271 );
3272 assert_eq!(
3273 last_store.parent_hash,
3274 Some(ExternalSequenceBlockHash(*middle))
3275 );
3276 }
3277
3278 #[test]
3279 fn destination_activation_validates_layout_before_committing_blocks() {
3280 let mut mgr = make_mgr(4, 4);
3281 let unpublished_blocks = expect_ready(mgr.allocate_unpublished_blocks(2, None));
3282 let reservation = VllmDestinationReservation {
3283 cached_prefix: Vec::new(),
3284 unpublished_blocks,
3285 layout: Some(MoveBlock::Use(
3286 vec![UniqueBlock::FullBlock(1), UniqueBlock::FullBlock(2)],
3287 Vec::new(),
3288 vec![plh(1)],
3289 None,
3290 None,
3291 )),
3292 };
3293
3294 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3295 mgr.activate_destination(reservation);
3296 }));
3297
3298 assert!(result.is_err());
3299 assert_eq!(mgr.num_active_blocks(), 0);
3300 assert!(mgr.active_full.is_empty());
3301 assert!(mgr.registered_blocks.is_empty());
3302 }
3303
3304 #[test]
3308 fn test_stored_suffix_anchors_to_last_reused_block() {
3309 let (mut mgr, sink) = make_mgr_capturing(8, 16);
3310
3311 mgr.process(&MoveBlock::Use(
3313 vec![UniqueBlock::FullBlock(10), UniqueBlock::FullBlock(11)],
3314 vec![],
3315 vec![plh(10), plh(11)],
3316 None,
3317 None,
3318 ));
3319 deref_full(&mut mgr, 10);
3321 deref_full(&mut mgr, 11);
3322
3323 sink.events.lock().unwrap().clear();
3325
3326 mgr.process(&MoveBlock::Use(
3328 vec![
3329 UniqueBlock::FullBlock(10),
3330 UniqueBlock::FullBlock(11),
3331 UniqueBlock::FullBlock(12),
3332 ],
3333 vec![],
3334 vec![plh(10), plh(11), plh(12)],
3335 None,
3336 None, ));
3338
3339 let events = sink.events.lock().unwrap();
3340 assert_eq!(events.len(), 1, "only new suffix should fire a Stored");
3342 let KvCacheEventData::Stored(ref data) = events[0].data else {
3343 panic!("expected Stored");
3344 };
3345 assert_eq!(data.blocks.len(), 1, "Stored must only include C");
3346 assert_eq!(data.blocks[0].block_hash, ExternalSequenceBlockHash(12));
3347 assert_eq!(
3348 data.parent_hash,
3349 Some(ExternalSequenceBlockHash(11)),
3350 "parent_hash must anchor to last reused full block (B=11)"
3351 );
3352 }
3353
3354 #[cfg(feature = "kvbm-offload")]
3355 #[test]
3356 fn test_swap_in_registration_anchors_suffix_to_reused_prefix_parent() {
3357 let (mut mgr, sink) = make_mgr_capturing(8, 16);
3358 let slots = match mgr.reserve_swap_in_destination_slots(2) {
3359 SwapInSlotReservation::Reserved(slots) => slots,
3360 SwapInSlotReservation::BlockedOnG1Offload(_) => {
3361 panic!("fresh manager should not need G1 offload")
3362 }
3363 SwapInSlotReservation::NoCapacity => panic!("fresh manager should have capacity"),
3364 };
3365
3366 let entries = vec![
3367 SwapInRegistrationBlock {
3368 seq_hash: 12,
3369 plh: plh(12),
3370 local_hash: Some(120),
3371 token_ids: Some(vec![1; 16]),
3372 },
3373 SwapInRegistrationBlock {
3374 seq_hash: 13,
3375 plh: plh(13),
3376 local_hash: Some(130),
3377 token_ids: Some(vec![2; 16]),
3378 },
3379 ];
3380 let entries_len = entries.len();
3381 let outcome = mgr.register_swapped_in_blocks(entries, Some(11), slots);
3382 assert_eq!(
3383 outcome.consumed_entries, entries_len,
3384 "all reserved swap-in slots should be consumed"
3385 );
3386
3387 let events = sink.events.lock().unwrap();
3388 assert_eq!(events.len(), 1, "swap-in suffix should publish one Stored");
3389 let KvCacheEventData::Stored(ref data) = events[0].data else {
3390 panic!("expected Stored");
3391 };
3392 assert_eq!(
3393 data.parent_hash,
3394 Some(ExternalSequenceBlockHash(11)),
3395 "swapped-in suffix must anchor to the last reused prefix block"
3396 );
3397 let blocks: Vec<u64> = data.blocks.iter().map(|block| block.block_hash.0).collect();
3398 assert_eq!(blocks, vec![12, 13]);
3399 let local_hashes: Vec<u64> = data
3400 .blocks
3401 .iter()
3402 .map(|block| block.tokens_hash.0)
3403 .collect();
3404 assert_eq!(local_hashes, vec![120, 130]);
3405 assert_eq!(
3406 mgr.num_inactive_blocks(),
3407 2,
3408 "registered swap-in blocks should land in inactive G1"
3409 );
3410 }
3411
3412 #[test]
3416 fn test_shared_prefix_distinct_vs_refcount() {
3417 let mut mgr = make_mgr(8, 16);
3418
3419 mgr.process(&MoveBlock::Use(
3421 vec![
3422 UniqueBlock::FullBlock(10),
3423 UniqueBlock::FullBlock(11),
3424 UniqueBlock::FullBlock(12),
3425 ],
3426 vec![],
3427 vec![plh(10), plh(11), plh(12)],
3428 None,
3429 None,
3430 ));
3431 assert_eq!(mgr.num_active_blocks(), 3);
3432 assert_eq!(mgr.num_active_block_refs(), 3);
3433
3434 mgr.process(&MoveBlock::Use(
3436 vec![
3437 UniqueBlock::FullBlock(10),
3438 UniqueBlock::FullBlock(11),
3439 UniqueBlock::FullBlock(13),
3440 ],
3441 vec![],
3442 vec![plh(10), plh(11), plh(13)],
3443 None,
3444 None,
3445 ));
3446
3447 assert_eq!(
3449 mgr.num_active_blocks(),
3450 4,
3451 "shared prefix must not inflate distinct count"
3452 );
3453 assert_eq!(
3455 mgr.num_active_block_refs(),
3456 6,
3457 "handle count should reflect per-request refcount"
3458 );
3459 }
3460
3461 #[test]
3464 fn test_random_plh_stable_across_preempt_retry() {
3465 let block_size = 16;
3467 let tokens: Vec<u32> = (0..64).collect();
3468 let mut seq = ActiveSequence::new(tokens, 100, Some(block_size), false, false);
3469
3470 let (mut mgr, sink) = make_mgr_capturing(8, block_size);
3471
3472 let signal = seq.take_creation_signal().unwrap();
3474 assert_eq!(expect_ready(mgr.process(&signal)), 4);
3475 assert_eq!(mgr.num_active_blocks(), 4);
3476
3477 let reset_signals = seq.reset_with_signal();
3480 for signal in &reset_signals {
3481 mgr.process(signal);
3482 }
3483 assert_eq!(mgr.num_active_blocks(), 0);
3484 assert_eq!(mgr.num_inactive_blocks(), 4);
3485
3486 let signal = seq.take_creation_signal().unwrap();
3489 assert_eq!(expect_ready(mgr.process(&signal)), 4);
3490 assert_eq!(mgr.num_active_blocks(), 4);
3491 assert_eq!(mgr.num_inactive_blocks(), 0);
3492
3493 let events = sink.events.lock().unwrap();
3495 let stored_count = events
3496 .iter()
3497 .filter(|e| matches!(e.data, KvCacheEventData::Stored(_)))
3498 .count();
3499 assert_eq!(
3500 stored_count, 1,
3501 "preempted request should self-match on re-admit (no duplicate Stored)"
3502 );
3503 }
3504
3505 #[test]
3506 fn test_eviction_emits_exact_removed_event() {
3507 let (mut mgr, sink) = make_mgr_capturing(2, 16);
3512
3513 mgr.process(&MoveBlock::Use(
3515 vec![UniqueBlock::FullBlock(10), UniqueBlock::FullBlock(11)],
3516 vec![],
3517 vec![plh(10), plh(11)],
3518 None,
3519 None,
3520 ));
3521 deref_full(&mut mgr, 10);
3522 deref_full(&mut mgr, 11);
3523 assert_eq!(mgr.num_active_blocks(), 0);
3524 assert_eq!(mgr.num_inactive_blocks(), 2);
3525
3526 sink.events.lock().unwrap().clear();
3527
3528 use_full(&mut mgr, 12, plh(12));
3530
3531 let events = sink.events.lock().unwrap();
3532 let removed: Vec<u64> = events
3533 .iter()
3534 .filter_map(|e| match &e.data {
3535 KvCacheEventData::Removed(data) => Some(
3536 data.block_hashes
3537 .iter()
3538 .map(|ExternalSequenceBlockHash(h)| *h)
3539 .collect::<Vec<_>>(),
3540 ),
3541 _ => None,
3542 })
3543 .flatten()
3544 .collect();
3545 let stored_count = events
3546 .iter()
3547 .filter(|e| matches!(e.data, KvCacheEventData::Stored(_)))
3548 .count();
3549
3550 assert_eq!(
3551 removed.len(),
3552 1,
3553 "exactly one block should be reported as evicted"
3554 );
3555 assert!(
3556 removed[0] == 10 || removed[0] == 11,
3557 "evicted hash must be one we seeded ({}), got {}",
3558 "10 or 11",
3559 removed[0]
3560 );
3561 assert_eq!(stored_count, 1, "one Stored event for the fresh block 12");
3562 }
3563
3564 #[cfg(feature = "kvbm-offload")]
3565 mod offload {
3566 use super::*;
3567 use crate::common::protocols::{RawKvEvent, RawKvEventSink};
3568 use crate::kvbm_offload::{KvbmOffloadConfig, MockOffloadEngine};
3569 use std::sync::{Arc, Mutex};
3570
3571 #[derive(Default)]
3572 struct TierCapturingSink {
3573 events: Mutex<Vec<(StorageTier, KvCacheEvent)>>,
3574 }
3575
3576 impl TierCapturingSink {
3577 fn clear(&self) {
3578 self.events.lock().unwrap().clear();
3579 }
3580
3581 fn take(&self) -> Vec<(StorageTier, KvCacheEvent)> {
3582 std::mem::take(&mut *self.events.lock().unwrap())
3583 }
3584 }
3585
3586 impl KvCacheEventSink for TierCapturingSink {
3587 fn publish(&self, event: KvCacheEvent) -> anyhow::Result<()> {
3588 self.publish_with_storage_tier(event, StorageTier::Device)
3589 }
3590
3591 fn publish_with_storage_tier(
3592 &self,
3593 event: KvCacheEvent,
3594 storage_tier: StorageTier,
3595 ) -> anyhow::Result<()> {
3596 self.events.lock().unwrap().push((storage_tier, event));
3597 Ok(())
3598 }
3599 }
3600
3601 #[derive(Default)]
3602 struct RawCapturingSink {
3603 events: Mutex<Vec<RawKvEvent>>,
3604 }
3605
3606 impl RawCapturingSink {
3607 fn clear(&self) {
3608 self.events.lock().unwrap().clear();
3609 }
3610
3611 fn take(&self) -> Vec<RawKvEvent> {
3612 std::mem::take(&mut *self.events.lock().unwrap())
3613 }
3614 }
3615
3616 impl RawKvEventSink for RawCapturingSink {
3617 fn publish(&self, event: RawKvEvent) -> anyhow::Result<()> {
3618 self.events.lock().unwrap().push(event);
3619 Ok(())
3620 }
3621 }
3622
3623 fn make_mgr_tier_capturing(
3624 capacity: usize,
3625 block_size: usize,
3626 ) -> (KvManager, Arc<TierCapturingSink>) {
3627 let sink = Arc::new(TierCapturingSink::default());
3628 let publishers = KvEventPublishers::new(Some(sink.clone() as _), None);
3629 (
3630 KvManager::new_with_event_sink(capacity, block_size, publishers, 0),
3631 sink,
3632 )
3633 }
3634
3635 fn make_mgr_raw_capturing(
3636 capacity: usize,
3637 block_size: usize,
3638 ) -> (KvManager, Arc<RawCapturingSink>) {
3639 let sink = Arc::new(RawCapturingSink::default());
3640 let publishers = KvEventPublishers::new(None, Some(sink.clone() as _));
3641 (
3642 KvManager::new_with_event_sink(capacity, block_size, publishers, 0),
3643 sink,
3644 )
3645 }
3646
3647 fn attach_test_offload_engine(
3648 mgr: &mut KvManager,
3649 num_g2_blocks: usize,
3650 block_size_tokens: usize,
3651 ) {
3652 let config = KvbmOffloadConfig {
3653 num_g2_blocks,
3654 block_size_tokens,
3655 block_size_bytes: Some(1_000_000),
3656 bandwidth_g1_to_g2_gbps: 1.0,
3657 ..Default::default()
3658 };
3659 let rt = tokio::runtime::Builder::new_multi_thread()
3660 .worker_threads(1)
3661 .enable_all()
3662 .build()
3663 .unwrap();
3664 let mut engine = rt
3665 .block_on(MockOffloadEngine::new(config))
3666 .expect("engine build");
3667 engine.attach_runtime(rt);
3668 mgr.attach_new_offload_engine(engine);
3669 }
3670
3671 fn seed_g2_block(mgr: &KvManager, p: PositionalLineageHash) {
3672 let engine = mgr
3673 .offload_engine
3674 .as_ref()
3675 .expect("offload engine attached")
3676 .lock()
3677 .expect("offload engine mutex poisoned");
3678 let g2 = engine.g2_manager();
3679 let (mut slots, _evicted) = g2
3680 .allocate_blocks_with_evictions(1)
3681 .expect("G2 test seed should fit");
3682 let mutable = slots.pop().expect("one G2 test slot");
3683 let complete = mutable
3684 .stage(p, g2.block_size())
3685 .expect("G2 test seed stage");
3686 drop(g2.register_block(complete));
3687 }
3688
3689 fn use_full_with_hash(
3690 mgr: &mut KvManager,
3691 seq_hash: u64,
3692 p: PositionalLineageHash,
3693 local_hash: BlockHash,
3694 token_ids: Vec<u32>,
3695 ) -> G1Acquire<usize> {
3696 mgr.process(&MoveBlock::Use(
3697 vec![UniqueBlock::FullBlock(seq_hash)],
3698 vec![local_hash],
3699 vec![p],
3700 Some(vec![token_ids]),
3701 None,
3702 ))
3703 }
3704
3705 fn has_removed(
3706 events: &[(StorageTier, KvCacheEvent)],
3707 storage_tier: StorageTier,
3708 seq_hash: u64,
3709 ) -> bool {
3710 events.iter().any(|(tier, event)| {
3711 *tier == storage_tier
3712 && matches!(
3713 &event.data,
3714 KvCacheEventData::Removed(data)
3715 if data.block_hashes.contains(&ExternalSequenceBlockHash(seq_hash))
3716 )
3717 })
3718 }
3719
3720 fn stored_block(
3721 events: &[(StorageTier, KvCacheEvent)],
3722 storage_tier: StorageTier,
3723 seq_hash: u64,
3724 ) -> Option<KvCacheStoredBlockData> {
3725 events.iter().find_map(|(tier, event)| {
3726 if *tier != storage_tier {
3727 return None;
3728 }
3729 let KvCacheEventData::Stored(data) = &event.data else {
3730 return None;
3731 };
3732 data.blocks
3733 .iter()
3734 .find(|block| block.block_hash == ExternalSequenceBlockHash(seq_hash))
3735 .cloned()
3736 })
3737 }
3738
3739 fn raw_stored_with_token_ids(
3740 events: &[RawKvEvent],
3741 storage_tier: StorageTier,
3742 seq_hash: u64,
3743 token_ids: &[u32],
3744 ) -> bool {
3745 let expected_token_ids = vec![token_ids.to_vec()];
3746 events.iter().any(|event| {
3747 event.storage_tier == storage_tier
3748 && event.block_token_ids.as_ref() == Some(&expected_token_ids)
3749 && matches!(
3750 &event.event.data,
3751 KvCacheEventData::Stored(data)
3752 if data.blocks.iter().any(|block| {
3753 block.block_hash == ExternalSequenceBlockHash(seq_hash)
3754 })
3755 )
3756 })
3757 }
3758
3759 #[test]
3760 fn unregistered_evictions_commit_without_offload_wait() {
3761 const SLOTS: usize = 2;
3762
3763 let (mut mgr, sink) = make_mgr_tier_capturing(SLOTS, 4);
3764 attach_test_offload_engine(&mut mgr, SLOTS + 1, 4);
3765
3766 let source_plhs: Vec<_> = (0..SLOTS).map(|index| plh(50_000 + index as u64)).collect();
3767 for (index, source_plh) in source_plhs.iter().copied().enumerate() {
3768 assert_eq!(use_full(&mut mgr, 60_000 + index as u64, source_plh), 1);
3769 deref_full(&mut mgr, 60_000 + index as u64);
3770 }
3771 for source_plh in &source_plhs {
3772 assert!(mgr.registered_blocks.remove(source_plh).is_some());
3773 }
3774 assert_eq!(mgr.block_manager.available_blocks(), SLOTS);
3775 sink.clear();
3776
3777 let mut sequence = ActiveSequence::new((0..8).collect(), 1, Some(4), true, true);
3778 let signal = sequence
3779 .prepare_allocation(sequence.num_input_tokens())
3780 .expect("two-block prompt must allocate");
3781 let MoveBlock::Use(blocks, _, target_plhs, _, _) = &signal else {
3782 panic!("creation signal must be Use");
3783 };
3784 let target_hashes: Vec<_> = blocks
3785 .iter()
3786 .map(|block| match block {
3787 UniqueBlock::FullBlock(seq_hash) => *seq_hash,
3788 UniqueBlock::PartialBlock(_) => panic!("exact prompt blocks must be full"),
3789 })
3790 .collect();
3791 assert_eq!(target_hashes.len(), SLOTS);
3792 assert!(
3793 target_plhs
3794 .iter()
3795 .all(|target| !mgr.registered_blocks.contains_key(target))
3796 );
3797 let generation_before = mgr.capacity_generation;
3798 let allocated_before = sequence.num_allocated_tokens();
3799
3800 assert_eq!(expect_ready(mgr.process(&signal)), SLOTS);
3801
3802 assert_eq!(mgr.capacity_generation, generation_before);
3803 assert!(mgr.earliest_offload_deadline().is_none());
3804 let (source_slot_ids, offload_block_ids) = mgr
3805 .offload_engine
3806 .as_ref()
3807 .expect("offload engine attached")
3808 .lock()
3809 .expect("offload engine mutex poisoned")
3810 .pending_g1_transfer_ownership();
3811 assert!(source_slot_ids.is_empty());
3812 assert!(offload_block_ids.is_empty());
3813 assert_eq!(mgr.num_active_block_refs(), SLOTS);
3814 assert_eq!(sequence.num_allocated_tokens(), allocated_before);
3815 assert!(
3816 target_plhs
3817 .iter()
3818 .all(|target| mgr.registered_blocks.contains_key(target))
3819 );
3820 assert!(target_hashes.iter().all(|target| {
3821 mgr.active_full
3822 .get(target)
3823 .is_some_and(|active| active.logical_refs == 1)
3824 }));
3825
3826 let committed = sink.take();
3827 assert!(target_hashes.iter().all(|target| {
3828 stored_block(&committed, StorageTier::Device, *target).is_some()
3829 }));
3830 sequence.commit_allocation(sequence.num_input_tokens());
3831 assert_eq!(sequence.num_allocated_tokens(), sequence.num_input_tokens());
3832 }
3833
3834 #[test]
3835 fn blocked_fresh_use_is_invisible_until_commit() {
3836 let (mut mgr, sink) = make_mgr_tier_capturing(2, 4);
3837 attach_test_offload_engine(&mut mgr, 4, 4);
3838
3839 assert_eq!(use_full(&mut mgr, 99, plh(99)), 1);
3840 deref_full(&mut mgr, 99);
3841 sink.clear();
3842
3843 let mut sequence = ActiveSequence::new((0..8).collect(), 1, Some(4), true, true);
3844 let signal = sequence
3845 .prepare_allocation(sequence.num_input_tokens())
3846 .expect("two-block prompt must allocate");
3847 let MoveBlock::Use(blocks, _, target_plhs, _, _) = &signal else {
3848 panic!("creation signal must be Use");
3849 };
3850 let target_hashes: Vec<_> = blocks
3851 .iter()
3852 .map(|block| match block {
3853 UniqueBlock::FullBlock(seq_hash) => *seq_hash,
3854 UniqueBlock::PartialBlock(_) => panic!("exact prompt blocks must be full"),
3855 })
3856 .collect();
3857 assert_eq!(target_hashes.len(), 2);
3858 let allocated_before = sequence.num_allocated_tokens();
3859 assert_eq!(allocated_before, 0);
3860 let cost_before = mgr.get_prefill_cost(&sequence);
3861 let cost_before = (
3862 cost_before.new_blocks,
3863 cost_before.new_tokens,
3864 cost_before.cached_tokens,
3865 cost_before.active_cached_tokens,
3866 );
3867 let refs_before = mgr.num_active_block_refs();
3868
3869 let outcome = mgr.process(&signal);
3870 assert!(matches!(outcome, G1Acquire::BlockedOnOffload { .. }));
3871 assert_eq!(mgr.num_active_block_refs(), refs_before);
3872 assert_eq!(sequence.num_allocated_tokens(), allocated_before);
3873 let cost_after = mgr.get_prefill_cost(&sequence);
3874 assert_eq!(
3875 (
3876 cost_after.new_blocks,
3877 cost_after.new_tokens,
3878 cost_after.cached_tokens,
3879 cost_after.active_cached_tokens,
3880 ),
3881 cost_before
3882 );
3883 assert!(
3884 target_plhs
3885 .iter()
3886 .all(|target| !mgr.registered_blocks.contains_key(target)),
3887 "failed Use must not register a fresh block"
3888 );
3889 let immediate = sink.take();
3890 assert!(target_hashes.iter().all(|target| {
3891 stored_block(&immediate, StorageTier::Device, *target).is_none()
3892 }));
3893
3894 let deadline = mgr
3895 .earliest_offload_deadline()
3896 .expect("real G1 eviction must expose its active transfer deadline");
3897 mgr.tick_offload_engine(deadline);
3898 assert_eq!(expect_ready(mgr.process(&signal)), blocks.len());
3899 assert_eq!(sequence.num_allocated_tokens(), allocated_before);
3900 sequence.commit_allocation(sequence.num_input_tokens());
3901 assert_eq!(sequence.num_allocated_tokens(), sequence.num_input_tokens());
3902
3903 let committed = sink.take();
3904 assert!(target_hashes.iter().all(|target| {
3905 stored_block(&committed, StorageTier::Device, *target).is_some()
3906 }));
3907 }
3908
3909 #[test]
3910 fn blocked_use_holds_only_actual_offload_sources() {
3911 const REQUESTED_SLOTS: usize = 40;
3912 const OFFLOADED_SLOTS: usize = 5;
3913
3914 let (mut mgr, sink) = make_mgr_tier_capturing(REQUESTED_SLOTS, 4);
3915 attach_test_offload_engine(&mut mgr, OFFLOADED_SLOTS + 1, 4);
3916
3917 let source_plhs: Vec<_> = (0..OFFLOADED_SLOTS)
3918 .map(|index| plh(10_000 + index as u64))
3919 .collect();
3920 for (index, source_plh) in source_plhs.iter().copied().enumerate() {
3921 assert_eq!(use_full(&mut mgr, 20_000 + index as u64, source_plh), 1);
3922 }
3923 let mut expected_source_ids: Vec<_> = source_plhs
3924 .iter()
3925 .map(|source_plh| mgr.registered_blocks[source_plh].block_id)
3926 .collect();
3927 expected_source_ids.sort_unstable();
3928 for index in 0..OFFLOADED_SLOTS {
3929 deref_full(&mut mgr, 20_000 + index as u64);
3930 }
3931 assert_eq!(mgr.block_manager.available_blocks(), REQUESTED_SLOTS);
3932 sink.clear();
3933
3934 let mut sequence = ActiveSequence::new(
3935 (0..(REQUESTED_SLOTS * 4) as u32).collect(),
3936 1,
3937 Some(4),
3938 true,
3939 true,
3940 );
3941 let signal = sequence
3942 .prepare_allocation(sequence.num_input_tokens())
3943 .expect("forty-block prompt must allocate");
3944 let MoveBlock::Use(blocks, _, target_plhs, _, _) = &signal else {
3945 panic!("creation signal must be Use");
3946 };
3947 assert_eq!(blocks.len(), REQUESTED_SLOTS);
3948 let target_hashes: Vec<_> = blocks
3949 .iter()
3950 .map(|block| match block {
3951 UniqueBlock::FullBlock(seq_hash) => *seq_hash,
3952 UniqueBlock::PartialBlock(_) => panic!("exact prompt blocks must be full"),
3953 })
3954 .collect();
3955 let allocated_before = sequence.num_allocated_tokens();
3956 let refs_before = mgr.num_active_block_refs();
3957 let generation_before = mgr.capacity_generation;
3958 let cost_before = mgr.get_prefill_cost(&sequence);
3959 let cost_before = (
3960 cost_before.new_blocks,
3961 cost_before.new_tokens,
3962 cost_before.cached_tokens,
3963 cost_before.active_cached_tokens,
3964 );
3965
3966 assert!(matches!(
3967 mgr.process(&signal),
3968 G1Acquire::BlockedOnOffload { .. }
3969 ));
3970
3971 assert_eq!(mgr.num_active_block_refs(), refs_before);
3972 assert_eq!(sequence.num_allocated_tokens(), allocated_before);
3973 assert_eq!(mgr.capacity_generation, generation_before);
3974 assert_eq!(
3975 mgr.block_manager.available_blocks(),
3976 REQUESTED_SLOTS - OFFLOADED_SLOTS
3977 );
3978 assert_eq!(mgr.num_active_blocks(), OFFLOADED_SLOTS);
3979 let (source_slot_ids, offload_block_ids) = mgr
3980 .offload_engine
3981 .as_ref()
3982 .expect("offload engine attached")
3983 .lock()
3984 .expect("offload engine mutex poisoned")
3985 .pending_g1_transfer_ownership();
3986 assert_eq!(source_slot_ids, expected_source_ids);
3987 assert_eq!(offload_block_ids, expected_source_ids);
3988 let cost_after = mgr.get_prefill_cost(&sequence);
3989 assert_eq!(
3990 (
3991 cost_after.new_blocks,
3992 cost_after.new_tokens,
3993 cost_after.cached_tokens,
3994 cost_after.active_cached_tokens,
3995 ),
3996 cost_before
3997 );
3998 assert!(
3999 target_plhs
4000 .iter()
4001 .all(|target| !mgr.registered_blocks.contains_key(target))
4002 );
4003 let blocked_events = sink.take();
4004 assert!(target_hashes.iter().all(|target| {
4005 stored_block(&blocked_events, StorageTier::Device, *target).is_none()
4006 }));
4007
4008 let deadline = mgr
4009 .earliest_offload_deadline()
4010 .expect("real G1 eviction must expose its active transfer deadline");
4011 mgr.tick_offload_engine(deadline);
4012 assert_eq!(mgr.block_manager.available_blocks(), REQUESTED_SLOTS);
4013 assert_eq!(
4014 mgr.capacity_generation,
4015 generation_before + OFFLOADED_SLOTS as u64
4016 );
4017
4018 assert_eq!(expect_ready(mgr.process(&signal)), REQUESTED_SLOTS);
4019 assert_eq!(sequence.num_allocated_tokens(), allocated_before);
4020 sequence.commit_allocation(sequence.num_input_tokens());
4021 assert_eq!(sequence.num_allocated_tokens(), sequence.num_input_tokens());
4022 assert_eq!(mgr.num_active_block_refs(), REQUESTED_SLOTS);
4023 let committed_events = sink.take();
4024 assert!(target_hashes.iter().all(|target| {
4025 stored_block(&committed_events, StorageTier::Device, *target).is_some()
4026 }));
4027 }
4028
4029 #[test]
4030 fn presence_filtered_eviction_retries_once() {
4031 const RELEASED_SLOTS: usize = 5;
4032
4033 let (mut mgr, sink) = make_mgr_tier_capturing(RELEASED_SLOTS, 4);
4034 attach_test_offload_engine(&mut mgr, RELEASED_SLOTS + 1, 4);
4035
4036 for index in 0..RELEASED_SLOTS {
4037 let source_plh = plh(30_000 + index as u64);
4038 assert_eq!(use_full(&mut mgr, 40_000 + index as u64, source_plh), 1);
4039 seed_g2_block(&mgr, source_plh);
4040 }
4041 for index in 0..RELEASED_SLOTS {
4042 deref_full(&mut mgr, 40_000 + index as u64);
4043 }
4044 sink.clear();
4045 let generation_before = mgr.capacity_generation;
4046
4047 let sequence = ActiveSequence::new(
4048 (100..100 + (RELEASED_SLOTS * 4) as u32).collect(),
4049 1,
4050 Some(4),
4051 true,
4052 true,
4053 );
4054 let signal = sequence
4055 .prepare_allocation(sequence.num_input_tokens())
4056 .expect("five-block prompt must allocate");
4057 assert_eq!(expect_ready(mgr.process(&signal)), RELEASED_SLOTS);
4058 assert_eq!(
4059 mgr.capacity_generation,
4060 generation_before + RELEASED_SLOTS as u64
4061 );
4062 assert!(mgr.earliest_offload_deadline().is_none());
4063 assert_eq!(mgr.num_active_block_refs(), RELEASED_SLOTS);
4064
4065 let events = sink.take();
4066 let MoveBlock::Use(blocks, _, _, _, _) = signal else {
4067 panic!("creation signal must be Use");
4068 };
4069 assert!(blocks.iter().all(|block| {
4070 let UniqueBlock::FullBlock(seq_hash) = block else {
4071 return false;
4072 };
4073 stored_block(&events, StorageTier::Device, *seq_hash).is_some()
4074 }));
4075 }
4076
4077 #[test]
4078 fn queued_use_tracks_exact_transfer_dependency() {
4079 let mut mgr = make_mgr(2, 4);
4080 let runtime = tokio::runtime::Builder::new_multi_thread()
4081 .worker_threads(1)
4082 .enable_all()
4083 .build()
4084 .unwrap();
4085 let mut engine = runtime
4086 .block_on(MockOffloadEngine::new(KvbmOffloadConfig {
4087 block_size_tokens: 4,
4088 block_size_bytes: Some(1_000_000),
4089 bandwidth_g1_to_g2_gbps: 1.0,
4090 offload_batch_size: 1,
4091 ..Default::default()
4092 }))
4093 .expect("engine build");
4094 engine.attach_runtime(runtime);
4095 mgr.attach_new_offload_engine(engine);
4096
4097 assert_eq!(use_full(&mut mgr, 1, plh(1)), 1);
4098 assert_eq!(use_full(&mut mgr, 2, plh(2)), 1);
4099 deref_full(&mut mgr, 1);
4100 deref_full(&mut mgr, 2);
4101
4102 let first_dependency =
4103 match use_full_with_hash(&mut mgr, 3, plh(3), 303, vec![9, 10, 11, 12]) {
4104 G1Acquire::BlockedOnOffload {
4105 offload_id,
4106 deadline_ms,
4107 } => OffloadDependency {
4108 offload_id,
4109 deadline_ms,
4110 },
4111 _ => panic!("first eviction must start an offload dependency"),
4112 };
4113 let first_id = first_dependency.offload_id;
4114 assert!(first_dependency.deadline_ms.is_some());
4115
4116 let queued_dependency =
4117 match use_full_with_hash(&mut mgr, 4, plh(4), 404, vec![13, 14, 15, 16]) {
4118 G1Acquire::BlockedOnOffload {
4119 offload_id,
4120 deadline_ms,
4121 } => OffloadDependency {
4122 offload_id,
4123 deadline_ms,
4124 },
4125 _ => panic!("second eviction must queue behind the active offload"),
4126 };
4127 assert_ne!(queued_dependency.offload_id, first_id);
4128 assert_eq!(queued_dependency.deadline_ms, first_dependency.deadline_ms);
4129 assert_eq!(
4130 mgr.refresh_offload_dependency(queued_dependency),
4131 Some(queued_dependency),
4132 "the queued request must stay protected while its exact lease is active"
4133 );
4134 mgr.tick_offload_engine(
4135 first_dependency
4136 .deadline_ms
4137 .expect("first offload dependency deadline"),
4138 );
4139 assert_eq!(
4140 mgr.refresh_offload_dependency(first_dependency),
4141 None,
4142 "a completed lease must not retarget to an unrelated live offload"
4143 );
4144 assert_eq!(
4145 mgr.refresh_offload_dependency(queued_dependency)
4146 .map(|dependency| dependency.offload_id),
4147 Some(queued_dependency.offload_id),
4148 "the queued lease must remain independently live"
4149 );
4150 assert_eq!(mgr.num_active_block_refs(), 0);
4151 }
4152
4153 #[test]
4154 fn fresh_manager_has_no_offload_engine() {
4155 let mgr = make_mgr(8, 4);
4156 assert!(!mgr.has_offload_engine());
4157 }
4158
4159 #[tokio::test]
4160 async fn attach_new_offload_engine_wires_in_after_construction() {
4161 let mut mgr = make_mgr(16, 4);
4162 assert!(!mgr.has_offload_engine());
4163
4164 let engine = MockOffloadEngine::new(KvbmOffloadConfig::default())
4165 .await
4166 .expect("engine build");
4167 mgr.attach_new_offload_engine(engine);
4168 assert!(mgr.has_offload_engine());
4169 }
4170
4171 #[test]
4172 fn g2_completion_publishes_host_pinned_stored_event() {
4173 let (mut mgr, sink) = make_mgr_tier_capturing(1, 4);
4174 attach_test_offload_engine(&mut mgr, 1, 4);
4175
4176 assert_eq!(
4177 expect_ready(use_full_with_hash(
4178 &mut mgr,
4179 1,
4180 plh(1),
4181 101,
4182 vec![1, 2, 3, 4],
4183 )),
4184 1
4185 );
4186 deref_full(&mut mgr, 1);
4187 sink.clear();
4188
4189 assert!(matches!(
4191 use_full_with_hash(&mut mgr, 2, plh(2), 202, vec![5, 6, 7, 8]),
4192 G1Acquire::BlockedOnOffload { .. }
4193 ));
4194 let immediate = sink.take();
4195 assert!(
4196 has_removed(&immediate, StorageTier::Device, 1),
4197 "G1 eviction should publish a Device-tier Removed event"
4198 );
4199 assert!(
4200 stored_block(&immediate, StorageTier::HostPinned, 1).is_none(),
4201 "G2 Stored must not publish before the transfer completes"
4202 );
4203
4204 let deadline = mgr
4205 .earliest_offload_deadline()
4206 .expect("G1→G2 offload should expose a completion deadline");
4207 mgr.tick_offload_engine(deadline);
4208
4209 let completed = sink.take();
4210 let stored = stored_block(&completed, StorageTier::HostPinned, 1)
4211 .expect("G2 completion should publish HostPinned Stored");
4212 assert_eq!(stored.tokens_hash, LocalBlockHash(101));
4213 }
4214
4215 #[test]
4216 fn g2_eviction_publishes_host_pinned_removed_event() {
4217 let (mut mgr, sink) = make_mgr_tier_capturing(1, 4);
4218 attach_test_offload_engine(&mut mgr, 1, 4);
4219
4220 assert_eq!(
4221 expect_ready(use_full_with_hash(
4222 &mut mgr,
4223 1,
4224 plh(1),
4225 101,
4226 vec![1, 2, 3, 4],
4227 )),
4228 1
4229 );
4230 deref_full(&mut mgr, 1);
4231 assert!(matches!(
4232 use_full_with_hash(&mut mgr, 2, plh(2), 202, vec![5, 6, 7, 8]),
4233 G1Acquire::BlockedOnOffload { .. }
4234 ));
4235 let deadline = mgr
4236 .earliest_offload_deadline()
4237 .expect("first G1→G2 offload should expose a deadline");
4238 mgr.tick_offload_engine(deadline);
4239
4240 assert_eq!(
4243 expect_ready(use_full_with_hash(
4244 &mut mgr,
4245 2,
4246 plh(2),
4247 202,
4248 vec![5, 6, 7, 8],
4249 )),
4250 1
4251 );
4252 deref_full(&mut mgr, 2);
4253 sink.clear();
4254
4255 assert!(matches!(
4256 use_full_with_hash(&mut mgr, 3, plh(3), 303, vec![9, 10, 11, 12]),
4257 G1Acquire::BlockedOnOffload { .. }
4258 ));
4259 let deadline = mgr
4260 .earliest_offload_deadline()
4261 .expect("second G1→G2 offload should expose a deadline");
4262 mgr.tick_offload_engine(deadline);
4263
4264 let events = sink.take();
4265 assert!(
4266 has_removed(&events, StorageTier::HostPinned, 1),
4267 "G2 capacity eviction should publish HostPinned Removed"
4268 );
4269 assert!(
4270 stored_block(&events, StorageTier::HostPinned, 2).is_some(),
4271 "second G2 completion should publish HostPinned Stored for block 2"
4272 );
4273 }
4274
4275 #[test]
4276 fn reoffloaded_swapped_in_block_keeps_token_ids_for_g2_raw_event() {
4277 let (mut mgr, sink) = make_mgr_raw_capturing(1, 4);
4278 attach_test_offload_engine(&mut mgr, 1, 4);
4279
4280 let slots = match mgr.reserve_swap_in_destination_slots(1) {
4281 SwapInSlotReservation::Reserved(slots) => slots,
4282 SwapInSlotReservation::BlockedOnG1Offload(_) => {
4283 panic!("fresh manager should not need G1 offload")
4284 }
4285 SwapInSlotReservation::NoCapacity => panic!("fresh manager should have capacity"),
4286 };
4287 let token_ids = vec![1, 2, 3, 4];
4288 let entries = vec![SwapInRegistrationBlock {
4289 seq_hash: 1,
4290 plh: plh(1),
4291 local_hash: Some(101),
4292 token_ids: Some(token_ids.clone()),
4293 }];
4294 let outcome = mgr.register_swapped_in_blocks(entries, None, slots);
4295 assert_eq!(outcome.consumed_entries, 1);
4296 sink.clear();
4297
4298 assert!(matches!(
4299 use_full_with_hash(&mut mgr, 2, plh(2), 202, vec![5, 6, 7, 8]),
4300 G1Acquire::BlockedOnOffload { .. }
4301 ));
4302 let deadline = mgr
4303 .earliest_offload_deadline()
4304 .expect("G1→G2 offload should expose a deadline");
4305 mgr.tick_offload_engine(deadline);
4306
4307 let events = sink.take();
4308 assert!(
4309 raw_stored_with_token_ids(&events, StorageTier::HostPinned, 1, &token_ids),
4310 "re-offloaded swapped-in block should preserve token ids for HostPinned raw Stored"
4311 );
4312 }
4313
4314 #[test]
4315 fn g1_eviction_offload_holds_source_slot_until_complete() {
4316 let mut mgr = make_mgr(1, 4);
4317 let config = KvbmOffloadConfig {
4318 block_size_tokens: 4,
4319 block_size_bytes: Some(1_000_000),
4320 bandwidth_g1_to_g2_gbps: 1.0,
4321 ..Default::default()
4322 };
4323 let rt = tokio::runtime::Builder::new_multi_thread()
4324 .worker_threads(1)
4325 .enable_all()
4326 .build()
4327 .unwrap();
4328 let mut engine = rt
4329 .block_on(MockOffloadEngine::new(config))
4330 .expect("engine build");
4331 engine.attach_runtime(rt);
4332 mgr.attach_new_offload_engine(engine);
4333
4334 assert_eq!(use_full(&mut mgr, 1, plh(1)), 1);
4335 deref_full(&mut mgr, 1);
4336 assert_eq!(mgr.num_active_blocks(), 0);
4337 assert_eq!(mgr.num_inactive_blocks(), 1);
4338
4339 assert!(matches!(
4343 mgr.process(&MoveBlock::Use(
4344 vec![UniqueBlock::FullBlock(2)],
4345 vec![],
4346 vec![plh(2)],
4347 None,
4348 None,
4349 )),
4350 G1Acquire::BlockedOnOffload { .. }
4351 ));
4352 assert_eq!(
4353 mgr.num_active_blocks(),
4354 1,
4355 "quarantined source slot must count against G1 capacity"
4356 );
4357 let deadline = mgr
4358 .earliest_offload_deadline()
4359 .expect("G1→G2 offload should expose a stall-advance deadline");
4360
4361 mgr.tick_offload_engine(deadline);
4362 assert_eq!(
4363 mgr.num_active_blocks(),
4364 0,
4365 "source slot should release after transfer completion"
4366 );
4367 assert_eq!(use_full(&mut mgr, 2, plh(2)), 1);
4368 assert_eq!(mgr.num_active_blocks(), 1);
4369 }
4370
4371 #[test]
4372 fn try_batch_swap_in_returns_no_hits_without_engine() {
4373 let mut mgr = make_mgr(8, 4);
4374 let plhs = [plh(1), plh(2), plh(3)];
4375 let outcome = mgr.try_batch_swap_in(&plhs, Vec::new(), None);
4376 assert!(matches!(outcome, BatchSwapInOutcome::NoHits));
4377 }
4378 }
4379}