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 is_eagle: Option<bool>,
35}
36
37#[inline]
38fn hash_block_no_mm(chunk: &[u32], seed: u64, scratch_bytes: &mut Vec<u8>) -> LocalBlockHash {
39 #[cfg(target_endian = "little")]
40 {
41 let _ = scratch_bytes;
42 let chunk_bytes = unsafe {
45 std::slice::from_raw_parts(chunk.as_ptr().cast::<u8>(), std::mem::size_of_val(chunk))
46 };
47 LocalBlockHash(xxh3::xxh3_64_with_seed(chunk_bytes, seed))
48 }
49
50 #[cfg(not(target_endian = "little"))]
51 {
52 scratch_bytes.clear();
53 for &token in chunk {
54 scratch_bytes.extend_from_slice(&token.to_le_bytes());
55 }
56 LocalBlockHash(xxh3::xxh3_64_with_seed(scratch_bytes, seed))
57 }
58}
59
60pub const MM_PAD_SHIFT_VALUE: u64 = 1_000_000;
64pub const MM_PAD_HASH_MASK: u64 = (1 << 30) - 1;
65
66pub fn pad_value_for_mm_hash(mm_hash: u64) -> u32 {
70 (MM_PAD_SHIFT_VALUE + (mm_hash & MM_PAD_HASH_MASK)) as u32
71}
72
73pub fn compute_block_hash_for_seq(
86 tokens: &[u32],
87 kv_block_size: u32,
88 options: BlockHashOptions<'_>,
89) -> Vec<LocalBlockHash> {
90 if kv_block_size == 0 {
91 return Vec::new();
92 }
93
94 let seed = match options.lora_name.filter(|n| !n.is_empty()) {
95 Some(name) => XXH3_SEED.wrapping_add(xxh3::xxh3_64(name.as_bytes())),
96 None => XXH3_SEED,
97 };
98 let is_eagle_flag = options.is_eagle.unwrap_or(false);
99 let stride = kv_block_size as usize;
100 let window_size = if is_eagle_flag { stride + 1 } else { stride };
101 let estimated_blocks = if is_eagle_flag {
102 tokens.len().saturating_sub(1) / stride
103 } else {
104 tokens.len() / stride
105 };
106 let mut hashes = Vec::with_capacity(estimated_blocks);
107 let mut bytes = Vec::with_capacity(window_size * std::mem::size_of::<u32>());
108 let mut mm_hashes = Vec::new();
109 let mut block_idx = 0;
110 let mut start = 0;
111
112 while start + window_size <= tokens.len() {
113 let chunk = &tokens[start..start + window_size];
114 if let Some(mm_infos) = options.block_mm_infos
115 && let Some(Some(block_mm_info)) = mm_infos.get(block_idx)
116 {
117 bytes.clear();
118 for &token in chunk {
119 bytes.extend_from_slice(&token.to_le_bytes());
120 }
121
122 mm_hashes.clear();
123 mm_hashes.extend(block_mm_info.mm_objects.iter().map(|obj| obj.mm_hash));
124 mm_hashes.sort_unstable();
125
126 for &mm_hash in &mm_hashes {
127 bytes.extend_from_slice(&mm_hash.to_le_bytes());
128 }
129
130 hashes.push(LocalBlockHash(xxh3::xxh3_64_with_seed(&bytes, seed)));
131 } else {
132 hashes.push(hash_block_no_mm(chunk, seed, &mut bytes));
133 }
134
135 start += stride;
136 block_idx += 1;
137 }
138
139 hashes
140}
141
142#[inline]
147pub fn compute_next_seq_hash(
148 parent_seq_hash: SequenceHash,
149 current_block_hash: LocalBlockHash,
150) -> SequenceHash {
151 compute_next_sequence_hash(parent_seq_hash, current_block_hash.0)
152}
153
154pub fn compute_seq_hash_for_block(block_hashes: &[LocalBlockHash]) -> Vec<SequenceHash> {
159 if block_hashes.is_empty() {
160 return Vec::new();
161 }
162
163 let mut sequence_hashes = Vec::with_capacity(block_hashes.len());
164 sequence_hashes.push(block_hashes[0].0);
165
166 for i in 1..block_hashes.len() {
167 let parent_seq_hash = sequence_hashes[i - 1];
168 sequence_hashes.push(compute_next_seq_hash(parent_seq_hash, block_hashes[i]));
169 }
170
171 sequence_hashes
172}
173
174pub trait WorkerConfigLike {
178 fn data_parallel_start_rank(&self) -> u32;
179 fn data_parallel_size(&self) -> u32;
180 fn max_num_batched_tokens(&self) -> Option<u64>;
181 fn total_kv_blocks(&self) -> Option<u64>;
182
183 fn taints(&self) -> &HashSet<String> {
184 &EMPTY_WORKER_TAINTS
185 }
186
187 fn stable_routing_id(&self) -> Option<&str> {
195 None
196 }
197
198 fn topology_domains(&self) -> Option<&HashMap<String, String>> {
203 None
204 }
205
206 fn kv_transfer_domain(&self) -> Option<&str> {
210 None
211 }
212
213 fn kv_transfer_enforcement(&self) -> Option<KvTransferEnforcement> {
215 None
216 }
217
218 fn kv_transfer_preferred_weight(&self) -> Option<f32> {
220 None
221 }
222}
223
224#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
225#[serde(rename_all = "snake_case")]
226pub enum KvTransferEnforcement {
227 Required,
229 Preferred,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
238pub struct RoutingConstraints {
239 #[serde(default, skip_serializing_if = "HashSet::is_empty")]
240 pub required_taints: HashSet<String>,
241 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
242 pub preferred_taints: HashMap<String, f32>,
243}
244
245impl RoutingConstraints {
246 pub fn is_empty(&self) -> bool {
247 self.required_taints.is_empty() && self.preferred_taints.is_empty()
248 }
249
250 pub fn has_hard_constraints(&self) -> bool {
251 !self.required_taints.is_empty()
252 }
253
254 pub fn is_compatible_with_worker_taints(&self, worker_taints: &HashSet<String>) -> bool {
255 if self.required_taints.is_empty() {
256 return true;
257 }
258
259 self.required_taints
260 .iter()
261 .all(|taint| worker_taints.contains(taint))
262 }
263
264 pub fn preferred_taint_matches(&self, worker_taints: &HashSet<String>) -> usize {
265 if self.preferred_taints.is_empty() {
266 return 0;
267 }
268
269 self.preferred_taints
270 .keys()
271 .filter(|taint| worker_taints.contains(*taint))
272 .count()
273 }
274
275 pub fn preferred_taint_multiplier(&self, worker_taints: &HashSet<String>) -> Option<f64> {
276 if self.preferred_taints.is_empty() {
277 return None;
278 }
279
280 let bias = self
285 .preferred_taints
286 .iter()
287 .filter(|(taint, _)| worker_taints.contains(*taint))
288 .map(|(_, weight)| f64::from(*weight))
289 .sum::<f64>()
290 .tanh();
291
292 Some((-bias).exp())
293 }
294}
295
296static EMPTY_WORKER_TAINTS: LazyLock<HashSet<String>> = LazyLock::new(HashSet::new);
297
298pub trait RouterEventSink: Send + Sync {
300 fn publish_event(&self, event: &RouterEvent)
301 -> impl Future<Output = anyhow::Result<()>> + Send;
302}
303
304pub type WorkerId = u64;
306
307pub type DpRank = u32;
309
310#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
314pub struct WorkerWithDpRank {
315 pub worker_id: WorkerId,
316 pub dp_rank: DpRank,
317}
318
319impl WorkerWithDpRank {
320 pub fn new(worker_id: WorkerId, dp_rank: DpRank) -> Self {
321 Self { worker_id, dp_rank }
322 }
323
324 pub fn from_worker_id(worker_id: WorkerId) -> Self {
325 Self {
326 worker_id,
327 dp_rank: 0,
328 }
329 }
330}
331
332#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
333#[serde(rename_all = "snake_case")]
334pub enum StorageTier {
335 #[default]
336 Device,
337 HostPinned,
338 Disk,
339 External,
340}
341
342impl StorageTier {
343 pub fn from_kv_medium(medium: &str) -> Option<Self> {
344 match medium {
345 "GPU" | "DEVICE" => Some(Self::Device),
346 "CPU" | "CPU_PINNED" | "CPU_TIER1" => Some(Self::HostPinned),
347 "CPU_TIER2" | "DISK" | "NVME" => Some(Self::Disk),
348 "EXTERNAL" | "NETWORK" | "REMOTE" | "SHARED" => Some(Self::External),
349 _ => None,
350 }
351 }
352
353 pub fn from_kv_medium_or_default(medium: Option<&str>) -> Self {
354 medium
355 .and_then(Self::from_kv_medium)
356 .unwrap_or(Self::Device)
357 }
358
359 pub fn to_kv_medium(self) -> Option<&'static str> {
362 match self {
363 Self::Device => None,
364 Self::HostPinned => Some("CPU_PINNED"),
365 Self::Disk => Some("DISK"),
366 Self::External => Some("EXTERNAL"),
367 }
368 }
369
370 pub fn is_gpu(self) -> bool {
371 matches!(self, Self::Device)
372 }
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
376pub enum PlacementOwner {
377 LocalWorker(WorkerWithDpRank),
378 Shared,
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
382pub struct Placement {
383 pub owner: PlacementOwner,
384 pub tier: StorageTier,
385}
386
387impl Placement {
388 pub fn local_worker(worker_id: WorkerId, dp_rank: DpRank, tier: StorageTier) -> Self {
389 Self {
390 owner: PlacementOwner::LocalWorker(WorkerWithDpRank::new(worker_id, dp_rank)),
391 tier,
392 }
393 }
394
395 pub fn local_gpu(worker_id: WorkerId, dp_rank: DpRank) -> Self {
396 Self::local_worker(worker_id, dp_rank, StorageTier::Device)
397 }
398
399 pub fn is_local_gpu(&self) -> bool {
400 matches!(self.owner, PlacementOwner::LocalWorker(_)) && self.tier.is_gpu()
401 }
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
405pub struct PlacementEvent {
406 pub placement: Placement,
407 pub event: KvCacheEvent,
408}
409
410impl PlacementEvent {
411 pub fn new(placement: Placement, event: KvCacheEvent) -> Self {
412 Self { placement, event }
413 }
414
415 pub fn local_gpu(worker_id: WorkerId, event: KvCacheEvent) -> Self {
416 Self::new(Placement::local_gpu(worker_id, event.dp_rank), event)
417 }
418
419 pub fn into_router_event(self) -> Option<RouterEvent> {
420 let PlacementOwner::LocalWorker(worker) = self.placement.owner else {
421 return None;
422 };
423 Some(RouterEvent::with_storage_tier(
424 worker.worker_id,
425 self.event,
426 self.placement.tier,
427 ))
428 }
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
432#[serde(tag = "method", rename_all = "snake_case")]
433pub enum RouterRequest {
434 #[serde(rename = "new")]
435 New {
436 tokens: Vec<Token>,
437 #[serde(default, skip_serializing_if = "Option::is_none")]
438 block_mm_infos: Option<Vec<Option<BlockExtraInfo>>>,
439 #[serde(default, skip_serializing_if = "RoutingConstraints::is_empty")]
440 routing_constraints: RoutingConstraints,
441 #[serde(default)]
442 priority_jump: f64,
443 #[serde(default, skip_serializing_if = "is_zero")]
444 strict_priority: u32,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 lora_name: Option<String>,
447 },
448 PotentialLoads {
449 tokens: Vec<Token>,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
451 block_mm_infos: Option<Vec<Option<BlockExtraInfo>>>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
453 lora_name: Option<String>,
454 },
455 MarkPrefill {
456 #[serde(default, skip_serializing_if = "Option::is_none")]
459 request_id: Option<String>,
460 },
461 MarkFree {
462 #[serde(default, skip_serializing_if = "Option::is_none")]
465 request_id: Option<String>,
466 },
467}
468
469impl Default for RouterRequest {
470 fn default() -> Self {
471 RouterRequest::New {
472 tokens: vec![],
473 block_mm_infos: None,
474 routing_constraints: RoutingConstraints::default(),
475 priority_jump: 0.0,
476 strict_priority: 0,
477 lora_name: None,
478 }
479 }
480}
481
482fn is_zero(value: &u32) -> bool {
483 *value == 0
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
487pub struct PotentialLoad {
488 pub worker_id: WorkerId,
489 pub dp_rank: DpRank,
490 pub potential_prefill_tokens: usize,
491 pub potential_decode_blocks: usize,
492 #[serde(default)]
493 pub active_requests: usize,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
497#[serde(tag = "method", rename_all = "snake_case")]
498pub enum RouterResponse {
499 New {
500 worker_id: WorkerId,
501 #[serde(default)]
502 dp_rank: DpRank,
503 overlap_blocks: u32,
504 },
505 QueueRejected {
506 rejection: crate::scheduling::QueueRejection,
507 },
508 PrefillMarked {
509 success: bool,
510 },
511 FreeMarked {
512 success: bool,
513 },
514 PotentialLoads {
515 loads: Vec<PotentialLoad>,
517 #[serde(default)]
519 pending_count: usize,
520 #[serde(default)]
521 pending_isl_tokens: usize,
522 },
523}
524
525#[derive(Debug)]
526pub struct WorkerSelectionResult {
527 pub worker: WorkerWithDpRank,
529
530 pub required_blocks: u64,
532
533 pub effective_overlap_blocks: f64,
536
537 pub cached_tokens: usize,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
546pub struct ActiveLoad {
547 pub worker_id: WorkerId,
548 #[serde(default)]
549 pub dp_rank: DpRank,
550 pub active_decode_blocks: Option<u64>,
552 pub active_prefill_tokens: Option<u64>,
554 #[serde(default)]
559 pub kv_used_blocks: Option<u64>,
560}
561
562#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
565pub struct LocalBlockHash(pub u64);
566
567#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
572pub struct ExternalSequenceBlockHash(pub u64);
573
574impl From<u64> for ExternalSequenceBlockHash {
576 fn from(value: u64) -> Self {
577 Self(value)
578 }
579}
580
581impl From<i64> for ExternalSequenceBlockHash {
582 fn from(value: i64) -> Self {
585 Self(value as u64)
586 }
587}
588
589#[derive(Serialize, Deserialize, Debug, Clone)]
590pub struct PrefillEvent {
591 pub request_id: String,
592 pub worker_id: WorkerId,
593 pub data: PrefillEventData,
594 pub router_id: u64,
595}
596
597#[derive(Serialize, Deserialize, Debug, Clone)]
602pub enum PrefillEventData {
603 NewPrefill(usize),
604 UpdatePrefill(usize),
605 CompletePrefill,
606}
607
608#[derive(Serialize, Deserialize, Debug, Clone)]
609pub struct ActiveSequenceEvent {
610 pub request_id: String,
611 pub worker: WorkerWithDpRank,
612 pub data: ActiveSequenceEventData,
613 pub router_id: u64,
614 #[serde(default)]
615 pub lora_name: Option<String>,
616}
617
618#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
619pub struct PrefillLoadHint {
620 pub initial_effective_prefill_tokens: usize,
621 pub expected_prefill_duration: Option<Duration>,
622}
623
624#[derive(Serialize, Deserialize, Debug, Clone)]
625pub enum ActiveSequenceEventData {
626 AddRequest {
627 token_sequence: Option<Vec<SequenceHash>>,
628 #[serde(default = "default_track_prefill_tokens")]
629 track_prefill_tokens: bool,
630 expected_output_tokens: Option<u32>,
631 #[serde(default)]
632 prefill_load_hint: Option<PrefillLoadHint>,
633 },
634 Free,
637 MarkPrefillCompleted,
638}
639
640#[derive(Serialize, Deserialize, Debug, Clone)]
641pub struct ActiveBlockEvent {
642 pub request_id: String,
643 pub data: ActiveBlockEventData,
644}
645
646#[derive(Serialize, Deserialize, Debug, Clone)]
647pub enum ActiveBlockEventData {
648 NewBlock(Vec<SequenceHash>),
649 FreeBlock,
650}
651
652#[derive(Serialize, Deserialize, Debug, Clone)]
654pub struct KvCacheEvents {
655 pub events: Vec<KvCacheEvent>,
657 pub shutdown: bool,
659}
660
661#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
663pub struct KvCacheEvent {
664 pub event_id: u64,
666 pub data: KvCacheEventData,
668 #[serde(default)]
670 pub dp_rank: DpRank,
671}
672
673#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
677#[serde(rename_all = "snake_case")]
678pub enum KvCacheEventData {
679 Stored(KvCacheStoreData),
680 Removed(KvCacheRemoveData),
681 Cleared,
682}
683
684#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
686pub struct KvCacheStoreData {
687 pub parent_hash: Option<ExternalSequenceBlockHash>,
689 #[serde(default)]
691 pub start_position: Option<u32>,
692 pub blocks: Vec<KvCacheStoredBlockData>,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
699pub struct BlockMmObjectInfo {
700 pub mm_hash: u64,
702 pub offsets: Vec<(usize, usize)>,
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
709pub struct BlockExtraInfo {
710 pub mm_objects: Vec<BlockMmObjectInfo>,
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
717pub struct RequestMmObjectInfo {
718 pub mm_hash: u64,
720 pub offsets: Vec<(usize, usize)>,
723}
724
725#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
727pub struct RequestExtraInfo {
728 pub mm_objects: Vec<RequestMmObjectInfo>,
730}
731
732impl RequestExtraInfo {
733 pub fn to_block_level(
746 &self,
747 block_size: usize,
748 total_tokens: usize,
749 ) -> Vec<Option<BlockExtraInfo>> {
750 let num_blocks = total_tokens.div_ceil(block_size);
751 let mut block_infos: Vec<Option<BlockExtraInfo>> = vec![None; num_blocks];
752
753 for req_mm_obj in &self.mm_objects {
754 for (req_start, req_end) in &req_mm_obj.offsets {
755 let start_block = req_start / block_size;
757 let end_block = (req_end.saturating_sub(1)) / block_size;
758
759 let upper_bound = end_block.min(num_blocks - 1) + 1;
760 for (block_idx, block_info_opt) in block_infos
761 .iter_mut()
762 .enumerate()
763 .take(upper_bound)
764 .skip(start_block)
765 {
766 let block_start_global = block_idx * block_size;
767 let block_end_global = ((block_idx + 1) * block_size).min(total_tokens);
768
769 let local_start = (*req_start).max(block_start_global) - block_start_global;
771 let local_end = (*req_end).min(block_end_global) - block_start_global;
772
773 if local_start < local_end {
774 let block_info = block_info_opt
775 .get_or_insert_with(|| BlockExtraInfo { mm_objects: vec![] });
776
777 if let Some(existing) = block_info
779 .mm_objects
780 .iter_mut()
781 .find(|obj| obj.mm_hash == req_mm_obj.mm_hash)
782 {
783 existing.offsets.push((local_start, local_end));
785 } else {
786 block_info.mm_objects.push(BlockMmObjectInfo {
788 mm_hash: req_mm_obj.mm_hash,
789 offsets: vec![(local_start, local_end)],
790 });
791 }
792 }
793 }
794 }
795 }
796
797 block_infos
798 }
799}
800
801#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
803pub struct KvCacheStoredBlockData {
804 pub block_hash: ExternalSequenceBlockHash,
806 pub tokens_hash: LocalBlockHash,
808 #[serde(default)]
812 pub mm_extra_info: Option<BlockExtraInfo>,
813}
814
815#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
817pub struct KvCacheRemoveData {
818 pub block_hashes: Vec<ExternalSequenceBlockHash>,
820}
821
822impl Serialize for LocalBlockHash {
823 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
824 where
825 S: serde::Serializer,
826 {
827 serializer.serialize_u64(self.0)
828 }
829}
830
831impl<'de> Deserialize<'de> for LocalBlockHash {
832 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
833 where
834 D: serde::Deserializer<'de>,
835 {
836 let value = u64::deserialize(deserializer)?;
837 Ok(LocalBlockHash(value))
838 }
839}
840
841impl Serialize for ExternalSequenceBlockHash {
842 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
843 where
844 S: serde::Serializer,
845 {
846 serializer.serialize_u64(self.0)
847 }
848}
849
850impl<'de> Deserialize<'de> for ExternalSequenceBlockHash {
851 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
852 where
853 D: serde::Deserializer<'de>,
854 {
855 let value = u64::deserialize(deserializer)?;
856 Ok(ExternalSequenceBlockHash(value))
857 }
858}
859
860#[derive(Debug, thiserror::Error)]
866pub enum KvCacheEventError {
867 #[error("Failed to find parent block")]
868 ParentBlockNotFound,
869
870 #[error("Failed to find block")]
871 BlockNotFound,
872
873 #[error("Invalid block sequence")]
874 InvalidBlockSequence,
875}
876
877#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
879pub struct RouterEvent {
880 pub worker_id: WorkerId,
882 #[serde(default)]
884 pub storage_tier: StorageTier,
885 pub event: KvCacheEvent,
887}
888
889impl RouterEvent {
890 pub fn new(worker_id: WorkerId, event: KvCacheEvent) -> Self {
901 Self::with_storage_tier(worker_id, event, StorageTier::Device)
902 }
903
904 pub fn with_storage_tier(
905 worker_id: WorkerId,
906 event: KvCacheEvent,
907 storage_tier: StorageTier,
908 ) -> Self {
909 Self {
910 worker_id,
911 storage_tier,
912 event,
913 }
914 }
915}
916
917#[derive(Debug, Clone, Default)]
924pub struct SharedCacheHits {
925 pub ranges: Vec<Range<u32>>,
928 pub total_hits: u32,
930}
931
932impl SharedCacheHits {
933 pub fn from_ranges(ranges: Vec<Range<u32>>) -> Self {
935 let total_hits = ranges.iter().map(|r| r.end - r.start).sum();
936 Self { ranges, total_hits }
937 }
938
939 pub fn from_hits(hits: &[bool]) -> Self {
942 let mut ranges = Vec::new();
943 let mut i = 0;
944 while i < hits.len() {
945 if hits[i] {
946 let start = i as u32;
947 while i < hits.len() && hits[i] {
948 i += 1;
949 }
950 ranges.push(start..i as u32);
951 } else {
952 i += 1;
953 }
954 }
955 Self::from_ranges(ranges)
956 }
957
958 pub fn hits_beyond(&self, from_position: u32) -> u32 {
961 self.ranges
962 .iter()
963 .map(|r| {
964 if r.end <= from_position {
965 0
966 } else if r.start >= from_position {
967 r.end - r.start
968 } else {
969 r.end - from_position
970 }
971 })
972 .sum()
973 }
974}
975
976#[derive(Debug, Clone, Serialize, Deserialize)]
978pub struct OverlapScores {
979 pub scores: FxHashMap<WorkerWithDpRank, u32>,
981 pub frequencies: Vec<usize>,
983}
984
985impl Default for OverlapScores {
986 fn default() -> Self {
987 Self::new()
988 }
989}
990
991impl OverlapScores {
992 pub fn new() -> Self {
998 Self {
999 scores: FxHashMap::default(),
1000 frequencies: Vec::new(),
1001 }
1002 }
1003
1004 pub fn update_scores<'a, I>(&mut self, workers: I)
1010 where
1011 I: IntoIterator<Item = &'a WorkerWithDpRank>,
1012 {
1013 for worker in workers {
1014 let score = self.scores.entry(*worker).or_insert(0);
1015 *score += 1;
1016 }
1017 }
1018}
1019
1020#[derive(Debug, Clone)]
1031pub struct TokensWithHashes {
1032 tokens: Vec<u32>,
1033 block_size: u32,
1034 block_mm_infos: Option<Vec<Option<BlockExtraInfo>>>,
1035 lora_name: Option<String>,
1036 block_hashes: Option<Vec<LocalBlockHash>>,
1037 seq_hashes: Option<Vec<SequenceHash>>,
1038 is_eagle: Option<bool>,
1039}
1040
1041impl TokensWithHashes {
1042 pub fn new(tokens: Vec<u32>, block_size: u32) -> Self {
1044 Self {
1045 tokens,
1046 block_size,
1047 block_mm_infos: None,
1048 lora_name: None,
1049 block_hashes: None,
1050 seq_hashes: None,
1051 is_eagle: None,
1052 }
1053 }
1054
1055 pub fn with_mm_infos(mut self, infos: Vec<Option<BlockExtraInfo>>) -> Self {
1057 self.block_mm_infos = Some(infos);
1058 self.invalidate_hashes();
1059 self
1060 }
1061
1062 pub fn with_lora_name(mut self, name: String) -> Self {
1064 self.lora_name = Some(name);
1065 self.invalidate_hashes();
1066 self
1067 }
1068
1069 pub fn with_is_eagle(mut self, is_eagle: bool) -> Self {
1071 self.set_is_eagle(is_eagle);
1072 self
1073 }
1074
1075 pub fn set_is_eagle(&mut self, is_eagle: bool) {
1077 let is_eagle = Some(is_eagle);
1078 if self.is_eagle == is_eagle {
1079 return;
1080 }
1081
1082 self.is_eagle = is_eagle;
1083 self.invalidate_hashes();
1084 }
1085
1086 fn invalidate_hashes(&mut self) {
1087 self.block_hashes = None;
1088 self.seq_hashes = None;
1089 }
1090
1091 pub fn tokens(&self) -> &[u32] {
1093 &self.tokens
1094 }
1095
1096 pub fn len(&self) -> usize {
1098 self.tokens.len()
1099 }
1100
1101 pub fn is_empty(&self) -> bool {
1103 self.tokens.is_empty()
1104 }
1105
1106 pub fn block_size(&self) -> u32 {
1108 self.block_size
1109 }
1110
1111 pub fn block_mm_infos(&self) -> Option<&[Option<BlockExtraInfo>]> {
1113 self.block_mm_infos.as_deref()
1114 }
1115
1116 pub fn get_or_compute_block_hashes(&mut self) -> &[LocalBlockHash] {
1118 if self.block_hashes.is_none() {
1119 self.block_hashes = Some(compute_block_hash_for_seq(
1120 &self.tokens,
1121 self.block_size,
1122 BlockHashOptions {
1123 block_mm_infos: self.block_mm_infos.as_deref(),
1124 lora_name: self.lora_name.as_deref(),
1125 is_eagle: self.is_eagle,
1126 },
1127 ));
1128 }
1129 self.block_hashes.as_ref().unwrap()
1130 }
1131
1132 pub fn get_or_compute_seq_hashes(&mut self) -> &[SequenceHash] {
1136 if self.seq_hashes.is_none() {
1137 let block_hashes = self.get_or_compute_block_hashes();
1139 self.seq_hashes = Some(compute_seq_hash_for_block(block_hashes));
1140 }
1141 self.seq_hashes.as_ref().unwrap()
1142 }
1143
1144 pub fn block_hashes(&self) -> Option<&[LocalBlockHash]> {
1146 self.block_hashes.as_deref()
1147 }
1148
1149 pub fn seq_hashes(&self) -> Option<&[SequenceHash]> {
1151 self.seq_hashes.as_deref()
1152 }
1153}
1154
1155#[cfg(test)]
1159mod tests {
1160 use super::*;
1161 use rstest::rstest;
1162 use serde_json;
1163
1164 #[test]
1169 fn pad_value_matches_sglang_protocol() {
1170 assert_eq!(MM_PAD_SHIFT_VALUE, 1_000_000);
1171 assert_eq!(MM_PAD_HASH_MASK, (1u64 << 30) - 1);
1172 assert_eq!(pad_value_for_mm_hash(0), MM_PAD_SHIFT_VALUE as u32);
1173 let fits = (1u64 << 30) - 1;
1174 assert_eq!(
1175 pad_value_for_mm_hash(fits),
1176 (MM_PAD_SHIFT_VALUE + fits) as u32
1177 );
1178 let overflow = (1u64 << 30) | 0xCAFE;
1179 assert_eq!(
1180 pad_value_for_mm_hash(overflow),
1181 (MM_PAD_SHIFT_VALUE + 0xCAFE) as u32,
1182 "high bits above the 30-bit mask must be discarded"
1183 );
1184 }
1185
1186 #[test]
1187 fn test_router_event_new() {
1188 let worker_id = 0;
1189 let kv_cache_event = KvCacheEvent {
1190 event_id: 1,
1191 data: KvCacheEventData::Stored(KvCacheStoreData {
1192 parent_hash: None,
1193 start_position: None,
1194 blocks: vec![KvCacheStoredBlockData {
1195 block_hash: ExternalSequenceBlockHash(0),
1196 mm_extra_info: None,
1197 tokens_hash: LocalBlockHash(13226331709069118873),
1198 }],
1199 }),
1200 dp_rank: 0,
1201 };
1202 let router_event = RouterEvent::new(worker_id, kv_cache_event);
1203
1204 assert_eq!(router_event.worker_id, worker_id);
1205 assert_eq!(router_event.event.event_id, 1);
1206 if let KvCacheEventData::Stored(store_op) = &router_event.event.data {
1207 assert_eq!(store_op.blocks.len(), 1);
1208 assert_eq!(
1209 store_op.blocks[0].tokens_hash,
1210 compute_block_hash(b"test data")
1211 );
1212 assert_eq!(store_op.blocks[0].block_hash, ExternalSequenceBlockHash(0));
1213 } else {
1214 panic!("Expected KvCacheEventData::Stored");
1215 }
1216 }
1217
1218 #[rstest]
1219 #[case(11)]
1220 #[case(32)]
1221 #[case(64)]
1222 fn test_compute_block_hash_for_seq(#[case] kv_block_size: u32) {
1223 let sequence = (0..kv_block_size).collect::<Vec<u32>>();
1224 let hashes =
1225 compute_block_hash_for_seq(&sequence, kv_block_size, BlockHashOptions::default());
1226 assert_eq!(hashes.len(), 1);
1227
1228 let sequence = (0..(kv_block_size + 1)).collect::<Vec<u32>>();
1229 let hashes =
1230 compute_block_hash_for_seq(&sequence, kv_block_size, BlockHashOptions::default());
1231 assert_eq!(hashes.len(), 1);
1232
1233 let sequence = (0..(2 * kv_block_size + 1)).collect::<Vec<u32>>();
1234 let hashes =
1235 compute_block_hash_for_seq(&sequence, kv_block_size, BlockHashOptions::default());
1236 assert_eq!(hashes.len(), 2);
1237 }
1238
1239 #[test]
1240 fn test_compute_next_seq_hash_matches_rolling_hash() {
1241 let block_hashes = [LocalBlockHash(11), LocalBlockHash(22), LocalBlockHash(33)];
1242 let seq_hashes = compute_seq_hash_for_block(&block_hashes);
1243
1244 assert_eq!(
1245 seq_hashes[1],
1246 compute_next_seq_hash(seq_hashes[0], block_hashes[1])
1247 );
1248 assert_eq!(
1249 seq_hashes[2],
1250 compute_next_seq_hash(seq_hashes[1], block_hashes[2])
1251 );
1252 }
1253
1254 #[test]
1255 fn test_lora_name_produces_different_hash() {
1256 let tokens: Vec<u32> = (0..4).collect();
1257 let base = compute_block_hash_for_seq(&tokens, 4, BlockHashOptions::default());
1258 let lora_a = compute_block_hash_for_seq(
1259 &tokens,
1260 4,
1261 BlockHashOptions {
1262 lora_name: Some("adapter-a"),
1263 ..Default::default()
1264 },
1265 );
1266 let lora_b = compute_block_hash_for_seq(
1267 &tokens,
1268 4,
1269 BlockHashOptions {
1270 lora_name: Some("adapter-b"),
1271 ..Default::default()
1272 },
1273 );
1274
1275 assert_ne!(base[0], lora_a[0]);
1276 assert_ne!(base[0], lora_b[0]);
1277 assert_ne!(lora_a[0], lora_b[0]);
1278 }
1279
1280 #[test]
1281 fn test_lora_name_empty_string_normalized_to_none() {
1282 let tokens: Vec<u32> = (0..4).collect();
1283 let base = compute_block_hash_for_seq(&tokens, 4, BlockHashOptions::default());
1284 let empty = compute_block_hash_for_seq(
1285 &tokens,
1286 4,
1287 BlockHashOptions {
1288 lora_name: Some(""),
1289 ..Default::default()
1290 },
1291 );
1292 assert_eq!(
1293 base, empty,
1294 "empty lora_name should be treated as base model"
1295 );
1296 }
1297
1298 #[test]
1299 fn test_tokens_with_hashes_lora() {
1300 let tokens: Vec<u32> = (0..8).collect();
1301
1302 let mut base = TokensWithHashes::new(tokens.clone(), 4);
1303 let base_hashes = base.get_or_compute_block_hashes().to_vec();
1304
1305 let mut with_lora =
1306 TokensWithHashes::new(tokens, 4).with_lora_name("my-adapter".to_string());
1307 let lora_hashes = with_lora.get_or_compute_block_hashes().to_vec();
1308
1309 assert_eq!(base_hashes.len(), lora_hashes.len());
1310 for (b, l) in base_hashes.iter().zip(lora_hashes.iter()) {
1311 assert_ne!(b, l);
1312 }
1313 }
1314
1315 #[test]
1316 fn test_tokens_with_hashes_lora_change_recomputes_cached_hashes() {
1317 let tokens: Vec<u32> = (0..8).collect();
1318 let mut with_hashes = TokensWithHashes::new(tokens.clone(), 4);
1319 let base_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1320
1321 let mut with_hashes = with_hashes.with_lora_name("my-adapter".to_string());
1322 let actual_block_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1323 let actual_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1324 let expected_block_hashes = compute_block_hash_for_seq(
1325 &tokens,
1326 4,
1327 BlockHashOptions {
1328 lora_name: Some("my-adapter"),
1329 ..Default::default()
1330 },
1331 );
1332 let expected_sequence_hashes = compute_seq_hash_for_block(&expected_block_hashes);
1333
1334 assert_eq!(actual_block_hashes, expected_block_hashes);
1335 assert_eq!(actual_sequence_hashes, expected_sequence_hashes);
1336 assert_ne!(actual_sequence_hashes, base_sequence_hashes);
1337 }
1338
1339 #[test]
1340 fn test_tokens_with_hashes_mm_change_recomputes_cached_hashes() {
1341 let tokens: Vec<u32> = (0..4).collect();
1342 let mm_infos = vec![
1343 Some(BlockExtraInfo {
1344 mm_objects: vec![BlockMmObjectInfo {
1345 mm_hash: 42,
1346 offsets: vec![(0, 1)],
1347 }],
1348 }),
1349 None,
1350 ];
1351 let mut with_hashes = TokensWithHashes::new(tokens.clone(), 2);
1352 let text_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1353
1354 let mut with_hashes = with_hashes.with_mm_infos(mm_infos.clone());
1355 let actual_block_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1356 let actual_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1357 let expected_block_hashes = compute_block_hash_for_seq(
1358 &tokens,
1359 2,
1360 BlockHashOptions {
1361 block_mm_infos: Some(&mm_infos),
1362 ..Default::default()
1363 },
1364 );
1365 let expected_sequence_hashes = compute_seq_hash_for_block(&expected_block_hashes);
1366
1367 assert_eq!(actual_block_hashes, expected_block_hashes);
1368 assert_eq!(actual_sequence_hashes, expected_sequence_hashes);
1369 assert_ne!(actual_sequence_hashes, text_sequence_hashes);
1370 }
1371
1372 #[test]
1373 fn test_compute_block_hash_for_seq_eagle_windows() {
1374 let tokens: Vec<u32> = (0..6).collect();
1375
1376 let default_hashes = compute_block_hash_for_seq(&tokens, 2, BlockHashOptions::default());
1377 let eagle_hashes = compute_block_hash_for_seq(
1378 &tokens,
1379 2,
1380 BlockHashOptions {
1381 is_eagle: Some(true),
1382 ..Default::default()
1383 },
1384 );
1385 let expected_first = compute_block_hash_for_seq(
1386 &[0, 1, 2],
1387 2,
1388 BlockHashOptions {
1389 is_eagle: Some(true),
1390 ..Default::default()
1391 },
1392 );
1393 let expected_second = compute_block_hash_for_seq(
1394 &[2, 3, 4],
1395 2,
1396 BlockHashOptions {
1397 is_eagle: Some(true),
1398 ..Default::default()
1399 },
1400 );
1401
1402 assert_eq!(default_hashes.len(), 3);
1403 assert_eq!(eagle_hashes.len(), 2);
1404 assert_eq!(eagle_hashes, vec![expected_first[0], expected_second[0]]);
1405 assert_ne!(default_hashes[0], eagle_hashes[0]);
1406 }
1407
1408 #[test]
1409 fn test_tokens_with_hashes_set_is_eagle_invalidates_cache() {
1410 let tokens: Vec<u32> = (0..6).collect();
1411 let mut with_hashes = TokensWithHashes::new(tokens, 2);
1412
1413 let default_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1414 with_hashes.set_is_eagle(true);
1415 let eagle_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1416 let expected_first = compute_block_hash_for_seq(
1417 &[0, 1, 2],
1418 2,
1419 BlockHashOptions {
1420 is_eagle: Some(true),
1421 ..Default::default()
1422 },
1423 );
1424 let expected_second = compute_block_hash_for_seq(
1425 &[2, 3, 4],
1426 2,
1427 BlockHashOptions {
1428 is_eagle: Some(true),
1429 ..Default::default()
1430 },
1431 );
1432
1433 assert_eq!(default_hashes.len(), 3);
1434 assert_eq!(eagle_hashes.len(), 2);
1435 assert_eq!(eagle_hashes, vec![expected_first[0], expected_second[0]]);
1436 assert_ne!(default_hashes[0], eagle_hashes[0]);
1437 }
1438
1439 #[test]
1440 fn test_local_block_hash_serialization() {
1441 let hash = LocalBlockHash(12345);
1442 let serialized = serde_json::to_string(&hash).unwrap();
1443 assert_eq!(serialized, "12345");
1444
1445 let deserialized: LocalBlockHash = serde_json::from_str(&serialized).unwrap();
1446 assert_eq!(deserialized, hash);
1447 }
1448
1449 #[test]
1450 fn test_external_sequence_block_hash_serialization() {
1451 let hash = ExternalSequenceBlockHash(67890);
1452 let serialized = serde_json::to_string(&hash).unwrap();
1453 assert_eq!(serialized, "67890");
1454
1455 let deserialized: ExternalSequenceBlockHash = serde_json::from_str(&serialized).unwrap();
1456 assert_eq!(deserialized, hash);
1457 }
1458
1459 #[test]
1460 fn test_router_request_mark_free_backwards_compatible_deserialization() {
1461 let request: RouterRequest = serde_json::from_str(r#"{"method":"mark_free"}"#).unwrap();
1462
1463 assert!(matches!(
1464 request,
1465 RouterRequest::MarkFree { request_id: None }
1466 ));
1467 }
1468
1469 #[test]
1470 fn test_shared_cache_hits_from_hits() {
1471 let hits = SharedCacheHits::from_hits(&[true, true, true, true]);
1473 assert_eq!(hits.ranges, vec![0..4]);
1474 assert_eq!(hits.total_hits, 4);
1475
1476 let hits = SharedCacheHits::from_hits(&[true, false, true, true, false, true]);
1478 assert_eq!(hits.ranges, vec![0..1, 2..4, 5..6]);
1479 assert_eq!(hits.total_hits, 4);
1480
1481 let hits = SharedCacheHits::from_hits(&[false, false, false]);
1483 assert!(hits.ranges.is_empty());
1484 assert_eq!(hits.total_hits, 0);
1485
1486 let hits = SharedCacheHits::from_hits(&[]);
1488 assert!(hits.ranges.is_empty());
1489 assert_eq!(hits.total_hits, 0);
1490 }
1491
1492 #[test]
1493 fn test_shared_cache_hits_beyond() {
1494 #[allow(clippy::single_range_in_vec_init)]
1496 let hits = SharedCacheHits::from_ranges(vec![0..4]);
1497
1498 assert_eq!(hits.hits_beyond(2), 2);
1500
1501 assert_eq!(hits.hits_beyond(0), 4);
1503
1504 assert_eq!(hits.hits_beyond(4), 0);
1506
1507 assert_eq!(hits.hits_beyond(10), 0);
1509 }
1510
1511 #[test]
1512 fn test_shared_cache_hits_beyond_sparse() {
1513 let hits = SharedCacheHits::from_ranges(vec![1..3, 5..8]);
1515 assert_eq!(hits.total_hits, 5);
1516
1517 assert_eq!(hits.hits_beyond(0), 5);
1519 assert_eq!(hits.hits_beyond(2), 4);
1521 assert_eq!(hits.hits_beyond(3), 3);
1523 assert_eq!(hits.hits_beyond(6), 2);
1525 assert_eq!(hits.hits_beyond(8), 0);
1527 }
1528
1529 #[test]
1530 fn test_kv_transfer_enforcement_serde() {
1531 assert_eq!(
1532 serde_json::to_string(&KvTransferEnforcement::Required).unwrap(),
1533 r#""required""#
1534 );
1535 assert_eq!(
1536 serde_json::from_str::<KvTransferEnforcement>(r#""preferred""#).unwrap(),
1537 KvTransferEnforcement::Preferred
1538 );
1539 assert!(serde_json::from_str::<KvTransferEnforcement>(r#""fallback""#).is_err());
1540 }
1541
1542 #[test]
1543 fn test_worker_config_like_topology_domains_default() {
1544 struct MinimalConfig;
1546 impl WorkerConfigLike for MinimalConfig {
1547 fn data_parallel_start_rank(&self) -> u32 {
1548 0
1549 }
1550 fn data_parallel_size(&self) -> u32 {
1551 1
1552 }
1553 fn max_num_batched_tokens(&self) -> Option<u64> {
1554 None
1555 }
1556 fn total_kv_blocks(&self) -> Option<u64> {
1557 None
1558 }
1559 }
1560
1561 let config = MinimalConfig;
1562 assert!(
1563 config.topology_domains().is_none(),
1564 "Default topology_domains() should return None"
1565 );
1566 assert!(
1567 config.kv_transfer_domain().is_none(),
1568 "Default kv_transfer_domain() should return None"
1569 );
1570 assert!(
1571 config.kv_transfer_enforcement().is_none(),
1572 "Default kv_transfer_enforcement() should return None"
1573 );
1574 assert!(
1575 config.kv_transfer_preferred_weight().is_none(),
1576 "Default kv_transfer_preferred_weight() should return None"
1577 );
1578 }
1579
1580 #[test]
1581 fn test_router_request_mark_free_serialization_with_request_id() {
1582 let request = RouterRequest::MarkFree {
1583 request_id: Some("req-123".to_string()),
1584 };
1585
1586 let serialized = serde_json::to_string(&request).unwrap();
1587 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1588
1589 assert_eq!(
1590 serialized,
1591 r#"{"method":"mark_free","request_id":"req-123"}"#
1592 );
1593 assert!(matches!(
1594 deserialized,
1595 RouterRequest::MarkFree {
1596 request_id: Some(ref request_id)
1597 } if request_id == "req-123"
1598 ));
1599 }
1600
1601 #[test]
1602 fn test_router_request_new_serialization_with_priority_jump() {
1603 let request = RouterRequest::New {
1604 tokens: vec![1, 2, 3],
1605 block_mm_infos: None,
1606 routing_constraints: RoutingConstraints::default(),
1607 priority_jump: 5.0,
1608 strict_priority: 0,
1609 lora_name: None,
1610 };
1611
1612 let serialized = serde_json::to_string(&request).unwrap();
1613 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1614
1615 assert_eq!(
1616 serialized,
1617 r#"{"method":"new","tokens":[1,2,3],"priority_jump":5.0}"#
1618 );
1619 assert!(matches!(
1620 deserialized,
1621 RouterRequest::New {
1622 priority_jump,
1623 ..
1624 } if priority_jump == 5.0
1625 ));
1626 }
1627
1628 #[test]
1629 fn test_router_request_new_serialization_with_lora_name() {
1630 let request = RouterRequest::New {
1631 tokens: vec![1, 2, 3],
1632 block_mm_infos: None,
1633 routing_constraints: RoutingConstraints::default(),
1634 priority_jump: 0.0,
1635 strict_priority: 0,
1636 lora_name: Some("adapter-a".to_string()),
1637 };
1638
1639 let serialized = serde_json::to_string(&request).unwrap();
1640 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1641
1642 assert_eq!(
1643 serialized,
1644 r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0,"lora_name":"adapter-a"}"#
1645 );
1646 assert!(matches!(
1647 deserialized,
1648 RouterRequest::New {
1649 tokens,
1650 lora_name: Some(ref lora_name),
1651 ..
1652 } if tokens == vec![1, 2, 3] && lora_name == "adapter-a"
1653 ));
1654 }
1655
1656 #[test]
1657 fn test_router_request_new_defaults_lora_name() {
1658 let deserialized: RouterRequest =
1659 serde_json::from_str(r#"{"method":"new","tokens":[1,2,3]}"#).unwrap();
1660
1661 assert!(matches!(
1662 deserialized,
1663 RouterRequest::New {
1664 tokens,
1665 lora_name: None,
1666 ..
1667 } if tokens == vec![1, 2, 3]
1668 ));
1669 }
1670
1671 #[test]
1672 fn test_router_request_new_strict_priority_compatibility() {
1673 let request = RouterRequest::New {
1674 tokens: vec![1, 2, 3],
1675 block_mm_infos: None,
1676 routing_constraints: RoutingConstraints::default(),
1677 priority_jump: 0.0,
1678 strict_priority: 4,
1679 lora_name: None,
1680 };
1681
1682 let serialized = serde_json::to_string(&request).unwrap();
1683 assert_eq!(
1684 serialized,
1685 r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0,"strict_priority":4}"#
1686 );
1687
1688 let missing: RouterRequest =
1689 serde_json::from_str(r#"{"method":"new","tokens":[1,2,3]}"#).unwrap();
1690 assert!(matches!(
1691 missing,
1692 RouterRequest::New {
1693 strict_priority: 0,
1694 ..
1695 }
1696 ));
1697
1698 let zero = RouterRequest::New {
1699 tokens: vec![1, 2, 3],
1700 block_mm_infos: None,
1701 routing_constraints: RoutingConstraints::default(),
1702 priority_jump: 0.0,
1703 strict_priority: 0,
1704 lora_name: None,
1705 };
1706 assert_eq!(
1707 serde_json::to_string(&zero).unwrap(),
1708 r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0}"#
1709 );
1710 }
1711
1712 #[test]
1713 fn test_router_request_potential_loads_serialization_with_lora_name() {
1714 let request = RouterRequest::PotentialLoads {
1715 tokens: vec![1, 2, 3],
1716 block_mm_infos: None,
1717 lora_name: Some("adapter-a".to_string()),
1718 };
1719
1720 let serialized = serde_json::to_string(&request).unwrap();
1721 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1722
1723 assert_eq!(
1724 serialized,
1725 r#"{"method":"potential_loads","tokens":[1,2,3],"lora_name":"adapter-a"}"#
1726 );
1727 assert!(matches!(
1728 deserialized,
1729 RouterRequest::PotentialLoads {
1730 tokens,
1731 block_mm_infos: None,
1732 lora_name: Some(ref lora_name),
1733 } if tokens == vec![1, 2, 3] && lora_name == "adapter-a"
1734 ));
1735 }
1736
1737 #[test]
1738 fn test_router_request_potential_loads_defaults_lora_name() {
1739 let deserialized: RouterRequest =
1740 serde_json::from_str(r#"{"method":"potential_loads","tokens":[1,2,3]}"#).unwrap();
1741
1742 assert!(matches!(
1743 deserialized,
1744 RouterRequest::PotentialLoads {
1745 tokens,
1746 block_mm_infos: None,
1747 lora_name: None,
1748 } if tokens == vec![1, 2, 3]
1749 ));
1750 }
1751
1752 #[test]
1753 fn test_router_request_mark_prefill_serialization_with_request_id() {
1754 let request = RouterRequest::MarkPrefill {
1755 request_id: Some("req-123".to_string()),
1756 };
1757
1758 let serialized = serde_json::to_string(&request).unwrap();
1759 let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1760
1761 assert_eq!(
1762 serialized,
1763 r#"{"method":"mark_prefill","request_id":"req-123"}"#
1764 );
1765 assert!(matches!(
1766 deserialized,
1767 RouterRequest::MarkPrefill {
1768 request_id: Some(ref request_id)
1769 } if request_id == "req-123"
1770 ));
1771 }
1772
1773 #[test]
1774 fn test_potential_load_defaults_active_requests() {
1775 let load = serde_json::from_str::<PotentialLoad>(
1776 r#"{"worker_id":1,"dp_rank":0,"potential_prefill_tokens":16,"potential_decode_blocks":4}"#,
1777 )
1778 .unwrap();
1779
1780 assert_eq!(load.worker_id, 1);
1781 assert_eq!(load.dp_rank, 0);
1782 assert_eq!(load.potential_prefill_tokens, 16);
1783 assert_eq!(load.potential_decode_blocks, 4);
1784 assert_eq!(load.active_requests, 0);
1785 }
1786
1787 #[test]
1788 fn test_potential_load_serializes_active_requests() {
1789 let load = PotentialLoad {
1790 worker_id: 1,
1791 dp_rank: 0,
1792 potential_prefill_tokens: 16,
1793 potential_decode_blocks: 4,
1794 active_requests: 2,
1795 };
1796
1797 assert_eq!(
1798 serde_json::to_string(&load).unwrap(),
1799 r#"{"worker_id":1,"dp_rank":0,"potential_prefill_tokens":16,"potential_decode_blocks":4,"active_requests":2}"#
1800 );
1801 }
1802}