1use 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#[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
67pub(crate) struct AllocResult {
69 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 idx_to_block_hash: FxHashMap<usize, ExternalSequenceBlockHash>,
82 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 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 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 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 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 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 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 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 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 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 fn collect_path_indices(&self, last_node: NodeId) -> Vec<usize> {
557 if last_node == self.cache.root() {
558 return Vec::new();
559 }
560
561 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 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 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 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 let r1 = mgr.allocate_for_request(&[1, 2, 3, 4, 5]).unwrap();
1141 assert_eq!(r1.lease.kv_indices.len(), 5); 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 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); assert_eq!(mgr.cache().page_pool.available(), 93); }
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 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); mgr.cache_finished_req(&[1, 2, 3], &r.lease.kv_indices, r.lease.last_node(), 0);
1220
1221 let r2 = mgr.allocate_for_request(&[1, 2, 3]).unwrap();
1223 assert_eq!(r2.prefix_len, 3);
1224 assert_eq!(sink.event_count(), 1); }
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 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 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 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 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}