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 cache_namespace: Option<&'a str>,
35    pub is_eagle: Option<bool>,
36}
37
38fn block_hash_seed(options: BlockHashOptions<'_>) -> u64 {
39    dynamo_kv_hashing::compute_salt_hash(options.cache_namespace, options.lora_name)
40        .expect("string salt derivation is infallible")
41}
42
43#[inline]
44fn hash_block_no_mm(chunk: &[u32], seed: u64, scratch_bytes: &mut Vec<u8>) -> LocalBlockHash {
45    #[cfg(target_endian = "little")]
46    {
47        let _ = scratch_bytes;
48        // SAFETY: `u32` is plain-old-data, and on little-endian targets its in-memory
49        // representation matches the `to_le_bytes()` sequence used for hashing.
50        let chunk_bytes = unsafe {
51            std::slice::from_raw_parts(chunk.as_ptr().cast::<u8>(), std::mem::size_of_val(chunk))
52        };
53        LocalBlockHash(xxh3::xxh3_64_with_seed(chunk_bytes, seed))
54    }
55
56    #[cfg(not(target_endian = "little"))]
57    {
58        scratch_bytes.clear();
59        for &token in chunk {
60            scratch_bytes.extend_from_slice(&token.to_le_bytes());
61        }
62        LocalBlockHash(xxh3::xxh3_64_with_seed(scratch_bytes, seed))
63    }
64}
65
66/// sglang's `MultimodalItem._compute_pad_value` constants — must track upstream;
67/// if they drift, MM routing silently degrades to text-prefix. Pinned by
68/// `pad_value_matches_sglang_protocol`.
69pub const MM_PAD_SHIFT_VALUE: u64 = 1_000_000;
70pub const MM_PAD_HASH_MASK: u64 = (1 << 30) - 1;
71
72/// Canonical per-image pad_value from a routing-side `mm_hash`, called by both
73/// the frontend and the kv-router so request- and event-side hashes agree.
74/// Keeps the low 30 bits only (sglang's limit).
75pub fn pad_value_for_mm_hash(mm_hash: u64) -> u32 {
76    (MM_PAD_SHIFT_VALUE + (mm_hash & MM_PAD_HASH_MASK)) as u32
77}
78
79/// Compute the hash for a sequence of tokens, optionally including multimodal metadata,
80/// LoRA adapter identity, and cache namespace.
81///
82/// When multimodal extra info is provided, the mm_hashes are included in the hash computation
83/// to ensure that blocks with identical tokens but different multimodal objects produce
84/// different hashes.
85///
86/// When `lora_name` or `cache_namespace` is provided, those request-wide identities are
87/// mixed into the XXH3 seed so blocks cached under different adapters or namespaces produce
88/// distinct hashes. Empty strings are treated as absent.
89pub fn compute_block_hash_for_seq(
90    tokens: &[u32],
91    kv_block_size: u32,
92    options: BlockHashOptions<'_>,
93) -> Vec<LocalBlockHash> {
94    compute_block_hash_for_seq_with_seed(tokens, kv_block_size, options, block_hash_seed(options))
95}
96
97/// Count complete canonical blocks for normal and Eagle token windows.
98pub(crate) fn complete_block_count(
99    token_count: usize,
100    kv_block_size: u32,
101    is_eagle: bool,
102) -> usize {
103    let stride = kv_block_size as usize;
104    if stride == 0 {
105        return 0;
106    }
107    if is_eagle {
108        token_count.saturating_sub(1) / stride
109    } else {
110        token_count / stride
111    }
112}
113
114/// Compute local block hashes with an explicit XXH3 seed while preserving the
115/// canonical token, multimodal, and Eagle encodings used by the public hash path.
116pub(crate) fn compute_block_hash_for_seq_with_seed(
117    tokens: &[u32],
118    kv_block_size: u32,
119    options: BlockHashOptions<'_>,
120    seed: u64,
121) -> Vec<LocalBlockHash> {
122    let estimated_blocks = complete_block_count(
123        tokens.len(),
124        kv_block_size,
125        options.is_eagle.unwrap_or(false),
126    );
127    let mut hashes = Vec::with_capacity(estimated_blocks);
128    for_each_block_hash_for_seq_with_seed(tokens, kv_block_size, options, seed, |hash| {
129        hashes.push(hash);
130    });
131    hashes
132}
133
134fn for_each_block_hash_for_seq_with_seed(
135    tokens: &[u32],
136    kv_block_size: u32,
137    options: BlockHashOptions<'_>,
138    seed: u64,
139    mut visit: impl FnMut(LocalBlockHash),
140) {
141    if kv_block_size == 0 {
142        return;
143    }
144
145    let is_eagle_flag = options.is_eagle.unwrap_or(false);
146    let stride = kv_block_size as usize;
147    let window_size = if is_eagle_flag { stride + 1 } else { stride };
148    let mut bytes = Vec::with_capacity(window_size * std::mem::size_of::<u32>());
149    let mut mm_hashes = Vec::new();
150    let mut block_idx = 0;
151    let mut start = 0;
152
153    while start + window_size <= tokens.len() {
154        let chunk = &tokens[start..start + window_size];
155        if let Some(mm_infos) = options.block_mm_infos
156            && let Some(Some(block_mm_info)) = mm_infos.get(block_idx)
157        {
158            bytes.clear();
159            for &token in chunk {
160                bytes.extend_from_slice(&token.to_le_bytes());
161            }
162
163            mm_hashes.clear();
164            mm_hashes.extend(block_mm_info.mm_objects.iter().map(|obj| obj.mm_hash));
165            mm_hashes.sort_unstable();
166
167            for &mm_hash in &mm_hashes {
168                bytes.extend_from_slice(&mm_hash.to_le_bytes());
169            }
170
171            visit(LocalBlockHash(xxh3::xxh3_64_with_seed(&bytes, seed)));
172        } else {
173            visit(hash_block_no_mm(chunk, seed, &mut bytes));
174        }
175
176        start += stride;
177        block_idx += 1;
178    }
179}
180
181/// Compute the next rolling sequence hash from a parent sequence hash and the
182/// current block hash. Delegates to [`dynamo_tokens::compute_next_sequence_hash`] — the
183/// single source of truth for the chain recurrence shared across kv-router,
184/// kvbm-logical, and the universal hashing crate.
185#[inline]
186pub fn compute_next_seq_hash(
187    parent_seq_hash: SequenceHash,
188    current_block_hash: LocalBlockHash,
189) -> SequenceHash {
190    compute_next_sequence_hash(parent_seq_hash, current_block_hash.0)
191}
192
193/// Compute rolling sequence hashes for a vector of block hashes.
194///
195/// - The first block's sequence hash equals its block hash
196/// - Subsequent blocks' sequence hash = hash([parent_sequence_hash, current_block_hash], seed)
197pub fn compute_seq_hash_for_block(block_hashes: &[LocalBlockHash]) -> Vec<SequenceHash> {
198    compute_seq_hash_for_block_with(block_hashes, compute_next_seq_hash)
199}
200
201/// Compute rolling sequence hashes directly from canonical token blocks with
202/// separate XXH3 block and chain seeds, without materializing block hashes.
203pub(crate) fn compute_seq_hash_for_tokens_with_seeds(
204    tokens: &[u32],
205    kv_block_size: u32,
206    options: BlockHashOptions<'_>,
207    block_seed: u64,
208    chain_seed: u64,
209) -> Vec<SequenceHash> {
210    let estimated_blocks = complete_block_count(
211        tokens.len(),
212        kv_block_size,
213        options.is_eagle.unwrap_or(false),
214    );
215    let mut sequence_hashes = Vec::with_capacity(estimated_blocks);
216    for_each_block_hash_for_seq_with_seed(
217        tokens,
218        kv_block_size,
219        options,
220        block_seed,
221        |block_hash| {
222            let sequence_hash = sequence_hashes
223                .last()
224                .copied()
225                .map_or(block_hash.0, |parent| {
226                    compute_next_seq_hash_with_seed(parent, block_hash, chain_seed)
227                });
228            sequence_hashes.push(sequence_hash);
229        },
230    );
231    sequence_hashes
232}
233
234#[inline]
235fn compute_next_seq_hash_with_seed(
236    parent: SequenceHash,
237    block: LocalBlockHash,
238    seed: u64,
239) -> SequenceHash {
240    let mut bytes = [0_u8; 16];
241    bytes[..8].copy_from_slice(&parent.to_le_bytes());
242    bytes[8..].copy_from_slice(&block.0.to_le_bytes());
243    xxh3::xxh3_64_with_seed(&bytes, seed)
244}
245
246fn compute_seq_hash_for_block_with(
247    block_hashes: &[LocalBlockHash],
248    next_hash: impl Fn(SequenceHash, LocalBlockHash) -> SequenceHash,
249) -> Vec<SequenceHash> {
250    if block_hashes.is_empty() {
251        return Vec::new();
252    }
253
254    let mut sequence_hashes = Vec::with_capacity(block_hashes.len());
255    sequence_hashes.push(block_hashes[0].0);
256
257    for i in 1..block_hashes.len() {
258        let parent_seq_hash = sequence_hashes[i - 1];
259        sequence_hashes.push(next_hash(parent_seq_hash, block_hashes[i]));
260    }
261
262    sequence_hashes
263}
264
265/// Trait abstracting the worker configuration fields needed by the scheduling layer.
266///
267/// `ModelRuntimeConfig` (in `lib/llm`) implements this directly so no adapter type is needed.
268pub trait WorkerConfigLike {
269    fn data_parallel_start_rank(&self) -> u32;
270    fn data_parallel_size(&self) -> u32;
271    fn max_num_batched_tokens(&self) -> Option<u64>;
272    fn total_kv_blocks(&self) -> Option<u64>;
273
274    /// Tokens retained by the backend's native KV offloading tier, if available.
275    fn native_offloading_capacity_tokens(&self) -> Option<u64> {
276        None
277    }
278
279    fn taints(&self) -> &HashSet<String> {
280        &EMPTY_WORKER_TAINTS
281    }
282
283    /// Stable identifier for the worker, preserved across process restarts.
284    ///
285    /// In Kubernetes StatefulSet deployments this is the pod hostname (`worker-0`, `worker-1`,
286    /// …). Used by rendezvous-style routing (HRW hashing) so cache assignments survive worker
287    /// restarts and minimise cache movement when the set of live workers churns. Returns
288    /// `None` when the worker did not publish a stable id, in which case callers should fall
289    /// back to the (ephemeral) `worker_id`.
290    fn stable_routing_id(&self) -> Option<&str> {
291        None
292    }
293
294    /// Returns the worker's topology domain labels (e.g. {"zone": "us-east-1a", "rack": "rack1"}).
295    /// Topology-aware routing turns these labels into canonical worker taints such as
296    /// `dynamo.topology/zone=us-east-1a`.
297    /// Returns `None` by default for backward compatibility.
298    fn topology_domains(&self) -> Option<&HashMap<String, String>> {
299        None
300    }
301
302    /// Returns the topology domain to enforce for KV-cache transfers (e.g. "zone").
303    /// When set, decode worker selection is constrained to workers sharing the same
304    /// topology domain value as the prefill worker.
305    fn kv_transfer_domain(&self) -> Option<&str> {
306        None
307    }
308
309    /// Returns the KV transfer topology enforcement mode.
310    fn kv_transfer_enforcement(&self) -> Option<KvTransferEnforcement> {
311        None
312    }
313
314    /// Returns the taint preference weight used when KV transfer topology enforcement is preferred.
315    fn kv_transfer_preferred_weight(&self) -> Option<f32> {
316        None
317    }
318}
319
320#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
321#[serde(rename_all = "snake_case")]
322pub enum KvTransferEnforcement {
323    /// Put the generated topology taint in `RoutingConstraints.required_taints`.
324    Required,
325    /// Put the generated topology taint in `RoutingConstraints.preferred_taints`.
326    Preferred,
327}
328
329/// Request-level taint constraints evaluated against each worker's published taints.
330///
331/// Topology-aware routing uses the same fields with canonical taints such as
332/// `dynamo.topology/zone=us-east-1a`.
333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
334pub struct RoutingConstraints {
335    #[serde(default, skip_serializing_if = "HashSet::is_empty")]
336    pub required_taints: HashSet<String>,
337    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
338    pub preferred_taints: HashMap<String, f32>,
339}
340
341impl RoutingConstraints {
342    pub fn is_empty(&self) -> bool {
343        self.required_taints.is_empty() && self.preferred_taints.is_empty()
344    }
345
346    pub fn has_hard_constraints(&self) -> bool {
347        !self.required_taints.is_empty()
348    }
349
350    pub fn is_compatible_with_worker_taints(&self, worker_taints: &HashSet<String>) -> bool {
351        if self.required_taints.is_empty() {
352            return true;
353        }
354
355        self.required_taints
356            .iter()
357            .all(|taint| worker_taints.contains(taint))
358    }
359
360    pub fn preferred_taint_matches(&self, worker_taints: &HashSet<String>) -> usize {
361        if self.preferred_taints.is_empty() {
362            return 0;
363        }
364
365        self.preferred_taints
366            .keys()
367            .filter(|taint| worker_taints.contains(*taint))
368            .count()
369    }
370
371    pub fn preferred_taint_multiplier(&self, worker_taints: &HashSet<String>) -> Option<f64> {
372        if self.preferred_taints.is_empty() {
373            return None;
374        }
375
376        // Use exp(-tanh(sum)) so equal-magnitude positive and negative preferences
377        // have reciprocal effect around the neutral multiplier 1.0, while keeping the
378        // multiplier strictly positive and bounded to [exp(-1), exp(1)] ~= [0.368, 2.718]
379        // for numerically stable composition with the existing linear work score.
380        let bias = self
381            .preferred_taints
382            .iter()
383            .filter(|(taint, _)| worker_taints.contains(*taint))
384            .map(|(_, weight)| f64::from(*weight))
385            .sum::<f64>()
386            .tanh();
387
388        Some((-bias).exp())
389    }
390}
391
392static EMPTY_WORKER_TAINTS: LazyLock<HashSet<String>> = LazyLock::new(HashSet::new);
393
394/// Transport abstraction for publishing batched router-visible KV cache events.
395pub trait RouterEventSink: Send + Sync {
396    fn publish_event(&self, event: &RouterEvent)
397    -> impl Future<Output = anyhow::Result<()>> + Send;
398}
399
400/// A worker identifier.
401pub type WorkerId = u64;
402
403/// A data parallel rank identifier.
404pub type DpRank = u32;
405
406/// A worker identifier combined with its data parallel rank.
407/// Used for routing decisions in data parallel setups.
408/// dp_rank = 0 indicates either DP not enabled or the first rank.
409#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
410pub struct WorkerWithDpRank {
411    pub worker_id: WorkerId,
412    pub dp_rank: DpRank,
413}
414
415impl WorkerWithDpRank {
416    pub fn new(worker_id: WorkerId, dp_rank: DpRank) -> Self {
417        Self { worker_id, dp_rank }
418    }
419
420    pub fn from_worker_id(worker_id: WorkerId) -> Self {
421        Self {
422            worker_id,
423            dp_rank: 0,
424        }
425    }
426}
427
428#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
429#[serde(rename_all = "snake_case")]
430pub enum StorageTier {
431    #[default]
432    Device,
433    HostPinned,
434    Disk,
435    External,
436}
437
438impl StorageTier {
439    pub fn from_kv_medium(medium: &str) -> Option<Self> {
440        match medium {
441            "GPU" | "DEVICE" => Some(Self::Device),
442            "CPU" | "CPU_PINNED" | "CPU_TIER1" => Some(Self::HostPinned),
443            "CPU_TIER2" | "DISK" | "NVME" => Some(Self::Disk),
444            "EXTERNAL" | "NETWORK" | "REMOTE" | "SHARED" => Some(Self::External),
445            _ => None,
446        }
447    }
448
449    pub fn from_kv_medium_or_default(medium: Option<&str>) -> Self {
450        medium
451            .and_then(Self::from_kv_medium)
452            .unwrap_or(Self::Device)
453    }
454
455    /// Canonical wire-format medium string. `None` for the default GPU tier so
456    /// existing consumers that omit the field continue to round-trip.
457    pub fn to_kv_medium(self) -> Option<&'static str> {
458        match self {
459            Self::Device => None,
460            Self::HostPinned => Some("CPU_PINNED"),
461            Self::Disk => Some("DISK"),
462            Self::External => Some("EXTERNAL"),
463        }
464    }
465
466    pub fn is_gpu(self) -> bool {
467        matches!(self, Self::Device)
468    }
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
472pub enum PlacementOwner {
473    LocalWorker(WorkerWithDpRank),
474    Shared,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
478pub struct Placement {
479    pub owner: PlacementOwner,
480    pub tier: StorageTier,
481}
482
483impl Placement {
484    pub fn local_worker(worker_id: WorkerId, dp_rank: DpRank, tier: StorageTier) -> Self {
485        Self {
486            owner: PlacementOwner::LocalWorker(WorkerWithDpRank::new(worker_id, dp_rank)),
487            tier,
488        }
489    }
490
491    pub fn local_gpu(worker_id: WorkerId, dp_rank: DpRank) -> Self {
492        Self::local_worker(worker_id, dp_rank, StorageTier::Device)
493    }
494
495    pub fn is_local_gpu(&self) -> bool {
496        matches!(self.owner, PlacementOwner::LocalWorker(_)) && self.tier.is_gpu()
497    }
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
501pub struct PlacementEvent {
502    pub placement: Placement,
503    pub event: KvCacheEvent,
504}
505
506impl PlacementEvent {
507    pub fn new(placement: Placement, event: KvCacheEvent) -> Self {
508        Self { placement, event }
509    }
510
511    pub fn local_gpu(worker_id: WorkerId, event: KvCacheEvent) -> Self {
512        Self::new(Placement::local_gpu(worker_id, event.dp_rank), event)
513    }
514
515    pub fn into_router_event(self) -> Option<RouterEvent> {
516        let PlacementOwner::LocalWorker(worker) = self.placement.owner else {
517            return None;
518        };
519        Some(RouterEvent::with_storage_tier(
520            worker.worker_id,
521            self.event,
522            self.placement.tier,
523        ))
524    }
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
528#[serde(tag = "method", rename_all = "snake_case")]
529pub enum RouterRequest {
530    #[serde(rename = "new")]
531    New {
532        tokens: Vec<Token>,
533        #[serde(default, skip_serializing_if = "Option::is_none")]
534        block_mm_infos: Option<Vec<Option<BlockExtraInfo>>>,
535        #[serde(default, skip_serializing_if = "RoutingConstraints::is_empty")]
536        routing_constraints: RoutingConstraints,
537        #[serde(default)]
538        priority_jump: f64,
539        #[serde(default, skip_serializing_if = "is_zero")]
540        strict_priority: u32,
541        #[serde(default, skip_serializing_if = "Option::is_none")]
542        lora_name: Option<String>,
543        #[serde(default, skip_serializing_if = "Option::is_none")]
544        cache_namespace: Option<String>,
545    },
546    PotentialLoads {
547        tokens: Vec<Token>,
548        #[serde(default, skip_serializing_if = "Option::is_none")]
549        block_mm_infos: Option<Vec<Option<BlockExtraInfo>>>,
550        #[serde(default, skip_serializing_if = "Option::is_none")]
551        lora_name: Option<String>,
552        #[serde(default, skip_serializing_if = "Option::is_none")]
553        cache_namespace: Option<String>,
554    },
555    MarkPrefill {
556        // once prefill completes, the frontend might not be allowed to send a
557        // request with linking the id. In this case, the request_id is provided in the payload.
558        #[serde(default, skip_serializing_if = "Option::is_none")]
559        request_id: Option<String>,
560    },
561    MarkFree {
562        // once request is cancelled, the frontend might not be allowed to send a
563        // request with linking the id. In this case, the request_id is provided in the payload.
564        #[serde(default, skip_serializing_if = "Option::is_none")]
565        request_id: Option<String>,
566    },
567}
568
569impl Default for RouterRequest {
570    fn default() -> Self {
571        RouterRequest::New {
572            tokens: vec![],
573            block_mm_infos: None,
574            routing_constraints: RoutingConstraints::default(),
575            priority_jump: 0.0,
576            strict_priority: 0,
577            lora_name: None,
578            cache_namespace: None,
579        }
580    }
581}
582
583fn is_zero(value: &u32) -> bool {
584    *value == 0
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct PotentialLoad {
589    pub worker_id: WorkerId,
590    pub dp_rank: DpRank,
591    pub potential_prefill_tokens: usize,
592    pub potential_decode_blocks: usize,
593    #[serde(default)]
594    pub active_requests: usize,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize)]
598#[serde(tag = "method", rename_all = "snake_case")]
599pub enum RouterResponse {
600    New {
601        worker_id: WorkerId,
602        #[serde(default)]
603        dp_rank: DpRank,
604        overlap_blocks: u32,
605    },
606    QueueRejected {
607        rejection: crate::scheduling::QueueRejection,
608    },
609    PrefillMarked {
610        success: bool,
611    },
612    FreeMarked {
613        success: bool,
614    },
615    PotentialLoads {
616        // loads of every worker tracked by the scheduler.
617        loads: Vec<PotentialLoad>,
618        // the queue sizes for this specific router instance.
619        #[serde(default)]
620        pending_count: usize,
621        #[serde(default)]
622        pending_isl_tokens: usize,
623    },
624}
625
626#[derive(Debug)]
627pub struct WorkerSelectionResult {
628    /// The full worker information including dp_rank
629    pub worker: WorkerWithDpRank,
630
631    /// The total number of blocks required to prefill the request
632    pub required_blocks: u64,
633
634    /// Approximate effective cache hit on the selected worker in fractional blocks.
635    /// Use `.round() as u32` for a block-count approximation.
636    pub effective_overlap_blocks: f64,
637
638    /// Approximate cached-token count derived from the weighted cache hit.
639    pub cached_tokens: usize,
640
641    /// Selected worker's projected decode load after adding this request's
642    /// prompt blocks, in scheduler-tracked block units.
643    pub potential_decode_blocks: usize,
644}
645
646/// Active load metrics for a worker, used for overload detection.
647///
648/// Published by workers (with `kv_used_blocks`) and by the scheduler (with
649/// `active_decode_blocks` and `active_prefill_tokens`).
650#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
651pub struct ActiveLoad {
652    pub worker_id: WorkerId,
653    #[serde(default)]
654    pub dp_rank: DpRank,
655    /// Scheduler-reported decode block load.
656    pub active_decode_blocks: Option<u64>,
657    /// Number of active prefill tokens (from scheduler's view).
658    pub active_prefill_tokens: Option<u64>,
659    /// Total KV blocks currently in use on the worker.
660    ///
661    /// This is published by workers only and is the authoritative signal for
662    /// backend KV occupancy used by overload detection.
663    #[serde(default)]
664    pub kv_used_blocks: Option<u64>,
665}
666
667/// A [`LocalBlockHash`] is a hash computed from the token IDs, optional multimodal metadata,
668/// and optional LoRA adapter name of a block.
669#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
670pub struct LocalBlockHash(pub u64);
671
672/// A sequence-aware hash of a block computed by the engine from token IDs, optional metadata,
673/// and the hash of the parent block.
674///
675/// In this case, the hashing function is external and unknown.
676#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
677pub struct ExternalSequenceBlockHash(pub u64);
678
679// Implement From trait for convenient conversion
680impl From<u64> for ExternalSequenceBlockHash {
681    fn from(value: u64) -> Self {
682        Self(value)
683    }
684}
685
686impl From<i64> for ExternalSequenceBlockHash {
687    /// Bitwise reinterpretation: preserves all bits, including negatives.
688    /// This is lossless, but negative i64 values will appear as large u64 values.
689    fn from(value: i64) -> Self {
690        Self(value as u64)
691    }
692}
693
694#[derive(Serialize, Deserialize, Debug, Clone)]
695pub struct PrefillEvent {
696    pub request_id: String,
697    pub worker_id: WorkerId,
698    pub data: PrefillEventData,
699    pub router_id: u64,
700}
701
702/// Represents the different stages of prefilling tokens for a request.
703///
704/// Each variant contains a `usize` representing the number of tokens
705/// that are pending prefill in the request.
706#[derive(Serialize, Deserialize, Debug, Clone)]
707pub enum PrefillEventData {
708    NewPrefill(usize),
709    UpdatePrefill(usize),
710    CompletePrefill,
711}
712
713#[derive(Serialize, Deserialize, Debug, Clone)]
714pub struct ActiveSequenceEvent {
715    pub request_id: String,
716    pub worker: WorkerWithDpRank,
717    pub data: ActiveSequenceEventData,
718    pub router_id: u64,
719    #[serde(default)]
720    pub lora_name: Option<String>,
721}
722
723/// Active-sequence lifecycle events carried in publisher-queue arrival order.
724#[derive(Serialize, Deserialize, Debug, Clone)]
725pub struct ActiveSequenceEventBatch {
726    pub events: Vec<ActiveSequenceEvent>,
727}
728
729/// Shared cooperative batch limits for active-sequence replica sync.
730pub const MAX_REPLICA_BATCH_EVENTS: usize = 256;
731pub const MAX_REPLICA_BATCH_DURATION: Duration = Duration::from_millis(1);
732
733#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
734pub struct PrefillLoadHint {
735    pub initial_effective_prefill_tokens: usize,
736    pub expected_prefill_duration: Option<Duration>,
737}
738
739#[derive(Serialize, Deserialize, Debug, Clone)]
740pub enum ActiveSequenceEventData {
741    AddRequest {
742        token_sequence: Option<Vec<SequenceHash>>,
743        #[serde(default = "default_track_prefill_tokens")]
744        track_prefill_tokens: bool,
745        expected_output_tokens: Option<u32>,
746        #[serde(default)]
747        prefill_load_hint: Option<PrefillLoadHint>,
748    },
749    // NOTE: Output-block growth is intentionally not a replica-sync event. It can occur
750    // at high frequency, and broadcasting it would consume disproportionate network bandwidth.
751    Free,
752    MarkPrefillCompleted,
753}
754
755#[derive(Serialize, Deserialize, Debug, Clone)]
756pub struct ActiveBlockEvent {
757    pub request_id: String,
758    pub data: ActiveBlockEventData,
759}
760
761#[derive(Serialize, Deserialize, Debug, Clone)]
762pub enum ActiveBlockEventData {
763    NewBlock(Vec<SequenceHash>),
764    FreeBlock,
765}
766
767/// Represents a collection of cache events and a shutdown flag.
768#[derive(Serialize, Deserialize, Debug, Clone)]
769pub struct KvCacheEvents {
770    /// A list of cache events.
771    pub events: Vec<KvCacheEvent>,
772    /// A flag indicating whether the cache is shutting down.
773    pub shutdown: bool,
774}
775
776/// Represents a single cache event with an ID and associated data.
777#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
778pub struct KvCacheEvent {
779    /// The unique identifier of the event.
780    pub event_id: u64,
781    /// The data associated with the event.
782    pub data: KvCacheEventData,
783    /// The data parallel rank of the worker emitting this event (0 if DP not enabled).
784    #[serde(default)]
785    pub dp_rank: DpRank,
786}
787
788/// Represents the data associated with a cache event.
789///
790/// Data is either stored or removed.
791#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
792#[serde(rename_all = "snake_case")]
793pub enum KvCacheEventData {
794    Stored(KvCacheStoreData),
795    Removed(KvCacheRemoveData),
796    /// Remove all KV ownership for the emitting `(worker_id, dp_rank)`.
797    ///
798    /// This is ordered only within that rank publisher's event sequence. Worker-wide removal is
799    /// a separate serving-membership lifecycle operation.
800    Cleared,
801}
802
803/// Represents the data associated with a stored cache event.
804#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
805pub struct KvCacheStoreData {
806    /// The optional hash of the parent block.
807    pub parent_hash: Option<ExternalSequenceBlockHash>,
808    /// Absolute position of the first block in this batch for positional replay.
809    #[serde(default)]
810    pub start_position: Option<u32>,
811    /// A list of stored blocked data.
812    pub blocks: Vec<KvCacheStoredBlockData>,
813}
814
815/// Multimodal object information within a block.
816/// Offsets are relative to the block (0 to block_size-1).
817#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
818pub struct BlockMmObjectInfo {
819    /// Hash identifying this multimodal object
820    pub mm_hash: u64,
821    /// Token offset ranges where this MM object's placeholders appear within THIS block
822    /// Each tuple is (start_offset, end_offset) relative to block start
823    pub offsets: Vec<(usize, usize)>,
824}
825
826/// Extra metadata for a block containing multimodal objects
827#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
828pub struct BlockExtraInfo {
829    /// All multimodal objects referenced in this block
830    pub mm_objects: Vec<BlockMmObjectInfo>,
831}
832
833/// Request-level multimodal object information.
834/// Offsets are relative to the entire request token sequence.
835#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
836pub struct RequestMmObjectInfo {
837    /// Hash identifying this multimodal object
838    pub mm_hash: u64,
839    /// Token offset ranges where this MM object's placeholders appear in the ENTIRE request
840    /// Each tuple is (start_offset, end_offset) relative to request start
841    pub offsets: Vec<(usize, usize)>,
842}
843
844/// Request-level multimodal metadata
845#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
846pub struct RequestExtraInfo {
847    /// All multimodal objects in this request
848    pub mm_objects: Vec<RequestMmObjectInfo>,
849}
850
851impl RequestExtraInfo {
852    /// Convert request-level MM info to block-level MM info for a sequence of blocks.
853    ///
854    /// This function splits request-level offsets (relative to the entire request token sequence)
855    /// into block-level offsets (relative to each block).
856    ///
857    /// # Arguments
858    /// * `block_size` - The size of each block in tokens
859    /// * `total_tokens` - Total number of tokens in the request
860    ///
861    /// # Returns
862    /// A vector of `Option<BlockExtraInfo>` where each element corresponds to a block.
863    /// `None` indicates a block with no multimodal objects.
864    pub fn to_block_level(
865        &self,
866        block_size: usize,
867        total_tokens: usize,
868    ) -> Vec<Option<BlockExtraInfo>> {
869        let num_blocks = total_tokens.div_ceil(block_size);
870        let mut block_infos: Vec<Option<BlockExtraInfo>> = vec![None; num_blocks];
871
872        for req_mm_obj in &self.mm_objects {
873            for (req_start, req_end) in &req_mm_obj.offsets {
874                // Find which blocks this offset range spans
875                let start_block = req_start / block_size;
876                let end_block = (req_end.saturating_sub(1)) / block_size;
877
878                let upper_bound = end_block.min(num_blocks - 1) + 1;
879                for (block_idx, block_info_opt) in block_infos
880                    .iter_mut()
881                    .enumerate()
882                    .take(upper_bound)
883                    .skip(start_block)
884                {
885                    let block_start_global = block_idx * block_size;
886                    let block_end_global = ((block_idx + 1) * block_size).min(total_tokens);
887
888                    // Calculate the intersection of this MM object's range with this block
889                    let local_start = (*req_start).max(block_start_global) - block_start_global;
890                    let local_end = (*req_end).min(block_end_global) - block_start_global;
891
892                    if local_start < local_end {
893                        let block_info = block_info_opt
894                            .get_or_insert_with(|| BlockExtraInfo { mm_objects: vec![] });
895
896                        // Check if we already have this mm_hash in this block
897                        if let Some(existing) = block_info
898                            .mm_objects
899                            .iter_mut()
900                            .find(|obj| obj.mm_hash == req_mm_obj.mm_hash)
901                        {
902                            // Add the offset range to existing object
903                            existing.offsets.push((local_start, local_end));
904                        } else {
905                            // Create new MM object entry for this block
906                            block_info.mm_objects.push(BlockMmObjectInfo {
907                                mm_hash: req_mm_obj.mm_hash,
908                                offsets: vec![(local_start, local_end)],
909                            });
910                        }
911                    }
912                }
913            }
914        }
915
916        block_infos
917    }
918}
919
920/// Represents data for a stored block.
921#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
922pub struct KvCacheStoredBlockData {
923    /// The hash of the block.
924    pub block_hash: ExternalSequenceBlockHash,
925    /// The hash of the tokens in the block.
926    pub tokens_hash: LocalBlockHash,
927    /// Extra multimodal metadata for this block
928    /// Note: Do NOT use skip_serializing_if with bincode - it breaks deserialization
929    /// because bincode is positional and expects all fields to be present.
930    #[serde(default)]
931    pub mm_extra_info: Option<BlockExtraInfo>,
932}
933
934/// Represents the data associated with a removed cache event.
935#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
936pub struct KvCacheRemoveData {
937    /// A list of block hashes to remove.
938    pub block_hashes: Vec<ExternalSequenceBlockHash>,
939}
940
941impl Serialize for LocalBlockHash {
942    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
943    where
944        S: serde::Serializer,
945    {
946        serializer.serialize_u64(self.0)
947    }
948}
949
950impl<'de> Deserialize<'de> for LocalBlockHash {
951    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
952    where
953        D: serde::Deserializer<'de>,
954    {
955        let value = u64::deserialize(deserializer)?;
956        Ok(LocalBlockHash(value))
957    }
958}
959
960impl Serialize for ExternalSequenceBlockHash {
961    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
962    where
963        S: serde::Serializer,
964    {
965        serializer.serialize_u64(self.0)
966    }
967}
968
969impl<'de> Deserialize<'de> for ExternalSequenceBlockHash {
970    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
971    where
972        D: serde::Deserializer<'de>,
973    {
974        let value = u64::deserialize(deserializer)?;
975        Ok(ExternalSequenceBlockHash(value))
976    }
977}
978
979// ------
980// Router Event Types
981// ------
982
983/// Errors that can occur during KV Cache Event processing.
984///
985/// Indexer backends may introduce additional failure modes.
986/// Downstream matches must include a wildcard arm because this enum is non-exhaustive.
987#[non_exhaustive]
988#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
989pub enum KvCacheEventError {
990    #[error("Failed to find parent block")]
991    ParentBlockNotFound,
992
993    #[error("Failed to find block")]
994    BlockNotFound,
995
996    #[error("Invalid block sequence")]
997    InvalidBlockSequence,
998
999    /// A bounded, pre-commit index omission; this does not prove the backing table is full.
1000    #[error("Indexer capacity exhausted")]
1001    CapacityExhausted,
1002
1003    /// A pre-commit allocation or reservation failed; no lossy-capacity policy is implied.
1004    #[error("Indexer allocation failed")]
1005    AllocationFailed,
1006
1007    /// An exact ownership degree overflowed before mutation.
1008    #[error("Indexer ownership degree overflow")]
1009    OwnershipDegreeOverflow,
1010
1011    #[error("Indexer invariant violated")]
1012    IndexerInvariantViolation,
1013}
1014
1015/// A [`KvCacheEvent`] on a specific LLM worker denoted by [`WorkerId`].
1016#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1017pub struct RouterEvent {
1018    /// The ID of the worker emitting the event.
1019    pub worker_id: WorkerId,
1020    /// The storage tier associated with the event.
1021    #[serde(default)]
1022    pub storage_tier: StorageTier,
1023    /// The cache event associated with the worker.
1024    pub event: KvCacheEvent,
1025}
1026
1027impl RouterEvent {
1028    /// Create a new `RouterEvent`.
1029    ///
1030    /// ### Arguments
1031    ///
1032    /// * `worker_id` - The ID of the worker emitting the event.
1033    /// * `event` - The cache event.
1034    ///
1035    /// ### Returns
1036    ///
1037    /// A new `RouterEvent`.
1038    pub fn new(worker_id: WorkerId, event: KvCacheEvent) -> Self {
1039        Self::with_storage_tier(worker_id, event, StorageTier::Device)
1040    }
1041
1042    pub fn with_storage_tier(
1043        worker_id: WorkerId,
1044        event: KvCacheEvent,
1045        storage_tier: StorageTier,
1046    ) -> Self {
1047        Self {
1048            worker_id,
1049            storage_tier,
1050            event,
1051        }
1052    }
1053}
1054
1055/// Shared cache hit information, represented as sorted non-overlapping half-open ranges.
1056///
1057/// Ranges encode which block positions exist in the external shared KV cache pool.
1058/// Using ranges instead of `Vec<bool>` avoids iterating over potentially thousands
1059/// of blocks per worker. Typical shared cache patterns produce few contiguous regions,
1060/// making `hits_beyond` O(num_ranges) ~ O(1-5).
1061#[derive(Debug, Clone, Default)]
1062pub struct SharedCacheHits {
1063    /// Ranges of block positions that exist in the shared cache.
1064    /// Half-open ranges [start, end), sorted and non-overlapping.
1065    pub ranges: Vec<Range<u32>>,
1066    /// Total number of hits (sum of range lengths).
1067    pub total_hits: u32,
1068}
1069
1070impl SharedCacheHits {
1071    /// Create from sorted, non-overlapping ranges.
1072    pub fn from_ranges(ranges: Vec<Range<u32>>) -> Self {
1073        let total_hits = ranges.iter().map(|r| r.end - r.start).sum();
1074        Self { ranges, total_hits }
1075    }
1076
1077    /// Create from a boolean hit vector (convenience for tests and simple backends).
1078    /// Coalesces consecutive `true` entries into ranges.
1079    pub fn from_hits(hits: &[bool]) -> Self {
1080        let mut ranges = Vec::new();
1081        let mut i = 0;
1082        while i < hits.len() {
1083            if hits[i] {
1084                let start = i as u32;
1085                while i < hits.len() && hits[i] {
1086                    i += 1;
1087                }
1088                ranges.push(start..i as u32);
1089            } else {
1090                i += 1;
1091            }
1092        }
1093        Self::from_ranges(ranges)
1094    }
1095
1096    /// Count hits at positions >= `from_position`.
1097    /// O(num_ranges), not O(num_blocks).
1098    pub fn hits_beyond(&self, from_position: u32) -> u32 {
1099        self.ranges
1100            .iter()
1101            .map(|r| {
1102                if r.end <= from_position {
1103                    0
1104                } else if r.start >= from_position {
1105                    r.end - r.start
1106                } else {
1107                    r.end - from_position
1108                }
1109            })
1110            .sum()
1111    }
1112}
1113
1114/// Scores representing the overlap of workers (with their dp_rank).
1115#[derive(Debug, Clone, Serialize, Deserialize)]
1116pub struct OverlapScores {
1117    /// Map of worker (with dp_rank) to score.
1118    pub scores: FxHashMap<WorkerWithDpRank, u32>,
1119    /// List of frequencies that the blocks have been accessed. Entries with value 0 are omitted.
1120    pub frequencies: Vec<usize>,
1121}
1122
1123impl Default for OverlapScores {
1124    fn default() -> Self {
1125        Self::new()
1126    }
1127}
1128
1129impl OverlapScores {
1130    /// Create a new `OverlapScores`.
1131    ///
1132    /// ### Returns
1133    ///
1134    /// A new `OverlapScores`.
1135    pub fn new() -> Self {
1136        Self {
1137            scores: FxHashMap::default(),
1138            frequencies: Vec::new(),
1139        }
1140    }
1141
1142    /// Update the scores with a set of workers.
1143    ///
1144    /// ### Arguments
1145    ///
1146    /// * `workers` - An iterator over `WorkerWithDpRank` references.
1147    pub fn update_scores<'a, I>(&mut self, workers: I)
1148    where
1149        I: IntoIterator<Item = &'a WorkerWithDpRank>,
1150    {
1151        for worker in workers {
1152            let score = self.scores.entry(*worker).or_insert(0);
1153            *score += 1;
1154        }
1155    }
1156}
1157
1158// ------
1159// TokensWithHashes
1160// ------
1161
1162/// A container for tokens with lazily computed block and sequence hashes.
1163///
1164/// This struct avoids redundant hash computations by caching results:
1165/// - `get_or_compute_block_hashes()` computes block hashes if not cached
1166/// - `get_or_compute_seq_hashes()` computes seq hashes if not cached,
1167///   and will also compute block hashes first if needed (since seq hashes depend on them)
1168#[derive(Debug, Clone)]
1169pub struct TokensWithHashes {
1170    tokens: Vec<u32>,
1171    block_size: u32,
1172    block_mm_infos: Option<Vec<Option<BlockExtraInfo>>>,
1173    lora_name: Option<String>,
1174    cache_namespace: Option<String>,
1175    block_hashes: Option<Vec<LocalBlockHash>>,
1176    seq_hashes: Option<Vec<SequenceHash>>,
1177    is_eagle: Option<bool>,
1178}
1179
1180impl TokensWithHashes {
1181    /// Creates a new TokensWithHashes from tokens and block size.
1182    pub fn new(tokens: Vec<u32>, block_size: u32) -> Self {
1183        Self {
1184            tokens,
1185            block_size,
1186            block_mm_infos: None,
1187            lora_name: None,
1188            cache_namespace: None,
1189            block_hashes: None,
1190            seq_hashes: None,
1191            is_eagle: None,
1192        }
1193    }
1194
1195    /// Adds multimodal extra info for blocks.
1196    pub fn with_mm_infos(mut self, infos: Vec<Option<BlockExtraInfo>>) -> Self {
1197        self.block_mm_infos = Some(infos);
1198        self.invalidate_hashes();
1199        self
1200    }
1201
1202    /// Sets the LoRA adapter name for hash computation.
1203    pub fn with_lora_name(mut self, name: String) -> Self {
1204        self.lora_name = Some(name);
1205        self.invalidate_hashes();
1206        self
1207    }
1208
1209    /// Sets the cache namespace for hash computation.
1210    pub fn with_cache_namespace(mut self, namespace: String) -> Self {
1211        self.cache_namespace = Some(namespace);
1212        self.invalidate_hashes();
1213        self
1214    }
1215
1216    /// Sets Eagle hashing semantics for this token sequence.
1217    pub fn with_is_eagle(mut self, is_eagle: bool) -> Self {
1218        self.set_is_eagle(is_eagle);
1219        self
1220    }
1221
1222    /// Updates Eagle hashing semantics and invalidates cached hashes when it changes.
1223    pub fn set_is_eagle(&mut self, is_eagle: bool) {
1224        let is_eagle = Some(is_eagle);
1225        if self.is_eagle == is_eagle {
1226            return;
1227        }
1228
1229        self.is_eagle = is_eagle;
1230        self.invalidate_hashes();
1231    }
1232
1233    fn invalidate_hashes(&mut self) {
1234        self.block_hashes = None;
1235        self.seq_hashes = None;
1236    }
1237
1238    /// Returns a reference to the tokens.
1239    pub fn tokens(&self) -> &[u32] {
1240        &self.tokens
1241    }
1242
1243    /// Returns the number of tokens.
1244    pub fn len(&self) -> usize {
1245        self.tokens.len()
1246    }
1247
1248    /// Returns true if there are no tokens.
1249    pub fn is_empty(&self) -> bool {
1250        self.tokens.is_empty()
1251    }
1252
1253    /// Returns the block size.
1254    pub fn block_size(&self) -> u32 {
1255        self.block_size
1256    }
1257
1258    /// Returns the multimodal extra info, if set.
1259    pub fn block_mm_infos(&self) -> Option<&[Option<BlockExtraInfo>]> {
1260        self.block_mm_infos.as_deref()
1261    }
1262
1263    /// Returns block hashes, computing them if not already cached.
1264    pub fn get_or_compute_block_hashes(&mut self) -> &[LocalBlockHash] {
1265        if self.block_hashes.is_none() {
1266            self.block_hashes = Some(compute_block_hash_for_seq(
1267                &self.tokens,
1268                self.block_size,
1269                BlockHashOptions {
1270                    block_mm_infos: self.block_mm_infos.as_deref(),
1271                    lora_name: self.lora_name.as_deref(),
1272                    cache_namespace: self.cache_namespace.as_deref(),
1273                    is_eagle: self.is_eagle,
1274                },
1275            ));
1276        }
1277        self.block_hashes.as_ref().unwrap()
1278    }
1279
1280    /// Returns sequence hashes, computing them if not already cached.
1281    /// This will also compute block hashes if they haven't been computed yet,
1282    /// since sequence hashes depend on block hashes.
1283    pub fn get_or_compute_seq_hashes(&mut self) -> &[SequenceHash] {
1284        if self.seq_hashes.is_none() {
1285            // Ensure block hashes are computed first
1286            let block_hashes = self.get_or_compute_block_hashes();
1287            self.seq_hashes = Some(compute_seq_hash_for_block(block_hashes));
1288        }
1289        self.seq_hashes.as_ref().unwrap()
1290    }
1291
1292    /// Returns cached block hashes without computing. Returns None if not yet computed.
1293    pub fn block_hashes(&self) -> Option<&[LocalBlockHash]> {
1294        self.block_hashes.as_deref()
1295    }
1296
1297    /// Returns cached seq hashes without computing. Returns None if not yet computed.
1298    pub fn seq_hashes(&self) -> Option<&[SequenceHash]> {
1299        self.seq_hashes.as_deref()
1300    }
1301}
1302
1303// ------
1304// Tests
1305// ------
1306#[cfg(test)]
1307mod tests {
1308    use super::*;
1309    use rstest::rstest;
1310    use serde_json;
1311
1312    /// Pin the sglang pad_value constants and formula against upstream
1313    /// `MultimodalItem._compute_pad_value`. If sglang bumps a constant, this
1314    /// fails — otherwise routing-side pad_value would silently diverge from
1315    /// sglang's `BlockStored` bytes and MM-routing would degrade to text-prefix.
1316    #[test]
1317    fn pad_value_matches_sglang_protocol() {
1318        assert_eq!(MM_PAD_SHIFT_VALUE, 1_000_000);
1319        assert_eq!(MM_PAD_HASH_MASK, (1u64 << 30) - 1);
1320        assert_eq!(pad_value_for_mm_hash(0), MM_PAD_SHIFT_VALUE as u32);
1321        let fits = (1u64 << 30) - 1;
1322        assert_eq!(
1323            pad_value_for_mm_hash(fits),
1324            (MM_PAD_SHIFT_VALUE + fits) as u32
1325        );
1326        let overflow = (1u64 << 30) | 0xCAFE;
1327        assert_eq!(
1328            pad_value_for_mm_hash(overflow),
1329            (MM_PAD_SHIFT_VALUE + 0xCAFE) as u32,
1330            "high bits above the 30-bit mask must be discarded"
1331        );
1332    }
1333
1334    #[test]
1335    fn test_router_event_new() {
1336        let worker_id = 0;
1337        let kv_cache_event = KvCacheEvent {
1338            event_id: 1,
1339            data: KvCacheEventData::Stored(KvCacheStoreData {
1340                parent_hash: None,
1341                start_position: None,
1342                blocks: vec![KvCacheStoredBlockData {
1343                    block_hash: ExternalSequenceBlockHash(0),
1344                    mm_extra_info: None,
1345                    tokens_hash: LocalBlockHash(13226331709069118873),
1346                }],
1347            }),
1348            dp_rank: 0,
1349        };
1350        let router_event = RouterEvent::new(worker_id, kv_cache_event);
1351
1352        assert_eq!(router_event.worker_id, worker_id);
1353        assert_eq!(router_event.event.event_id, 1);
1354        if let KvCacheEventData::Stored(store_op) = &router_event.event.data {
1355            assert_eq!(store_op.blocks.len(), 1);
1356            assert_eq!(
1357                store_op.blocks[0].tokens_hash,
1358                compute_block_hash(b"test data")
1359            );
1360            assert_eq!(store_op.blocks[0].block_hash, ExternalSequenceBlockHash(0));
1361        } else {
1362            panic!("Expected KvCacheEventData::Stored");
1363        }
1364    }
1365
1366    #[rstest]
1367    #[case(11)]
1368    #[case(32)]
1369    #[case(64)]
1370    fn test_compute_block_hash_for_seq(#[case] kv_block_size: u32) {
1371        let sequence = (0..kv_block_size).collect::<Vec<u32>>();
1372        let hashes =
1373            compute_block_hash_for_seq(&sequence, kv_block_size, BlockHashOptions::default());
1374        assert_eq!(hashes.len(), 1);
1375
1376        let sequence = (0..(kv_block_size + 1)).collect::<Vec<u32>>();
1377        let hashes =
1378            compute_block_hash_for_seq(&sequence, kv_block_size, BlockHashOptions::default());
1379        assert_eq!(hashes.len(), 1);
1380
1381        let sequence = (0..(2 * kv_block_size + 1)).collect::<Vec<u32>>();
1382        let hashes =
1383            compute_block_hash_for_seq(&sequence, kv_block_size, BlockHashOptions::default());
1384        assert_eq!(hashes.len(), 2);
1385    }
1386
1387    #[test]
1388    fn test_compute_next_seq_hash_matches_rolling_hash() {
1389        let block_hashes = [LocalBlockHash(11), LocalBlockHash(22), LocalBlockHash(33)];
1390        let seq_hashes = compute_seq_hash_for_block(&block_hashes);
1391
1392        assert_eq!(
1393            seq_hashes[1],
1394            compute_next_seq_hash(seq_hashes[0], block_hashes[1])
1395        );
1396        assert_eq!(
1397            seq_hashes[2],
1398            compute_next_seq_hash(seq_hashes[1], block_hashes[2])
1399        );
1400    }
1401
1402    #[test]
1403    fn test_lora_name_produces_different_hash() {
1404        let tokens: Vec<u32> = (0..4).collect();
1405        let base = compute_block_hash_for_seq(&tokens, 4, BlockHashOptions::default());
1406        let lora_a = compute_block_hash_for_seq(
1407            &tokens,
1408            4,
1409            BlockHashOptions {
1410                lora_name: Some("adapter-a"),
1411                ..Default::default()
1412            },
1413        );
1414        let lora_b = compute_block_hash_for_seq(
1415            &tokens,
1416            4,
1417            BlockHashOptions {
1418                lora_name: Some("adapter-b"),
1419                ..Default::default()
1420            },
1421        );
1422
1423        assert_ne!(base[0], lora_a[0]);
1424        assert_ne!(base[0], lora_b[0]);
1425        assert_ne!(lora_a[0], lora_b[0]);
1426    }
1427
1428    #[test]
1429    fn test_lora_hash_matches_kv_hashing_contract() {
1430        let tokens: Vec<u32> = (0..4).collect();
1431        let lora_name = "adapter-a";
1432        let actual = compute_block_hash_for_seq(
1433            &tokens,
1434            4,
1435            BlockHashOptions {
1436                lora_name: Some(lora_name),
1437                ..Default::default()
1438            },
1439        );
1440        let token_bytes = tokens
1441            .iter()
1442            .flat_map(|token| token.to_le_bytes())
1443            .collect::<Vec<_>>();
1444        let salt_hash = dynamo_kv_hashing::compute_salt_hash(None, Some(lora_name)).unwrap();
1445        let expected = LocalBlockHash(dynamo_kv_hashing::compute_hash_v2(&token_bytes, salt_hash));
1446
1447        assert_eq!(actual, vec![expected]);
1448    }
1449
1450    #[test]
1451    fn test_lora_name_empty_string_normalized_to_none() {
1452        let tokens: Vec<u32> = (0..4).collect();
1453        let base = compute_block_hash_for_seq(&tokens, 4, BlockHashOptions::default());
1454        let empty = compute_block_hash_for_seq(
1455            &tokens,
1456            4,
1457            BlockHashOptions {
1458                lora_name: Some(""),
1459                ..Default::default()
1460            },
1461        );
1462        assert_eq!(
1463            base, empty,
1464            "empty lora_name should be treated as base model"
1465        );
1466    }
1467
1468    #[test]
1469    fn test_cache_namespace_produces_different_hash() {
1470        let tokens: Vec<u32> = (0..4).collect();
1471        let base = compute_block_hash_for_seq(&tokens, 4, BlockHashOptions::default());
1472        let namespace_a = compute_block_hash_for_seq(
1473            &tokens,
1474            4,
1475            BlockHashOptions {
1476                cache_namespace: Some("tenant-a"),
1477                ..Default::default()
1478            },
1479        );
1480        let namespace_b = compute_block_hash_for_seq(
1481            &tokens,
1482            4,
1483            BlockHashOptions {
1484                cache_namespace: Some("tenant-b"),
1485                ..Default::default()
1486            },
1487        );
1488        let lora_a = compute_block_hash_for_seq(
1489            &tokens,
1490            4,
1491            BlockHashOptions {
1492                lora_name: Some("tenant-a"),
1493                ..Default::default()
1494            },
1495        );
1496
1497        assert_ne!(base[0], namespace_a[0]);
1498        assert_ne!(base[0], namespace_b[0]);
1499        assert_ne!(namespace_a[0], namespace_b[0]);
1500        assert_ne!(
1501            namespace_a[0], lora_a[0],
1502            "namespace and lora salts must use independent seed domains"
1503        );
1504    }
1505
1506    #[test]
1507    fn test_cache_namespace_hash_matches_kv_hashing_contract() {
1508        let tokens: Vec<u32> = (0..4).collect();
1509        let cache_namespace = "tenant-a";
1510        let lora_name = "adapter-a";
1511        let actual = compute_block_hash_for_seq(
1512            &tokens,
1513            4,
1514            BlockHashOptions {
1515                lora_name: Some(lora_name),
1516                cache_namespace: Some(cache_namespace),
1517                ..Default::default()
1518            },
1519        );
1520        let token_bytes = tokens
1521            .iter()
1522            .flat_map(|token| token.to_le_bytes())
1523            .collect::<Vec<_>>();
1524        let salt_hash =
1525            dynamo_kv_hashing::compute_salt_hash(Some(cache_namespace), Some(lora_name)).unwrap();
1526        let expected = LocalBlockHash(dynamo_kv_hashing::compute_hash_v2(&token_bytes, salt_hash));
1527
1528        assert_eq!(actual, vec![expected]);
1529    }
1530
1531    #[test]
1532    fn test_cache_namespace_empty_string_normalized_to_none() {
1533        let tokens: Vec<u32> = (0..4).collect();
1534        let base = compute_block_hash_for_seq(&tokens, 4, BlockHashOptions::default());
1535        let empty = compute_block_hash_for_seq(
1536            &tokens,
1537            4,
1538            BlockHashOptions {
1539                cache_namespace: Some(""),
1540                ..Default::default()
1541            },
1542        );
1543        assert_eq!(
1544            base, empty,
1545            "empty cache_namespace should be treated as absent"
1546        );
1547    }
1548
1549    #[test]
1550    fn test_tokens_with_hashes_lora() {
1551        let tokens: Vec<u32> = (0..8).collect();
1552
1553        let mut base = TokensWithHashes::new(tokens.clone(), 4);
1554        let base_hashes = base.get_or_compute_block_hashes().to_vec();
1555
1556        let mut with_lora =
1557            TokensWithHashes::new(tokens, 4).with_lora_name("my-adapter".to_string());
1558        let lora_hashes = with_lora.get_or_compute_block_hashes().to_vec();
1559
1560        assert_eq!(base_hashes.len(), lora_hashes.len());
1561        for (b, l) in base_hashes.iter().zip(lora_hashes.iter()) {
1562            assert_ne!(b, l);
1563        }
1564    }
1565
1566    #[test]
1567    fn test_tokens_with_hashes_lora_change_recomputes_cached_hashes() {
1568        let tokens: Vec<u32> = (0..8).collect();
1569        let mut with_hashes = TokensWithHashes::new(tokens.clone(), 4);
1570        let base_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1571
1572        let mut with_hashes = with_hashes.with_lora_name("my-adapter".to_string());
1573        let actual_block_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1574        let actual_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1575        let expected_block_hashes = compute_block_hash_for_seq(
1576            &tokens,
1577            4,
1578            BlockHashOptions {
1579                lora_name: Some("my-adapter"),
1580                ..Default::default()
1581            },
1582        );
1583        let expected_sequence_hashes = compute_seq_hash_for_block(&expected_block_hashes);
1584
1585        assert_eq!(actual_block_hashes, expected_block_hashes);
1586        assert_eq!(actual_sequence_hashes, expected_sequence_hashes);
1587        assert_ne!(actual_sequence_hashes, base_sequence_hashes);
1588    }
1589
1590    #[test]
1591    fn test_tokens_with_hashes_mm_change_recomputes_cached_hashes() {
1592        let tokens: Vec<u32> = (0..4).collect();
1593        let mm_infos = vec![
1594            Some(BlockExtraInfo {
1595                mm_objects: vec![BlockMmObjectInfo {
1596                    mm_hash: 42,
1597                    offsets: vec![(0, 1)],
1598                }],
1599            }),
1600            None,
1601        ];
1602        let mut with_hashes = TokensWithHashes::new(tokens.clone(), 2);
1603        let text_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1604
1605        let mut with_hashes = with_hashes.with_mm_infos(mm_infos.clone());
1606        let actual_block_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1607        let actual_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1608        let expected_block_hashes = compute_block_hash_for_seq(
1609            &tokens,
1610            2,
1611            BlockHashOptions {
1612                block_mm_infos: Some(&mm_infos),
1613                ..Default::default()
1614            },
1615        );
1616        let expected_sequence_hashes = compute_seq_hash_for_block(&expected_block_hashes);
1617
1618        assert_eq!(actual_block_hashes, expected_block_hashes);
1619        assert_eq!(actual_sequence_hashes, expected_sequence_hashes);
1620        assert_ne!(actual_sequence_hashes, text_sequence_hashes);
1621    }
1622
1623    #[test]
1624    fn test_tokens_with_hashes_cache_namespace() {
1625        let tokens: Vec<u32> = (0..8).collect();
1626
1627        let mut base = TokensWithHashes::new(tokens.clone(), 4);
1628        let base_hashes = base.get_or_compute_block_hashes().to_vec();
1629
1630        let mut with_namespace =
1631            TokensWithHashes::new(tokens, 4).with_cache_namespace("tenant-a".to_string());
1632        let namespace_hashes = with_namespace.get_or_compute_block_hashes().to_vec();
1633
1634        assert_eq!(base_hashes.len(), namespace_hashes.len());
1635        for (base, namespaced) in base_hashes.iter().zip(namespace_hashes.iter()) {
1636            assert_ne!(base, namespaced);
1637        }
1638    }
1639
1640    #[test]
1641    fn test_tokens_with_hashes_cache_namespace_change_recomputes_cached_hashes() {
1642        let tokens: Vec<u32> = (0..8).collect();
1643        let mut with_hashes = TokensWithHashes::new(tokens.clone(), 4);
1644        let base_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1645
1646        let mut with_hashes = with_hashes.with_cache_namespace("tenant-a".to_string());
1647        let actual_block_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1648        let actual_sequence_hashes = with_hashes.get_or_compute_seq_hashes().to_vec();
1649        let expected_block_hashes = compute_block_hash_for_seq(
1650            &tokens,
1651            4,
1652            BlockHashOptions {
1653                cache_namespace: Some("tenant-a"),
1654                ..Default::default()
1655            },
1656        );
1657        let expected_sequence_hashes = compute_seq_hash_for_block(&expected_block_hashes);
1658
1659        assert_eq!(actual_block_hashes, expected_block_hashes);
1660        assert_eq!(actual_sequence_hashes, expected_sequence_hashes);
1661        assert_ne!(actual_sequence_hashes, base_sequence_hashes);
1662    }
1663
1664    #[test]
1665    fn test_compute_block_hash_for_seq_eagle_windows() {
1666        let tokens: Vec<u32> = (0..6).collect();
1667
1668        let default_hashes = compute_block_hash_for_seq(&tokens, 2, BlockHashOptions::default());
1669        let eagle_hashes = compute_block_hash_for_seq(
1670            &tokens,
1671            2,
1672            BlockHashOptions {
1673                is_eagle: Some(true),
1674                ..Default::default()
1675            },
1676        );
1677        let expected_first = compute_block_hash_for_seq(
1678            &[0, 1, 2],
1679            2,
1680            BlockHashOptions {
1681                is_eagle: Some(true),
1682                ..Default::default()
1683            },
1684        );
1685        let expected_second = compute_block_hash_for_seq(
1686            &[2, 3, 4],
1687            2,
1688            BlockHashOptions {
1689                is_eagle: Some(true),
1690                ..Default::default()
1691            },
1692        );
1693
1694        assert_eq!(default_hashes.len(), 3);
1695        assert_eq!(eagle_hashes.len(), 2);
1696        assert_eq!(eagle_hashes, vec![expected_first[0], expected_second[0]]);
1697        assert_ne!(default_hashes[0], eagle_hashes[0]);
1698    }
1699
1700    #[test]
1701    fn test_tokens_with_hashes_set_is_eagle_invalidates_cache() {
1702        let tokens: Vec<u32> = (0..6).collect();
1703        let mut with_hashes = TokensWithHashes::new(tokens, 2);
1704
1705        let default_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1706        with_hashes.set_is_eagle(true);
1707        let eagle_hashes = with_hashes.get_or_compute_block_hashes().to_vec();
1708        let expected_first = compute_block_hash_for_seq(
1709            &[0, 1, 2],
1710            2,
1711            BlockHashOptions {
1712                is_eagle: Some(true),
1713                ..Default::default()
1714            },
1715        );
1716        let expected_second = compute_block_hash_for_seq(
1717            &[2, 3, 4],
1718            2,
1719            BlockHashOptions {
1720                is_eagle: Some(true),
1721                ..Default::default()
1722            },
1723        );
1724
1725        assert_eq!(default_hashes.len(), 3);
1726        assert_eq!(eagle_hashes.len(), 2);
1727        assert_eq!(eagle_hashes, vec![expected_first[0], expected_second[0]]);
1728        assert_ne!(default_hashes[0], eagle_hashes[0]);
1729    }
1730
1731    #[test]
1732    fn test_local_block_hash_serialization() {
1733        let hash = LocalBlockHash(12345);
1734        let serialized = serde_json::to_string(&hash).unwrap();
1735        assert_eq!(serialized, "12345");
1736
1737        let deserialized: LocalBlockHash = serde_json::from_str(&serialized).unwrap();
1738        assert_eq!(deserialized, hash);
1739    }
1740
1741    #[test]
1742    fn test_external_sequence_block_hash_serialization() {
1743        let hash = ExternalSequenceBlockHash(67890);
1744        let serialized = serde_json::to_string(&hash).unwrap();
1745        assert_eq!(serialized, "67890");
1746
1747        let deserialized: ExternalSequenceBlockHash = serde_json::from_str(&serialized).unwrap();
1748        assert_eq!(deserialized, hash);
1749    }
1750
1751    #[test]
1752    fn test_router_request_mark_free_backwards_compatible_deserialization() {
1753        let request: RouterRequest = serde_json::from_str(r#"{"method":"mark_free"}"#).unwrap();
1754
1755        assert!(matches!(
1756            request,
1757            RouterRequest::MarkFree { request_id: None }
1758        ));
1759    }
1760
1761    #[test]
1762    fn test_shared_cache_hits_from_hits() {
1763        // All hits contiguous
1764        let hits = SharedCacheHits::from_hits(&[true, true, true, true]);
1765        assert_eq!(hits.ranges, vec![0..4]);
1766        assert_eq!(hits.total_hits, 4);
1767
1768        // Sparse hits
1769        let hits = SharedCacheHits::from_hits(&[true, false, true, true, false, true]);
1770        assert_eq!(hits.ranges, vec![0..1, 2..4, 5..6]);
1771        assert_eq!(hits.total_hits, 4);
1772
1773        // No hits
1774        let hits = SharedCacheHits::from_hits(&[false, false, false]);
1775        assert!(hits.ranges.is_empty());
1776        assert_eq!(hits.total_hits, 0);
1777
1778        // Empty
1779        let hits = SharedCacheHits::from_hits(&[]);
1780        assert!(hits.ranges.is_empty());
1781        assert_eq!(hits.total_hits, 0);
1782    }
1783
1784    #[test]
1785    fn test_shared_cache_hits_beyond() {
1786        // Shared has [A, B, C, D] => range 0..4
1787        #[allow(clippy::single_range_in_vec_init)]
1788        let hits = SharedCacheHits::from_ranges(vec![0..4]);
1789
1790        // Device has overlap=2 (positions 0,1 on device) => shared_beyond should count positions 2,3
1791        assert_eq!(hits.hits_beyond(2), 2);
1792
1793        // Device has overlap=0 => all 4 shared hits count
1794        assert_eq!(hits.hits_beyond(0), 4);
1795
1796        // Device has overlap=4 => nothing beyond
1797        assert_eq!(hits.hits_beyond(4), 0);
1798
1799        // Device overlap exceeds range
1800        assert_eq!(hits.hits_beyond(10), 0);
1801    }
1802
1803    #[test]
1804    fn test_shared_cache_hits_beyond_sparse() {
1805        // Ranges: [1..3, 5..8] => positions 1,2,5,6,7
1806        let hits = SharedCacheHits::from_ranges(vec![1..3, 5..8]);
1807        assert_eq!(hits.total_hits, 5);
1808
1809        // from_position=0 => all 5 hits
1810        assert_eq!(hits.hits_beyond(0), 5);
1811        // from_position=2 => pos 2 (from first range) + 5,6,7 (from second) = 4
1812        assert_eq!(hits.hits_beyond(2), 4);
1813        // from_position=3 => only second range: 3 hits
1814        assert_eq!(hits.hits_beyond(3), 3);
1815        // from_position=6 => positions 6,7 from second range = 2
1816        assert_eq!(hits.hits_beyond(6), 2);
1817        // from_position=8 => nothing
1818        assert_eq!(hits.hits_beyond(8), 0);
1819    }
1820
1821    #[test]
1822    fn test_kv_transfer_enforcement_serde() {
1823        assert_eq!(
1824            serde_json::to_string(&KvTransferEnforcement::Required).unwrap(),
1825            r#""required""#
1826        );
1827        assert_eq!(
1828            serde_json::from_str::<KvTransferEnforcement>(r#""preferred""#).unwrap(),
1829            KvTransferEnforcement::Preferred
1830        );
1831        assert!(serde_json::from_str::<KvTransferEnforcement>(r#""fallback""#).is_err());
1832    }
1833
1834    #[test]
1835    fn test_worker_config_like_topology_domains_default() {
1836        // A minimal implementor that does NOT override topology_domains()
1837        struct MinimalConfig;
1838        impl WorkerConfigLike for MinimalConfig {
1839            fn data_parallel_start_rank(&self) -> u32 {
1840                0
1841            }
1842            fn data_parallel_size(&self) -> u32 {
1843                1
1844            }
1845            fn max_num_batched_tokens(&self) -> Option<u64> {
1846                None
1847            }
1848            fn total_kv_blocks(&self) -> Option<u64> {
1849                None
1850            }
1851        }
1852
1853        let config = MinimalConfig;
1854        assert!(
1855            config.topology_domains().is_none(),
1856            "Default topology_domains() should return None"
1857        );
1858        assert!(
1859            config.kv_transfer_domain().is_none(),
1860            "Default kv_transfer_domain() should return None"
1861        );
1862        assert!(
1863            config.kv_transfer_enforcement().is_none(),
1864            "Default kv_transfer_enforcement() should return None"
1865        );
1866        assert!(
1867            config.kv_transfer_preferred_weight().is_none(),
1868            "Default kv_transfer_preferred_weight() should return None"
1869        );
1870        assert!(config.native_offloading_capacity_tokens().is_none());
1871    }
1872
1873    #[test]
1874    fn test_router_request_mark_free_serialization_with_request_id() {
1875        let request = RouterRequest::MarkFree {
1876            request_id: Some("req-123".to_string()),
1877        };
1878
1879        let serialized = serde_json::to_string(&request).unwrap();
1880        let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1881
1882        assert_eq!(
1883            serialized,
1884            r#"{"method":"mark_free","request_id":"req-123"}"#
1885        );
1886        assert!(matches!(
1887            deserialized,
1888            RouterRequest::MarkFree {
1889                request_id: Some(ref request_id)
1890            } if request_id == "req-123"
1891        ));
1892    }
1893
1894    #[test]
1895    fn test_router_request_new_serialization_with_priority_jump() {
1896        let request = RouterRequest::New {
1897            tokens: vec![1, 2, 3],
1898            block_mm_infos: None,
1899            routing_constraints: RoutingConstraints::default(),
1900            priority_jump: 5.0,
1901            strict_priority: 0,
1902            lora_name: None,
1903            cache_namespace: None,
1904        };
1905
1906        let serialized = serde_json::to_string(&request).unwrap();
1907        let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1908
1909        assert_eq!(
1910            serialized,
1911            r#"{"method":"new","tokens":[1,2,3],"priority_jump":5.0}"#
1912        );
1913        assert!(matches!(
1914            deserialized,
1915            RouterRequest::New {
1916                priority_jump,
1917                ..
1918            } if priority_jump == 5.0
1919        ));
1920    }
1921
1922    #[test]
1923    fn test_router_request_new_serialization_with_lora_name() {
1924        let request = RouterRequest::New {
1925            tokens: vec![1, 2, 3],
1926            block_mm_infos: None,
1927            routing_constraints: RoutingConstraints::default(),
1928            priority_jump: 0.0,
1929            strict_priority: 0,
1930            lora_name: Some("adapter-a".to_string()),
1931            cache_namespace: None,
1932        };
1933
1934        let serialized = serde_json::to_string(&request).unwrap();
1935        let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
1936
1937        assert_eq!(
1938            serialized,
1939            r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0,"lora_name":"adapter-a"}"#
1940        );
1941        assert!(matches!(
1942            deserialized,
1943            RouterRequest::New {
1944                tokens,
1945                lora_name: Some(ref lora_name),
1946                ..
1947            } if tokens == vec![1, 2, 3] && lora_name == "adapter-a"
1948        ));
1949    }
1950
1951    #[test]
1952    fn test_router_request_new_defaults_lora_name() {
1953        let deserialized: RouterRequest =
1954            serde_json::from_str(r#"{"method":"new","tokens":[1,2,3]}"#).unwrap();
1955
1956        assert!(matches!(
1957            deserialized,
1958            RouterRequest::New {
1959                tokens,
1960                lora_name: None,
1961                ..
1962            } if tokens == vec![1, 2, 3]
1963        ));
1964    }
1965
1966    #[test]
1967    fn test_router_request_new_strict_priority_compatibility() {
1968        let request = RouterRequest::New {
1969            tokens: vec![1, 2, 3],
1970            block_mm_infos: None,
1971            routing_constraints: RoutingConstraints::default(),
1972            priority_jump: 0.0,
1973            strict_priority: 4,
1974            lora_name: None,
1975            cache_namespace: None,
1976        };
1977
1978        let serialized = serde_json::to_string(&request).unwrap();
1979        assert_eq!(
1980            serialized,
1981            r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0,"strict_priority":4}"#
1982        );
1983
1984        let missing: RouterRequest =
1985            serde_json::from_str(r#"{"method":"new","tokens":[1,2,3]}"#).unwrap();
1986        assert!(matches!(
1987            missing,
1988            RouterRequest::New {
1989                strict_priority: 0,
1990                ..
1991            }
1992        ));
1993
1994        let zero = RouterRequest::New {
1995            tokens: vec![1, 2, 3],
1996            block_mm_infos: None,
1997            routing_constraints: RoutingConstraints::default(),
1998            priority_jump: 0.0,
1999            strict_priority: 0,
2000            lora_name: None,
2001            cache_namespace: None,
2002        };
2003        assert_eq!(
2004            serde_json::to_string(&zero).unwrap(),
2005            r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0}"#
2006        );
2007    }
2008
2009    #[test]
2010    fn test_router_request_new_serialization_with_cache_namespace() {
2011        let request = RouterRequest::New {
2012            tokens: vec![1, 2, 3],
2013            block_mm_infos: None,
2014            routing_constraints: RoutingConstraints::default(),
2015            priority_jump: 0.0,
2016            strict_priority: 0,
2017            lora_name: None,
2018            cache_namespace: Some("tenant-a".to_string()),
2019        };
2020
2021        let serialized = serde_json::to_string(&request).unwrap();
2022        let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
2023
2024        assert_eq!(
2025            serialized,
2026            r#"{"method":"new","tokens":[1,2,3],"priority_jump":0.0,"cache_namespace":"tenant-a"}"#
2027        );
2028        assert!(matches!(
2029            deserialized,
2030            RouterRequest::New {
2031                tokens,
2032                cache_namespace: Some(ref cache_namespace),
2033                ..
2034            } if tokens == vec![1, 2, 3] && cache_namespace == "tenant-a"
2035        ));
2036    }
2037
2038    #[test]
2039    fn test_router_request_potential_loads_serialization_with_lora_name() {
2040        let request = RouterRequest::PotentialLoads {
2041            tokens: vec![1, 2, 3],
2042            block_mm_infos: None,
2043            lora_name: Some("adapter-a".to_string()),
2044            cache_namespace: None,
2045        };
2046
2047        let serialized = serde_json::to_string(&request).unwrap();
2048        let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
2049
2050        assert_eq!(
2051            serialized,
2052            r#"{"method":"potential_loads","tokens":[1,2,3],"lora_name":"adapter-a"}"#
2053        );
2054        assert!(matches!(
2055            deserialized,
2056            RouterRequest::PotentialLoads {
2057                tokens,
2058                block_mm_infos: None,
2059                lora_name: Some(ref lora_name),
2060                cache_namespace: None,
2061            } if tokens == vec![1, 2, 3] && lora_name == "adapter-a"
2062        ));
2063    }
2064
2065    #[test]
2066    fn test_router_request_potential_loads_serialization_with_cache_namespace() {
2067        let request = RouterRequest::PotentialLoads {
2068            tokens: vec![1, 2, 3],
2069            block_mm_infos: None,
2070            lora_name: None,
2071            cache_namespace: Some("tenant-a".to_string()),
2072        };
2073
2074        let serialized = serde_json::to_string(&request).unwrap();
2075        let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
2076
2077        assert_eq!(
2078            serialized,
2079            r#"{"method":"potential_loads","tokens":[1,2,3],"cache_namespace":"tenant-a"}"#
2080        );
2081        assert!(matches!(
2082            deserialized,
2083            RouterRequest::PotentialLoads {
2084                tokens,
2085                cache_namespace: Some(ref cache_namespace),
2086                ..
2087            } if tokens == vec![1, 2, 3] && cache_namespace == "tenant-a"
2088        ));
2089    }
2090
2091    #[test]
2092    fn test_router_request_potential_loads_defaults_lora_name() {
2093        let deserialized: RouterRequest =
2094            serde_json::from_str(r#"{"method":"potential_loads","tokens":[1,2,3]}"#).unwrap();
2095
2096        assert!(matches!(
2097            deserialized,
2098            RouterRequest::PotentialLoads {
2099                tokens,
2100                block_mm_infos: None,
2101                lora_name: None,
2102                cache_namespace: None,
2103            } if tokens == vec![1, 2, 3]
2104        ));
2105    }
2106
2107    #[test]
2108    fn test_router_request_mark_prefill_serialization_with_request_id() {
2109        let request = RouterRequest::MarkPrefill {
2110            request_id: Some("req-123".to_string()),
2111        };
2112
2113        let serialized = serde_json::to_string(&request).unwrap();
2114        let deserialized: RouterRequest = serde_json::from_str(&serialized).unwrap();
2115
2116        assert_eq!(
2117            serialized,
2118            r#"{"method":"mark_prefill","request_id":"req-123"}"#
2119        );
2120        assert!(matches!(
2121            deserialized,
2122            RouterRequest::MarkPrefill {
2123                request_id: Some(ref request_id)
2124            } if request_id == "req-123"
2125        ));
2126    }
2127
2128    #[test]
2129    fn test_potential_load_defaults_active_requests() {
2130        let load = serde_json::from_str::<PotentialLoad>(
2131            r#"{"worker_id":1,"dp_rank":0,"potential_prefill_tokens":16,"potential_decode_blocks":4}"#,
2132        )
2133        .unwrap();
2134
2135        assert_eq!(load.worker_id, 1);
2136        assert_eq!(load.dp_rank, 0);
2137        assert_eq!(load.potential_prefill_tokens, 16);
2138        assert_eq!(load.potential_decode_blocks, 4);
2139        assert_eq!(load.active_requests, 0);
2140    }
2141
2142    #[test]
2143    fn test_potential_load_serializes_active_requests() {
2144        let load = PotentialLoad {
2145            worker_id: 1,
2146            dp_rank: 0,
2147            potential_prefill_tokens: 16,
2148            potential_decode_blocks: 4,
2149            active_requests: 2,
2150        };
2151
2152        assert_eq!(
2153            serde_json::to_string(&load).unwrap(),
2154            r#"{"worker_id":1,"dp_rank":0,"potential_prefill_tokens":16,"potential_decode_blocks":4,"active_requests":2}"#
2155        );
2156    }
2157}