1use std::collections::{HashMap, HashSet};
5use std::future::Future;
6use std::ops::Range;
7use std::sync::LazyLock;
8use std::time::Duration;
9
10use dynamo_tokens::{SequenceHash, Token, compute_hash_v2, compute_next_sequence_hash};
11use rustc_hash::FxHashMap;
12use serde::{Deserialize, Serialize};
13use xxhash_rust::xxh3;
14
15const fn default_track_prefill_tokens() -> bool {
16 true
17}
18
19pub const KV_EVENT_SUBJECT: &str = "kv-events";
21
22pub const XXH3_SEED: u64 = 1337;
24
25pub fn compute_block_hash(data: &[u8]) -> LocalBlockHash {
27 LocalBlockHash(compute_hash_v2(data, XXH3_SEED))
28}
29
30#[derive(Debug, Clone, Copy, Default)]
31pub struct BlockHashOptions<'a> {
32 pub block_mm_infos: Option<&'a [Option<BlockExtraInfo>]>,
33 pub lora_name: Option<&'a str>,
34 pub cache_namespace: Option<&'a str>,
35 pub is_eagle: Option<bool>,
36}
37
38fn block_hash_seed(options: BlockHashOptions<'_>) -> u64 {
39 dynamo_kv_hashing::compute_salt_hash(options.cache_namespace, options.lora_name)
40 .expect("string salt derivation is infallible")
41}
42
43#[inline]
44fn hash_block_no_mm(chunk: &[u32], seed: u64, scratch_bytes: &mut Vec<u8>) -> LocalBlockHash {
45 #[cfg(target_endian = "little")]
46 {
47 let _ = scratch_bytes;
48 let chunk_bytes = unsafe {
51 std::slice::from_raw_parts(chunk.as_ptr().cast::<u8>(), std::mem::size_of_val(chunk))
52 };
53 LocalBlockHash(xxh3::xxh3_64_with_seed(chunk_bytes, seed))
54 }
55
56 #[cfg(not(target_endian = "little"))]
57 {
58 scratch_bytes.clear();
59 for &token in chunk {
60 scratch_bytes.extend_from_slice(&token.to_le_bytes());
61 }
62 LocalBlockHash(xxh3::xxh3_64_with_seed(scratch_bytes, seed))
63 }
64}
65
66pub const MM_PAD_SHIFT_VALUE: u64 = 1_000_000;
70pub const MM_PAD_HASH_MASK: u64 = (1 << 30) - 1;
71
72pub fn pad_value_for_mm_hash(mm_hash: u64) -> u32 {
76 (MM_PAD_SHIFT_VALUE + (mm_hash & MM_PAD_HASH_MASK)) as u32
77}
78
79pub fn compute_block_hash_for_seq(
90 tokens: &[u32],
91 kv_block_size: u32,
92 options: BlockHashOptions<'_>,
93) -> Vec<LocalBlockHash> {
94 compute_block_hash_for_seq_with_seed(tokens, kv_block_size, options, block_hash_seed(options))
95}
96
97pub(crate) fn complete_block_count(
99 token_count: usize,
100 kv_block_size: u32,
101 is_eagle: bool,
102) -> usize {
103 let stride = kv_block_size as usize;
104 if stride == 0 {
105 return 0;
106 }
107 if is_eagle {
108 token_count.saturating_sub(1) / stride
109 } else {
110 token_count / stride
111 }
112}
113
114pub(crate) fn compute_block_hash_for_seq_with_seed(
117 tokens: &[u32],
118 kv_block_size: u32,
119 options: BlockHashOptions<'_>,
120 seed: u64,
121) -> Vec<LocalBlockHash> {
122 let estimated_blocks = complete_block_count(
123 tokens.len(),
124 kv_block_size,
125 options.is_eagle.unwrap_or(false),
126 );
127 let mut hashes = Vec::with_capacity(estimated_blocks);
128 for_each_block_hash_for_seq_with_seed(tokens, kv_block_size, options, seed, |hash| {
129 hashes.push(hash);
130 });
131 hashes
132}
133
134fn for_each_block_hash_for_seq_with_seed(
135 tokens: &[u32],
136 kv_block_size: u32,
137 options: BlockHashOptions<'_>,
138 seed: u64,
139 mut visit: impl FnMut(LocalBlockHash),
140) {
141 if kv_block_size == 0 {
142 return;
143 }
144
145 let is_eagle_flag = options.is_eagle.unwrap_or(false);
146 let stride = kv_block_size as usize;
147 let window_size = if is_eagle_flag { stride + 1 } else { stride };
148 let mut bytes = Vec::with_capacity(window_size * std::mem::size_of::<u32>());
149 let mut mm_hashes = Vec::new();
150 let mut block_idx = 0;
151 let mut start = 0;
152
153 while start + window_size <= tokens.len() {
154 let chunk = &tokens[start..start + window_size];
155 if let Some(mm_infos) = options.block_mm_infos
156 && let Some(Some(block_mm_info)) = mm_infos.get(block_idx)
157 {
158 bytes.clear();
159 for &token in chunk {
160 bytes.extend_from_slice(&token.to_le_bytes());
161 }
162
163 mm_hashes.clear();
164 mm_hashes.extend(block_mm_info.mm_objects.iter().map(|obj| obj.mm_hash));
165 mm_hashes.sort_unstable();
166
167 for &mm_hash in &mm_hashes {
168 bytes.extend_from_slice(&mm_hash.to_le_bytes());
169 }
170
171 visit(LocalBlockHash(xxh3::xxh3_64_with_seed(&bytes, seed)));
172 } else {
173 visit(hash_block_no_mm(chunk, seed, &mut bytes));
174 }
175
176 start += stride;
177 block_idx += 1;
178 }
179}
180
181#[inline]
186pub fn compute_next_seq_hash(
187 parent_seq_hash: SequenceHash,
188 current_block_hash: LocalBlockHash,
189) -> SequenceHash {
190 compute_next_sequence_hash(parent_seq_hash, current_block_hash.0)
191}
192
193pub fn compute_seq_hash_for_block(block_hashes: &[LocalBlockHash]) -> Vec<SequenceHash> {
198 compute_seq_hash_for_block_with(block_hashes, compute_next_seq_hash)
199}
200
201pub(crate) fn compute_seq_hash_for_tokens_with_seeds(
204 tokens: &[u32],
205 kv_block_size: u32,
206 options: BlockHashOptions<'_>,
207 block_seed: u64,
208 chain_seed: u64,
209) -> Vec<SequenceHash> {
210 let estimated_blocks = complete_block_count(
211 tokens.len(),
212 kv_block_size,
213 options.is_eagle.unwrap_or(false),
214 );
215 let mut sequence_hashes = Vec::with_capacity(estimated_blocks);
216 for_each_block_hash_for_seq_with_seed(
217 tokens,
218 kv_block_size,
219 options,
220 block_seed,
221 |block_hash| {
222 let sequence_hash = sequence_hashes
223 .last()
224 .copied()
225 .map_or(block_hash.0, |parent| {
226 compute_next_seq_hash_with_seed(parent, block_hash, chain_seed)
227 });
228 sequence_hashes.push(sequence_hash);
229 },
230 );
231 sequence_hashes
232}
233
234#[inline]
235fn compute_next_seq_hash_with_seed(
236 parent: SequenceHash,
237 block: LocalBlockHash,
238 seed: u64,
239) -> SequenceHash {
240 let mut bytes = [0_u8; 16];
241 bytes[..8].copy_from_slice(&parent.to_le_bytes());
242 bytes[8..].copy_from_slice(&block.0.to_le_bytes());
243 xxh3::xxh3_64_with_seed(&bytes, seed)
244}
245
246fn compute_seq_hash_for_block_with(
247 block_hashes: &[LocalBlockHash],
248 next_hash: impl Fn(SequenceHash, LocalBlockHash) -> SequenceHash,
249) -> Vec<SequenceHash> {
250 if block_hashes.is_empty() {
251 return Vec::new();
252 }
253
254 let mut sequence_hashes = Vec::with_capacity(block_hashes.len());
255 sequence_hashes.push(block_hashes[0].0);
256
257 for i in 1..block_hashes.len() {
258 let parent_seq_hash = sequence_hashes[i - 1];
259 sequence_hashes.push(next_hash(parent_seq_hash, block_hashes[i]));
260 }
261
262 sequence_hashes
263}
264
265pub trait WorkerConfigLike {
269 fn data_parallel_start_rank(&self) -> u32;
270 fn data_parallel_size(&self) -> u32;
271 fn max_num_batched_tokens(&self) -> Option<u64>;
272 fn total_kv_blocks(&self) -> Option<u64>;
273
274 fn native_offloading_capacity_tokens(&self) -> Option<u64> {
276 None
277 }
278
279 fn taints(&self) -> &HashSet<String> {
280 &EMPTY_WORKER_TAINTS
281 }
282
283 fn stable_routing_id(&self) -> Option<&str> {
291 None
292 }
293
294 fn topology_domains(&self) -> Option<&HashMap<String, String>> {
299 None
300 }
301
302 fn kv_transfer_domain(&self) -> Option<&str> {
306 None
307 }
308
309 fn kv_transfer_enforcement(&self) -> Option<KvTransferEnforcement> {
311 None
312 }
313
314 fn kv_transfer_preferred_weight(&self) -> Option<f32> {
316 None
317 }
318}
319
320#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
321#[serde(rename_all = "snake_case")]
322pub enum KvTransferEnforcement {
323 Required,
325 Preferred,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
334pub struct RoutingConstraints {
335 #[serde(default, skip_serializing_if = "HashSet::is_empty")]
336 pub required_taints: HashSet<String>,
337 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
338 pub preferred_taints: HashMap<String, f32>,
339}
340
341impl RoutingConstraints {
342 pub fn is_empty(&self) -> bool {
343 self.required_taints.is_empty() && self.preferred_taints.is_empty()
344 }
345
346 pub fn has_hard_constraints(&self) -> bool {
347 !self.required_taints.is_empty()
348 }
349
350 pub fn is_compatible_with_worker_taints(&self, worker_taints: &HashSet<String>) -> bool {
351 if self.required_taints.is_empty() {
352 return true;
353 }
354
355 self.required_taints
356 .iter()
357 .all(|taint| worker_taints.contains(taint))
358 }
359
360 pub fn preferred_taint_matches(&self, worker_taints: &HashSet<String>) -> usize {
361 if self.preferred_taints.is_empty() {
362 return 0;
363 }
364
365 self.preferred_taints
366 .keys()
367 .filter(|taint| worker_taints.contains(*taint))
368 .count()
369 }
370
371 pub fn preferred_taint_multiplier(&self, worker_taints: &HashSet<String>) -> Option<f64> {
372 if self.preferred_taints.is_empty() {
373 return None;
374 }
375
376 let bias = self
381 .preferred_taints
382 .iter()
383 .filter(|(taint, _)| worker_taints.contains(*taint))
384 .map(|(_, weight)| f64::from(*weight))
385 .sum::<f64>()
386 .tanh();
387
388 Some((-bias).exp())
389 }
390}
391
392static EMPTY_WORKER_TAINTS: LazyLock<HashSet<String>> = LazyLock::new(HashSet::new);
393
394pub trait RouterEventSink: Send + Sync {
396 fn publish_event(&self, event: &RouterEvent)
397 -> impl Future<Output = anyhow::Result<()>> + Send;
398}
399
400pub type WorkerId = u64;
402
403pub type DpRank = u32;
405
406#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
410pub struct WorkerWithDpRank {
411 pub worker_id: WorkerId,
412 pub dp_rank: DpRank,
413}
414
415impl WorkerWithDpRank {
416 pub fn new(worker_id: WorkerId, dp_rank: DpRank) -> Self {
417 Self { worker_id, dp_rank }
418 }
419
420 pub fn from_worker_id(worker_id: WorkerId) -> Self {
421 Self {
422 worker_id,
423 dp_rank: 0,
424 }
425 }
426}
427
428#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
429#[serde(rename_all = "snake_case")]
430pub enum StorageTier {
431 #[default]
432 Device,
433 HostPinned,
434 Disk,
435 External,
436}
437
438impl StorageTier {
439 pub fn from_kv_medium(medium: &str) -> Option<Self> {
440 match medium {
441 "GPU" | "DEVICE" => Some(Self::Device),
442 "CPU" | "CPU_PINNED" | "CPU_TIER1" => Some(Self::HostPinned),
443 "CPU_TIER2" | "DISK" | "NVME" => Some(Self::Disk),
444 "EXTERNAL" | "NETWORK" | "REMOTE" | "SHARED" => Some(Self::External),
445 _ => None,
446 }
447 }
448
449 pub fn from_kv_medium_or_default(medium: Option<&str>) -> Self {
450 medium
451 .and_then(Self::from_kv_medium)
452 .unwrap_or(Self::Device)
453 }
454
455 pub fn to_kv_medium(self) -> Option<&'static str> {
458 match self {
459 Self::Device => None,
460 Self::HostPinned => Some("CPU_PINNED"),
461 Self::Disk => Some("DISK"),
462 Self::External => Some("EXTERNAL"),
463 }
464 }
465
466 pub fn is_gpu(self) -> bool {
467 matches!(self, Self::Device)
468 }
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
472pub enum PlacementOwner {
473 LocalWorker(WorkerWithDpRank),
474 Shared,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
478pub struct Placement {
479 pub owner: PlacementOwner,
480 pub tier: StorageTier,
481}
482
483impl Placement {
484 pub fn local_worker(worker_id: WorkerId, dp_rank: DpRank, tier: StorageTier) -> Self {
485 Self {
486 owner: PlacementOwner::LocalWorker(WorkerWithDpRank::new(worker_id, dp_rank)),
487 tier,
488 }
489 }
490
491 pub fn local_gpu(worker_id: WorkerId, dp_rank: DpRank) -> Self {
492 Self::local_worker(worker_id, dp_rank, StorageTier::Device)
493 }
494
495 pub fn is_local_gpu(&self) -> bool {
496 matches!(self.owner, PlacementOwner::LocalWorker(_)) && self.tier.is_gpu()
497 }
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
501pub struct PlacementEvent {
502 pub placement: Placement,
503 pub event: KvCacheEvent,
504}
505
506impl PlacementEvent {
507 pub fn new(placement: Placement, event: KvCacheEvent) -> Self {
508 Self { placement, event }
509 }
510
511 pub fn local_gpu(worker_id: WorkerId, event: KvCacheEvent) -> Self {
512 Self::new(Placement::local_gpu(worker_id, event.dp_rank), event)
513 }
514
515 pub fn into_router_event(self) -> Option<RouterEvent> {
516 let PlacementOwner::LocalWorker(worker) = self.placement.owner else {
517 return None;
518 };
519 Some(RouterEvent::with_storage_tier(
520 worker.worker_id,
521 self.event,
522 self.placement.tier,
523 ))
524 }
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
528#[serde(tag = "method", rename_all = "snake_case")]
529pub enum RouterRequest {
530 #[serde(rename = "new")]
531 New {
532 tokens: Vec<Token>,
533 #[serde(default, skip_serializing_if = "Option::is_none")]
534 block_mm_infos: Option<Vec<Option<BlockExtraInfo>>>,
535 #[serde(default, skip_serializing_if = "RoutingConstraints::is_empty")]
536 routing_constraints: RoutingConstraints,
537 #[serde(default)]
538 priority_jump: f64,
539 #[serde(default, skip_serializing_if = "is_zero")]
540 strict_priority: u32,
541 #[serde(default, skip_serializing_if = "Option::is_none")]
542 lora_name: Option<String>,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 cache_namespace: Option<String>,
545 },
546 PotentialLoads {
547 tokens: Vec<Token>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
549 block_mm_infos: Option<Vec<Option<BlockExtraInfo>>>,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
551 lora_name: Option<String>,
552 #[serde(default, skip_serializing_if = "Option::is_none")]
553 cache_namespace: Option<String>,
554 },
555 MarkPrefill {
556 #[serde(default, skip_serializing_if = "Option::is_none")]
559 request_id: Option<String>,
560 },
561 MarkFree {
562 #[serde(default, skip_serializing_if = "Option::is_none")]
565 request_id: Option<String>,
566 },
567}
568
569impl Default for RouterRequest {
570 fn default() -> Self {
571 RouterRequest::New {
572 tokens: vec![],
573 block_mm_infos: None,
574 routing_constraints: RoutingConstraints::default(),
575 priority_jump: 0.0,
576 strict_priority: 0,
577 lora_name: None,
578 cache_namespace: None,
579 }
580 }
581}
582
583fn is_zero(value: &u32) -> bool {
584 *value == 0
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct PotentialLoad {
589 pub worker_id: WorkerId,
590 pub dp_rank: DpRank,
591 pub potential_prefill_tokens: usize,
592 pub potential_decode_blocks: usize,
593 #[serde(default)]
594 pub active_requests: usize,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize)]
598#[serde(tag = "method", rename_all = "snake_case")]
599pub enum RouterResponse {
600 New {
601 worker_id: WorkerId,
602 #[serde(default)]
603 dp_rank: DpRank,
604 overlap_blocks: u32,
605 },
606 QueueRejected {
607 rejection: crate::scheduling::QueueRejection,
608 },
609 PrefillMarked {
610 success: bool,
611 },
612 FreeMarked {
613 success: bool,
614 },
615 PotentialLoads {
616 loads: Vec<PotentialLoad>,
618 #[serde(default)]
620 pending_count: usize,
621 #[serde(default)]
622 pending_isl_tokens: usize,
623 },
624}
625
626#[derive(Debug)]
627pub struct WorkerSelectionResult {
628 pub worker: WorkerWithDpRank,
630
631 pub required_blocks: u64,
633
634 pub effective_overlap_blocks: f64,
637
638 pub cached_tokens: usize,
640
641 pub potential_decode_blocks: usize,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
651pub struct ActiveLoad {
652 pub worker_id: WorkerId,
653 #[serde(default)]
654 pub dp_rank: DpRank,
655 pub active_decode_blocks: Option<u64>,
657 pub active_prefill_tokens: Option<u64>,
659 #[serde(default)]
664 pub kv_used_blocks: Option<u64>,
665}
666
667#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
670pub struct LocalBlockHash(pub u64);
671
672#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
677pub struct ExternalSequenceBlockHash(pub u64);
678
679impl From<u64> for ExternalSequenceBlockHash {
681 fn from(value: u64) -> Self {
682 Self(value)
683 }
684}
685
686impl From<i64> for ExternalSequenceBlockHash {
687 fn from(value: i64) -> Self {
690 Self(value as u64)
691 }
692}
693
694#[derive(Serialize, Deserialize, Debug, Clone)]
695pub struct PrefillEvent {
696 pub request_id: String,
697 pub worker_id: WorkerId,
698 pub data: PrefillEventData,
699 pub router_id: u64,
700}
701
702#[derive(Serialize, Deserialize, Debug, Clone)]
707pub enum PrefillEventData {
708 NewPrefill(usize),
709 UpdatePrefill(usize),
710 CompletePrefill,
711}
712
713#[derive(Serialize, Deserialize, Debug, Clone)]
714pub struct ActiveSequenceEvent {
715 pub request_id: String,
716 pub worker: WorkerWithDpRank,
717 pub data: ActiveSequenceEventData,
718 pub router_id: u64,
719 #[serde(default)]
720 pub lora_name: Option<String>,
721}
722
723#[derive(Serialize, Deserialize, Debug, Clone)]
725pub struct ActiveSequenceEventBatch {
726 pub events: Vec<ActiveSequenceEvent>,
727}
728
729pub const MAX_REPLICA_BATCH_EVENTS: usize = 256;
731pub const MAX_REPLICA_BATCH_DURATION: Duration = Duration::from_millis(1);
732
733#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
734pub struct PrefillLoadHint {
735 pub initial_effective_prefill_tokens: usize,
736 pub expected_prefill_duration: Option<Duration>,
737}
738
739#[derive(Serialize, Deserialize, Debug, Clone)]
740pub enum ActiveSequenceEventData {
741 AddRequest {
742 token_sequence: Option<Vec<SequenceHash>>,
743 #[serde(default = "default_track_prefill_tokens")]
744 track_prefill_tokens: bool,
745 expected_output_tokens: Option<u32>,
746 #[serde(default)]
747 prefill_load_hint: Option<PrefillLoadHint>,
748 },
749 Free,
752 MarkPrefillCompleted,
753}
754
755#[derive(Serialize, Deserialize, Debug, Clone)]
756pub struct ActiveBlockEvent {
757 pub request_id: String,
758 pub data: ActiveBlockEventData,
759}
760
761#[derive(Serialize, Deserialize, Debug, Clone)]
762pub enum ActiveBlockEventData {
763 NewBlock(Vec<SequenceHash>),
764 FreeBlock,
765}
766
767#[derive(Serialize, Deserialize, Debug, Clone)]
769pub struct KvCacheEvents {
770 pub events: Vec<KvCacheEvent>,
772 pub shutdown: bool,
774}
775
776#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
778pub struct KvCacheEvent {
779 pub event_id: u64,
781 pub data: KvCacheEventData,
783 #[serde(default)]
785 pub dp_rank: DpRank,
786}
787
788#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
792#[serde(rename_all = "snake_case")]
793pub enum KvCacheEventData {
794 Stored(KvCacheStoreData),
795 Removed(KvCacheRemoveData),
796 Cleared,
801}
802
803#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
805pub struct KvCacheStoreData {
806 pub parent_hash: Option<ExternalSequenceBlockHash>,
808 #[serde(default)]
810 pub start_position: Option<u32>,
811 pub blocks: Vec<KvCacheStoredBlockData>,
813}
814
815#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
818pub struct BlockMmObjectInfo {
819 pub mm_hash: u64,
821 pub offsets: Vec<(usize, usize)>,
824}
825
826#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
828pub struct BlockExtraInfo {
829 pub mm_objects: Vec<BlockMmObjectInfo>,
831}
832
833#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
836pub struct RequestMmObjectInfo {
837 pub mm_hash: u64,
839 pub offsets: Vec<(usize, usize)>,
842}
843
844#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
846pub struct RequestExtraInfo {
847 pub mm_objects: Vec<RequestMmObjectInfo>,
849}
850
851impl RequestExtraInfo {
852 pub fn to_block_level(
865 &self,
866 block_size: usize,
867 total_tokens: usize,
868 ) -> Vec<Option<BlockExtraInfo>> {
869 let num_blocks = total_tokens.div_ceil(block_size);
870 let mut block_infos: Vec<Option<BlockExtraInfo>> = vec![None; num_blocks];
871
872 for req_mm_obj in &self.mm_objects {
873 for (req_start, req_end) in &req_mm_obj.offsets {
874 let start_block = req_start / block_size;
876 let end_block = (req_end.saturating_sub(1)) / block_size;
877
878 let upper_bound = end_block.min(num_blocks - 1) + 1;
879 for (block_idx, block_info_opt) in block_infos
880 .iter_mut()
881 .enumerate()
882 .take(upper_bound)
883 .skip(start_block)
884 {
885 let block_start_global = block_idx * block_size;
886 let block_end_global = ((block_idx + 1) * block_size).min(total_tokens);
887
888 let local_start = (*req_start).max(block_start_global) - block_start_global;
890 let local_end = (*req_end).min(block_end_global) - block_start_global;
891
892 if local_start < local_end {
893 let block_info = block_info_opt
894 .get_or_insert_with(|| BlockExtraInfo { mm_objects: vec![] });
895
896 if let Some(existing) = block_info
898 .mm_objects
899 .iter_mut()
900 .find(|obj| obj.mm_hash == req_mm_obj.mm_hash)
901 {
902 existing.offsets.push((local_start, local_end));
904 } else {
905 block_info.mm_objects.push(BlockMmObjectInfo {
907 mm_hash: req_mm_obj.mm_hash,
908 offsets: vec![(local_start, local_end)],
909 });
910 }
911 }
912 }
913 }
914 }
915
916 block_infos
917 }
918}
919
920#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
922pub struct KvCacheStoredBlockData {
923 pub block_hash: ExternalSequenceBlockHash,
925 pub tokens_hash: LocalBlockHash,
927 #[serde(default)]
931 pub mm_extra_info: Option<BlockExtraInfo>,
932}
933
934#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
936pub struct KvCacheRemoveData {
937 pub block_hashes: Vec<ExternalSequenceBlockHash>,
939}
940
941impl Serialize for LocalBlockHash {
942 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
943 where
944 S: serde::Serializer,
945 {
946 serializer.serialize_u64(self.0)
947 }
948}
949
950impl<'de> Deserialize<'de> for LocalBlockHash {
951 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
952 where
953 D: serde::Deserializer<'de>,
954 {
955 let value = u64::deserialize(deserializer)?;
956 Ok(LocalBlockHash(value))
957 }
958}
959
960impl Serialize for ExternalSequenceBlockHash {
961 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
962 where
963 S: serde::Serializer,
964 {
965 serializer.serialize_u64(self.0)
966 }
967}
968
969impl<'de> Deserialize<'de> for ExternalSequenceBlockHash {
970 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
971 where
972 D: serde::Deserializer<'de>,
973 {
974 let value = u64::deserialize(deserializer)?;
975 Ok(ExternalSequenceBlockHash(value))
976 }
977}
978
979#[non_exhaustive]
988#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
989pub enum KvCacheEventError {
990 #[error("Failed to find parent block")]
991 ParentBlockNotFound,
992
993 #[error("Failed to find block")]
994 BlockNotFound,
995
996 #[error("Invalid block sequence")]
997 InvalidBlockSequence,
998
999 #[error("Indexer capacity exhausted")]
1001 CapacityExhausted,
1002
1003 #[error("Indexer allocation failed")]
1005 AllocationFailed,
1006
1007 #[error("Indexer ownership degree overflow")]
1009 OwnershipDegreeOverflow,
1010
1011 #[error("Indexer invariant violated")]
1012 IndexerInvariantViolation,
1013}
1014
1015#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1017pub struct RouterEvent {
1018 pub worker_id: WorkerId,
1020 #[serde(default)]
1022 pub storage_tier: StorageTier,
1023 pub event: KvCacheEvent,
1025}
1026
1027impl RouterEvent {
1028 pub fn new(worker_id: WorkerId, event: KvCacheEvent) -> Self {
1039 Self::with_storage_tier(worker_id, event, StorageTier::Device)
1040 }
1041
1042 pub fn with_storage_tier(
1043 worker_id: WorkerId,
1044 event: KvCacheEvent,
1045 storage_tier: StorageTier,
1046 ) -> Self {
1047 Self {
1048 worker_id,
1049 storage_tier,
1050 event,
1051 }
1052 }
1053}
1054
1055#[derive(Debug, Clone, Default)]
1062pub struct SharedCacheHits {
1063 pub ranges: Vec<Range<u32>>,
1066 pub total_hits: u32,
1068}
1069
1070impl SharedCacheHits {
1071 pub fn from_ranges(ranges: Vec<Range<u32>>) -> Self {
1073 let total_hits = ranges.iter().map(|r| r.end - r.start).sum();
1074 Self { ranges, total_hits }
1075 }
1076
1077 pub fn from_hits(hits: &[bool]) -> Self {
1080 let mut ranges = Vec::new();
1081 let mut i = 0;
1082 while i < hits.len() {
1083 if hits[i] {
1084 let start = i as u32;
1085 while i < hits.len() && hits[i] {
1086 i += 1;
1087 }
1088 ranges.push(start..i as u32);
1089 } else {
1090 i += 1;
1091 }
1092 }
1093 Self::from_ranges(ranges)
1094 }
1095
1096 pub fn hits_beyond(&self, from_position: u32) -> u32 {
1099 self.ranges
1100 .iter()
1101 .map(|r| {
1102 if r.end <= from_position {
1103 0
1104 } else if r.start >= from_position {
1105 r.end - r.start
1106 } else {
1107 r.end - from_position
1108 }
1109 })
1110 .sum()
1111 }
1112}
1113
1114#[derive(Debug, Clone, Serialize, Deserialize)]
1116pub struct OverlapScores {
1117 pub scores: FxHashMap<WorkerWithDpRank, u32>,
1119 pub frequencies: Vec<usize>,
1121}
1122
1123impl Default for OverlapScores {
1124 fn default() -> Self {
1125 Self::new()
1126 }
1127}
1128
1129impl OverlapScores {
1130 pub fn new() -> Self {
1136 Self {
1137 scores: FxHashMap::default(),
1138 frequencies: Vec::new(),
1139 }
1140 }
1141
1142 pub fn update_scores<'a, I>(&mut self, workers: I)
1148 where
1149 I: IntoIterator<Item = &'a WorkerWithDpRank>,
1150 {
1151 for worker in workers {
1152 let score = self.scores.entry(*worker).or_insert(0);
1153 *score += 1;
1154 }
1155 }
1156}
1157
1158#[derive(Debug, Clone)]
1169pub struct TokensWithHashes {
1170 tokens: Vec<u32>,
1171 block_size: u32,
1172 block_mm_infos: Option<Vec<Option<BlockExtraInfo>>>,
1173 lora_name: Option<String>,
1174 cache_namespace: Option<String>,
1175 block_hashes: Option<Vec<LocalBlockHash>>,
1176 seq_hashes: Option<Vec<SequenceHash>>,
1177 is_eagle: Option<bool>,
1178}
1179
1180impl TokensWithHashes {
1181 pub fn new(tokens: Vec<u32>, block_size: u32) -> Self {
1183 Self {
1184 tokens,
1185 block_size,
1186 block_mm_infos: None,
1187 lora_name: None,
1188 cache_namespace: None,
1189 block_hashes: None,
1190 seq_hashes: None,
1191 is_eagle: None,
1192 }
1193 }
1194
1195 pub fn with_mm_infos(mut self, infos: Vec<Option<BlockExtraInfo>>) -> Self {
1197 self.block_mm_infos = Some(infos);
1198 self.invalidate_hashes();
1199 self
1200 }
1201
1202 pub fn with_lora_name(mut self, name: String) -> Self {
1204 self.lora_name = Some(name);
1205 self.invalidate_hashes();
1206 self
1207 }
1208
1209 pub fn with_cache_namespace(mut self, namespace: String) -> Self {
1211 self.cache_namespace = Some(namespace);
1212 self.invalidate_hashes();
1213 self
1214 }
1215
1216 pub fn with_is_eagle(mut self, is_eagle: bool) -> Self {
1218 self.set_is_eagle(is_eagle);
1219 self
1220 }
1221
1222 pub fn set_is_eagle(&mut self, is_eagle: bool) {
1224 let is_eagle = Some(is_eagle);
1225 if self.is_eagle == is_eagle {
1226 return;
1227 }
1228
1229 self.is_eagle = is_eagle;
1230 self.invalidate_hashes();
1231 }
1232
1233 fn invalidate_hashes(&mut self) {
1234 self.block_hashes = None;
1235 self.seq_hashes = None;
1236 }
1237
1238 pub fn tokens(&self) -> &[u32] {
1240 &self.tokens
1241 }
1242
1243 pub fn len(&self) -> usize {
1245 self.tokens.len()
1246 }
1247
1248 pub fn is_empty(&self) -> bool {
1250 self.tokens.is_empty()
1251 }
1252
1253 pub fn block_size(&self) -> u32 {
1255 self.block_size
1256 }
1257
1258 pub fn block_mm_infos(&self) -> Option<&[Option<BlockExtraInfo>]> {
1260 self.block_mm_infos.as_deref()
1261 }
1262
1263 pub fn get_or_compute_block_hashes(&mut self) -> &[LocalBlockHash] {
1265 if self.block_hashes.is_none() {
1266 self.block_hashes = Some(compute_block_hash_for_seq(
1267 &self.tokens,
1268 self.block_size,
1269 BlockHashOptions {
1270 block_mm_infos: self.block_mm_infos.as_deref(),
1271 lora_name: self.lora_name.as_deref(),
1272 cache_namespace: self.cache_namespace.as_deref(),
1273 is_eagle: self.is_eagle,
1274 },
1275 ));
1276 }
1277 self.block_hashes.as_ref().unwrap()
1278 }
1279
1280 pub fn get_or_compute_seq_hashes(&mut self) -> &[SequenceHash] {
1284 if self.seq_hashes.is_none() {
1285 let block_hashes = self.get_or_compute_block_hashes();
1287 self.seq_hashes = Some(compute_seq_hash_for_block(block_hashes));
1288 }
1289 self.seq_hashes.as_ref().unwrap()
1290 }
1291
1292 pub fn block_hashes(&self) -> Option<&[LocalBlockHash]> {
1294 self.block_hashes.as_deref()
1295 }
1296
1297 pub fn seq_hashes(&self) -> Option<&[SequenceHash]> {
1299 self.seq_hashes.as_deref()
1300 }
1301}
1302
1303#[cfg(test)]
1307mod tests {
1308 use super::*;
1309 use rstest::rstest;
1310 use serde_json;
1311
1312 #[test]
1317 fn pad_value_matches_sglang_protocol() {
1318 assert_eq!(MM_PAD_SHIFT_VALUE, 1_000_000);
1319 assert_eq!(MM_PAD_HASH_MASK, (1u64 << 30) - 1);
1320 assert_eq!(pad_value_for_mm_hash(0), MM_PAD_SHIFT_VALUE as u32);
1321 let fits = (1u64 << 30) - 1;
1322 assert_eq!(
1323 pad_value_for_mm_hash(fits),
1324 (MM_PAD_SHIFT_VALUE + fits) as u32
1325 );
1326 let overflow = (1u64 << 30) | 0xCAFE;
1327 assert_eq!(
1328 pad_value_for_mm_hash(overflow),
1329 (MM_PAD_SHIFT_VALUE + 0xCAFE) as u32,
1330 "high bits above the 30-bit mask must be discarded"
1331 );
1332 }
1333
1334 #[test]
1335 fn test_router_event_new() {
1336 let worker_id = 0;
1337 let kv_cache_event = KvCacheEvent {
1338 event_id: 1,
1339 data: KvCacheEventData::Stored(KvCacheStoreData {
1340 parent_hash: None,
1341 start_position: None,
1342 blocks: vec![KvCacheStoredBlockData {
1343 block_hash: ExternalSequenceBlockHash(0),
1344 mm_extra_info: None,
1345 tokens_hash: LocalBlockHash(13226331709069118873),
1346 }],
1347 }),
1348 dp_rank: 0,
1349 };
1350 let router_event = RouterEvent::new(worker_id, kv_cache_event);
1351
1352 assert_eq!(router_event.worker_id, worker_id);
1353 assert_eq!(router_event.event.event_id, 1);
1354 if let KvCacheEventData::Stored(store_op) = &router_event.event.data {
1355 assert_eq!(store_op.blocks.len(), 1);
1356 assert_eq!(
1357 store_op.blocks[0].tokens_hash,
1358 compute_block_hash(b"test data")
1359 );
1360 assert_eq!(store_op.blocks[0].block_hash, ExternalSequenceBlockHash(0));
1361 } else {
1362 panic!("Expected KvCacheEventData::Stored");
1363 }
1364 }
1365
1366 #[rstest]
1367 #[case(11)]
1368 #[case(32)]
1369 #[case(64)]
1370 fn test_compute_block_hash_for_seq(#[case] kv_block_size: u32) {
1371 let sequence = (0..kv_block_size).collect::<Vec<u32>>();
1372 let hashes =
1373 compute_block_hash_for_seq(&sequence, kv_block_size, BlockHashOptions::default());
1374 assert_eq!(hashes.len(), 1);
1375
1376 let sequence = (0..(kv_block_size + 1)).collect::<Vec<u32>>();
1377 let hashes =
1378 compute_block_hash_for_seq(&sequence, kv_block_size, BlockHashOptions::default());
1379 assert_eq!(hashes.len(), 1);
1380
1381 let sequence = (0..(2 * kv_block_size + 1)).collect::<Vec<u32>>();
1382 let hashes =
1383 compute_block_hash_for_seq(&sequence, kv_block_size, BlockHashOptions::default());
1384 assert_eq!(hashes.len(), 2);
1385 }
1386
1387 #[test]
1388 fn test_compute_next_seq_hash_matches_rolling_hash() {
1389 let block_hashes = [LocalBlockHash(11), LocalBlockHash(22), LocalBlockHash(33)];
1390 let seq_hashes = compute_seq_hash_for_block(&block_hashes);
1391
1392 assert_eq!(
1393 seq_hashes[1],
1394 compute_next_seq_hash(seq_hashes[0], block_hashes[1])
1395 );
1396 assert_eq!(
1397 seq_hashes[2],
1398 compute_next_seq_hash(seq_hashes[1], block_hashes[2])
1399 );
1400 }
1401
1402 #[test]
1403 fn test_lora_name_produces_different_hash() {
1404 let tokens: Vec<u32> = (0..4).collect();
1405 let base = compute_block_hash_for_seq(&tokens, 4, BlockHashOptions::default());
1406 let lora_a = compute_block_hash_for_seq(
1407 &tokens,
1408 4,
1409 BlockHashOptions {
1410 lora_name: Some("adapter-a"),
1411 ..Default::default()
1412 },
1413 );
1414 let lora_b = compute_block_hash_for_seq(
1415 &tokens,
1416 4,
1417 BlockHashOptions {
1418 lora_name: Some("adapter-b"),
1419 ..Default::default()
1420 },
1421 );
1422
1423 assert_ne!(base[0], lora_a[0]);
1424 assert_ne!(base[0], lora_b[0]);
1425 assert_ne!(lora_a[0], lora_b[0]);
1426 }
1427
1428 #[test]
1429 fn test_lora_hash_matches_kv_hashing_contract() {
1430 let tokens: Vec<u32> = (0..4).collect();
1431 let lora_name = "adapter-a";
1432 let actual = compute_block_hash_for_seq(
1433 &tokens,
1434 4,
1435 BlockHashOptions {
1436 lora_name: Some(lora_name),
1437 ..Default::default()
1438 },
1439 );
1440 let token_bytes = tokens
1441 .iter()
1442 .flat_map(|token| token.to_le_bytes())
1443 .collect::<Vec<_>>();
1444 let salt_hash = dynamo_kv_hashing::compute_salt_hash(None, Some(lora_name)).unwrap();
1445 let expected = LocalBlockHash(dynamo_kv_hashing::compute_hash_v2(&token_bytes, salt_hash));
1446
1447 assert_eq!(actual, vec![expected]);
1448 }
1449
1450 #[test]
1451 fn test_lora_name_empty_string_normalized_to_none() {
1452 let tokens: Vec<u32> = (0..4).collect();
1453 let base = compute_block_hash_for_seq(&tokens, 4, BlockHashOptions::default());
1454 let empty = compute_block_hash_for_seq(
1455 &tokens,
1456 4,
1457 BlockHashOptions {
1458 lora_name: Some(""),
1459 ..Default::default()
1460 },
1461 );
1462 assert_eq!(
1463 base, empty,
1464 "empty lora_name should be treated as base model"
1465 );
1466 }
1467
1468 #[test]
1469 fn test_cache_namespace_produces_different_hash() {
1470 let tokens: Vec<u32> = (0..4).collect();
1471 let base = compute_block_hash_for_seq(&tokens, 4, BlockHashOptions::default());
1472 let namespace_a = compute_block_hash_for_seq(
1473 &tokens,
1474 4,
1475 BlockHashOptions {
1476 cache_namespace: Some("tenant-a"),
1477 ..Default::default()
1478 },
1479 );
1480 let namespace_b = compute_block_hash_for_seq(
1481 &tokens,
1482 4,
1483 BlockHashOptions {
1484 cache_namespace: Some("tenant-b"),
1485 ..Default::default()
1486 },
1487 );
1488 let lora_a = compute_block_hash_for_seq(
1489 &tokens,
1490 4,
1491 BlockHashOptions {
1492 lora_name: Some("tenant-a"),
1493 ..Default::default()
1494 },
1495 );
1496
1497 assert_ne!(base[0], namespace_a[0]);
1498 assert_ne!(base[0], namespace_b[0]);
1499 assert_ne!(namespace_a[0], namespace_b[0]);
1500 assert_ne!(
1501 namespace_a[0], lora_a[0],
1502 "namespace and lora salts must use independent seed domains"
1503 );
1504 }
1505
1506 #[test]
1507 fn test_cache_namespace_hash_matches_kv_hashing_contract() {
1508 let tokens: Vec<u32> = (0..4).collect();
1509 let cache_namespace = "tenant-a";
1510 let lora_name = "adapter-a";
1511 let actual = compute_block_hash_for_seq(
1512 &tokens,
1513 4,
1514 BlockHashOptions {
1515 lora_name: Some(lora_name),
1516 cache_namespace: Some(cache_namespace),
1517 ..Default::default()
1518 },
1519 );
1520 let token_bytes = tokens
1521 .iter()
1522 .flat_map(|token| token.to_le_bytes())
1523 .collect::<Vec<_>>();
1524 let salt_hash =
1525 dynamo_kv_hashing::compute_salt_hash(Some(cache_namespace), Some(lora_name)).unwrap();
1526 let expected = LocalBlockHash(dynamo_kv_hashing::compute_hash_v2(&token_bytes, salt_hash));
1527
1528 assert_eq!(actual, vec![expected]);
1529 }
1530
1531 #[test]
1532 fn test_cache_namespace_empty_string_normalized_to_none() {
1533 let tokens: Vec<u32> = (0..4).collect();
1534 let base = compute_block_hash_for_seq(&tokens, 4, BlockHashOptions::default());
1535 let empty = compute_block_hash_for_seq(
1536 &tokens,
1537 4,
1538 BlockHashOptions {
1539 cache_namespace: Some(""),
1540 ..Default::default()
1541 },
1542 );
1543 assert_eq!(
1544 base, empty,
1545 "empty cache_namespace should be treated as absent"
1546 );
1547 }
1548
1549 #[test]
1550 fn test_tokens_with_hashes_lora() {
1551 let tokens: Vec<u32> = (0..8).collect();
1552
1553 let mut base = TokensWithHashes::new(tokens.clone(), 4);
1554 let base_hashes = base.get_or_compute_block_hashes().to_vec();
1555
1556 let mut with_lora =
1557 TokensWithHashes::new(tokens, 4).with_lora_name("my-adapter".to_string());
1558 let lora_hashes = with_lora.get_or_compute_block_hashes().to_vec();
1559
1560 assert_eq!(base_hashes.len(), lora_hashes.len());
1561 for (b, l) in base_hashes.iter().zip(lora_hashes.iter()) {
1562 assert_ne!(b, l);
1563 }
1564 }
1565
1566 #[test]
1567 fn test_tokens_with_hashes_lora_change_recomputes_cached_hashes() {
1568 let tokens: Vec<u32> = (0..8).collect();
1569 let mut with_hashes = TokensWithHashes::new(tokens.clone(), 4);
1570 let base_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1571
1572 let mut with_hashes = with_hashes.with_lora_name("my-adapter".to_string());
1573 let actual_block_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1574 let actual_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1575 let expected_block_hashes = compute_block_hash_for_seq(
1576 &tokens,
1577 4,
1578 BlockHashOptions {
1579 lora_name: Some("my-adapter"),
1580 ..Default::default()
1581 },
1582 );
1583 let expected_sequence_hashes = compute_seq_hash_for_block(&expected_block_hashes);
1584
1585 assert_eq!(actual_block_hashes, expected_block_hashes);
1586 assert_eq!(actual_sequence_hashes, expected_sequence_hashes);
1587 assert_ne!(actual_sequence_hashes, base_sequence_hashes);
1588 }
1589
1590 #[test]
1591 fn test_tokens_with_hashes_mm_change_recomputes_cached_hashes() {
1592 let tokens: Vec<u32> = (0..4).collect();
1593 let mm_infos = vec![
1594 Some(BlockExtraInfo {
1595 mm_objects: vec![BlockMmObjectInfo {
1596 mm_hash: 42,
1597 offsets: vec![(0, 1)],
1598 }],
1599 }),
1600 None,
1601 ];
1602 let mut with_hashes = TokensWithHashes::new(tokens.clone(), 2);
1603 let text_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1604
1605 let mut with_hashes = with_hashes.with_mm_infos(mm_infos.clone());
1606 let actual_block_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1607 let actual_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1608 let expected_block_hashes = compute_block_hash_for_seq(
1609 &tokens,
1610 2,
1611 BlockHashOptions {
1612 block_mm_infos: Some(&mm_infos),
1613 ..Default::default()
1614 },
1615 );
1616 let expected_sequence_hashes = compute_seq_hash_for_block(&expected_block_hashes);
1617
1618 assert_eq!(actual_block_hashes, expected_block_hashes);
1619 assert_eq!(actual_sequence_hashes, expected_sequence_hashes);
1620 assert_ne!(actual_sequence_hashes, text_sequence_hashes);
1621 }
1622
1623 #[test]
1624 fn test_tokens_with_hashes_cache_namespace() {
1625 let tokens: Vec<u32> = (0..8).collect();
1626
1627 let mut base = TokensWithHashes::new(tokens.clone(), 4);
1628 let base_hashes = base.get_or_compute_block_hashes().to_vec();
1629
1630 let mut with_namespace =
1631 TokensWithHashes::new(tokens, 4).with_cache_namespace("tenant-a".to_string());
1632 let namespace_hashes = with_namespace.get_or_compute_block_hashes().to_vec();
1633
1634 assert_eq!(base_hashes.len(), namespace_hashes.len());
1635 for (base, namespaced) in base_hashes.iter().zip(namespace_hashes.iter()) {
1636 assert_ne!(base, namespaced);
1637 }
1638 }
1639
1640 #[test]
1641 fn test_tokens_with_hashes_cache_namespace_change_recomputes_cached_hashes() {
1642 let tokens: Vec<u32> = (0..8).collect();
1643 let mut with_hashes = TokensWithHashes::new(tokens.clone(), 4);
1644 let base_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1645
1646 let mut with_hashes = with_hashes.with_cache_namespace("tenant-a".to_string());
1647 let actual_block_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1648 let actual_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1649 let expected_block_hashes = compute_block_hash_for_seq(
1650 &tokens,
1651 4,
1652 BlockHashOptions {
1653 cache_namespace: Some("tenant-a"),
1654 ..Default::default()
1655 },
1656 );
1657 let expected_sequence_hashes = compute_seq_hash_for_block(&expected_block_hashes);
1658
1659 assert_eq!(actual_block_hashes, expected_block_hashes);
1660 assert_eq!(actual_sequence_hashes, expected_sequence_hashes);
1661 assert_ne!(actual_sequence_hashes, base_sequence_hashes);
1662 }
1663
1664 #[test]
1665 fn test_compute_block_hash_for_seq_eagle_windows() {
1666 let tokens: Vec<u32> = (0..6).collect();
1667
1668 let default_hashes = compute_block_hash_for_seq(&tokens, 2, BlockHashOptions::default());
1669 let eagle_hashes = compute_block_hash_for_seq(
1670 &tokens,
1671 2,
1672 BlockHashOptions {
1673 is_eagle: Some(true),
1674 ..Default::default()
1675 },
1676 );
1677 let expected_first = compute_block_hash_for_seq(
1678 &[0, 1, 2],
1679 2,
1680 BlockHashOptions {
1681 is_eagle: Some(true),
1682 ..Default::default()
1683 },
1684 );
1685 let expected_second = compute_block_hash_for_seq(
1686 &[2, 3, 4],
1687 2,
1688 BlockHashOptions {
1689 is_eagle: Some(true),
1690 ..Default::default()
1691 },
1692 );
1693
1694 assert_eq!(default_hashes.len(), 3);
1695 assert_eq!(eagle_hashes.len(), 2);
1696 assert_eq!(eagle_hashes, vec![expected_first[0], expected_second[0]]);
1697 assert_ne!(default_hashes[0], eagle_hashes[0]);
1698 }
1699
1700 #[test]
1701 fn test_tokens_with_hashes_set_is_eagle_invalidates_cache() {
1702 let tokens: Vec<u32> = (0..6).collect();
1703 let mut with_hashes = TokensWithHashes::new(tokens, 2);
1704
1705 let default_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1706 with_hashes.set_is_eagle(true);
1707 let eagle_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1708 let expected_first = compute_block_hash_for_seq(
1709 &[0, 1, 2],
1710 2,
1711 BlockHashOptions {
1712 is_eagle: Some(true),
1713 ..Default::default()
1714 },
1715 );
1716 let expected_second = compute_block_hash_for_seq(
1717 &[2, 3, 4],
1718 2,
1719 BlockHashOptions {
1720 is_eagle: Some(true),
1721 ..Default::default()
1722 },
1723 );
1724
1725 assert_eq!(default_hashes.len(), 3);
1726 assert_eq!(eagle_hashes.len(), 2);
1727 assert_eq!(eagle_hashes, vec![expected_first[0], expected_second[0]]);
1728 assert_ne!(default_hashes[0], eagle_hashes[0]);
1729 }
1730
1731 #[test]
1732 fn test_local_block_hash_serialization() {
1733 let hash = LocalBlockHash(12345);
1734 let serialized = serde_json::to_string(&hash).unwrap();
1735 assert_eq!(serialized, "12345");
1736
1737 let deserialized: LocalBlockHash = serde_json::from_str(&serialized).unwrap();
1738 assert_eq!(deserialized, hash);
1739 }
1740
1741 #[test]
1742 fn test_external_sequence_block_hash_serialization() {
1743 let hash = ExternalSequenceBlockHash(67890);
1744 let serialized = serde_json::to_string(&hash).unwrap();
1745 assert_eq!(serialized, "67890");
1746
1747 let deserialized: ExternalSequenceBlockHash = serde_json::from_str(&serialized).unwrap();
1748 assert_eq!(deserialized, hash);
1749 }
1750
1751 #[test]
1752 fn test_router_request_mark_free_backwards_compatible_deserialization() {
1753 let request: RouterRequest = serde_json::from_str(r#"{"method":"mark_free"}"#).unwrap();
1754
1755 assert!(matches!(
1756 request,
1757 RouterRequest::MarkFree { request_id: None }
1758 ));
1759 }
1760
1761 #[test]
1762 fn test_shared_cache_hits_from_hits() {
1763 let hits = SharedCacheHits::from_hits(&[true, true, true, true]);
1765 assert_eq!(hits.ranges, vec![0..4]);
1766 assert_eq!(hits.total_hits, 4);
1767
1768 let hits = SharedCacheHits::from_hits(&[true, false, true, true, false, true]);
1770 assert_eq!(hits.ranges, vec![0..1, 2..4, 5..6]);
1771 assert_eq!(hits.total_hits, 4);
1772
1773 let hits = SharedCacheHits::from_hits(&[false, false, false]);
1775 assert!(hits.ranges.is_empty());
1776 assert_eq!(hits.total_hits, 0);
1777
1778 let hits = SharedCacheHits::from_hits(&[]);
1780 assert!(hits.ranges.is_empty());
1781 assert_eq!(hits.total_hits, 0);
1782 }
1783
1784 #[test]
1785 fn test_shared_cache_hits_beyond() {
1786 #[allow(clippy::single_range_in_vec_init)]
1788 let hits = SharedCacheHits::from_ranges(vec![0..4]);
1789
1790 assert_eq!(hits.hits_beyond(2), 2);
1792
1793 assert_eq!(hits.hits_beyond(0), 4);
1795
1796 assert_eq!(hits.hits_beyond(4), 0);
1798
1799 assert_eq!(hits.hits_beyond(10), 0);
1801 }
1802
1803 #[test]
1804 fn test_shared_cache_hits_beyond_sparse() {
1805 let hits = SharedCacheHits::from_ranges(vec![1..3, 5..8]);
1807 assert_eq!(hits.total_hits, 5);
1808
1809 assert_eq!(hits.hits_beyond(0), 5);
1811 assert_eq!(hits.hits_beyond(2), 4);
1813 assert_eq!(hits.hits_beyond(3), 3);
1815 assert_eq!(hits.hits_beyond(6), 2);
1817 assert_eq!(hits.hits_beyond(8), 0);
1819 }
1820
1821 #[test]
1822 fn test_kv_transfer_enforcement_serde() {
1823 assert_eq!(
1824 serde_json::to_string(&KvTransferEnforcement::Required).unwrap(),
1825 r#""required""#
1826 );
1827 assert_eq!(
1828 serde_json::from_str::<KvTransferEnforcement>(r#""preferred""#).unwrap(),
1829 KvTransferEnforcement::Preferred
1830 );
1831 assert!(serde_json::from_str::<KvTransferEnforcement>(r#""fallback""#).is_err());
1832 }
1833
1834 #[test]
1835 fn test_worker_config_like_topology_domains_default() {
1836 struct MinimalConfig;
1838 impl WorkerConfigLike for MinimalConfig {
1839 fn data_parallel_start_rank(&self) -> u32 {
1840 0
1841 }
1842 fn data_parallel_size(&self) -> u32 {
1843 1
1844 }
1845 fn max_num_batched_tokens(&self) -> Option<u64> {
1846 None
1847 }
1848 fn total_kv_blocks(&self) -> Option<u64> {
1849 None
1850 }
1851 }
1852
1853 let config = MinimalConfig;
1854 assert!(
1855 config.topology_domains().is_none(),
1856 "Default topology_domains() should return None"
1857 );
1858 assert!(
1859 config.kv_transfer_domain().is_none(),
1860 "Default kv_transfer_domain() should return None"
1861 );
1862 assert!(
1863 config.kv_transfer_enforcement().is_none(),
1864 "Default kv_transfer_enforcement() should return None"
1865 );
1866 assert!(
1867 config.kv_transfer_preferred_weight().is_none(),
1868 "Default kv_transfer_preferred_weight() should return None"
1869 );
1870 assert!(config.native_offloading_capacity_tokens().is_none());
1871 }
1872
1873 #[test]
1874 fn test_router_request_mark_free_serialization_with_request_id() {
1875 let request = RouterRequest::MarkFree {
1876 request_id: Some("req-123".to_string()),
1877 };
1878
1879 let serialized = serde_json::to_string(&request).unwrap();
1880 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1881
1882 assert_eq!(
1883 serialized,
1884 r#"{"method":"mark_free","request_id":"req-123"}"#
1885 );
1886 assert!(matches!(
1887 deserialized,
1888 RouterRequest::MarkFree {
1889 request_id: Some(ref request_id)
1890 } if request_id == "req-123"
1891 ));
1892 }
1893
1894 #[test]
1895 fn test_router_request_new_serialization_with_priority_jump() {
1896 let request = RouterRequest::New {
1897 tokens: vec![1, 2, 3],
1898 block_mm_infos: None,
1899 routing_constraints: RoutingConstraints::default(),
1900 priority_jump: 5.0,
1901 strict_priority: 0,
1902 lora_name: None,
1903 cache_namespace: None,
1904 };
1905
1906 let serialized = serde_json::to_string(&request).unwrap();
1907 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1908
1909 assert_eq!(
1910 serialized,
1911 r#"{"method":"new","tokens":[1,2,3],"priority_jump":5.0}"#
1912 );
1913 assert!(matches!(
1914 deserialized,
1915 RouterRequest::New {
1916 priority_jump,
1917 ..
1918 } if priority_jump == 5.0
1919 ));
1920 }
1921
1922 #[test]
1923 fn test_router_request_new_serialization_with_lora_name() {
1924 let request = RouterRequest::New {
1925 tokens: vec![1, 2, 3],
1926 block_mm_infos: None,
1927 routing_constraints: RoutingConstraints::default(),
1928 priority_jump: 0.0,
1929 strict_priority: 0,
1930 lora_name: Some("adapter-a".to_string()),
1931 cache_namespace: None,
1932 };
1933
1934 let serialized = serde_json::to_string(&request).unwrap();
1935 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1936
1937 assert_eq!(
1938 serialized,
1939 r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0,"lora_name":"adapter-a"}"#
1940 );
1941 assert!(matches!(
1942 deserialized,
1943 RouterRequest::New {
1944 tokens,
1945 lora_name: Some(ref lora_name),
1946 ..
1947 } if tokens == vec![1, 2, 3] && lora_name == "adapter-a"
1948 ));
1949 }
1950
1951 #[test]
1952 fn test_router_request_new_defaults_lora_name() {
1953 let deserialized: RouterRequest =
1954 serde_json::from_str(r#"{"method":"new","tokens":[1,2,3]}"#).unwrap();
1955
1956 assert!(matches!(
1957 deserialized,
1958 RouterRequest::New {
1959 tokens,
1960 lora_name: None,
1961 ..
1962 } if tokens == vec![1, 2, 3]
1963 ));
1964 }
1965
1966 #[test]
1967 fn test_router_request_new_strict_priority_compatibility() {
1968 let request = RouterRequest::New {
1969 tokens: vec![1, 2, 3],
1970 block_mm_infos: None,
1971 routing_constraints: RoutingConstraints::default(),
1972 priority_jump: 0.0,
1973 strict_priority: 4,
1974 lora_name: None,
1975 cache_namespace: None,
1976 };
1977
1978 let serialized = serde_json::to_string(&request).unwrap();
1979 assert_eq!(
1980 serialized,
1981 r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0,"strict_priority":4}"#
1982 );
1983
1984 let missing: RouterRequest =
1985 serde_json::from_str(r#"{"method":"new","tokens":[1,2,3]}"#).unwrap();
1986 assert!(matches!(
1987 missing,
1988 RouterRequest::New {
1989 strict_priority: 0,
1990 ..
1991 }
1992 ));
1993
1994 let zero = RouterRequest::New {
1995 tokens: vec![1, 2, 3],
1996 block_mm_infos: None,
1997 routing_constraints: RoutingConstraints::default(),
1998 priority_jump: 0.0,
1999 strict_priority: 0,
2000 lora_name: None,
2001 cache_namespace: None,
2002 };
2003 assert_eq!(
2004 serde_json::to_string(&zero).unwrap(),
2005 r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0}"#
2006 );
2007 }
2008
2009 #[test]
2010 fn test_router_request_new_serialization_with_cache_namespace() {
2011 let request = RouterRequest::New {
2012 tokens: vec![1, 2, 3],
2013 block_mm_infos: None,
2014 routing_constraints: RoutingConstraints::default(),
2015 priority_jump: 0.0,
2016 strict_priority: 0,
2017 lora_name: None,
2018 cache_namespace: Some("tenant-a".to_string()),
2019 };
2020
2021 let serialized = serde_json::to_string(&request).unwrap();
2022 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
2023
2024 assert_eq!(
2025 serialized,
2026 r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0,"cache_namespace":"tenant-a"}"#
2027 );
2028 assert!(matches!(
2029 deserialized,
2030 RouterRequest::New {
2031 tokens,
2032 cache_namespace: Some(ref cache_namespace),
2033 ..
2034 } if tokens == vec![1, 2, 3] && cache_namespace == "tenant-a"
2035 ));
2036 }
2037
2038 #[test]
2039 fn test_router_request_potential_loads_serialization_with_lora_name() {
2040 let request = RouterRequest::PotentialLoads {
2041 tokens: vec![1, 2, 3],
2042 block_mm_infos: None,
2043 lora_name: Some("adapter-a".to_string()),
2044 cache_namespace: None,
2045 };
2046
2047 let serialized = serde_json::to_string(&request).unwrap();
2048 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
2049
2050 assert_eq!(
2051 serialized,
2052 r#"{"method":"potential_loads","tokens":[1,2,3],"lora_name":"adapter-a"}"#
2053 );
2054 assert!(matches!(
2055 deserialized,
2056 RouterRequest::PotentialLoads {
2057 tokens,
2058 block_mm_infos: None,
2059 lora_name: Some(ref lora_name),
2060 cache_namespace: None,
2061 } if tokens == vec![1, 2, 3] && lora_name == "adapter-a"
2062 ));
2063 }
2064
2065 #[test]
2066 fn test_router_request_potential_loads_serialization_with_cache_namespace() {
2067 let request = RouterRequest::PotentialLoads {
2068 tokens: vec![1, 2, 3],
2069 block_mm_infos: None,
2070 lora_name: None,
2071 cache_namespace: Some("tenant-a".to_string()),
2072 };
2073
2074 let serialized = serde_json::to_string(&request).unwrap();
2075 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
2076
2077 assert_eq!(
2078 serialized,
2079 r#"{"method":"potential_loads","tokens":[1,2,3],"cache_namespace":"tenant-a"}"#
2080 );
2081 assert!(matches!(
2082 deserialized,
2083 RouterRequest::PotentialLoads {
2084 tokens,
2085 cache_namespace: Some(ref cache_namespace),
2086 ..
2087 } if tokens == vec![1, 2, 3] && cache_namespace == "tenant-a"
2088 ));
2089 }
2090
2091 #[test]
2092 fn test_router_request_potential_loads_defaults_lora_name() {
2093 let deserialized: RouterRequest =
2094 serde_json::from_str(r#"{"method":"potential_loads","tokens":[1,2,3]}"#).unwrap();
2095
2096 assert!(matches!(
2097 deserialized,
2098 RouterRequest::PotentialLoads {
2099 tokens,
2100 block_mm_infos: None,
2101 lora_name: None,
2102 cache_namespace: None,
2103 } if tokens == vec![1, 2, 3]
2104 ));
2105 }
2106
2107 #[test]
2108 fn test_router_request_mark_prefill_serialization_with_request_id() {
2109 let request = RouterRequest::MarkPrefill {
2110 request_id: Some("req-123".to_string()),
2111 };
2112
2113 let serialized = serde_json::to_string(&request).unwrap();
2114 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
2115
2116 assert_eq!(
2117 serialized,
2118 r#"{"method":"mark_prefill","request_id":"req-123"}"#
2119 );
2120 assert!(matches!(
2121 deserialized,
2122 RouterRequest::MarkPrefill {
2123 request_id: Some(ref request_id)
2124 } if request_id == "req-123"
2125 ));
2126 }
2127
2128 #[test]
2129 fn test_potential_load_defaults_active_requests() {
2130 let load = serde_json::from_str::<PotentialLoad>(
2131 r#"{"worker_id":1,"dp_rank":0,"potential_prefill_tokens":16,"potential_decode_blocks":4}"#,
2132 )
2133 .unwrap();
2134
2135 assert_eq!(load.worker_id, 1);
2136 assert_eq!(load.dp_rank, 0);
2137 assert_eq!(load.potential_prefill_tokens, 16);
2138 assert_eq!(load.potential_decode_blocks, 4);
2139 assert_eq!(load.active_requests, 0);
2140 }
2141
2142 #[test]
2143 fn test_potential_load_serializes_active_requests() {
2144 let load = PotentialLoad {
2145 worker_id: 1,
2146 dp_rank: 0,
2147 potential_prefill_tokens: 16,
2148 potential_decode_blocks: 4,
2149 active_requests: 2,
2150 };
2151
2152 assert_eq!(
2153 serde_json::to_string(&load).unwrap(),
2154 r#"{"worker_id":1,"dp_rank":0,"potential_prefill_tokens":16,"potential_decode_blocks":4,"active_requests":2}"#
2155 );
2156 }
2157}