Skip to main content

dynamo_mocker/kv_manager/
kvbm_backend.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! # KV Manager (kvbm-logical G1 backend)
5//!
6//! Synchronous vLLM-flavour G1 block manager built on `kvbm-logical::BlockManager<G1>`.
7//! Translates the mocker's `MoveBlock` protocol into the RAII lifecycle
8//! (allocate → stage → register → drop) exposed by kvbm-logical.
9//!
10//! ## MoveBlock semantics
11//!
12//! - **Use**: prepare all active/inactive hits and fresh slots as one
13//!   transaction, then commit the whole request atomically. Capacity exhaustion
14//!   leaves ownership and sequence state unchanged so the scheduler can decide
15//!   whether to preempt a running request.
16//! - **Deref**: release one logical request owner. For `PartialBlock` this
17//!   drops the unique `MutableBlock` and returns it to the reset pool. For
18//!   `FullBlock` this decrements an explicit logical refcount; the final
19//!   release drops the canonical `ImmutableBlock` and transitions the block to
20//!   kvbm-logical's inactive pool (RAII return).
21//! - **Promote**: PartialBlock (`MutableBlock`) → FullBlock (`ImmutableBlock`).
22//!   Collapses onto an existing registered handle if the PLH / SequenceHash is
23//!   already present; otherwise stages + registers a new block.
24//!
25//! ## Eviction backends
26//!
27//! Three backends are exposed via [`MockerEvictionBackend`]:
28//! - `Lineage` (default) — parent-chain aware, evicts leaves first. Subsumes
29//!   the `push_front` preemption-priority behaviour of the old `LRUEvictor`.
30//! - `Lru` — simple recency-based LRU.
31//! - `MultiLru` — 4-tier frequency-aware LRU (requires TinyLFU tracker).
32
33use 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/// Outcome of [`KvManager::try_batch_swap_in`]. The caller uses this to
64/// decide whether to park the request on a pending-swap-in queue or to
65/// fall through to normal G1 allocation.
66#[cfg(feature = "kvbm-offload")]
67pub enum BatchSwapInOutcome {
68    /// No G2 hits (or no offload engine attached). Caller must allocate
69    /// fresh G1 blocks.
70    NoHits,
71    /// Swap-in reservation accepted. Caller parks the request with this
72    /// handle and polls `SwapInHandle::is_complete()` on subsequent
73    /// scheduler passes. The coordinator retains matched lower-tier blocks,
74    /// G1 destination slots, and any pinned cached prefix for the transfer.
75    Scheduled { handle: SwapInHandle },
76    /// G2 had a match, but reserving destination G1 slots first had to
77    /// trigger a G1→G2 eviction. Caller should retry after offload advances.
78    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/// Classification for each block processed inside `Use`.
102///
103/// - `ActiveHit`: block is already pinned in `active_full` / `active_partial`;
104///   commit bumps its explicit logical refcount without cloning a handle.
105/// - `InactiveHit`: block was in kvbm-logical's inactive pool and was
106///   reactivated by the aligned scattered batch lookup.
107/// - `NewStore`: block was freshly allocated, staged, and registered.
108///
109/// The router radix tree already knows about `ActiveHit` and `InactiveHit`
110/// (it only forgets on explicit `Removed`), so only `NewStore` should emit a
111/// `Stored` KV event. Both hit outcomes still advance the parent cursor so
112/// subsequent `NewStore` batches anchor to the last reused full block.
113#[cfg(not(feature = "kvbm-offload"))]
114enum G1EvictionOutcome {}
115
116enum PreparedUseBlock {
117    /// Already represented in `active_full`; no temporary RAII clone is
118    /// needed while the transaction reserves its fresh suffix.
119    ExistingActiveFull {
120        seq_hash: SequenceHash,
121    },
122    /// Resurrected from KVBM's inactive pool (or otherwise matched outside the
123    /// mocker's active map). The handle pins it until commit or rollback.
124    ExistingMatchedFull {
125        seq_hash: SequenceHash,
126        handle: ImmutableBlock<G1>,
127    },
128    /// Not present in `active_full`; resolved by the single aligned scattered
129    /// lookup after the initial classification pass.
130    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
226/// One physical full-block pin plus the number of logical request owners.
227///
228/// `ImmutableBlock` clones are physical-lifetime guards, not request block
229/// tables. Keeping a clone per logical owner needlessly makes KVBM's handle
230/// count, allocator traffic, and Arc traffic scale with prefix sharing. The
231/// canonical handle pins the physical block while `logical_refs` tracks the
232/// ownership semantics the mocker needs for Deref.
233struct 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
260/// Synchronous G1 KV block manager backed by `kvbm-logical::BlockManager<G1>`.
261pub 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    /// PartialBlocks (still filling tokens) held as `MutableBlock`.
270    /// Dropped blocks return to kvbm-logical's reset pool.
271    active_partial: FxHashMap<Uuid, MutableBlock<G1>>,
272
273    /// FullBlocks held as one canonical `ImmutableBlock` per physical block,
274    /// keyed by `SequenceHash`, plus the number of logical request owners.
275    /// The final logical `Deref` drops the canonical handle and transitions the
276    /// block to kvbm-logical's inactive pool.
277    active_full: FxHashMap<SequenceHash, ActiveFullBlock>,
278
279    /// Shadow registry for every block registered in kvbm-logical. The logical
280    /// registry is keyed by `PositionalLineageHash`, while the router's radix
281    /// tree is keyed by the mocker's u64 `SequenceHash`; the physical G1 block
282    /// id is kept so offload simulation can enqueue the actual block shape when
283    /// kvbm-logical later evicts it from the inactive pool.
284    registered_blocks: FxHashMap<PositionalLineageHash, RegisteredBlockInfo>,
285
286    /// Handle to the G1↔G2 offload engine. `None` until
287    /// [`attach_new_offload_engine`](Self::attach_new_offload_engine) wires
288    /// one in after construction (the engine is built async and cannot be
289    /// created inside `new_*`).
290    ///
291    /// Mocker source-lifetime note: G1 eviction hands kvbm-engine
292    /// `SourceBlocks::External(block_id, plh)` without a strong immutable G1
293    /// block ref. A real byte copy still needs the source HBM slot to stay
294    /// unavailable until DMA completes, so the mocker holds the reset
295    /// `MutableBlock<G1>` capacity token inside the offload engine until the
296    /// simulated transfer completes. The worker never reads source bytes;
297    /// destination presence is registered by `plh`.
298    #[cfg(feature = "kvbm-offload")]
299    offload_engine: Option<Arc<Mutex<MockOffloadEngine>>>,
300
301    /// Changes whenever modeled G1 allocability increases. Immediate retry
302    /// witnesses must name the current generation and a positive slot delta.
303    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        // Intentional vLLM drift: upstream permits duplicate physical blocks
345        // for append-only request block tables. The mocker has one canonical
346        // registered_blocks entry per PLH and no request-owned physical block
347        // tables or duplicate-reset events, so Allow would make offload metadata
348        // and KV-event cleanup unsafe. Reject may discard a reserved block,
349        // adopt an existing block ID, reduce occupancy, and omit duplicate Stored.
350        // Revisit Allow only with per-request physical ownership, duplicate-aware
351        // offload metadata, and balanced duplicate Stored/Removed lifecycle events.
352        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    /// Install a newly acquired physical handle or merge it into an entry that
382    /// became active earlier in the same serial commit.
383    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                // `handle` is a redundant physical pin. Dropping it leaves the
396                // canonical entry alive while logical ownership is tracked by
397                // `logical_refs`.
398                drop(handle);
399            }
400        }
401    }
402
403    /// Add one logical owner for a block known to be present in the active map.
404    /// This is called only after every fallible reservation for the surrounding
405    /// Use transaction has succeeded.
406    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    /// Release one logical owner. Removing the final entry drops the sole
414    /// physical handle and lets KVBM transition the block to inactive.
415    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    /// Wrap `engine` in `Arc<Mutex<_>>`, install it onto this
431    /// `KvManager`, and return a clone of the Arc to the caller.
432    /// Called once after construction by the scheduler's init helper;
433    /// a second call replaces the previous engine (primarily for tests).
434    #[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    /// `true` once an offload engine has been attached.
445    #[cfg(feature = "kvbm-offload")]
446    pub fn has_offload_engine(&self) -> bool {
447        self.offload_engine.is_some()
448    }
449
450    /// Advance the offload engine's PS models and fire any
451    /// completion sinks for drained transfers. Scheduler calls this at
452    /// the top of every pass so swap-in statuses publish before the
453    /// promote-completed loop runs, and offload awaiters fire before
454    /// the next enqueue measures the active-set size. No-op when no
455    /// engine is attached.
456    #[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    /// Earliest pending completion time across offload + onboard links,
477    /// or `None` when both are idle or no engine is attached. Scheduler
478    /// uses this to drive stall-advance in virtual-time replay.
479    #[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    /// Hand blocks that were actually evicted from G1 inactive to the
510    /// offload engine as mock `ExternalBlock`s (no strong immutable ref; see
511    /// `offload_engine` field docs). When capacity pressure tried to reuse
512    /// the same G1 slots, `source_slots` carries reset `MutableBlock` tokens
513    /// that must remain unavailable until the simulated source copy finishes.
514    #[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    /// Register a batch of completed G2-swapped-in blocks into the G1
535    /// inactive pool. `destination_slots` were reserved before the G2→G1
536    /// transfer started and are consumed here as DMA write targets.
537    ///
538    /// Entries already cached in G1 (active or inactive) are skipped, but still
539    /// advance the parent cursor so later fresh suffix stores publish the same
540    /// router tree shape as `process_use`.
541    #[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 ImmutableBlock → block lands in kvbm-logical's
606            // inactive pool, where `process_use`'s `match_blocks`
607            // later reactivates it.
608            drop(immutable);
609            // Clone token_ids only when downstream still needs both copies
610            // (registry + publish batch). The publish batch takes ownership.
611            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    /// Try to satisfy a request's remaining prefix via a G2→G1 swap-in.
675    ///
676    /// Admission path stays linear: `active → inactive → (this) →
677    /// allocate fresh`. Returns [`BatchSwapInOutcome::NoHits`] when no
678    /// engine is attached or when no configured lower tier holds
679    /// `remaining_plhs`.
680    ///
681    /// Lower tiers are keyed by `PositionalLineageHash` (kvbm-engine's
682    /// native identity), not the router-facing `u64` SequenceHash — the
683    /// caller already holds these on the admission path. We first prepare the
684    /// lower-tier match, then reserve destination G1 slots, and only then
685    /// reserve onboard bandwidth. That prevents swap-in from borrowing
686    /// imaginary HBM capacity while the transfer is in flight.
687    #[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        // Do not hold the offload-engine mutex while reserving G1 slots:
705        // allocation may evict G1 blocks and enqueue G1→G2 work back into
706        // the same engine. `PreparedSwapIn` pins ready G2 blocks, and for
707        // deferred G3 staging it holds only the G2 staging capacity so a failed
708        // admission probe does not start a G3→G2 copy.
709        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    /// Hold the G1 prefix that admission used when deciding to swap in only a
756    /// G2 suffix. The returned guards keep those blocks out of the inactive
757    /// eviction pool until the pending swap-in publishes its Device events.
758    #[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    /// Emit a `Stored` or `Removed` KV event to the router.
776    /// Ported verbatim from the old `vllm_backend::publish_kv_event` to
777    /// preserve KV-aware routing semantics (parent_hash chaining, token_ids).
778    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            // `local_hashes` is either empty (caller has no token-derived
824            // hashes to publish) or 1:1 with `full_blocks`. Match the
825            // front-door contract in `process_use`.
826            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    /// Process a `MoveBlock` instruction synchronously.
905    ///
906    /// `Use` is atomic: every block commits or none do. Capacity and offload
907    /// waits are returned explicitly so callers cannot mistake a dependency for
908    /// partial success.
909    #[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    /// Publish transferred destination blocks while reconciling any cache entry
1196    /// that appeared after reservation. Unlike upstream vLLM, a collision may
1197    /// replace the reserved block ID and reduce occupancy during activation;
1198    /// the mocker acquires the canonical handle instead of strictly moving the
1199    /// originally reserved handle.
1200    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        // Classify locally active blocks once, and preserve the existing
1533        // per-block scattered reuse semantics while collapsing all non-local
1534        // lookups into one store-lock acquisition. `match_blocks` cannot be
1535        // used here because it stops at the first miss, whereas the existing
1536        // singleton loop can still reuse a later registered block.
1537        //
1538        // Start empty rather than reserving for every full block: an all-active
1539        // request never needs storage for non-local lookup inputs.
1540        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                        // Allocate once, but only when the request actually
1558                        // contains a non-local full block. Every remaining
1559                        // full block is the largest possible suffix here.
1560                        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        // Complete every fresh full block first, then register the whole set
1708        // under one BlockStore lock. Registration results preserve input order,
1709        // so the second pass can consume them alongside the fresh prepared
1710        // entries while preserving router-event segmentation and metadata.
1711        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                        // Reject deduplication can resolve two fresh entries in
1809                        // this same batch to one canonical block. Finish the
1810                        // preceding Stored group, retain the returned handle as
1811                        // another logical owner, and advance the lineage cursor
1812                        // without replacing canonical shadow metadata or
1813                        // publishing a duplicate Stored event.
1814                        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    /// Translate PLHs that kvbm-logical evicted from its inactive pool
1890    /// (during an `allocate_blocks_with_evictions` call) into offload
1891    /// enqueues plus router `Removed` events. No-op when the input is empty
1892    /// or none of the PLHs are in our shadow registry.
1893    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    /// Same as [`handle_evictions`](Self::handle_evictions), but also hands
1901    /// reset source slots to the offload engine so G1 capacity remains pinned
1902    /// until the simulated G1→G2 transfer completes.
1903    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    /// Number of **distinct** physically-resident KV blocks currently pinned
2050    /// by mocker (not available for eviction).
2051    pub fn num_active_blocks(&self) -> usize {
2052        // kvbm-logical partitions physical blocks into three pools:
2053        //   total = reset + inactive + active
2054        // where `available = reset + inactive`. So `total - available`
2055        // includes request-owned Mutable/Immutable blocks plus any reset
2056        // source slots quarantined behind in-flight G1→G2 offloads.
2057        self.block_manager.total_blocks() - self.block_manager.available_blocks()
2058    }
2059
2060    /// Total number of logical block owners: one per held `MutableBlock` plus
2061    /// the explicit logical reference count of every full block. This remains
2062    /// a request-ownership metric even though KVBM's `inflight_immutable`
2063    /// metric now counts only the canonical physical handles retained here.
2064    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    /// Calculate the prefill cost for a sequence by scanning `unique_blocks` in
2111    /// order and counting the longest prefix that is cached (active or
2112    /// inactive). Stops at first cache miss — KV states are computed
2113    /// sequentially, so anything after a miss must be recomputed.
2114    pub fn get_prefill_cost(&self, sequence: &ActiveSequence) -> PrefillCost {
2115        let seq_blocks = sequence.unique_blocks();
2116
2117        // Without prefix caching, each `UniqueBlock::FullBlock` carries a
2118        // randomised hash that can't possibly be in the cache across requests
2119        // — skip the PLH lookup (PLH is deterministic from tokens) to stay
2120        // consistent with that no-reuse contract.
2121        // overlap = all reusable prefix blocks (compute); active_overlap = only
2122        // those backed by an active block (capacity — inactive reuse is re-consumed).
2123        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        // Preemption resets scheduler progress but retains generated tokens in
2154        // the logical request. vLLM performs prefix lookup over that complete
2155        // known context (`request.num_tokens`), not only the original prompt.
2156        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    /// Capturing event sink for router-publication assertions.
2177    #[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    /// `get_prefill_cost` must report an inactive cached prefix as reusable for
2334    /// compute (`cached_tokens`) but NOT for no-evict capacity reservation
2335    /// (`active_cached_tokens`), since reactivation re-consumes the block.
2336    #[test]
2337    fn prefill_cost_splits_active_and_inactive_cached_reuse() {
2338        let mut mgr = make_mgr(10, 4);
2339        // 2 full blocks (8 tokens, block_size 4), prefix caching on.
2340        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        // Register block 0, then deref so it falls inactive (still registered;
2348        // only eviction prunes registered_blocks).
2349        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        // Same seq_hash used twice: only one distinct physical block is
2389        // resident and pinned by one canonical RAII handle, while the mocker
2390        // tracks two logical request owners.
2391        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        // Use with same PLH reuses the inactive block.
2506        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        // Fill capacity in a single Use batch.
2845        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        // Inactive membership helper. Uses `check_presence::<G1>` (non-mutating)
2890        // against a snapshot of PLHs to confirm each expected id is present in
2891        // kvbm-logical AND absent from `active_full`. Also checks total count
2892        // matches so we catch stray inactive entries too.
2893        //
2894        // NOTE: under kvbm-logical, once the last `ImmutableBlock` handle is
2895        // dropped, the block returns to the inactive pool and remains matchable
2896        // until eviction.
2897        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        // Use blocks 0..=4, then 0, 1, 5, 6 — 0 and 1 bump refcount to 2.
2951        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        // Leaf-to-root release order is what makes the resulting inactive set
2966        // deterministic under the Lineage backend.
2967        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        // Release the second branch leaf-to-root too. Active drains; inactive = {0..=6}.
2974        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        // Re-use 0, 1, 2 (reactivates from inactive) + 7, 8, 9 (new, 3 free
2981        // slots). No eviction needed — inactive shrinks to {3, 4, 5, 6}.
2982        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        // Capacity pressure now forces exact leaf-first evictions: 4, then 3,
2990        // then 6. The sole inactive survivor is 5.
2991        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(); // 8 full blocks
3043        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        // Chunk 1: blocks 0..=3 (cumulative 256 tokens).
3048        let signal = seq.prepare_allocation(256).unwrap();
3049        mgr.process(&signal);
3050        seq.commit_allocation(256);
3051
3052        // Chunk 2: blocks 4..=7 (cumulative 512 tokens).
3053        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    /// When a FullBlock is used, deref'd (becomes inactive in kvbm-logical),
3119    /// then used again, the router already knows about it — reactivation must
3120    /// NOT emit a second `Stored` event.
3121    #[test]
3122    fn test_inactive_hit_does_not_republish_stored() {
3123        let (mut mgr, sink) = make_mgr_capturing(4, 16);
3124
3125        // First Use: fresh registration → 1 Stored.
3126        use_full(&mut mgr, 1, plh(100));
3127        // Deref → block transitions to inactive pool. No Removed (we don't
3128        // emit one on Deref).
3129        deref_full(&mut mgr, 1);
3130        // Second Use: match_blocks reactivates from inactive → InactiveHit.
3131        // No new Stored should fire.
3132        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        // handle_evictions consumes this entry when it later creates offload metadata.
3188        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    /// After reusing a prefix [A, B] and storing a new suffix [C], the
3305    /// `Stored` event for C must anchor `parent_hash` to B (the last reused
3306    /// full block), not to whatever parent the caller originally passed.
3307    #[test]
3308    fn test_stored_suffix_anchors_to_last_reused_block() {
3309        let (mut mgr, sink) = make_mgr_capturing(8, 16);
3310
3311        // Prime the cache with [A=10, B=11].
3312        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        // Drop both to inactive.
3320        deref_full(&mut mgr, 10);
3321        deref_full(&mut mgr, 11);
3322
3323        // Clear captured events from priming.
3324        sink.events.lock().unwrap().clear();
3325
3326        // New request reuses [A, B] and stores a new block C=12.
3327        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, // no explicit parent → scheduler would pass None for a head-chunk
3337        ));
3338
3339        let events = sink.events.lock().unwrap();
3340        // Only one Stored (for C); no Stored for reused A or B.
3341        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    /// Two requests sharing a prefix must not inflate scheduler-visible
3413    /// occupancy. The distinct count reflects physically-resident blocks; the
3414    /// refcount metric reflects held handles.
3415    #[test]
3416    fn test_shared_prefix_distinct_vs_refcount() {
3417        let mut mgr = make_mgr(8, 16);
3418
3419        // Request A uses [10, 11, 12].
3420        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        // Request B reuses prefix [10, 11] and adds its own block [13].
3435        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        // Distinct resident blocks: {10, 11, 12, 13} = 4 (scheduler view).
3448        assert_eq!(
3449            mgr.num_active_blocks(),
3450            4,
3451            "shared prefix must not inflate distinct count"
3452        );
3453        // Handle count: 10 and 11 each held twice, 12 once, 13 once → 6.
3454        assert_eq!(
3455            mgr.num_active_block_refs(),
3456            6,
3457            "handle count should reflect per-request refcount"
3458        );
3459    }
3460
3461    /// With `enable_prefix_caching=false`, each sequence should still be able
3462    /// to reactivate its OWN inactive blocks after preemption and re-admit.
3463    #[test]
3464    fn test_random_plh_stable_across_preempt_retry() {
3465        // 4 blocks of size 16 → 64 tokens of prompt.
3466        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        // Admit: allocate prompt blocks.
3473        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        // Preempt: reset_with_signal frees all active blocks (Deref) →
3478        // kvbm-logical keeps them in the inactive pool (no Removed events).
3479        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        // Re-admit: prompt blocks must reactivate via InactiveHit, NOT allocate
3487        // fresh. The cached per-sequence PLHs are what make this work.
3488        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        // Router-event witness: only ONE `Stored` (from the original admit).
3494        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        // Capacity = 2. Use three blocks (10, 11, 12); deref 10, 11 to push
3508        // them into the inactive pool; then use a third distinct block (12)
3509        // that isn't already in the active or inactive pool — this forces
3510        // allocation → inactive-pool eviction.
3511        let (mut mgr, sink) = make_mgr_capturing(2, 16);
3512
3513        // Seed 10 and 11 in the inactive pool.
3514        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        // Introduce block 12 → must evict exactly one of {10, 11}.
3529        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            // Capacity pressure evicts block 1 from G1 and starts G1→G2.
4190            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            // Now block 1 is resident in G2. Admit block 2 into G1, then evict
4241            // it to the one-block G2 tier; this must evict block 1 from G2.
4242            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            // Capacity pressure evicts block 1 and starts G1→G2. The returned
4340            // reset slot is held as the source-capacity token, so block 2
4341            // cannot be allocated until the simulated transfer completes.
4342            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}