Skip to main content

dynamo_kv_router/
protocols.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
19/// The event subject that workers publish KV cache events on.
20pub const KV_EVENT_SUBJECT: &str = "kv-events";
21
22/// Seed for XXH3 hashing, consistent with indexer.rs
23pub const XXH3_SEED: u64 = 1337;
24
25/// Compute the hash of a local block.
26pub 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        // SAFETY: `u32` is plain-old-data, and on little-endian targets its in-memory
43        // representation matches the `to_le_bytes()` sequence used for hashing.
44        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
60/// sglang's `MultimodalItem._compute_pad_value` constants — must track upstream;
61/// if they drift, MM routing silently degrades to text-prefix. Pinned by
62/// `pad_value_matches_sglang_protocol`.
63pub const MM_PAD_SHIFT_VALUE: u64 = 1_000_000;
64pub const MM_PAD_HASH_MASK: u64 = (1 << 30) - 1;
65
66/// Canonical per-image pad_value from a routing-side `mm_hash`, called by both
67/// the frontend and the kv-router so request- and event-side hashes agree.
68/// Keeps the low 30 bits only (sglang's limit).
69pub 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
73/// Compute the hash for a sequence of tokens, optionally including multimodal metadata
74/// and LoRA adapter identity.
75///
76/// When multimodal extra info is provided, the mm_hashes are included in the hash computation
77/// to ensure that blocks with identical tokens but different multimodal objects produce
78/// different hashes.
79///
80/// When `lora_name` is provided, the adapter name is mixed into the XXH3 seed so that
81/// blocks cached under different LoRA adapters (or the base model) produce distinct hashes.
82/// Because LoRA identity applies uniformly to every block in a sequence, encoding it in the
83/// seed is more efficient than appending per-block bytes and matches the approach used by
84/// KVBM's `SaltHash`.
85pub 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/// Compute the next rolling sequence hash from a parent sequence hash and the
143/// current block hash. Delegates to [`dynamo_tokens::compute_next_sequence_hash`] — the
144/// single source of truth for the chain recurrence shared across kv-router,
145/// kvbm-logical, and the universal hashing crate.
146#[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
154/// Compute rolling sequence hashes for a vector of block hashes.
155///
156/// - The first block's sequence hash equals its block hash
157/// - Subsequent blocks' sequence hash = hash([parent_sequence_hash, current_block_hash], seed)
158pub 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
174/// Trait abstracting the worker configuration fields needed by the scheduling layer.
175///
176/// `ModelRuntimeConfig` (in `lib/llm`) implements this directly so no adapter type is needed.
177pub 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    /// Stable identifier for the worker, preserved across process restarts.
188    ///
189    /// In Kubernetes StatefulSet deployments this is the pod hostname (`worker-0`, `worker-1`,
190    /// …). Used by rendezvous-style routing (HRW hashing) so cache assignments survive worker
191    /// restarts and minimise cache movement when the set of live workers churns. Returns
192    /// `None` when the worker did not publish a stable id, in which case callers should fall
193    /// back to the (ephemeral) `worker_id`.
194    fn stable_routing_id(&self) -> Option<&str> {
195        None
196    }
197
198    /// Returns the worker's topology domain labels (e.g. {"zone": "us-east-1a", "rack": "rack1"}).
199    /// Topology-aware routing turns these labels into canonical worker taints such as
200    /// `dynamo.topology/zone=us-east-1a`.
201    /// Returns `None` by default for backward compatibility.
202    fn topology_domains(&self) -> Option<&HashMap<String, String>> {
203        None
204    }
205
206    /// Returns the topology domain to enforce for KV-cache transfers (e.g. "zone").
207    /// When set, decode worker selection is constrained to workers sharing the same
208    /// topology domain value as the prefill worker.
209    fn kv_transfer_domain(&self) -> Option<&str> {
210        None
211    }
212
213    /// Returns the KV transfer topology enforcement mode.
214    fn kv_transfer_enforcement(&self) -> Option<KvTransferEnforcement> {
215        None
216    }
217
218    /// Returns the taint preference weight used when KV transfer topology enforcement is preferred.
219    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    /// Put the generated topology taint in `RoutingConstraints.required_taints`.
228    Required,
229    /// Put the generated topology taint in `RoutingConstraints.preferred_taints`.
230    Preferred,
231}
232
233/// Request-level taint constraints evaluated against each worker's published taints.
234///
235/// Topology-aware routing uses the same fields with canonical taints such as
236/// `dynamo.topology/zone=us-east-1a`.
237#[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        // Use exp(-tanh(sum)) so equal-magnitude positive and negative preferences
281        // have reciprocal effect around the neutral multiplier 1.0, while keeping the
282        // multiplier strictly positive and bounded to [exp(-1), exp(1)] ~= [0.368, 2.718]
283        // for numerically stable composition with the existing linear work score.
284        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
298/// Transport abstraction for publishing batched router-visible KV cache events.
299pub trait RouterEventSink: Send + Sync {
300    fn publish_event(&self, event: &RouterEvent)
301    -> impl Future<Output = anyhow::Result<()>> + Send;
302}
303
304/// A worker identifier.
305pub type WorkerId = u64;
306
307/// A data parallel rank identifier.
308pub type DpRank = u32;
309
310/// A worker identifier combined with its data parallel rank.
311/// Used for routing decisions in data parallel setups.
312/// dp_rank = 0 indicates either DP not enabled or the first rank.
313#[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    /// Canonical wire-format medium string. `None` for the default GPU tier so
360    /// existing consumers that omit the field continue to round-trip.
361    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        // once prefill completes, the frontend might not be allowed to send a
457        // request with linking the id. In this case, the request_id is provided in the payload.
458        #[serde(default, skip_serializing_if = "Option::is_none")]
459        request_id: Option<String>,
460    },
461    MarkFree {
462        // once request is cancelled, the frontend might not be allowed to send a
463        // request with linking the id. In this case, the request_id is provided in the payload.
464        #[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 of every worker tracked by the scheduler.
516        loads: Vec<PotentialLoad>,
517        // the queue sizes for this specific router instance.
518        #[serde(default)]
519        pending_count: usize,
520        #[serde(default)]
521        pending_isl_tokens: usize,
522    },
523}
524
525#[derive(Debug)]
526pub struct WorkerSelectionResult {
527    /// The full worker information including dp_rank
528    pub worker: WorkerWithDpRank,
529
530    /// The total number of blocks required to prefill the request
531    pub required_blocks: u64,
532
533    /// Approximate effective cache hit on the selected worker in fractional blocks.
534    /// Use `.round() as u32` for a block-count approximation.
535    pub effective_overlap_blocks: f64,
536
537    /// Approximate cached-token count derived from the weighted cache hit.
538    pub cached_tokens: usize,
539}
540
541/// Active load metrics for a worker, used for overload detection.
542///
543/// Published by workers (with `kv_used_blocks`) and by the scheduler (with
544/// `active_decode_blocks` and `active_prefill_tokens`).
545#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
546pub struct ActiveLoad {
547    pub worker_id: WorkerId,
548    #[serde(default)]
549    pub dp_rank: DpRank,
550    /// Scheduler-reported decode block load.
551    pub active_decode_blocks: Option<u64>,
552    /// Number of active prefill tokens (from scheduler's view).
553    pub active_prefill_tokens: Option<u64>,
554    /// Total KV blocks currently in use on the worker.
555    ///
556    /// This is published by workers only and is the authoritative signal for
557    /// backend KV occupancy used by overload detection.
558    #[serde(default)]
559    pub kv_used_blocks: Option<u64>,
560}
561
562/// A [`LocalBlockHash`] is a hash computed from the token IDs, optional multimodal metadata,
563/// and optional LoRA adapter name of a block.
564#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
565pub struct LocalBlockHash(pub u64);
566
567/// A sequence-aware hash of a block computed by the engine from token IDs, optional metadata,
568/// and the hash of the parent block.
569///
570/// In this case, the hashing function is external and unknown.
571#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
572pub struct ExternalSequenceBlockHash(pub u64);
573
574// Implement From trait for convenient conversion
575impl From<u64> for ExternalSequenceBlockHash {
576    fn from(value: u64) -> Self {
577        Self(value)
578    }
579}
580
581impl From<i64> for ExternalSequenceBlockHash {
582    /// Bitwise reinterpretation: preserves all bits, including negatives.
583    /// This is lossless, but negative i64 values will appear as large u64 values.
584    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/// Represents the different stages of prefilling tokens for a request.
598///
599/// Each variant contains a `usize` representing the number of tokens
600/// that are pending prefill in the request.
601#[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    // NOTE: Output-block growth is intentionally not a replica-sync event. It can occur
635    // at high frequency, and broadcasting it would consume disproportionate network bandwidth.
636    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/// Represents a collection of cache events and a shutdown flag.
653#[derive(Serialize, Deserialize, Debug, Clone)]
654pub struct KvCacheEvents {
655    /// A list of cache events.
656    pub events: Vec<KvCacheEvent>,
657    /// A flag indicating whether the cache is shutting down.
658    pub shutdown: bool,
659}
660
661/// Represents a single cache event with an ID and associated data.
662#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
663pub struct KvCacheEvent {
664    /// The unique identifier of the event.
665    pub event_id: u64,
666    /// The data associated with the event.
667    pub data: KvCacheEventData,
668    /// The data parallel rank of the worker emitting this event (0 if DP not enabled).
669    #[serde(default)]
670    pub dp_rank: DpRank,
671}
672
673/// Represents the data associated with a cache event.
674///
675/// Data is either stored or removed.
676#[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/// Represents the data associated with a stored cache event.
685#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
686pub struct KvCacheStoreData {
687    /// The optional hash of the parent block.
688    pub parent_hash: Option<ExternalSequenceBlockHash>,
689    /// Absolute position of the first block in this batch for positional replay.
690    #[serde(default)]
691    pub start_position: Option<u32>,
692    /// A list of stored blocked data.
693    pub blocks: Vec<KvCacheStoredBlockData>,
694}
695
696/// Multimodal object information within a block.
697/// Offsets are relative to the block (0 to block_size-1).
698#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
699pub struct BlockMmObjectInfo {
700    /// Hash identifying this multimodal object
701    pub mm_hash: u64,
702    /// Token offset ranges where this MM object's placeholders appear within THIS block
703    /// Each tuple is (start_offset, end_offset) relative to block start
704    pub offsets: Vec<(usize, usize)>,
705}
706
707/// Extra metadata for a block containing multimodal objects
708#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
709pub struct BlockExtraInfo {
710    /// All multimodal objects referenced in this block
711    pub mm_objects: Vec<BlockMmObjectInfo>,
712}
713
714/// Request-level multimodal object information.
715/// Offsets are relative to the entire request token sequence.
716#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
717pub struct RequestMmObjectInfo {
718    /// Hash identifying this multimodal object
719    pub mm_hash: u64,
720    /// Token offset ranges where this MM object's placeholders appear in the ENTIRE request
721    /// Each tuple is (start_offset, end_offset) relative to request start
722    pub offsets: Vec<(usize, usize)>,
723}
724
725/// Request-level multimodal metadata
726#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
727pub struct RequestExtraInfo {
728    /// All multimodal objects in this request
729    pub mm_objects: Vec<RequestMmObjectInfo>,
730}
731
732impl RequestExtraInfo {
733    /// Convert request-level MM info to block-level MM info for a sequence of blocks.
734    ///
735    /// This function splits request-level offsets (relative to the entire request token sequence)
736    /// into block-level offsets (relative to each block).
737    ///
738    /// # Arguments
739    /// * `block_size` - The size of each block in tokens
740    /// * `total_tokens` - Total number of tokens in the request
741    ///
742    /// # Returns
743    /// A vector of `Option<BlockExtraInfo>` where each element corresponds to a block.
744    /// `None` indicates a block with no multimodal objects.
745    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                // Find which blocks this offset range spans
756                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                    // Calculate the intersection of this MM object's range with this block
770                    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                        // Check if we already have this mm_hash in this block
778                        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                            // Add the offset range to existing object
784                            existing.offsets.push((local_start, local_end));
785                        } else {
786                            // Create new MM object entry for this block
787                            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/// Represents data for a stored block.
802#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
803pub struct KvCacheStoredBlockData {
804    /// The hash of the block.
805    pub block_hash: ExternalSequenceBlockHash,
806    /// The hash of the tokens in the block.
807    pub tokens_hash: LocalBlockHash,
808    /// Extra multimodal metadata for this block
809    /// Note: Do NOT use skip_serializing_if with bincode - it breaks deserialization
810    /// because bincode is positional and expects all fields to be present.
811    #[serde(default)]
812    pub mm_extra_info: Option<BlockExtraInfo>,
813}
814
815/// Represents the data associated with a removed cache event.
816#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
817pub struct KvCacheRemoveData {
818    /// A list of block hashes to remove.
819    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// ------
861// Router Event Types
862// ------
863
864/// Errors that can occur during KV Cache Event processing.
865#[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/// A [`KvCacheEvent`] on a specific LLM worker denoted by [`WorkerId`].
878#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
879pub struct RouterEvent {
880    /// The ID of the worker emitting the event.
881    pub worker_id: WorkerId,
882    /// The storage tier associated with the event.
883    #[serde(default)]
884    pub storage_tier: StorageTier,
885    /// The cache event associated with the worker.
886    pub event: KvCacheEvent,
887}
888
889impl RouterEvent {
890    /// Create a new `RouterEvent`.
891    ///
892    /// ### Arguments
893    ///
894    /// * `worker_id` - The ID of the worker emitting the event.
895    /// * `event` - The cache event.
896    ///
897    /// ### Returns
898    ///
899    /// A new `RouterEvent`.
900    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/// Shared cache hit information, represented as sorted non-overlapping half-open ranges.
918///
919/// Ranges encode which block positions exist in the external shared KV cache pool.
920/// Using ranges instead of `Vec<bool>` avoids iterating over potentially thousands
921/// of blocks per worker. Typical shared cache patterns produce few contiguous regions,
922/// making `hits_beyond` O(num_ranges) ~ O(1-5).
923#[derive(Debug, Clone, Default)]
924pub struct SharedCacheHits {
925    /// Ranges of block positions that exist in the shared cache.
926    /// Half-open ranges [start, end), sorted and non-overlapping.
927    pub ranges: Vec<Range<u32>>,
928    /// Total number of hits (sum of range lengths).
929    pub total_hits: u32,
930}
931
932impl SharedCacheHits {
933    /// Create from sorted, non-overlapping ranges.
934    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    /// Create from a boolean hit vector (convenience for tests and simple backends).
940    /// Coalesces consecutive `true` entries into ranges.
941    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    /// Count hits at positions >= `from_position`.
959    /// O(num_ranges), not O(num_blocks).
960    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/// Scores representing the overlap of workers (with their dp_rank).
977#[derive(Debug, Clone, Serialize, Deserialize)]
978pub struct OverlapScores {
979    /// Map of worker (with dp_rank) to score.
980    pub scores: FxHashMap<WorkerWithDpRank, u32>,
981    /// List of frequencies that the blocks have been accessed. Entries with value 0 are omitted.
982    pub frequencies: Vec<usize>,
983}
984
985impl Default for OverlapScores {
986    fn default() -> Self {
987        Self::new()
988    }
989}
990
991impl OverlapScores {
992    /// Create a new `OverlapScores`.
993    ///
994    /// ### Returns
995    ///
996    /// A new `OverlapScores`.
997    pub fn new() -> Self {
998        Self {
999            scores: FxHashMap::default(),
1000            frequencies: Vec::new(),
1001        }
1002    }
1003
1004    /// Update the scores with a set of workers.
1005    ///
1006    /// ### Arguments
1007    ///
1008    /// * `workers` - An iterator over `WorkerWithDpRank` references.
1009    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// ------
1021// TokensWithHashes
1022// ------
1023
1024/// A container for tokens with lazily computed block and sequence hashes.
1025///
1026/// This struct avoids redundant hash computations by caching results:
1027/// - `get_or_compute_block_hashes()` computes block hashes if not cached
1028/// - `get_or_compute_seq_hashes()` computes seq hashes if not cached,
1029///   and will also compute block hashes first if needed (since seq hashes depend on them)
1030#[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    /// Creates a new TokensWithHashes from tokens and block size.
1043    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    /// Adds multimodal extra info for blocks.
1056    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    /// Sets the LoRA adapter name for hash computation.
1063    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    /// Sets Eagle hashing semantics for this token sequence.
1070    pub fn with_is_eagle(mut self, is_eagle: bool) -> Self {
1071        self.set_is_eagle(is_eagle);
1072        self
1073    }
1074
1075    /// Updates Eagle hashing semantics and invalidates cached hashes when it changes.
1076    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    /// Returns a reference to the tokens.
1092    pub fn tokens(&self) -> &[u32] {
1093        &self.tokens
1094    }
1095
1096    /// Returns the number of tokens.
1097    pub fn len(&self) -> usize {
1098        self.tokens.len()
1099    }
1100
1101    /// Returns true if there are no tokens.
1102    pub fn is_empty(&self) -> bool {
1103        self.tokens.is_empty()
1104    }
1105
1106    /// Returns the block size.
1107    pub fn block_size(&self) -> u32 {
1108        self.block_size
1109    }
1110
1111    /// Returns the multimodal extra info, if set.
1112    pub fn block_mm_infos(&self) -> Option<&[Option<BlockExtraInfo>]> {
1113        self.block_mm_infos.as_deref()
1114    }
1115
1116    /// Returns block hashes, computing them if not already cached.
1117    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    /// Returns sequence hashes, computing them if not already cached.
1133    /// This will also compute block hashes if they haven't been computed yet,
1134    /// since sequence hashes depend on block hashes.
1135    pub fn get_or_compute_seq_hashes(&mut self) -> &[SequenceHash] {
1136        if self.seq_hashes.is_none() {
1137            // Ensure block hashes are computed first
1138            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    /// Returns cached block hashes without computing. Returns None if not yet computed.
1145    pub fn block_hashes(&self) -> Option<&[LocalBlockHash]> {
1146        self.block_hashes.as_deref()
1147    }
1148
1149    /// Returns cached seq hashes without computing. Returns None if not yet computed.
1150    pub fn seq_hashes(&self) -> Option<&[SequenceHash]> {
1151        self.seq_hashes.as_deref()
1152    }
1153}
1154
1155// ------
1156// Tests
1157// ------
1158#[cfg(test)]
1159mod tests {
1160    use super::*;
1161    use rstest::rstest;
1162    use serde_json;
1163
1164    /// Pin the sglang pad_value constants and formula against upstream
1165    /// `MultimodalItem._compute_pad_value`. If sglang bumps a constant, this
1166    /// fails — otherwise routing-side pad_value would silently diverge from
1167    /// sglang's `BlockStored` bytes and MM-routing would degrade to text-prefix.
1168    #[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        // All hits contiguous
1472        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        // Sparse hits
1477        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        // No hits
1482        let hits = SharedCacheHits::from_hits(&[false, false, false]);
1483        assert!(hits.ranges.is_empty());
1484        assert_eq!(hits.total_hits, 0);
1485
1486        // Empty
1487        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        // Shared has [A, B, C, D] => range 0..4
1495        #[allow(clippy::single_range_in_vec_init)]
1496        let hits = SharedCacheHits::from_ranges(vec![0..4]);
1497
1498        // Device has overlap=2 (positions 0,1 on device) => shared_beyond should count positions 2,3
1499        assert_eq!(hits.hits_beyond(2), 2);
1500
1501        // Device has overlap=0 => all 4 shared hits count
1502        assert_eq!(hits.hits_beyond(0), 4);
1503
1504        // Device has overlap=4 => nothing beyond
1505        assert_eq!(hits.hits_beyond(4), 0);
1506
1507        // Device overlap exceeds range
1508        assert_eq!(hits.hits_beyond(10), 0);
1509    }
1510
1511    #[test]
1512    fn test_shared_cache_hits_beyond_sparse() {
1513        // Ranges: [1..3, 5..8] => positions 1,2,5,6,7
1514        let hits = SharedCacheHits::from_ranges(vec![1..3, 5..8]);
1515        assert_eq!(hits.total_hits, 5);
1516
1517        // from_position=0 => all 5 hits
1518        assert_eq!(hits.hits_beyond(0), 5);
1519        // from_position=2 => pos 2 (from first range) + 5,6,7 (from second) = 4
1520        assert_eq!(hits.hits_beyond(2), 4);
1521        // from_position=3 => only second range: 3 hits
1522        assert_eq!(hits.hits_beyond(3), 3);
1523        // from_position=6 => positions 6,7 from second range = 2
1524        assert_eq!(hits.hits_beyond(6), 2);
1525        // from_position=8 => nothing
1526        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        // A minimal implementor that does NOT override topology_domains()
1545        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}