Skip to main content

dynamo_mocker/kv_manager/
sglang_backend.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! SGLang KV manager — wraps [`RadixCache`] with request-level lifecycle
5//! operations and KV event publishing.
6
7use crate::cache::radix_cache::{KvPageId, NodeId, RadixCache};
8use crate::common::kv_cache_trace;
9use crate::common::protocols::KvEventPublishers;
10use dynamo_kv_router::protocols::{
11    BlockHashOptions, ExternalSequenceBlockHash, KvCacheEvent, KvCacheEventData, KvCacheRemoveData,
12    KvCacheStoreData, KvCacheStoredBlockData, LocalBlockHash, compute_block_hash_for_seq,
13    compute_next_seq_hash,
14};
15use rustc_hash::FxHashMap;
16
17/// Move-only ownership of an active request's KV slots and protected radix path.
18#[derive(Debug, Default)]
19#[must_use = "an active KV lease must be finished, aborted, or retracted"]
20pub(crate) struct ActiveKvLease {
21    kv_indices: Vec<usize>,
22    cached_tokens: usize,
23    last_node: Option<NodeId>,
24}
25
26impl ActiveKvLease {
27    #[cfg(test)]
28    pub(crate) fn indices(&self) -> &[usize] {
29        &self.kv_indices
30    }
31
32    pub(crate) fn len(&self) -> usize {
33        self.kv_indices.len()
34    }
35
36    pub(crate) fn cached_tokens(&self) -> usize {
37        self.cached_tokens
38    }
39
40    pub(crate) fn last_index(&self) -> Option<usize> {
41        self.kv_indices.last().copied()
42    }
43
44    pub(crate) fn is_active(&self) -> bool {
45        self.last_node.is_some()
46    }
47
48    fn last_node(&self) -> NodeId {
49        self.last_node
50            .expect("active KV lease must retain a radix path")
51    }
52
53    #[cfg(test)]
54    pub(crate) fn from_parts(
55        kv_indices: Vec<usize>,
56        cached_tokens: usize,
57        last_node: NodeId,
58    ) -> Self {
59        Self {
60            kv_indices,
61            cached_tokens,
62            last_node: Some(last_node),
63        }
64    }
65}
66
67/// Result of `allocate_for_request`.
68pub(crate) struct AllocResult {
69    /// Number of tokens matched from the prefix cache.
70    pub(crate) prefix_len: usize,
71    pub(crate) lease: ActiveKvLease,
72}
73
74pub struct SglangKvManager {
75    cache: RadixCache,
76    kv_event_publishers: KvEventPublishers,
77    dp_rank: u32,
78    next_event_id: u64,
79    /// Maps each complete block's terminal pool_idx → block_hash assigned
80    /// during Stored events, so Removed events can use the same block_hash.
81    idx_to_block_hash: FxHashMap<usize, ExternalSequenceBlockHash>,
82    /// Tracks how many live pool slots currently advertise the same logical
83    /// block hash so router events reflect logical block visibility, not
84    /// transient slot ownership.
85    block_hash_refcounts: FxHashMap<ExternalSequenceBlockHash, usize>,
86}
87
88pub struct DecodeTokenReservation {
89    pages: Vec<KvPageId>,
90    next: usize,
91    page_size: usize,
92}
93
94pub struct SglangDestinationReservation {
95    pub(crate) prefix_len: usize,
96    prefix_indices: Vec<usize>,
97    last_node: NodeId,
98    unpublished_pages: Vec<KvPageId>,
99    page_size: usize,
100    missing_tokens: usize,
101    pub(crate) allocated_tokens: usize,
102}
103
104impl SglangDestinationReservation {
105    pub(crate) fn transferable_prompt_tokens(&self) -> usize {
106        self.unpublished_pages.len() * self.page_size
107    }
108
109    #[cfg(test)]
110    pub(crate) fn indices(&self) -> Vec<usize> {
111        self.prefix_indices
112            .iter()
113            .copied()
114            .chain(
115                self.unpublished_pages
116                    .iter()
117                    .flat_map(|page| {
118                        let start = page.first_token_index(self.page_size);
119                        start..start + self.page_size
120                    })
121                    .take(self.missing_tokens),
122            )
123            .collect()
124    }
125}
126
127impl DecodeTokenReservation {
128    fn take(&mut self, last_idx: Option<usize>) -> usize {
129        if let Some(last_idx) = last_idx
130            && (last_idx + 1) % self.page_size != 0
131        {
132            return last_idx + 1;
133        }
134        let page = *self
135            .pages
136            .get(self.next)
137            .expect("reserved decode page allocation must be infallible");
138        self.next += 1;
139        page.first_token_index(self.page_size)
140    }
141
142    pub(crate) fn len(&self) -> usize {
143        self.pages.len() - self.next
144    }
145}
146
147impl SglangKvManager {
148    pub fn new(
149        total_tokens: usize,
150        page_size: usize,
151        kv_event_publishers: KvEventPublishers,
152        dp_rank: u32,
153    ) -> Self {
154        Self {
155            cache: RadixCache::new(total_tokens, page_size),
156            kv_event_publishers,
157            dp_rank,
158            next_event_id: 0,
159            idx_to_block_hash: FxHashMap::default(),
160            block_hash_refcounts: FxHashMap::default(),
161        }
162    }
163
164    pub fn cache(&self) -> &RadixCache {
165        &self.cache
166    }
167
168    pub fn cache_mut(&mut self) -> &mut RadixCache {
169        &mut self.cache
170    }
171
172    /// Match and protect a reusable prefix, evict other cached pages if needed,
173    /// then allocate KV pages for a new request.
174    ///
175    /// Returns `None` if protected and free capacity cannot satisfy the request.
176    pub(crate) fn allocate_for_request(&mut self, token_ids: &[u32]) -> Option<AllocResult> {
177        let (prefix_len, last_node) = self.cache.match_prefix(token_ids);
178        let new_tokens = token_ids.len() - prefix_len;
179        let required_tokens = new_tokens.div_ceil(self.cache.page_size()) * self.cache.page_size();
180
181        // Protect the matched path before making room. Otherwise an LRU
182        // eviction can remove the prefix used to size this allocation, and a
183        // second match would require more pages than were freed.
184        self.cache.inc_lock_ref(last_node);
185        let reservable = self.cache.available_tokens() + self.cache.evictable_size;
186        if required_tokens > reservable {
187            self.cache.dec_lock_ref(last_node);
188            return None;
189        }
190        let available = self.cache.available_tokens();
191        if required_tokens > available {
192            self.evict(required_tokens - available);
193        }
194        let prefix_indices = self.collect_path_indices(last_node);
195
196        let mut kv_indices = prefix_indices;
197        let available_before = self.cache.available_tokens();
198        if !self
199            .cache
200            .page_pool
201            .allocate_indices_into(new_tokens, &mut kv_indices)
202        {
203            self.cache.dec_lock_ref(last_node);
204            return None;
205        }
206        let allocated_tokens = available_before - self.cache.available_tokens();
207
208        // Router-visible KV events are complete-block only.
209        self.publish_stored_event(token_ids, &kv_indices, prefix_len);
210
211        self.log_trace("allocation", allocated_tokens);
212
213        Some(AllocResult {
214            prefix_len,
215            lease: ActiveKvLease {
216                kv_indices,
217                cached_tokens: prefix_len,
218                last_node: Some(last_node),
219            },
220        })
221    }
222
223    /// Continue an in-flight request from an already materialized prefix.
224    ///
225    /// This is used by chunked-prefill continuation where the request still
226    /// owns token slots for a prefix that may extend past the radix-tree's
227    /// page-aligned cached prefix.
228    pub(crate) fn extend_allocation(
229        &mut self,
230        token_ids: &[u32],
231        lease: &mut ActiveKvLease,
232    ) -> bool {
233        let prefix_len = lease.kv_indices.len();
234        assert!(
235            lease.is_active() && prefix_len <= token_ids.len(),
236            "invalid SGLang KV lease extension: active={}, owned_tokens={prefix_len}, target_tokens={}",
237            lease.is_active(),
238            token_ids.len()
239        );
240        let new_tokens = token_ids.len() - prefix_len;
241        let available_before = self.cache.available_tokens();
242        if !self
243            .cache
244            .page_pool
245            .allocate_indices_into(new_tokens, &mut lease.kv_indices)
246        {
247            return false;
248        }
249        let allocated_tokens = available_before - self.cache.available_tokens();
250
251        self.publish_stored_event(token_ids, &lease.kv_indices, prefix_len);
252        self.log_trace("allocation", allocated_tokens);
253        true
254    }
255
256    pub(crate) fn extend_cached_prefix(&mut self, token_ids: &[u32], lease: &mut ActiveKvLease) {
257        let complete_len = token_ids.len() / self.cache.page_size() * self.cache.page_size();
258        if complete_len <= lease.cached_tokens {
259            return;
260        }
261        assert!(
262            lease.is_active() && complete_len <= lease.len(),
263            "invalid SGLang KV lease cache extension: active={}, cached_tokens={}, complete_tokens={complete_len}, owned_tokens={}",
264            lease.is_active(),
265            lease.cached_tokens,
266            lease.len()
267        );
268        let last_node = lease.last_node();
269        let new_last_node = self.cache_unfinished_req(
270            token_ids,
271            &mut lease.kv_indices[..complete_len],
272            last_node,
273            lease.cached_tokens,
274        );
275        lease.last_node = Some(new_last_node);
276        lease.cached_tokens = complete_len;
277    }
278
279    pub(crate) fn extend_decode(
280        &mut self,
281        lease: &mut ActiveKvLease,
282        reservation: &mut DecodeTokenReservation,
283    ) {
284        debug_assert!(lease.is_active());
285        let new_idx = reservation.take(lease.last_index());
286        lease.kv_indices.push(new_idx);
287    }
288
289    pub(crate) fn finish(&mut self, token_ids: &[u32], mut lease: ActiveKvLease) {
290        let Some(last_node) = lease.last_node.take() else {
291            debug_assert!(lease.kv_indices.is_empty());
292            debug_assert_eq!(lease.cached_tokens, 0);
293            return;
294        };
295        let complete_len =
296            token_ids.len().min(lease.len()) / self.cache.page_size() * self.cache.page_size();
297        assert!(
298            lease.cached_tokens <= complete_len,
299            "invalid SGLang KV lease finish: cached_tokens={}, complete_tokens={complete_len}, owned_tokens={}",
300            lease.cached_tokens,
301            lease.len()
302        );
303        self.free_indices(&lease.kv_indices[complete_len..]);
304        lease.kv_indices.truncate(complete_len);
305
306        if complete_len == 0 {
307            self.cache.dec_lock_ref(last_node);
308            return;
309        }
310        self.cache_finished_req(
311            &token_ids[..complete_len],
312            &lease.kv_indices,
313            last_node,
314            lease.cached_tokens,
315        );
316    }
317
318    pub(crate) fn abort(&mut self, lease: ActiveKvLease) -> bool {
319        self.release_active_lease(lease)
320    }
321
322    pub(crate) fn retract(&mut self, lease: ActiveKvLease) -> bool {
323        self.release_active_lease(lease)
324    }
325
326    /// Cache a completed request's full sequence into the radix tree.
327    ///
328    /// Inserts the full token sequence so future requests can reuse it,
329    /// then unlocks the path.
330    fn cache_finished_req(
331        &mut self,
332        token_ids: &[u32],
333        kv_indices: &[usize],
334        last_node: NodeId,
335        first_new_token: usize,
336    ) {
337        self.publish_stored_event(token_ids, kv_indices, first_new_token);
338        let new_last_node =
339            self.cache
340                .insert_from_node(last_node, first_new_token, token_ids, kv_indices);
341        let complete_len =
342            token_ids.len().min(kv_indices.len()) / self.cache.page_size() * self.cache.page_size();
343        self.release_unretained_finished_indices(
344            kv_indices,
345            new_last_node,
346            first_new_token,
347            complete_len,
348        );
349        self.cache.dec_lock_ref(last_node);
350    }
351
352    /// Cache a partial sequence after a chunked prefill step.
353    ///
354    /// Inserts the partial sequence, then transfers the lock from the old
355    /// path to the new (extended) path. The request is still active, so the
356    /// new deepest node stays locked.
357    ///
358    /// Returns the new `last_node` that the caller should use for
359    /// subsequent calls.
360    fn cache_unfinished_req(
361        &mut self,
362        token_ids: &[u32],
363        kv_indices: &mut [usize],
364        last_node: NodeId,
365        first_new_token: usize,
366    ) -> NodeId {
367        let block_size = self.cache.page_size();
368        let complete_len = token_ids.len() / block_size * block_size;
369        assert!(
370            first_new_token.is_multiple_of(block_size)
371                && first_new_token <= complete_len
372                && complete_len <= kv_indices.len(),
373            "invalid SGLang canonicalization range: first_new_token={first_new_token}, complete_len={complete_len}, kv_indices={}",
374            kv_indices.len()
375        );
376
377        self.publish_stored_event(token_ids, kv_indices, first_new_token);
378        let new_last_node =
379            self.cache
380                .insert_from_node(last_node, first_new_token, token_ids, kv_indices);
381
382        // An interleaved insert can retain different physical pages for the same prefix.
383        // Move the active request to canonical pages before releasing its duplicates.
384        // Acquire the extended path before releasing the old prefix so
385        // destination activation never leaves valid transferred KV unprotected.
386        if new_last_node != last_node {
387            self.cache.inc_lock_ref(new_last_node);
388        }
389        self.canonicalize_unfinished_indices(
390            kv_indices,
391            new_last_node,
392            first_new_token,
393            complete_len,
394        );
395        if new_last_node != last_node {
396            self.cache.dec_lock_ref(last_node);
397        }
398
399        new_last_node
400    }
401
402    /// Allocate a decode token, consuming a new page only at a page boundary.
403    /// Router-visible BlockStored events are published once a full block exists.
404    pub fn allocate_decode_token(&mut self, last_idx: Option<usize>) -> Option<usize> {
405        let mut reservation = self.reserve_decode_pages(usize::from(
406            last_idx.is_none_or(|idx| (idx + 1) % self.cache.page_size() == 0),
407        ))?;
408        let idx = reservation.take(last_idx);
409        Some(idx)
410    }
411
412    pub fn reserve_decode_pages(&mut self, count: usize) -> Option<DecodeTokenReservation> {
413        let pages = self.cache.page_pool.allocate_pages(count)?;
414        if !pages.is_empty() {
415            self.log_trace("allocation", pages.len() * self.cache.page_size());
416        }
417        Some(DecodeTokenReservation {
418            pages,
419            next: 0,
420            page_size: self.cache.page_size(),
421        })
422    }
423
424    pub(crate) fn reserve_destination(
425        &mut self,
426        token_ids: &[u32],
427    ) -> Option<SglangDestinationReservation> {
428        let (prefix_len, last_node) = self.cache.match_prefix(token_ids);
429        let mut prefix_indices = self.collect_path_indices(last_node);
430        prefix_indices.truncate(prefix_len);
431        self.cache.inc_lock_ref(last_node);
432
433        let allocated_tokens = if token_ids.is_empty() {
434            0
435        } else {
436            token_ids.len().div_ceil(self.cache.page_size()) * self.cache.page_size()
437        };
438        let fresh_tokens = allocated_tokens.saturating_sub(prefix_len);
439        let fresh_pages = fresh_tokens / self.cache.page_size();
440        let reservable = self.cache.available_tokens() + self.cache.evictable_size;
441        if fresh_tokens > reservable {
442            self.cache.dec_lock_ref(last_node);
443            return None;
444        }
445        let available = self.cache.available_tokens();
446        if fresh_tokens > available {
447            self.evict(fresh_tokens - available);
448        }
449        let Some(unpublished_pages) = self.cache.page_pool.allocate_pages(fresh_pages) else {
450            self.cache.dec_lock_ref(last_node);
451            return None;
452        };
453        self.log_trace("reserve_destination", fresh_tokens);
454        Some(SglangDestinationReservation {
455            prefix_len,
456            prefix_indices,
457            last_node,
458            unpublished_pages,
459            page_size: self.cache.page_size(),
460            missing_tokens: token_ids.len().saturating_sub(prefix_len),
461            allocated_tokens,
462        })
463    }
464
465    pub(crate) fn activate_destination(
466        &mut self,
467        reservation: SglangDestinationReservation,
468        token_ids: &[u32],
469    ) -> AllocResult {
470        let SglangDestinationReservation {
471            prefix_len,
472            mut prefix_indices,
473            last_node,
474            unpublished_pages,
475            page_size: _,
476            missing_tokens,
477            allocated_tokens: _,
478        } = reservation;
479        let mut unpublished_indices = self
480            .cache
481            .page_pool
482            .expand_pages(&unpublished_pages, missing_tokens);
483        prefix_indices.append(&mut unpublished_indices);
484        let new_last_node =
485            self.cache_unfinished_req(token_ids, &mut prefix_indices, last_node, prefix_len);
486        self.log_trace("activate_destination", missing_tokens);
487        AllocResult {
488            prefix_len,
489            lease: ActiveKvLease {
490                kv_indices: prefix_indices,
491                cached_tokens: token_ids.len() / self.cache.page_size() * self.cache.page_size(),
492                last_node: Some(new_last_node),
493            },
494        }
495    }
496
497    pub(crate) fn cancel_destination(&mut self, reservation: SglangDestinationReservation) {
498        self.cache.dec_lock_ref(reservation.last_node);
499        self.release_unpublished_pages(reservation.unpublished_pages);
500    }
501
502    pub fn release_decode_reservation(&mut self, reservation: DecodeTokenReservation) {
503        let pages = &reservation.pages[reservation.next..];
504        if pages.is_empty() {
505            return;
506        }
507        self.cache.page_pool.free_pages(pages);
508        self.log_trace("release_unpublished", pages.len() * self.cache.page_size());
509    }
510
511    fn release_unpublished_pages(&mut self, pages: Vec<KvPageId>) {
512        if pages.is_empty() {
513            return;
514        }
515        self.cache.page_pool.free_pages(&pages);
516        self.log_trace("release_unpublished", pages.len() * self.cache.page_size());
517    }
518
519    #[cfg(test)]
520    fn free_request(&mut self, last_node: NodeId) {
521        self.cache.dec_lock_ref(last_node);
522    }
523
524    /// Return request-owned token slots to the free pool and publish matching
525    /// removal events for any slots that were previously advertised to the router.
526    fn free_indices(&mut self, indices: &[usize]) {
527        if indices.is_empty() {
528            return;
529        }
530
531        let pages = self.cache.page_pool.free_indices(indices);
532        self.publish_removed_pages(&pages);
533        self.log_trace("free", pages.len() * self.cache.page_size());
534    }
535
536    fn release_active_lease(&mut self, mut lease: ActiveKvLease) -> bool {
537        let Some(last_node) = lease.last_node.take() else {
538            debug_assert!(lease.kv_indices.is_empty());
539            debug_assert_eq!(lease.cached_tokens, 0);
540            return false;
541        };
542        assert!(
543            lease.cached_tokens <= lease.len(),
544            "invalid SGLang KV lease release: cached_tokens={}, owned_tokens={}",
545            lease.cached_tokens,
546            lease.len()
547        );
548        let owned_suffix = &lease.kv_indices[lease.cached_tokens..];
549        let capacity_improved = !owned_suffix.is_empty() || last_node != self.cache.root();
550        self.free_indices(owned_suffix);
551        self.cache.dec_lock_ref(last_node);
552        capacity_improved
553    }
554
555    /// Collect token indices from the matched prefix path by walking root→last_node.
556    fn collect_path_indices(&self, last_node: NodeId) -> Vec<usize> {
557        if last_node == self.cache.root() {
558            return Vec::new();
559        }
560
561        // Walk from last_node to root, collecting node IDs
562        let mut path = Vec::new();
563        let mut current = last_node;
564        loop {
565            let node = self.cache.node(current);
566            if node.parent.is_none() {
567                break;
568            }
569            path.push(current);
570            current = node.parent.unwrap();
571        }
572        path.reverse();
573
574        // Expand cached page IDs only for the bounded active request.
575        let mut indices = Vec::new();
576        for node_id in path {
577            let node = self.cache.node(node_id);
578            indices.extend(
579                self.cache
580                    .page_pool
581                    .expand_pages(&node.value, node.value.len() * self.cache.page_size()),
582            );
583        }
584        indices
585    }
586
587    fn release_unretained_finished_indices(
588        &mut self,
589        kv_indices: &[usize],
590        last_node: NodeId,
591        first_new_token: usize,
592        complete_len: usize,
593    ) {
594        let block_size = self.cache.page_size();
595        if complete_len == 0 {
596            return;
597        }
598
599        let mut unretained_pages = Vec::new();
600        let mut current = last_node;
601        let first_new_page = first_new_token / block_size;
602        let mut path_end = complete_len / block_size;
603
604        while path_end > first_new_page {
605            debug_assert_ne!(current, self.cache.root());
606            if current == self.cache.root() {
607                tracing::error!(
608                    path_end,
609                    first_new_token,
610                    complete_len,
611                    "SGLang radix path ended before finished-request reconciliation"
612                );
613                break;
614            }
615
616            let node = self.cache.node(current);
617            let node_len = node.value.len();
618            debug_assert!(node_len <= path_end);
619            if node_len > path_end {
620                tracing::error!(
621                    node_len,
622                    path_end,
623                    complete_len,
624                    "SGLang radix node exceeds finished materialized prefix"
625                );
626                break;
627            }
628            let path_start = path_end - node_len;
629            let reconcile_start = path_start.max(first_new_page);
630
631            for page_idx in reconcile_start..path_end {
632                let token_start = page_idx * block_size;
633                let incoming_page = KvPageId::from_token_index(kv_indices[token_start], block_size);
634                let canonical_page = node.value[page_idx - path_start];
635                if incoming_page != canonical_page {
636                    unretained_pages.push(incoming_page);
637                }
638            }
639
640            path_end = path_start;
641            current = node.parent.unwrap_or(self.cache.root());
642        }
643
644        self.free_pages(&unretained_pages);
645    }
646
647    fn canonicalize_unfinished_indices(
648        &mut self,
649        kv_indices: &mut [usize],
650        last_node: NodeId,
651        first_new_token: usize,
652        complete_len: usize,
653    ) {
654        let block_size = self.cache.page_size();
655        debug_assert_eq!(complete_len % block_size, 0);
656        debug_assert_eq!(first_new_token % block_size, 0);
657        debug_assert!(complete_len <= kv_indices.len());
658        debug_assert!(first_new_token <= complete_len);
659
660        assert!(
661            first_new_token.is_multiple_of(block_size)
662                && complete_len.is_multiple_of(block_size)
663                && complete_len <= kv_indices.len()
664                && first_new_token <= complete_len
665                && self.radix_path_covers(last_node, first_new_token, complete_len),
666            "invalid SGLang canonicalization range or radix path: first_new_token={first_new_token}, complete_len={complete_len}, kv_indices={}",
667            kv_indices.len()
668        );
669
670        let mut unretained_pages = Vec::new();
671        let mut current = last_node;
672        let first_new_page = first_new_token / block_size;
673        let mut path_end = complete_len / block_size;
674
675        while path_end > first_new_page {
676            let node = self.cache.node(current);
677            let node_len = node.value.len();
678            let path_start = path_end - node_len;
679            let reconcile_start = path_start.max(first_new_page);
680
681            for page_idx in reconcile_start..path_end {
682                let token_start = page_idx * block_size;
683                let token_end = token_start + block_size;
684                let incoming_page = KvPageId::from_token_index(kv_indices[token_start], block_size);
685                let canonical_page = node.value[page_idx - path_start];
686                if incoming_page != canonical_page {
687                    unretained_pages.push(incoming_page);
688                    let canonical = self
689                        .cache
690                        .page_pool
691                        .expand_pages(&[canonical_page], block_size);
692                    kv_indices[token_start..token_end].copy_from_slice(&canonical);
693                }
694            }
695
696            path_end = path_start;
697            current = node.parent.unwrap_or_else(|| self.cache.root());
698        }
699
700        self.free_pages(&unretained_pages);
701    }
702
703    fn radix_path_covers(
704        &self,
705        mut current: NodeId,
706        first_new_token: usize,
707        path_end: usize,
708    ) -> bool {
709        let page_size = self.cache.page_size();
710        let first_new_page = first_new_token / page_size;
711        let mut path_end = path_end / page_size;
712        while path_end > first_new_page {
713            if current == self.cache.root() {
714                return false;
715            }
716            let node = self.cache.node(current);
717            if node.value.len() > path_end {
718                return false;
719            }
720            path_end -= node.value.len();
721            current = node.parent.unwrap_or_else(|| self.cache.root());
722        }
723        true
724    }
725
726    /// Evict tokens from the cache, publish BlockRemoved events, and log a trace.
727    pub fn evict(&mut self, num_tokens: usize) {
728        let (evicted, evicted_pages) = self.cache.evict(num_tokens);
729        if !evicted_pages.is_empty() {
730            self.publish_removed_pages(&evicted_pages);
731        }
732        self.log_trace("eviction", evicted);
733    }
734
735    fn free_pages(&mut self, pages: &[KvPageId]) {
736        if pages.is_empty() {
737            return;
738        }
739        self.cache.page_pool.free_pages(pages);
740        self.publish_removed_pages(pages);
741        self.log_trace("free", pages.len() * self.cache.page_size());
742    }
743
744    fn log_trace(&self, event: &str, num_tokens: usize) {
745        kv_cache_trace::log_sglang_trace(&kv_cache_trace::SglangCacheState {
746            event,
747            dp_rank: self.dp_rank,
748            num_tokens,
749            page_size: self.cache.page_size(),
750            available_tokens: self.cache.available_tokens(),
751            evictable_tokens: self.cache.evictable_size,
752            protected_tokens: self.cache.protected_size,
753            total_tokens: self.cache.total_tokens(),
754        });
755    }
756
757    fn publish_stored_event(
758        &mut self,
759        token_ids: &[u32],
760        indices: &[usize],
761        first_new_token: usize,
762    ) -> usize {
763        if self.kv_event_publishers.is_empty() {
764            return 0;
765        }
766
767        let block_size = self.cache.page_size();
768        let complete_len = token_ids.len().min(indices.len()) / block_size * block_size;
769        if complete_len == 0 || first_new_token >= complete_len {
770            return 0;
771        }
772
773        let first_block_start = first_new_token / block_size * block_size;
774        let Some(first_unpublished_block) = (first_block_start..complete_len)
775            .step_by(block_size)
776            .find(|&block_start| {
777                let representative_idx = indices[block_start + block_size - 1];
778                !self.idx_to_block_hash.contains_key(&representative_idx)
779            })
780        else {
781            return 0;
782        };
783
784        let mut computed_blocks = Vec::new();
785        let local_hashes =
786            self.local_hashes_for_range(&token_ids[first_unpublished_block..complete_len]);
787
788        for (block_idx, tokens_hash) in local_hashes.iter().copied().enumerate() {
789            let block_start = first_unpublished_block + block_idx * block_size;
790            let block_end = block_start + block_size;
791            let representative_idx = indices[block_end - 1];
792            if self.idx_to_block_hash.contains_key(&representative_idx) {
793                continue;
794            }
795
796            let parent_hash = if block_start == 0 {
797                None
798            } else {
799                self.idx_to_block_hash
800                    .get(&indices[block_start - 1])
801                    .copied()
802            };
803            let block_hash = match parent_hash {
804                Some(parent_hash) => {
805                    ExternalSequenceBlockHash(compute_next_seq_hash(parent_hash.0, tokens_hash))
806                }
807                None => ExternalSequenceBlockHash(tokens_hash.0),
808            };
809
810            self.idx_to_block_hash
811                .insert(representative_idx, block_hash);
812            let refcount = self.block_hash_refcounts.entry(block_hash).or_default();
813            *refcount += 1;
814            computed_blocks.push((
815                parent_hash,
816                KvCacheStoredBlockData {
817                    block_hash,
818                    tokens_hash,
819                    mm_extra_info: None,
820                },
821                *refcount,
822            ));
823        }
824
825        let hashed_blocks = local_hashes.len();
826
827        let first_new = computed_blocks
828            .iter()
829            .position(|(_, _, refcount)| *refcount == 1);
830        let Some(first_new) = first_new else {
831            return hashed_blocks;
832        };
833
834        let parent_hash = computed_blocks[first_new].0;
835        let blocks = computed_blocks
836            .into_iter()
837            .skip(first_new)
838            .map(|(_, block, _)| block)
839            .collect();
840
841        let event = KvCacheEvent {
842            event_id: self.next_event_id,
843            data: KvCacheEventData::Stored(KvCacheStoreData {
844                parent_hash,
845                start_position: None,
846                blocks,
847            }),
848            dp_rank: self.dp_rank,
849        };
850        self.next_event_id += 1;
851
852        if let Err(e) = self.kv_event_publishers.publish(event, None) {
853            tracing::warn!("Failed to publish SGLang KV event: {e}");
854        }
855
856        hashed_blocks
857    }
858
859    fn local_hashes_for_range(&self, token_ids: &[u32]) -> Vec<LocalBlockHash> {
860        compute_block_hash_for_seq(
861            token_ids,
862            self.cache.page_size() as u32,
863            BlockHashOptions::default(),
864        )
865    }
866
867    fn publish_removed_pages(&mut self, evicted_pages: &[KvPageId]) {
868        if self.kv_event_publishers.is_empty() {
869            return;
870        }
871
872        let mut block_hashes = Vec::new();
873        for &page in evicted_pages {
874            let idx = page.terminal_token_index(self.cache.page_size());
875            let Some(block_hash) = self.idx_to_block_hash.remove(&idx) else {
876                continue;
877            };
878            let Some(refcount) = self.block_hash_refcounts.get_mut(&block_hash) else {
879                continue;
880            };
881            if *refcount > 1 {
882                *refcount -= 1;
883                continue;
884            }
885            self.block_hash_refcounts.remove(&block_hash);
886            block_hashes.push(block_hash);
887        }
888
889        if block_hashes.is_empty() {
890            return;
891        }
892
893        let event = KvCacheEvent {
894            event_id: self.next_event_id,
895            data: KvCacheEventData::Removed(KvCacheRemoveData { block_hashes }),
896            dp_rank: self.dp_rank,
897        };
898        self.next_event_id += 1;
899
900        if let Err(e) = self.kv_event_publishers.publish(event, None) {
901            tracing::warn!("Failed to publish SGLang KV remove event: {e}");
902        }
903    }
904}
905
906#[cfg(test)]
907mod tests {
908    use super::*;
909    use std::sync::Arc;
910    use std::sync::Mutex;
911
912    use crate::common::protocols::KvCacheEventSink;
913    use crate::scheduler::capture_router_event_sink;
914    use crate::scheduler::test_utils::{RouterIndexerHarness, stored_hashes};
915    use dynamo_kv_router::RadixTree;
916    use dynamo_kv_router::protocols::{RouterEvent, WorkerId, compute_seq_hash_for_block};
917
918    const ROUTER_TEST_WORKER_ID: WorkerId = 31;
919
920    struct MockSink {
921        events: Mutex<Vec<KvCacheEvent>>,
922    }
923
924    impl MockSink {
925        fn new() -> Self {
926            Self {
927                events: Mutex::new(Vec::new()),
928            }
929        }
930
931        fn event_count(&self) -> usize {
932            self.events.lock().unwrap().len()
933        }
934
935        fn clone_events(&self) -> Vec<KvCacheEvent> {
936            self.events.lock().unwrap().clone()
937        }
938    }
939
940    impl KvCacheEventSink for MockSink {
941        fn publish(&self, event: KvCacheEvent) -> anyhow::Result<()> {
942            self.events.lock().unwrap().push(event);
943            Ok(())
944        }
945    }
946
947    fn stored_event_count(events: &[RouterEvent]) -> usize {
948        events
949            .iter()
950            .filter(|event| matches!(event.event.data, KvCacheEventData::Stored(_)))
951            .count()
952    }
953
954    fn removed_event_count(events: &[RouterEvent]) -> usize {
955        events
956            .iter()
957            .filter(|event| matches!(event.event.data, KvCacheEventData::Removed(_)))
958            .count()
959    }
960
961    fn removed_block_count(events: &[RouterEvent]) -> usize {
962        events
963            .iter()
964            .filter_map(|event| match &event.event.data {
965                KvCacheEventData::Removed(remove) => Some(remove.block_hashes.len()),
966                _ => None,
967            })
968            .sum()
969    }
970
971    #[test]
972    fn active_kv_lease_has_no_space_overhead() {
973        let previous_fields = std::mem::size_of::<Vec<usize>>()
974            + std::mem::size_of::<usize>()
975            + std::mem::size_of::<Option<NodeId>>();
976
977        assert_eq!(std::mem::size_of::<ActiveKvLease>(), previous_fields);
978    }
979
980    #[test]
981    fn active_partial_page_owns_full_capacity_and_extends_in_place() {
982        let mut mgr = SglangKvManager::new(12, 4, KvEventPublishers::default(), 0);
983        let mut alloc = mgr.allocate_for_request(&[1]).unwrap();
984        assert_eq!(mgr.cache().available_tokens(), 8);
985
986        assert!(mgr.extend_allocation(&[1, 2, 3, 4], &mut alloc.lease));
987        assert_eq!(
988            mgr.cache().available_tokens(),
989            8,
990            "filling an owned partial page must not allocate another page"
991        );
992
993        assert!(mgr.extend_allocation(&[1, 2, 3, 4, 5], &mut alloc.lease));
994        assert_eq!(mgr.cache().available_tokens(), 4);
995        assert!(mgr.retract(alloc.lease));
996        assert_eq!(mgr.cache().available_tokens(), 12);
997    }
998
999    #[test]
1000    fn fresh_allocation_protects_matched_prefix_before_eviction() {
1001        let mut mgr = SglangKvManager::new(12, 4, KvEventPublishers::default(), 0);
1002        let prefix = [1, 2, 3, 4];
1003        let other = [9, 10, 11, 12, 13, 14, 15, 16];
1004
1005        let prefix_alloc = mgr.allocate_for_request(&prefix).unwrap();
1006        mgr.finish(&prefix, prefix_alloc.lease);
1007        let other_alloc = mgr.allocate_for_request(&other).unwrap();
1008        mgr.finish(&other, other_alloc.lease);
1009        // Make `prefix` the LRU victim. The new request must protect it before
1010        // evicting one of `other`'s pages to satisfy its suffix allocation.
1011        assert_eq!(mgr.cache_mut().match_prefix(&other).0, other.len());
1012        assert_eq!(mgr.cache().available_tokens(), 0);
1013
1014        let extended = [1, 2, 3, 4, 5, 6, 7, 8];
1015        let alloc = mgr
1016            .allocate_for_request(&extended)
1017            .expect("protected prefix plus one evicted page should fit");
1018
1019        assert_eq!(alloc.prefix_len, prefix.len());
1020        assert_eq!(mgr.cache().prefix_match_len(&prefix), prefix.len());
1021        assert_eq!(mgr.cache().available_tokens(), 0);
1022    }
1023
1024    #[test]
1025    fn finish_drops_partial_tail_page_and_caches_compact_complete_page() {
1026        let mut mgr = SglangKvManager::new(12, 4, KvEventPublishers::default(), 0);
1027        let tokens = [1, 2, 3, 4, 5];
1028        let alloc = mgr.allocate_for_request(&tokens).unwrap();
1029        assert_eq!(mgr.cache().available_tokens(), 4);
1030
1031        mgr.finish(&tokens, alloc.lease);
1032
1033        assert_eq!(mgr.cache().available_tokens(), 8);
1034        let (matched, node) = mgr.cache_mut().match_prefix(&tokens);
1035        assert_eq!(matched, 4);
1036        assert_eq!(mgr.cache().node(node).key.len(), 1);
1037        assert_eq!(mgr.cache().node(node).value.len(), 1);
1038    }
1039
1040    #[test]
1041    fn partially_consumed_decode_reservation_releases_only_unused_slots() {
1042        let mut mgr = SglangKvManager::new(4, 1, KvEventPublishers::default(), 0);
1043        let mut reservation = mgr.reserve_decode_pages(3).unwrap();
1044        let consumed = reservation.take(None);
1045        assert_eq!(reservation.len(), 2);
1046        let mut expected_unused = reservation.pages[reservation.next..].to_vec();
1047
1048        mgr.release_decode_reservation(reservation);
1049        assert_eq!(mgr.cache().available_tokens(), 3);
1050
1051        let mut reallocated = mgr
1052            .cache_mut()
1053            .page_pool
1054            .allocate_pages(expected_unused.len())
1055            .unwrap();
1056        expected_unused.sort_unstable();
1057        reallocated.sort_unstable();
1058        assert_eq!(reallocated, expected_unused);
1059        assert!(reallocated.windows(2).all(|pair| pair[0] != pair[1]));
1060        assert!(!reallocated.contains(&KvPageId::from_token_index(consumed, 1)));
1061    }
1062
1063    #[test]
1064    fn retract_lease_releases_only_the_uncached_suffix() {
1065        let mut mgr = SglangKvManager::new(16, 4, KvEventPublishers::default(), 0);
1066        let mut alloc = mgr.allocate_for_request(&[1, 2, 3, 4]).unwrap();
1067        mgr.extend_cached_prefix(&[1, 2, 3, 4], &mut alloc.lease);
1068        assert!(mgr.extend_allocation(&[1, 2, 3, 4, 5, 6], &mut alloc.lease));
1069
1070        assert_eq!(alloc.lease.cached_tokens(), 4);
1071        assert_eq!(alloc.lease.len(), 6);
1072        assert!(mgr.retract(alloc.lease));
1073
1074        assert_eq!(mgr.cache().page_pool.available(), 12);
1075        assert_eq!(mgr.cache().protected_size, 0);
1076        assert_eq!(mgr.cache().evictable_size, 4);
1077        assert_eq!(mgr.cache().prefix_match_len(&[1, 2, 3, 4]), 4);
1078    }
1079
1080    #[test]
1081    fn retained_tail_split_releases_leases_before_eviction() {
1082        let (buffer, sink) = capture_router_event_sink(ROUTER_TEST_WORKER_ID);
1083        let mut mgr = SglangKvManager::new(16, 4, KvEventPublishers::new(Some(sink), None), 0);
1084        let mut indexer = RadixTree::new();
1085
1086        let first_tokens = [1, 2, 3, 4, 5, 6, 7, 8];
1087        let mut first = mgr.allocate_for_request(&first_tokens[..4]).unwrap();
1088        mgr.extend_cached_prefix(&first_tokens[..4], &mut first.lease);
1089        let retained_tail = first.lease.last_node();
1090        assert!(mgr.extend_allocation(&first_tokens, &mut first.lease));
1091        mgr.extend_cached_prefix(&first_tokens, &mut first.lease);
1092
1093        assert_eq!(first.lease.last_node(), retained_tail);
1094        assert_eq!(mgr.cache().num_nodes(), 2);
1095        for event in buffer.drain() {
1096            indexer.apply_event(event).unwrap();
1097        }
1098
1099        let second_tokens = [1, 2, 3, 4, 9, 10, 11, 12];
1100        let mut second = mgr.allocate_for_request(&second_tokens).unwrap();
1101        assert_eq!(second.prefix_len, 4);
1102        assert_eq!(first.lease.last_node(), retained_tail);
1103        assert_eq!(mgr.cache().num_nodes(), 3);
1104        mgr.extend_cached_prefix(&second_tokens, &mut second.lease);
1105        assert_eq!(mgr.cache().num_nodes(), 4);
1106        for event in buffer.drain() {
1107            indexer.apply_event(event).unwrap();
1108        }
1109
1110        mgr.finish(&first_tokens, first.lease);
1111        assert!(mgr.retract(second.lease));
1112        assert_eq!(mgr.cache().protected_size, 0);
1113        assert_eq!(mgr.cache().evictable_size, 12);
1114
1115        mgr.evict(12);
1116        for event in buffer.drain() {
1117            indexer.apply_event(event).unwrap();
1118        }
1119        assert_eq!(mgr.cache().page_pool.available(), 16);
1120        assert_eq!(mgr.cache().protected_size, 0);
1121        assert_eq!(mgr.cache().evictable_size, 0);
1122        assert_eq!(mgr.cache().num_nodes(), 1);
1123    }
1124
1125    #[test]
1126    fn test_allocate_cache_miss() {
1127        let mut mgr = SglangKvManager::new(100, 1, KvEventPublishers::default(), 0);
1128
1129        let result = mgr.allocate_for_request(&[1, 2, 3, 4, 5]).unwrap();
1130        assert_eq!(result.prefix_len, 0);
1131        assert_eq!(result.lease.kv_indices.len(), 5);
1132        assert_eq!(mgr.cache().page_pool.available(), 95);
1133    }
1134
1135    #[test]
1136    fn test_allocate_cache_hit() {
1137        let mut mgr = SglangKvManager::new(100, 1, KvEventPublishers::default(), 0);
1138
1139        // First request: allocate and cache
1140        let r1 = mgr.allocate_for_request(&[1, 2, 3, 4, 5]).unwrap();
1141        assert_eq!(r1.lease.kv_indices.len(), 5); // 5 pages (page_size=1)
1142        mgr.cache_finished_req(
1143            &[1, 2, 3, 4, 5],
1144            &r1.lease.kv_indices,
1145            r1.lease.last_node(),
1146            0,
1147        );
1148
1149        // Second request with shared prefix
1150        let r2 = mgr.allocate_for_request(&[1, 2, 3, 4, 5, 6, 7]).unwrap();
1151        assert_eq!(r2.prefix_len, 5);
1152        assert_eq!(r2.lease.kv_indices.len(), 7); // 5 reused + 2 new pages
1153        assert_eq!(mgr.cache().page_pool.available(), 93); // 100 - 5 - 2
1154    }
1155
1156    #[test]
1157    fn destination_transfer_footprint_uses_missing_physical_pages() {
1158        let mut mgr = SglangKvManager::new(64, 4, KvEventPublishers::default(), 0);
1159        let prompt = (0..10).collect::<Vec<_>>();
1160
1161        let cold = mgr
1162            .reserve_destination(&prompt)
1163            .expect("cold destination reservation should fit");
1164        assert_eq!(cold.transferable_prompt_tokens(), 12);
1165        mgr.cancel_destination(cold);
1166
1167        let prefix_tokens = &prompt[..4];
1168        let prefix = mgr
1169            .allocate_for_request(prefix_tokens)
1170            .expect("prefix allocation should fit");
1171        mgr.cache_finished_req(
1172            prefix_tokens,
1173            &prefix.lease.kv_indices,
1174            prefix.lease.last_node(),
1175            prefix.prefix_len,
1176        );
1177        let partial = mgr
1178            .reserve_destination(&prompt)
1179            .expect("partially cached destination reservation should fit");
1180        assert_eq!(partial.transferable_prompt_tokens(), 8);
1181        mgr.cancel_destination(partial);
1182
1183        let aligned_tokens = (20..28).collect::<Vec<_>>();
1184        let aligned = mgr
1185            .allocate_for_request(&aligned_tokens)
1186            .expect("aligned prompt allocation should fit");
1187        mgr.cache_finished_req(
1188            &aligned_tokens,
1189            &aligned.lease.kv_indices,
1190            aligned.lease.last_node(),
1191            aligned.prefix_len,
1192        );
1193        let full_hit = mgr
1194            .reserve_destination(&aligned_tokens)
1195            .expect("fully cached destination reservation should fit");
1196        assert_eq!(full_hit.transferable_prompt_tokens(), 0);
1197    }
1198
1199    #[test]
1200    fn test_free_request_without_caching() {
1201        let mut mgr = SglangKvManager::new(100, 1, KvEventPublishers::default(), 0);
1202
1203        let result = mgr.allocate_for_request(&[1, 2, 3]).unwrap();
1204        mgr.free_request(result.lease.last_node());
1205
1206        // Path is unlocked, tokens still allocated in pool
1207        assert_eq!(mgr.cache().protected_size, 0);
1208    }
1209
1210    #[test]
1211    fn test_event_publishing() {
1212        let sink = Arc::new(MockSink::new());
1213        let mut mgr =
1214            SglangKvManager::new(100, 1, KvEventPublishers::new(Some(sink.clone()), None), 0);
1215
1216        let r = mgr.allocate_for_request(&[1, 2, 3]).unwrap();
1217        assert_eq!(sink.event_count(), 1); // BlockStored for 3 new pages
1218
1219        mgr.cache_finished_req(&[1, 2, 3], &r.lease.kv_indices, r.lease.last_node(), 0);
1220
1221        // Second request with full cache hit → no new events
1222        let r2 = mgr.allocate_for_request(&[1, 2, 3]).unwrap();
1223        assert_eq!(r2.prefix_len, 3);
1224        assert_eq!(sink.event_count(), 1); // no new event
1225    }
1226
1227    #[test]
1228    fn test_event_publishing_uses_router_block_hashes() {
1229        let sink = Arc::new(MockSink::new());
1230        let mut mgr =
1231            SglangKvManager::new(100, 4, KvEventPublishers::new(Some(sink.clone()), None), 0);
1232
1233        let r = mgr.allocate_for_request(&[1, 2, 3, 4, 5, 6]).unwrap();
1234        mgr.cache_finished_req(
1235            &[1, 2, 3, 4, 5, 6],
1236            &r.lease.kv_indices,
1237            r.lease.last_node(),
1238            0,
1239        );
1240
1241        let events = sink.clone_events();
1242        assert_eq!(events.len(), 1);
1243        let KvCacheEventData::Stored(store) = &events[0].data else {
1244            panic!("expected stored event");
1245        };
1246        assert_eq!(store.blocks.len(), 1);
1247
1248        let expected_local =
1249            compute_block_hash_for_seq(&[1, 2, 3, 4], 4, BlockHashOptions::default());
1250        let expected_sequence = compute_seq_hash_for_block(&expected_local);
1251        assert_eq!(store.blocks[0].tokens_hash, expected_local[0]);
1252        assert_eq!(
1253            store.blocks[0].block_hash,
1254            ExternalSequenceBlockHash(expected_sequence[0])
1255        );
1256    }
1257
1258    #[test]
1259    fn test_published_prefix_hashes_only_unseen_suffix() {
1260        let sink = Arc::new(MockSink::new());
1261        let mut mgr =
1262            SglangKvManager::new(16, 4, KvEventPublishers::new(Some(sink.clone()), None), 0);
1263        let tokens = [1, 2, 3, 4, 5, 6, 7, 8];
1264        let indices = mgr.cache_mut().page_pool.allocate(tokens.len()).unwrap();
1265
1266        assert_eq!(mgr.publish_stored_event(&tokens[..4], &indices[..4], 0), 1);
1267        assert_eq!(mgr.publish_stored_event(&tokens, &indices, 0), 1);
1268        assert_eq!(mgr.publish_stored_event(&tokens, &indices, 0), 0);
1269
1270        let events = sink.clone_events();
1271        assert_eq!(events.len(), 2);
1272        let KvCacheEventData::Stored(first) = &events[0].data else {
1273            panic!("expected first stored event");
1274        };
1275        let KvCacheEventData::Stored(second) = &events[1].data else {
1276            panic!("expected suffix stored event");
1277        };
1278        assert_eq!(first.blocks.len(), 1);
1279        assert_eq!(second.blocks.len(), 1);
1280        assert_eq!(second.parent_hash, Some(first.blocks[0].block_hash));
1281    }
1282
1283    #[test]
1284    fn test_cache_materialization_processes_only_newly_completed_blocks() {
1285        let sink = Arc::new(MockSink::new());
1286        let mut mgr = SglangKvManager::new(100, 2, KvEventPublishers::default(), 0);
1287        let tokens = [1, 2, 3, 4, 5, 6];
1288
1289        let mut alloc = mgr.allocate_for_request(&tokens[..2]).unwrap();
1290        let alloc_last_node = alloc.lease.last_node();
1291        let first_last_node = mgr.cache_unfinished_req(
1292            &tokens[..2],
1293            &mut alloc.lease.kv_indices,
1294            alloc_last_node,
1295            0,
1296        );
1297
1298        let mut kv_indices = alloc.lease.kv_indices;
1299        kv_indices.extend_from_slice(&mgr.cache_mut().page_pool.allocate(4).unwrap());
1300        mgr.kv_event_publishers = KvEventPublishers::new(Some(sink.clone()), None);
1301
1302        let last_after_first_cache =
1303            mgr.cache_unfinished_req(&tokens[..4], &mut kv_indices[..4], first_last_node, 2);
1304        let events = sink.clone_events();
1305        assert_eq!(events.len(), 1);
1306        let KvCacheEventData::Stored(first_store) = &events[0].data else {
1307            panic!("expected first cache event to be Stored");
1308        };
1309        assert_eq!(
1310            first_store.blocks.len(),
1311            1,
1312            "first unfinished cache should store only the newly completed block"
1313        );
1314
1315        mgr.cache_finished_req(&tokens, &kv_indices, last_after_first_cache, 4);
1316        let events = sink.clone_events();
1317        assert_eq!(events.len(), 2);
1318        let KvCacheEventData::Stored(final_store) = &events[1].data else {
1319            panic!("expected final cache event to be Stored");
1320        };
1321        assert_eq!(
1322            final_store.blocks.len(),
1323            1,
1324            "finished cache should store only the newly completed block"
1325        );
1326    }
1327
1328    #[test]
1329    fn test_duplicate_logical_blocks_publish_once_and_remove_once() {
1330        let sink = Arc::new(MockSink::new());
1331        let mut mgr =
1332            SglangKvManager::new(100, 1, KvEventPublishers::new(Some(sink.clone()), None), 0);
1333
1334        let req1 = mgr.allocate_for_request(&[1, 2, 3]).unwrap();
1335        let req2 = mgr.allocate_for_request(&[1, 2, 3]).unwrap();
1336
1337        let events = sink.clone_events();
1338        assert_eq!(events.len(), 1);
1339        let KvCacheEventData::Stored(store) = &events[0].data else {
1340            panic!("expected stored event");
1341        };
1342        assert_eq!(store.blocks.len(), 3);
1343
1344        mgr.free_indices(&req1.lease.kv_indices);
1345        assert_eq!(sink.event_count(), 1);
1346
1347        mgr.free_indices(&req2.lease.kv_indices);
1348        let events = sink.clone_events();
1349        assert_eq!(events.len(), 2);
1350        let KvCacheEventData::Removed(remove) = &events[1].data else {
1351            panic!("expected removed event");
1352        };
1353        assert_eq!(remove.block_hashes.len(), 3);
1354    }
1355
1356    #[tokio::test]
1357    async fn test_duplicate_completion_releases_unretained_indices_and_removes_on_eviction() {
1358        let (buffer, sink) = capture_router_event_sink(ROUTER_TEST_WORKER_ID);
1359        let harness = RouterIndexerHarness::new(1, ROUTER_TEST_WORKER_ID);
1360        let mut mgr = SglangKvManager::new(100, 1, KvEventPublishers::new(Some(sink), None), 0);
1361        let tokens = [1, 2, 3];
1362
1363        let req1 = mgr.allocate_for_request(&tokens).unwrap();
1364        let req2 = mgr.allocate_for_request(&tokens).unwrap();
1365        assert_eq!(
1366            mgr.cache().page_pool.available(),
1367            94,
1368            "both identical requests should allocate before either is cached"
1369        );
1370
1371        let allocation_events = buffer.drain();
1372        assert_eq!(
1373            stored_event_count(&allocation_events),
1374            1,
1375            "duplicate allocation should emit only one logical Stored event"
1376        );
1377        let query_hashes = stored_hashes(&allocation_events);
1378        assert_eq!(query_hashes.len(), tokens.len());
1379        harness.apply_events(allocation_events).await;
1380
1381        mgr.cache_finished_req(&tokens, &req1.lease.kv_indices, req1.lease.last_node(), 0);
1382        let req1_completion_events = buffer.drain();
1383        assert_eq!(
1384            stored_event_count(&req1_completion_events),
1385            0,
1386            "first completion should not re-emit Stored blocks"
1387        );
1388        assert_eq!(
1389            removed_event_count(&req1_completion_events),
1390            0,
1391            "canonical completion should not emit Removed blocks"
1392        );
1393        assert_eq!(
1394            mgr.cache().page_pool.available(),
1395            94,
1396            "canonical completion should retain the first request's slots"
1397        );
1398
1399        mgr.cache_finished_req(&tokens, &req2.lease.kv_indices, req2.lease.last_node(), 0);
1400        let req2_completion_events = buffer.drain();
1401        assert_eq!(
1402            stored_event_count(&req2_completion_events),
1403            0,
1404            "duplicate completion should not re-emit Stored blocks"
1405        );
1406        assert_eq!(
1407            removed_event_count(&req2_completion_events),
1408            0,
1409            "duplicate completion should only decrement duplicate refcounts"
1410        );
1411        assert_eq!(
1412            mgr.cache().page_pool.available(),
1413            97,
1414            "duplicate completion should return unretained request slots"
1415        );
1416        assert_eq!(harness.overlap_for_hashes(query_hashes.clone()).await, 3);
1417
1418        mgr.evict(tokens.len());
1419        let eviction_events = buffer.drain();
1420        assert_eq!(
1421            removed_event_count(&eviction_events),
1422            1,
1423            "evicting the canonical sequence should emit one logical Removed event"
1424        );
1425        assert_eq!(
1426            removed_block_count(&eviction_events),
1427            tokens.len(),
1428            "Removed event should cover every cached block"
1429        );
1430        harness.apply_events(eviction_events).await;
1431        assert_eq!(harness.overlap_for_hashes(query_hashes).await, 0);
1432        harness.shutdown();
1433    }
1434
1435    #[tokio::test]
1436    async fn retained_tail_eviction_preserves_page_granular_prefix_reuse() {
1437        let (buffer, sink) = capture_router_event_sink(ROUTER_TEST_WORKER_ID);
1438        let harness = RouterIndexerHarness::new(4, ROUTER_TEST_WORKER_ID);
1439        let mut mgr = SglangKvManager::new(8, 4, KvEventPublishers::new(Some(sink), None), 0);
1440        let tokens = [1, 2, 3, 4, 5, 6, 7, 8];
1441
1442        let mut request = mgr.allocate_for_request(&tokens[..4]).unwrap();
1443        mgr.extend_cached_prefix(&tokens[..4], &mut request.lease);
1444        assert!(mgr.extend_allocation(&tokens, &mut request.lease));
1445        mgr.extend_cached_prefix(&tokens, &mut request.lease);
1446        mgr.finish(&tokens, request.lease);
1447
1448        let stored_events = buffer.drain();
1449        let query_hashes = stored_hashes(&stored_events);
1450        assert_eq!(query_hashes.len(), 2);
1451        harness.apply_events(stored_events).await;
1452        assert_eq!(harness.overlap_for_hashes(query_hashes.clone()).await, 2);
1453        assert_eq!(mgr.cache().evictable_size, 8);
1454        assert_eq!(mgr.cache().page_pool.available(), 0);
1455
1456        mgr.evict(4);
1457        let eviction_events = buffer.drain();
1458        assert_eq!(removed_event_count(&eviction_events), 1);
1459        assert_eq!(removed_block_count(&eviction_events), 1);
1460        harness.apply_events(eviction_events).await;
1461
1462        assert_eq!(mgr.cache().prefix_match_len(&tokens), 4);
1463        assert_eq!(mgr.cache().evictable_size, 4);
1464        assert_eq!(mgr.cache().protected_size, 0);
1465        assert_eq!(mgr.cache().page_pool.available(), 4);
1466        assert_eq!(harness.overlap_for_hashes(query_hashes).await, 1);
1467        harness.shutdown();
1468    }
1469
1470    #[test]
1471    fn unfinished_duplicate_canonicalization_prevents_missing_parent() {
1472        let (buffer, sink) = capture_router_event_sink(ROUTER_TEST_WORKER_ID);
1473        let mut mgr = SglangKvManager::new(32, 4, KvEventPublishers::new(Some(sink), None), 0);
1474        let mut indexer = RadixTree::new();
1475
1476        let seed_tokens = [1, 2, 3, 4];
1477        let seed = mgr.allocate_for_request(&seed_tokens).unwrap();
1478        mgr.finish(&seed_tokens, seed.lease);
1479        for event in buffer.drain() {
1480            indexer.apply_event(event).unwrap();
1481        }
1482
1483        // Two requests miss the same suffix before either inserts it. Their
1484        // physical suffix pages are distinct even though the logical block is
1485        // identical.
1486        let shared_tokens = [1, 2, 3, 4, 5, 6, 7, 8];
1487        let mut first = mgr.allocate_for_request(&shared_tokens).unwrap();
1488        let mut duplicate = mgr.allocate_for_request(&shared_tokens).unwrap();
1489        let duplicate_suffix = duplicate.lease.indices()[seed_tokens.len()..].to_vec();
1490        assert_ne!(
1491            first.lease.indices()[seed_tokens.len()..],
1492            duplicate.lease.indices()[seed_tokens.len()..]
1493        );
1494        for event in buffer.drain() {
1495            indexer.apply_event(event).unwrap();
1496        }
1497
1498        mgr.extend_cached_prefix(&shared_tokens, &mut first.lease);
1499        mgr.extend_cached_prefix(&shared_tokens, &mut duplicate.lease);
1500        assert_eq!(
1501            duplicate.lease.indices(),
1502            first.lease.indices(),
1503            "the active duplicate must switch to radix-owned canonical pages"
1504        );
1505        assert_eq!(
1506            mgr.cache().page_pool.available(),
1507            24,
1508            "every duplicate slot in the four-token page must return to the pool"
1509        );
1510        assert!(
1511            duplicate_suffix
1512                .iter()
1513                .all(|idx| !duplicate.lease.indices().contains(idx)),
1514            "no duplicate physical slot may remain attached to the active request"
1515        );
1516        for event in buffer.drain() {
1517            indexer.apply_event(event).unwrap();
1518        }
1519
1520        // Mirror retracting the duplicate after its full prefix was cached,
1521        // then finish and evict the canonical request.
1522        assert!(mgr.retract(duplicate.lease));
1523        mgr.finish(&shared_tokens, first.lease);
1524        mgr.evict(seed_tokens.len());
1525        mgr.evict(seed_tokens.len());
1526        for event in buffer.drain() {
1527            indexer.apply_event(event).unwrap();
1528        }
1529
1530        // Restore only the first block, then extend through the formerly
1531        // duplicated block. A leaked duplicate publisher refcount would
1532        // suppress re-storing block 2 and emit block 3 with a missing parent.
1533        let restored = mgr.allocate_for_request(&seed_tokens).unwrap();
1534        mgr.finish(&seed_tokens, restored.lease);
1535        for event in buffer.drain() {
1536            indexer.apply_event(event).unwrap();
1537        }
1538
1539        let extended = mgr
1540            .allocate_for_request(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
1541            .unwrap();
1542        let extension_events = buffer.drain();
1543        assert_eq!(stored_event_count(&extension_events), 1);
1544        let store = extension_events
1545            .iter()
1546            .find_map(|event| match &event.event.data {
1547                KvCacheEventData::Stored(store) => Some(store),
1548                _ => None,
1549            })
1550            .unwrap();
1551        assert_eq!(
1552            store.blocks.len(),
1553            2,
1554            "both missing descendants must be stored"
1555        );
1556        for event in extension_events {
1557            indexer.apply_event(event).unwrap();
1558        }
1559
1560        assert!(mgr.abort(extended.lease));
1561    }
1562
1563    #[test]
1564    #[should_panic(expected = "invalid SGLang canonicalization range or radix path")]
1565    fn invalid_canonical_path_is_fatal() {
1566        let mut mgr = SglangKvManager::new(8, 4, KvEventPublishers::default(), 0);
1567        let mut indices = mgr.cache_mut().page_pool.allocate(4).unwrap();
1568        let root = mgr.cache().root();
1569
1570        mgr.canonicalize_unfinished_indices(&mut indices, root, 0, 4);
1571    }
1572
1573    #[test]
1574    fn cache_unfinished_rejects_invalid_range_before_publishing() {
1575        let sink = Arc::new(MockSink::new());
1576        let mut mgr =
1577            SglangKvManager::new(8, 4, KvEventPublishers::new(Some(sink.clone()), None), 0);
1578        let tokens = [1, 2, 3, 4];
1579        let mut alloc = mgr.allocate_for_request(&tokens).unwrap();
1580        let events_before = sink.event_count();
1581        let last_node = alloc.lease.last_node();
1582
1583        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1584            mgr.cache_unfinished_req(&tokens, &mut alloc.lease.kv_indices, last_node, 2)
1585        }));
1586
1587        assert!(result.is_err());
1588        assert_eq!(sink.event_count(), events_before);
1589    }
1590
1591    #[test]
1592    fn cache_unfinished_rejects_short_indices_before_publishing() {
1593        let sink = Arc::new(MockSink::new());
1594        let mut mgr =
1595            SglangKvManager::new(8, 4, KvEventPublishers::new(Some(sink.clone()), None), 0);
1596        let tokens = [1, 2, 3, 4, 5, 6, 7, 8];
1597        let mut alloc = mgr.allocate_for_request(&tokens).unwrap();
1598        alloc.lease.kv_indices.truncate(4);
1599        let events_before = sink.event_count();
1600        let last_node = alloc.lease.last_node();
1601
1602        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1603            mgr.cache_unfinished_req(&tokens, &mut alloc.lease.kv_indices, last_node, 0)
1604        }));
1605
1606        assert!(result.is_err());
1607        assert_eq!(sink.event_count(), events_before);
1608    }
1609
1610    #[test]
1611    fn test_allocate_oom() {
1612        let mut mgr = SglangKvManager::new(3, 1, KvEventPublishers::default(), 0);
1613
1614        let _r = mgr.allocate_for_request(&[1, 2, 3]).unwrap();
1615        // Pool is full
1616        let result = mgr.allocate_for_request(&[4, 5, 6]);
1617        assert!(result.is_none());
1618    }
1619
1620    #[test]
1621    fn test_chunked_prefill_parent_hash() {
1622        let sink = Arc::new(MockSink::new());
1623        let mut mgr =
1624            SglangKvManager::new(32, 1, KvEventPublishers::new(Some(sink.clone()), None), 0);
1625        let tokens = [11, 22, 33, 44, 55, 66];
1626        let chunk1_len = 3;
1627        let chunk2_len = 6;
1628
1629        let mut alloc1 = mgr.allocate_for_request(&tokens[..chunk1_len]).unwrap();
1630        let previous_last = alloc1.lease.last_node();
1631        let new_last = mgr.cache_unfinished_req(
1632            &tokens[..chunk1_len],
1633            &mut alloc1.lease.kv_indices,
1634            previous_last,
1635            0,
1636        );
1637
1638        let alloc2 = mgr.allocate_for_request(&tokens[..chunk2_len]).unwrap();
1639        mgr.free_request(new_last);
1640
1641        let events = sink.events.lock().unwrap();
1642        assert_eq!(events.len(), 2, "expected two stored events");
1643
1644        let KvCacheEventData::Stored(store1) = &events[0].data else {
1645            panic!("expected first event to be Stored");
1646        };
1647        let KvCacheEventData::Stored(store2) = &events[1].data else {
1648            panic!("expected second event to be Stored");
1649        };
1650
1651        assert!(
1652            store1.parent_hash.is_none(),
1653            "first chunk should start from the root"
1654        );
1655
1656        let last_block_hash = store1
1657            .blocks
1658            .last()
1659            .expect("first chunk should store at least one block")
1660            .block_hash;
1661        assert_eq!(
1662            store2.parent_hash,
1663            Some(last_block_hash),
1664            "second chunk should chain from the last block of chunk 1"
1665        );
1666        assert_eq!(
1667            store2.blocks.len(),
1668            chunk2_len - chunk1_len,
1669            "second chunk should only emit new blocks"
1670        );
1671        assert_eq!(
1672            alloc2.prefix_len, chunk1_len,
1673            "second chunk should reuse the cached partial prefix"
1674        );
1675    }
1676}