Skip to main content

dynamo_mocker/common/
protocols.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use derive_builder::Builder;
5use dynamo_kv_router::config::RouterQueuePolicy;
6use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8use std::str::FromStr;
9use std::sync::Arc;
10use uuid::Uuid;
11use validator::{Validate, ValidationError};
12
13use crate::common::perf_model::PerfModel;
14use dynamo_kv_router::protocols::{KvCacheEvent, StorageTier};
15use dynamo_tokens::blocks::UniqueBlock;
16use dynamo_tokens::{BlockHash, PositionalLineageHash, SequenceHash, Token};
17
18/// Metadata marker type for kvbm-logical blocks in the mocker's G1 pool.
19#[derive(Clone, Debug)]
20pub struct G1;
21
22/// Eviction strategy for the kvbm-logical inactive pool.
23///
24/// `Lineage` is the default and matches kvbm-logical's own default — it evicts
25/// leaf blocks first, which subsumes the preemption-priority behaviour that the
26/// mocker's old `LRUEvictor::push_front` provided.
27#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
28pub enum MockerEvictionBackend {
29    Lru,
30    MultiLru,
31    #[default]
32    Lineage,
33}
34
35/// G1 implementation used by the shared vLLM/TRT-LLM mock scheduler.
36///
37/// `Native` is the default self-contained physical-copy pool. `Kvbm` preserves
38/// the existing kvbm-logical implementation and is selected automatically when
39/// the legacy G2/G3/G4 offload path is enabled. SGLang ignores this setting and
40/// uses its own KV manager.
41#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
42#[serde(rename_all = "snake_case")]
43pub enum G1Backend {
44    Kvbm,
45    #[default]
46    Native,
47}
48
49/// Trait for publishing KV cache events.
50/// This abstracts the runtime dependency so mocker components can remain generic.
51pub trait KvCacheEventSink: Send + Sync {
52    fn publish(&self, event: KvCacheEvent) -> anyhow::Result<()>;
53
54    fn publish_with_storage_tier(
55        &self,
56        event: KvCacheEvent,
57        _storage_tier: StorageTier,
58    ) -> anyhow::Result<()> {
59        self.publish(event)
60    }
61
62    /// Publishes events that share one source visibility boundary.
63    ///
64    /// Implementations that do not have a native batch representation retain
65    /// singleton delivery semantics by default.
66    fn publish_batch_with_storage_tiers(
67        &self,
68        events: Vec<(KvCacheEvent, StorageTier)>,
69    ) -> anyhow::Result<()> {
70        let mut first_error = None;
71        for (event, storage_tier) in events {
72            if let Err(error) = self.publish_with_storage_tier(event, storage_tier) {
73                first_error.get_or_insert(error);
74            }
75        }
76        first_error.map_or(Ok(()), Err)
77    }
78}
79
80/// Raw KV event payload used by transport-specific publishers such as the
81/// vLLM-native ZMQ event stream.
82#[derive(Debug, Clone)]
83pub struct RawKvEvent {
84    pub event: KvCacheEvent,
85    pub block_token_ids: Option<Vec<Vec<u32>>>,
86    pub storage_tier: StorageTier,
87}
88
89/// Trait for publishing transport-specific raw KV event payloads.
90pub trait RawKvEventSink: Send + Sync {
91    fn publish(&self, event: RawKvEvent) -> anyhow::Result<()>;
92
93    /// Publishes raw events that share one source visibility boundary.
94    ///
95    /// Implementations that do not have a native batch representation retain
96    /// singleton delivery semantics by default.
97    fn publish_batch(&self, events: Vec<RawKvEvent>) -> anyhow::Result<()> {
98        let mut first_error = None;
99        for event in events {
100            if let Err(error) = self.publish(event) {
101                first_error.get_or_insert(error);
102            }
103        }
104        first_error.map_or(Ok(()), Err)
105    }
106}
107
108/// Shared KV event publisher bundle used by schedulers and KV managers.
109#[derive(Clone, Default)]
110pub struct KvEventPublishers {
111    event_sink: Option<Arc<dyn KvCacheEventSink>>,
112    raw_sink: Option<Arc<dyn RawKvEventSink>>,
113}
114
115impl KvEventPublishers {
116    pub fn new(
117        event_sink: Option<Arc<dyn KvCacheEventSink>>,
118        raw_sink: Option<Arc<dyn RawKvEventSink>>,
119    ) -> Self {
120        Self {
121            event_sink,
122            raw_sink,
123        }
124    }
125
126    pub fn raw_enabled(&self) -> bool {
127        self.raw_sink.is_some()
128    }
129
130    pub fn is_empty(&self) -> bool {
131        self.event_sink.is_none() && self.raw_sink.is_none()
132    }
133
134    pub fn publish(
135        &self,
136        event: KvCacheEvent,
137        block_token_ids: Option<&[Vec<u32>]>,
138    ) -> anyhow::Result<()> {
139        self.publish_with_storage_tier(event, block_token_ids, StorageTier::Device)
140    }
141
142    pub fn publish_with_storage_tier(
143        &self,
144        event: KvCacheEvent,
145        block_token_ids: Option<&[Vec<u32>]>,
146        storage_tier: StorageTier,
147    ) -> anyhow::Result<()> {
148        if let Some(sink) = self.event_sink.as_ref() {
149            sink.publish_with_storage_tier(event.clone(), storage_tier)?;
150        }
151
152        if let Some(sink) = self.raw_sink.as_ref() {
153            sink.publish(RawKvEvent {
154                event,
155                block_token_ids: block_token_ids.map(|token_ids| token_ids.to_vec()),
156                storage_tier,
157            })?;
158        }
159
160        Ok(())
161    }
162
163    /// Publishes normal KV events without also forwarding them to a raw sink.
164    ///
165    /// Deferred live-scheduler forwarding uses this to preserve its source
166    /// visibility boundary for normal and raw sinks independently.
167    pub(crate) fn publish_event_sink_batch_only(
168        &self,
169        events: Vec<(KvCacheEvent, StorageTier)>,
170    ) -> anyhow::Result<()> {
171        if let Some(sink) = self.event_sink.as_ref() {
172            sink.publish_batch_with_storage_tiers(events)?;
173        }
174        Ok(())
175    }
176
177    /// Publishes raw events as one source visibility boundary.
178    pub(crate) fn publish_raw_batch(&self, events: Vec<RawKvEvent>) -> anyhow::Result<()> {
179        if let Some(sink) = self.raw_sink.as_ref() {
180            sink.publish_batch(events)?;
181        }
182        Ok(())
183    }
184}
185
186/// Per-iteration forward pass snapshot, mirroring the Python `ForwardPassMetrics`
187/// schema in `components/src/dynamo/common/forward_pass_metrics.py`.
188///
189/// Produced by the scheduler core after each `execute_pass_internal()` call.
190/// Runtime publishers may either stamp identity at serialization time or fill
191/// the identity fields directly when snapshots are consumed in-process.
192#[derive(Debug, Clone, Default)]
193pub struct ForwardPassSnapshot {
194    // -- identity --
195    // `Default::default()` leaves `version == 0` and identity fields empty or
196    // zero, which means an unstamped local snapshot. Runtime publishers may
197    // stamp or overwrite these fields at the serialization boundary.
198    pub version: u32,
199    pub worker_id: String,
200    pub dp_rank: u32,
201    pub counter_id: u64,
202    // -- scheduled requests (executed this iteration) --
203    pub num_prefill_requests: u32,
204    pub sum_prefill_tokens: u64,
205    pub var_prefill_length: f64,
206    pub sum_prefill_kv_tokens: u64,
207    pub num_decode_requests: u32,
208    pub sum_decode_kv_tokens: u64,
209    pub var_decode_kv_tokens: f64,
210    // -- queued requests (waiting, not scheduled) --
211    pub num_queued_prefill: u32,
212    pub sum_queued_prefill_tokens: u64,
213    pub var_queued_prefill_length: f64,
214    pub num_queued_decode: u32,
215    pub sum_queued_decode_kv_tokens: u64,
216    pub var_queued_decode_kv_tokens: f64,
217    // -- timing --
218    pub wall_time_secs: f64,
219}
220
221/// Trait for publishing forward pass metrics snapshots.
222/// This abstracts the FPM publishing pipeline so mocker schedulers remain generic.
223pub trait FpmSink: Send + Sync {
224    fn publish(&self, snapshot: ForwardPassSnapshot) -> anyhow::Result<()>;
225}
226
227/// Optional FPM sink used by schedulers.
228/// Wraps `Option<Arc<dyn FpmSink>>` for ergonomic passing and no-op default behavior.
229#[derive(Clone, Default)]
230pub struct FpmPublisher {
231    sink: Option<Arc<dyn FpmSink>>,
232}
233
234impl FpmPublisher {
235    pub fn new(sink: Option<Arc<dyn FpmSink>>) -> Self {
236        Self { sink }
237    }
238
239    pub fn publish(&self, snapshot: ForwardPassSnapshot) -> anyhow::Result<()> {
240        if let Some(sink) = &self.sink {
241            sink.publish(snapshot)?;
242        }
243        Ok(())
244    }
245}
246
247pub type NumBlocks = usize;
248
249/// Represents different block movement operations in the cache
250/// For Use and Promote variants, block hashes are included for KV event publishing
251#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
252pub enum MoveBlock {
253    Use(
254        Vec<UniqueBlock>,
255        Vec<BlockHash>,
256        Vec<PositionalLineageHash>,
257        Option<Vec<Vec<u32>>>,
258        Option<UniqueBlock>,
259    ),
260    Deref(Vec<UniqueBlock>),
261    Promote(
262        Uuid,
263        SequenceHash,
264        Option<u64>,
265        Option<BlockHash>,
266        PositionalLineageHash,
267        Option<Vec<u32>>,
268    ),
269}
270
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
272pub enum MoveBlockResponse {
273    Store(Vec<SequenceHash>, Option<u64>),
274    Remove(Vec<SequenceHash>),
275}
276
277#[derive(Debug, Clone, Default, Serialize, Deserialize)]
278pub struct DirectRequest {
279    pub tokens: Vec<Token>,
280    pub max_output_tokens: usize,
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub output_token_ids: Option<Vec<Token>>,
283    pub uuid: Option<Uuid>,
284    pub dp_rank: u32,
285    pub arrival_timestamp_ms: Option<f64>,
286    /// TODO: Replay maps this to router queue priority only; mock-engine
287    /// scheduling does not consume it yet.
288    #[serde(default, skip_serializing_if = "is_zero_i32")]
289    pub priority: i32,
290    /// NOTE: Strict priority orders the router's pending queue only. It does
291    /// not affect scheduling inside the selected mock engine.
292    #[serde(default, skip_serializing_if = "is_zero_u32")]
293    pub strict_priority: u32,
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub policy_class: Option<String>,
296}
297
298impl DirectRequest {
299    pub fn router_priorities(&self) -> (f64, u32) {
300        (f64::from(self.priority.max(0)), self.strict_priority)
301    }
302}
303
304fn is_zero_i32(value: &i32) -> bool {
305    *value == 0
306}
307
308fn is_zero_u32(value: &u32) -> bool {
309    *value == 0
310}
311
312/// Represents the cost of prefilling content in the cache
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct PrefillCost {
315    pub new_blocks: usize,
316    pub new_tokens: usize,
317    /// Number of tokens already cached (prefix hit).
318    /// isl = cached_tokens + new_tokens
319    pub cached_tokens: usize,
320    /// Subset of `cached_tokens` backed by active blocks. Physical-capacity
321    /// admission discounts only these because inactive reuse is re-consumed.
322    pub active_cached_tokens: usize,
323}
324
325impl PrefillCost {
326    pub fn predict_prefill_compute(
327        &self,
328        new_tokens: Option<usize>,
329        perf_model: &PerfModel,
330    ) -> anyhow::Result<f64> {
331        let tokens = new_tokens.unwrap_or(self.new_tokens);
332        let isl = self.cached_tokens + tokens;
333        perf_model.predict_prefill_time(1, isl, self.cached_tokens)
334    }
335}
336
337/// Signal for output token generation with completion status
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct OutputSignal {
340    pub uuid: Uuid,
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub token_id: Option<Token>,
343    /// Terminal flag: the request's lifecycle has ended. Replay drivers free
344    /// resources and advance/notify on this.
345    pub completed: bool,
346    /// Set with `completed` when the request was rejected without ever running
347    /// (its footprint exceeds the whole KV pool); drivers free/advance but
348    /// exclude it from token/latency/throughput stats.
349    #[serde(default)]
350    pub rejected: bool,
351    #[serde(default, skip_serializing_if = "Option::is_none")]
352    pub handoff_delay_ms: Option<f64>,
353}
354
355/// Preemption policy for evicting decode requests under memory pressure
356#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
357#[serde(rename_all = "lowercase")]
358pub enum PreemptionMode {
359    /// Evict the newest request (matches vLLM v1 default)
360    #[default]
361    Lifo,
362    /// Evict the oldest request
363    Fifo,
364}
365
366impl FromStr for PreemptionMode {
367    type Err = String;
368
369    fn from_str(value: &str) -> Result<Self, Self::Err> {
370        match value.to_ascii_lowercase().as_str() {
371            "lifo" => Ok(Self::Lifo),
372            "fifo" => Ok(Self::Fifo),
373            _ => Err(format!(
374                "Invalid preemption_mode: '{value}'. Must be 'lifo' or 'fifo'."
375            )),
376        }
377    }
378}
379
380/// Engine type for selecting scheduling and KV cache simulation behavior
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
382#[serde(rename_all = "lowercase")]
383pub enum EngineType {
384    /// vLLM-style scheduling with hash-based block KV cache
385    #[default]
386    Vllm,
387    /// SGLang-style scheduling with radix-tree KV cache
388    Sglang,
389    /// TensorRT-LLM-style scheduling. Reuses the vLLM scheduler
390    /// core with a TensorRT-LLM-style admission policy.
391    Trtllm,
392}
393
394impl FromStr for EngineType {
395    type Err = String;
396
397    fn from_str(value: &str) -> Result<Self, Self::Err> {
398        match value.to_ascii_lowercase().as_str() {
399            "vllm" => Ok(Self::Vllm),
400            "sglang" => Ok(Self::Sglang),
401            "trtllm" => Ok(Self::Trtllm),
402            _ => Err(format!(
403                "Invalid engine_type '{value}'. Must be 'vllm', 'sglang', or 'trtllm'."
404            )),
405        }
406    }
407}
408
409/// Scheduling policy applied by the shared vLLM scheduler core.
410///
411/// Derived from [`EngineType`] (+ engine-specific args) so the core reads a
412/// single discriminant instead of re-deriving engine behavior per pass.
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
414pub enum SchedulingPolicy {
415    /// vLLM semantics: require the current known sequence to fit at waiting
416    /// admission, then permit preemption under later KV pressure.
417    #[default]
418    Vllm,
419    /// TRT-LLM `GUARANTEED_NO_EVICT`: reserve `prompt + max_output` per
420    /// admitted request up front; never preempt.
421    TrtllmGuaranteedNoEvict,
422}
423
424/// Worker type for disaggregated serving configurations
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
426#[serde(rename_all = "lowercase")]
427pub enum WorkerType {
428    /// Standard aggregated worker handling both prefill and decode
429    #[default]
430    Aggregated,
431    /// Dedicated prefill worker in disaggregated mode
432    Prefill,
433    /// Dedicated decode worker in disaggregated mode
434    Decode,
435}
436
437impl FromStr for WorkerType {
438    type Err = String;
439
440    fn from_str(value: &str) -> Result<Self, Self::Err> {
441        match value.to_ascii_lowercase().as_str() {
442            "aggregated" => Ok(Self::Aggregated),
443            "prefill" => Ok(Self::Prefill),
444            "decode" => Ok(Self::Decode),
445            _ => Err(format!(
446                "Invalid worker_type '{value}'. Must be 'aggregated', 'prefill', or 'decode'."
447            )),
448        }
449    }
450}
451
452/// Physical KV footprint used to model a coordinated disaggregated transfer.
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
454#[serde(rename_all = "snake_case")]
455pub enum KvTransferTimingMode {
456    /// Charge the source request's full logical prompt length.
457    #[default]
458    FullPrompt,
459    /// Charge only the physical prompt footprint missing at the destination.
460    DestinationMissing,
461}
462
463impl FromStr for KvTransferTimingMode {
464    type Err = String;
465
466    fn from_str(value: &str) -> Result<Self, Self::Err> {
467        match value.to_ascii_lowercase().as_str() {
468            "full_prompt" => Ok(Self::FullPrompt),
469            "destination_missing" => Ok(Self::DestinationMissing),
470            _ => Err(format!(
471                "Invalid kv_transfer_timing_mode '{value}'. Must be 'full_prompt' or 'destination_missing'."
472            )),
473        }
474    }
475}
476
477/// Configuration for reasoning/thinking token output in the mocker.
478///
479/// When set, the mocker wraps the first portion of each response in thinking
480/// boundary tokens: `[start_token, random..., end_token, random...]`.
481#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
482pub struct ReasoningConfig {
483    pub start_thinking_token_id: u32,
484    pub end_thinking_token_id: u32,
485    #[validate(range(min = 0.0, max = 1.0))]
486    pub thinking_ratio: f64,
487}
488
489impl ReasoningConfig {
490    /// Number of thinking tokens (including start/end boundaries) for a given osl.
491    /// Returns 0 if osl < 2 (thinking disabled). Otherwise clamps to [2, osl].
492    pub fn num_thinking_tokens(&self, max_output_tokens: usize) -> usize {
493        if max_output_tokens < 2 {
494            return 0;
495        }
496        let raw = (max_output_tokens as f64 * self.thinking_ratio).floor() as usize;
497        if raw == 0 {
498            return 0;
499        }
500        raw.max(2).min(max_output_tokens)
501    }
502
503    /// Number of response tokens after the thinking block.
504    pub fn num_response_tokens(&self, max_output_tokens: usize) -> usize {
505        max_output_tokens.saturating_sub(self.num_thinking_tokens(max_output_tokens))
506    }
507}
508
509/// SGLang-specific configuration parameters.
510///
511/// Grouped into a nested struct to keep the `MockEngineArgs` namespace clean,
512/// following the same pattern as [`ReasoningConfig`].
513#[derive(Debug, Clone, Serialize, Deserialize, Validate, Default)]
514pub struct SglangArgs {
515    /// Scheduling policy: "fifo"/"fcfs" or "lpm". Default: "fifo".
516    pub schedule_policy: Option<String>,
517    /// Radix cache page size in tokens. Default: 1.
518    #[validate(range(min = 1))]
519    pub page_size: Option<usize>,
520    /// Maximum prefill tokens budget per batch. Default: 16384.
521    #[validate(range(min = 1))]
522    pub max_prefill_tokens: Option<usize>,
523    /// Chunked prefill size (max tokens per chunk). Default: 8192.
524    #[validate(range(min = 1))]
525    pub chunked_prefill_size: Option<usize>,
526    /// Clip max new tokens for admission budget. Default: 4096.
527    #[validate(range(min = 1))]
528    pub clip_max_new_tokens: Option<usize>,
529    /// Schedule conservativeness factor (0.0–1.0). Default: 1.0.
530    #[validate(range(min = 0.0, max = 1.0))]
531    pub schedule_conservativeness: Option<f64>,
532}
533
534/// TensorRT-LLM-specific configuration parameters.
535///
536/// Grouped into a nested struct to keep the `MockEngineArgs` namespace clean,
537/// following the same pattern as [`SglangArgs`].
538#[derive(Debug, Clone, Serialize, Deserialize, Validate, Default)]
539pub struct TrtllmArgs {
540    /// Capacity scheduler policy, supported only `"guaranteed_no_evict"`
541    /// (TensorRT-LLM's default). Default: `"guaranteed_no_evict"`.
542    pub capacity_scheduler_policy: Option<String>,
543}
544
545/// Keeps omitted JSON fields distinct from explicit `null` so serde can replace
546/// the old hand-written parser without losing input-config semantics.
547#[derive(Debug, Clone, Default)]
548enum OptionalConfigValue<T> {
549    #[default]
550    Missing,
551    Present(Option<T>),
552}
553
554impl<'de, T> Deserialize<'de> for OptionalConfigValue<T>
555where
556    T: Deserialize<'de>,
557{
558    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
559    where
560        D: serde::Deserializer<'de>,
561    {
562        Option::<T>::deserialize(deserializer).map(Self::Present)
563    }
564}
565
566impl<T> OptionalConfigValue<T> {
567    fn into_nullable(self) -> Option<Option<T>> {
568        match self {
569            Self::Missing => None,
570            Self::Present(value) => Some(value),
571        }
572    }
573
574    fn into_non_null(self, field: &str) -> Result<Option<T>, String> {
575        match self {
576            Self::Missing => Ok(None),
577            Self::Present(Some(value)) => Ok(Some(value)),
578            Self::Present(None) => Err(format!("{field} must not be null")),
579        }
580    }
581}
582
583#[derive(Debug, Clone, Default, Deserialize)]
584#[serde(default, deny_unknown_fields)]
585struct MockEngineArgsSerde {
586    engine_type: OptionalConfigValue<String>,
587    num_gpu_blocks: OptionalConfigValue<usize>,
588    block_size: OptionalConfigValue<usize>,
589    max_model_len: OptionalConfigValue<usize>,
590    max_num_seqs: OptionalConfigValue<usize>,
591    max_num_batched_tokens: OptionalConfigValue<usize>,
592    enable_prefix_caching: OptionalConfigValue<bool>,
593    g1_backend: OptionalConfigValue<G1Backend>,
594    enable_chunked_prefill: OptionalConfigValue<bool>,
595    speedup_ratio: OptionalConfigValue<f64>,
596    decode_speedup_ratio: OptionalConfigValue<f64>,
597    dp_size: OptionalConfigValue<u32>,
598    startup_time: OptionalConfigValue<f64>,
599    worker_type: OptionalConfigValue<String>,
600    is_prefill: OptionalConfigValue<bool>,
601    is_decode: OptionalConfigValue<bool>,
602    planner_profile_data: OptionalConfigValue<PathBuf>,
603    aic_backend: OptionalConfigValue<String>,
604    aic_system: OptionalConfigValue<String>,
605    aic_backend_version: OptionalConfigValue<String>,
606    aic_tp_size: OptionalConfigValue<usize>,
607    aic_model_path: OptionalConfigValue<String>,
608    aic_moe_tp_size: OptionalConfigValue<usize>,
609    aic_moe_ep_size: OptionalConfigValue<usize>,
610    aic_attention_dp_size: OptionalConfigValue<usize>,
611    aic_gemm_dtype: OptionalConfigValue<String>,
612    aic_moe_dtype: OptionalConfigValue<String>,
613    aic_fmha_dtype: OptionalConfigValue<String>,
614    aic_kv_cache_dtype: OptionalConfigValue<String>,
615    aic_comm_dtype: OptionalConfigValue<String>,
616    aic_nextn: OptionalConfigValue<usize>,
617    aic_nextn_accept_rates: OptionalConfigValue<String>,
618    aic_mtp_seed: OptionalConfigValue<u64>,
619    gpu_memory_utilization: OptionalConfigValue<f64>,
620    mem_fraction_static: OptionalConfigValue<f64>,
621    free_gpu_memory_fraction: OptionalConfigValue<f64>,
622    enable_local_indexer: OptionalConfigValue<bool>,
623    bootstrap_port: OptionalConfigValue<u16>,
624    handoff_session_timeout_ms: OptionalConfigValue<u64>,
625    kv_bytes_per_token: OptionalConfigValue<usize>,
626    kv_transfer_bandwidth: OptionalConfigValue<f64>,
627    kv_transfer_timing_mode: OptionalConfigValue<String>,
628    num_g2_blocks: OptionalConfigValue<usize>,
629    num_g3_blocks: OptionalConfigValue<usize>,
630    enable_g4_storage: OptionalConfigValue<bool>,
631    offload_batch_size: OptionalConfigValue<usize>,
632    bandwidth_g1_to_g2_gbps: OptionalConfigValue<f64>,
633    bandwidth_g2_to_g1_gbps: OptionalConfigValue<f64>,
634    bandwidth_g2_to_g3_gbps: OptionalConfigValue<f64>,
635    bandwidth_g3_to_g2_gbps: OptionalConfigValue<f64>,
636    bandwidth_g2_to_g4_gbps: OptionalConfigValue<f64>,
637    bandwidth_g4_to_g2_gbps: OptionalConfigValue<f64>,
638    reasoning: OptionalConfigValue<ReasoningConfig>,
639    response_replay_trace_path: OptionalConfigValue<PathBuf>,
640    zmq_kv_events_port: OptionalConfigValue<u16>,
641    zmq_replay_port: OptionalConfigValue<u16>,
642    preemption_mode: OptionalConfigValue<String>,
643    router_queue_policy: OptionalConfigValue<String>,
644    sglang: OptionalConfigValue<SglangArgs>,
645    trtllm: OptionalConfigValue<TrtllmArgs>,
646    #[serde(rename = "has_perf_model")]
647    _has_perf_model: OptionalConfigValue<serde_json::Value>,
648}
649
650fn load_perf_model(path: &Path) -> Arc<PerfModel> {
651    match PerfModel::from_npz(path) {
652        Ok(model) => {
653            tracing::info!("Successfully loaded performance model from: {:?}", path);
654            Arc::new(model)
655        }
656        Err(e) => {
657            tracing::error!(
658                "Failed to load performance model from {:?}: {}. Falling back to polynomial model.",
659                path,
660                e
661            );
662            Arc::new(PerfModel::default())
663        }
664    }
665}
666
667/// Configuration arguments for MockEngine
668#[derive(Debug, Clone, Serialize, Deserialize, Builder, Validate)]
669#[serde(try_from = "MockEngineArgsSerde")]
670#[validate(schema(function = "validate_mock_engine_args"))]
671#[builder(pattern = "owned", build_fn(public))]
672pub struct MockEngineArgs {
673    /// Engine type: vLLM, SGLang, or TensorRT-LLM simulation
674    #[builder(default = "EngineType::Vllm")]
675    pub engine_type: EngineType,
676
677    /// Usable simulated G1 capacity. This preserves the mocker's historical
678    /// convention across backends. A raw vLLM `num_gpu_blocks` value also
679    /// includes its reserved null block, so parity runs configure real vLLM
680    /// with one additional total block.
681    #[builder(default = "16384")]
682    #[validate(range(min = 1))]
683    pub num_gpu_blocks: usize,
684
685    #[builder(default = "0")]
686    pub block_size: usize,
687
688    /// Optional vLLM sequence-length limit, including prompt and generated
689    /// tokens. Requests with no room to generate are rejected before admission.
690    #[builder(default = "None")]
691    #[validate(range(min = 1))]
692    pub max_model_len: Option<usize>,
693
694    // This was 1024 in the past but reverted back to 256
695    #[builder(default = Some(256))]
696    #[validate(range(min = 1))]
697    pub max_num_seqs: Option<usize>,
698
699    // default for open api server, for llm class it's 16384
700    #[builder(default = Some(8192))]
701    #[validate(range(min = 1))]
702    pub max_num_batched_tokens: Option<usize>,
703
704    #[builder(default = true)]
705    pub enable_prefix_caching: bool,
706
707    /// Requested G1 block-manager implementation for the shared vLLM/TRT-LLM
708    /// scheduler. `None` selects native unless legacy offload requires KVBM.
709    /// Ignored by the SGLang scheduler, which uses `SglangKvManager`.
710    #[builder(default = "None", setter(strip_option))]
711    #[serde(skip_serializing_if = "Option::is_none")]
712    pub g1_backend: Option<G1Backend>,
713
714    #[builder(default = true)]
715    pub enable_chunked_prefill: bool,
716
717    #[builder(default = "1.0")]
718    #[validate(range(min = 0.0))]
719    pub speedup_ratio: f64,
720
721    /// Additional speedup multiplier applied only to decode steps.
722    /// Models speculative decoding (e.g. Eagle) where decode throughput improves
723    /// without affecting prefill latency. The effective decode speedup is
724    /// `speedup_ratio * decode_speedup_ratio`.
725    #[builder(default = "1.0")]
726    #[validate(range(min = 0.0))]
727    pub decode_speedup_ratio: f64,
728
729    #[builder(default = "1")]
730    #[validate(range(min = 1))]
731    pub dp_size: u32,
732
733    /// Optional startup time in seconds to simulate engine initialization delay
734    #[builder(default = "None")]
735    #[validate(range(min = 0.0))]
736    pub startup_time: Option<f64>,
737
738    /// Worker type for disaggregated serving (Aggregated, Prefill, or Decode)
739    #[builder(default = "WorkerType::Aggregated")]
740    pub worker_type: WorkerType,
741
742    /// Original planner profile NPZ path used to materialize `perf_model`.
743    #[builder(default = "None")]
744    pub planner_profile_data: Option<PathBuf>,
745
746    /// Performance model for timing predictions (not serialized, loaded from planner_profile_data)
747    #[serde(skip)]
748    #[builder(default = "Arc::new(PerfModel::default())")]
749    pub perf_model: Arc<PerfModel>,
750
751    /// If set, indicates direct AIC SDK calls should be used.
752    /// The value is the backend name (e.g., "sglang", "vllm").
753    /// The Python layer reads this and overrides perf_model with an Aiconfigurator callback.
754    #[serde(skip)]
755    #[builder(default = "None")]
756    pub aic_backend: Option<String>,
757
758    /// AIC GPU system name (e.g., "h200_sxm"). Required when aic_backend is set.
759    #[serde(skip)]
760    #[builder(default = "None")]
761    pub aic_system: Option<String>,
762
763    /// AIC backend engine version (e.g., "0.12.0" for vLLM, "0.5.6.post2" for SGLang).
764    /// If None, uses the default version for the backend.
765    #[serde(skip)]
766    #[builder(default = "None")]
767    pub aic_backend_version: Option<String>,
768
769    /// Tensor parallel size for AIC latency prediction.
770    /// Only affects AIC performance model lookups, not mocker scheduling.
771    #[serde(skip)]
772    #[builder(default = "None")]
773    pub aic_tp_size: Option<usize>,
774
775    /// HuggingFace model path for AIC latency prediction (e.g., "nvidia/Llama-3.1-8B-Instruct-FP8").
776    #[serde(skip)]
777    #[builder(default = "None")]
778    pub aic_model_path: Option<String>,
779
780    /// MoE tensor-parallel size for AIC latency prediction (e.g., 4 for pure MoE-TP).
781    /// Required for MoE models; must satisfy: aic_tp_size * aic_attention_dp_size == aic_moe_tp_size * aic_moe_ep_size.
782    #[serde(skip)]
783    #[builder(default = "None")]
784    pub aic_moe_tp_size: Option<usize>,
785
786    /// MoE expert-parallel size for AIC latency prediction (e.g., 4 for pure EP).
787    /// Required for MoE models; must satisfy: aic_tp_size * aic_attention_dp_size == aic_moe_tp_size * aic_moe_ep_size.
788    #[serde(skip)]
789    #[builder(default = "None")]
790    pub aic_moe_ep_size: Option<usize>,
791
792    /// Attention data-parallel size for AIC latency prediction (default: 1).
793    /// Corresponds to the `dp` dimension in AIC CLI output.
794    /// Must satisfy: aic_tp_size * aic_attention_dp_size == aic_moe_tp_size * aic_moe_ep_size.
795    #[serde(skip)]
796    #[builder(default = "None")]
797    pub aic_attention_dp_size: Option<usize>,
798
799    /// Weight dtype override for AIC latency prediction.
800    #[serde(skip)]
801    #[builder(default = "None")]
802    pub aic_gemm_dtype: Option<String>,
803
804    /// MoE kernel dtype override for AIC latency prediction.
805    #[serde(skip)]
806    #[builder(default = "None")]
807    pub aic_moe_dtype: Option<String>,
808
809    /// Activation dtype override for AIC latency prediction.
810    #[serde(skip)]
811    #[builder(default = "None")]
812    pub aic_fmha_dtype: Option<String>,
813
814    /// KV-cache dtype override for AIC latency prediction.
815    #[serde(skip)]
816    #[builder(default = "None")]
817    pub aic_kv_cache_dtype: Option<String>,
818
819    /// Communication (collective) dtype override for AIC latency prediction.
820    #[serde(skip)]
821    #[builder(default = "None")]
822    pub aic_comm_dtype: Option<String>,
823
824    /// MTP/Eagle speculative-decoding draft-token count (1..=5).
825    /// The mocker samples accepted drafts while AIC supplies undiscounted
826    /// verification-round latency.
827    #[builder(default = "None")]
828    #[validate(range(min = 1, max = 5))]
829    pub aic_nextn: Option<usize>,
830
831    /// Conditional acceptance rates for draft tokens, comma-separated.
832    /// Entry i is P(draft i accepted | every earlier draft was accepted).
833    #[builder(default = "None")]
834    pub aic_nextn_accept_rates: Option<String>,
835
836    /// Base RNG seed for MTP burst sampling. Worker rank is added with
837    /// wrapping arithmetic before constructing each worker-local sampler.
838    #[builder(default = "42")]
839    pub aic_mtp_seed: u64,
840
841    /// GPU memory fraction for AIC KV capacity estimation with vLLM.
842    #[builder(default = "None")]
843    #[validate(range(min = 0.0, max = 1.0))]
844    pub gpu_memory_utilization: Option<f64>,
845
846    /// Static memory fraction for AIC KV capacity estimation with SGLang.
847    #[builder(default = "None")]
848    #[validate(range(min = 0.0, max = 1.0))]
849    pub mem_fraction_static: Option<f64>,
850
851    /// Fraction of *free* GPU memory (after weights/buffers) allocated to the KV
852    /// cache, for AIC KV capacity estimation with TRT-LLM. Mirrors TRT-LLM's
853    /// `KvCacheConfig.free_gpu_memory_fraction`. Unlike vLLM's
854    /// `gpu_memory_utilization` (a fraction of *total* memory), this is a
855    /// fraction of what remains after the model is loaded.
856    #[builder(default = "None")]
857    #[validate(range(min = 0.0, max = 1.0))]
858    pub free_gpu_memory_fraction: Option<f64>,
859
860    /// Enable worker-local KV indexer for tracking this worker's own KV cache state
861    #[builder(default = "false")]
862    pub enable_local_indexer: bool,
863
864    /// Bootstrap port for disaggregated serving rendezvous.
865    /// Prefill workers listen on this port; decode workers connect to it.
866    /// If None, bootstrap rendezvous is disabled.
867    #[builder(default = "None")]
868    pub bootstrap_port: Option<u16>,
869
870    /// Absolute live handoff session timeout, excluding modeled transfer delay.
871    #[builder(default = "300_000")]
872    #[validate(range(min = 1))]
873    pub handoff_session_timeout_ms: u64,
874
875    /// KV cache bytes per token, auto-computed from model config by Python CLI.
876    /// Formula: num_layers * 2 * num_kv_heads * head_dim * dtype_bytes
877    #[builder(default = "None")]
878    pub kv_bytes_per_token: Option<usize>,
879
880    /// KV cache transfer bandwidth in GB/s for disaggregated serving latency simulation.
881    /// Default: 64.0 (inter-node InfiniBand). Set to 0 to disable KV transfer delay.
882    /// For intra-node NVLink, typical value is ~450.
883    #[builder(default = "None")]
884    #[validate(range(min = 0.0))]
885    pub kv_transfer_bandwidth: Option<f64>,
886
887    /// Selects whether disaggregated transfer timing charges the full prompt
888    /// or only the physical prompt footprint missing at the destination.
889    #[builder(default = "KvTransferTimingMode::FullPrompt")]
890    pub kv_transfer_timing_mode: KvTransferTimingMode,
891
892    /// KVBM G2 (host DRAM) block capacity. When the `kvbm-offload`
893    /// feature is enabled, setting this explicitly opts the mocker into
894    /// G2 offload simulation. When unset or set to 0, no G2 offload engine
895    /// is attached.
896    #[builder(default = "None")]
897    #[validate(range(min = 1))]
898    pub num_g2_blocks: Option<usize>,
899
900    /// KVBM G3 shared lower-tier block capacity. Positive values require
901    /// `num_g2_blocks` and a resolvable KV block byte size; 0 disables G3.
902    #[builder(default = "None")]
903    #[validate(range(min = 1))]
904    pub num_g3_blocks: Option<usize>,
905
906    /// Enable KVBM mock G4 object-storage simulation. G4 stages through G2
907    /// and uses object presence operations instead of a `BlockManager<G4>`.
908    #[builder(default = "false")]
909    pub enable_g4_storage: bool,
910
911    /// Batch size for the G1→G2 offload pipeline. Offloads are grouped
912    /// into batches of this size before being handed to the worker.
913    /// Only consulted when the `kvbm-offload` feature is enabled;
914    /// falls back to the `KvbmOffloadConfig` default when unset or 0.
915    #[builder(default = "None")]
916    #[validate(range(min = 1))]
917    pub offload_batch_size: Option<usize>,
918
919    /// G1→G2 offload bandwidth in GB/s for the PS-queue simulation.
920    /// Only consulted when the `kvbm-offload` feature is enabled;
921    /// falls back to the `KvbmOffloadConfig` default (host DRAM PCIe
922    /// ballpark) when unset.
923    #[builder(default = "None")]
924    #[validate(range(min = 0.0))]
925    pub bandwidth_g1_to_g2_gbps: Option<f64>,
926
927    /// G2→G1 onboard bandwidth in GB/s for the PS-queue simulation.
928    /// Only consulted when the `kvbm-offload` feature is enabled;
929    /// falls back to the `KvbmOffloadConfig` default when unset.
930    #[builder(default = "None")]
931    #[validate(range(min = 0.0))]
932    pub bandwidth_g2_to_g1_gbps: Option<f64>,
933
934    /// G2→G3 offload bandwidth in GB/s for the shared PS-queue simulation.
935    #[builder(default = "None")]
936    #[validate(range(min = 0.0))]
937    pub bandwidth_g2_to_g3_gbps: Option<f64>,
938
939    /// G3→G2 staging bandwidth in GB/s for the shared PS-queue simulation.
940    #[builder(default = "None")]
941    #[validate(range(min = 0.0))]
942    pub bandwidth_g3_to_g2_gbps: Option<f64>,
943
944    /// G2→G4 object offload bandwidth in GB/s for the shared PS-queue simulation.
945    #[builder(default = "None")]
946    #[validate(range(min = 0.0))]
947    pub bandwidth_g2_to_g4_gbps: Option<f64>,
948
949    /// G4→G2 object staging bandwidth in GB/s for the shared PS-queue simulation.
950    #[builder(default = "None")]
951    #[validate(range(min = 0.0))]
952    pub bandwidth_g4_to_g2_gbps: Option<f64>,
953
954    /// Reasoning/thinking token configuration.
955    /// When set, the mocker wraps output in thinking boundary tokens.
956    #[builder(default = "None")]
957    pub reasoning: Option<ReasoningConfig>,
958
959    /// Optional Mooncake trace with exact output token IDs keyed by
960    /// `output_replay_id` annotations. Direct replay paths carry the same token
961    /// IDs on `DirectRequest` and do not need this lookup.
962    #[builder(default = "None")]
963    pub response_replay_trace_path: Option<PathBuf>,
964
965    /// ZMQ port for publishing KV events in vLLM's native wire format.
966    /// When set, the scheduler publishes to a ZMQ PUB socket instead of directly to NATS.
967    /// A KvEventPublisher relay subscribes to this socket and forwards events to NATS.
968    #[builder(default = "None")]
969    pub zmq_kv_events_port: Option<u16>,
970
971    /// ZMQ ROUTER port for replay of buffered KV event batches.
972    /// When set alongside `zmq_kv_events_port`, the mocker binds a ROUTER socket
973    /// that streams back buffered batches by sequence number on request.
974    /// Port is offset by dp_rank (replay_port + dp_rank).
975    #[builder(default = "None")]
976    pub zmq_replay_port: Option<u16>,
977
978    /// Preemption mode for decode eviction under memory pressure.
979    /// Lifo (default) evicts the newest request; Fifo evicts the oldest.
980    #[builder(default)]
981    pub preemption_mode: PreemptionMode,
982
983    /// Optional replay-only override for the router queue policy.
984    #[builder(default = "None")]
985    pub router_queue_policy: Option<RouterQueuePolicy>,
986
987    /// SGLang-specific configuration. Only used when `engine_type == Sglang`.
988    #[builder(default = "None")]
989    pub sglang: Option<SglangArgs>,
990
991    /// TensorRT-LLM-specific configuration. Only used when `engine_type == Trtllm`.
992    #[builder(default = "None")]
993    pub trtllm: Option<TrtllmArgs>,
994}
995
996fn mock_engine_args_validation_error(code: &'static str, message: String) -> ValidationError {
997    let mut error = ValidationError::new(code);
998    error.message = Some(message.into());
999    error
1000}
1001
1002fn validate_mock_engine_args(args: &MockEngineArgs) -> Result<(), ValidationError> {
1003    if args.block_size == 0 {
1004        return Err(mock_engine_args_validation_error(
1005            "block_size_zero",
1006            "block_size must be greater than 0".to_string(),
1007        ));
1008    }
1009
1010    if args.g1_backend == Some(G1Backend::Native) && args.requires_kvbm_g1() {
1011        return Err(mock_engine_args_validation_error(
1012            "native_g1_legacy_offload_conflict",
1013            "g1_backend=native cannot be combined with KVBM G2/G3/G4 offload; omit g1_backend to select KVBM automatically or set g1_backend=kvbm explicitly"
1014                .to_string(),
1015        ));
1016    }
1017
1018    if args.num_g3_blocks.is_some() && args.num_g2_blocks.is_none() {
1019        return Err(mock_engine_args_validation_error(
1020            "g3_requires_g2",
1021            "num_g3_blocks requires num_g2_blocks because mocker stages G3 through G2".to_string(),
1022        ));
1023    }
1024
1025    if args.max_model_len.is_some() && args.engine_type != EngineType::Vllm {
1026        return Err(mock_engine_args_validation_error(
1027            "max_model_len_requires_vllm",
1028            format!(
1029                "max_model_len is supported only for engine_type=vllm, got engine_type={:?}",
1030                args.engine_type
1031            ),
1032        ));
1033    }
1034    if args.enable_g4_storage && args.num_g2_blocks.is_none() {
1035        return Err(mock_engine_args_validation_error(
1036            "g4_requires_g2",
1037            "enable_g4_storage requires num_g2_blocks because mocker stages G4 through G2"
1038                .to_string(),
1039        ));
1040    }
1041
1042    if args.aic_nextn.is_some() && args.decode_speedup_ratio != 1.0 {
1043        return Err(mock_engine_args_validation_error(
1044            "mtp_decode_speedup_conflict",
1045            format!(
1046                "aic_nextn requires decode_speedup_ratio=1.0 because MTP output acceleration is modeled by burst sampling, got {}",
1047                args.decode_speedup_ratio
1048            ),
1049        ));
1050    }
1051
1052    if args.aic_nextn.is_none() && args.aic_nextn_accept_rates.is_some() {
1053        return Err(mock_engine_args_validation_error(
1054            "mtp_rates_without_nextn",
1055            "aic_nextn_accept_rates requires aic_nextn".to_string(),
1056        ));
1057    }
1058
1059    if let Some(policy) = args
1060        .trtllm
1061        .as_ref()
1062        .and_then(|trtllm| trtllm.capacity_scheduler_policy.as_deref())
1063        && policy != "guaranteed_no_evict"
1064    {
1065        return Err(mock_engine_args_validation_error(
1066            "trtllm_unsupported_capacity_scheduler_policy",
1067            format!(
1068                "engine_type=trtllm v1 supports only capacity_scheduler_policy='guaranteed_no_evict', got '{policy}'",
1069            ),
1070        ));
1071    }
1072
1073    if args.engine_type != EngineType::Sglang {
1074        return Ok(());
1075    }
1076
1077    if let Some(page_size) = args.sglang.as_ref().and_then(|sglang| sglang.page_size)
1078        && args.block_size != page_size
1079    {
1080        return Err(mock_engine_args_validation_error(
1081            "sglang_block_size_page_size_mismatch",
1082            format!(
1083                "engine_type=sglang requires block_size and sglang.page_size to match when both are set, got block_size={} and sglang.page_size={page_size}",
1084                args.block_size,
1085            ),
1086        ));
1087    }
1088
1089    if let Some(chunked_prefill_size) = args
1090        .sglang
1091        .as_ref()
1092        .and_then(|sglang| sglang.chunked_prefill_size)
1093        && chunked_prefill_size % args.block_size != 0
1094    {
1095        return Err(mock_engine_args_validation_error(
1096            "sglang_chunked_prefill_size_not_divisible_by_block_size",
1097            format!(
1098                "engine_type=sglang requires sglang.chunked_prefill_size to be divisible by block_size, got chunked_prefill_size={} and block_size={}",
1099                chunked_prefill_size, args.block_size,
1100            ),
1101        ));
1102    }
1103
1104    Ok(())
1105}
1106
1107impl TryFrom<MockEngineArgsSerde> for MockEngineArgs {
1108    type Error = String;
1109
1110    fn try_from(compat: MockEngineArgsSerde) -> Result<Self, Self::Error> {
1111        let mut builder = Self::builder();
1112
1113        if let Some(engine_type) = compat.engine_type.into_non_null("engine_type")? {
1114            builder = builder.engine_type(engine_type.parse()?);
1115        }
1116        if let Some(Some(num_gpu_blocks)) = compat.num_gpu_blocks.into_nullable() {
1117            builder = builder.num_gpu_blocks(num_gpu_blocks);
1118        }
1119        if let Some(block_size) = compat.block_size.into_non_null("block_size")? {
1120            builder = builder.block_size(block_size);
1121        }
1122        if let Some(max_model_len) = compat.max_model_len.into_nullable() {
1123            builder = builder.max_model_len(max_model_len);
1124        }
1125        if let Some(max_num_seqs) = compat.max_num_seqs.into_nullable() {
1126            builder = builder.max_num_seqs(max_num_seqs);
1127        }
1128        if let Some(max_num_batched_tokens) = compat.max_num_batched_tokens.into_nullable() {
1129            builder = builder.max_num_batched_tokens(max_num_batched_tokens);
1130        }
1131        if let Some(enable_prefix_caching) = compat
1132            .enable_prefix_caching
1133            .into_non_null("enable_prefix_caching")?
1134        {
1135            builder = builder.enable_prefix_caching(enable_prefix_caching);
1136        }
1137        if let Some(g1_backend) = compat.g1_backend.into_non_null("g1_backend")? {
1138            builder = builder.g1_backend(g1_backend);
1139        }
1140        if let Some(enable_chunked_prefill) = compat
1141            .enable_chunked_prefill
1142            .into_non_null("enable_chunked_prefill")?
1143        {
1144            builder = builder.enable_chunked_prefill(enable_chunked_prefill);
1145        }
1146        if let Some(speedup_ratio) = compat.speedup_ratio.into_non_null("speedup_ratio")? {
1147            builder = builder.speedup_ratio(speedup_ratio);
1148        }
1149        if let Some(decode_speedup_ratio) = compat
1150            .decode_speedup_ratio
1151            .into_non_null("decode_speedup_ratio")?
1152        {
1153            builder = builder.decode_speedup_ratio(decode_speedup_ratio);
1154        }
1155        if let Some(dp_size) = compat.dp_size.into_non_null("dp_size")? {
1156            builder = builder.dp_size(dp_size);
1157        }
1158        if let Some(startup_time) = compat.startup_time.into_nullable() {
1159            builder = builder.startup_time(startup_time);
1160        }
1161
1162        let worker_type = if let Some(worker_type) =
1163            compat.worker_type.into_non_null("worker_type")?
1164        {
1165            worker_type.parse()?
1166        } else {
1167            let is_prefill = compat
1168                .is_prefill
1169                .into_non_null("is_prefill")?
1170                .unwrap_or(false);
1171            let is_decode = compat
1172                .is_decode
1173                .into_non_null("is_decode")?
1174                .unwrap_or(false);
1175
1176            match (is_prefill, is_decode) {
1177                (false, false) => WorkerType::Aggregated,
1178                (true, false) => WorkerType::Prefill,
1179                (false, true) => WorkerType::Decode,
1180                (true, true) => {
1181                    return Err(
1182                        "Invalid worker configuration: is_prefill and is_decode cannot both be true."
1183                            .to_string(),
1184                    );
1185                }
1186            }
1187        };
1188        builder = builder.worker_type(worker_type);
1189
1190        if let Some(planner_profile_data) = compat.planner_profile_data.into_nullable() {
1191            builder = builder.planner_profile_data(planner_profile_data.clone());
1192            if let Some(path) = planner_profile_data {
1193                builder = builder.perf_model(load_perf_model(&path));
1194            }
1195        }
1196
1197        if let Some(aic_backend) = compat.aic_backend.into_nullable() {
1198            builder = builder.aic_backend(aic_backend);
1199        }
1200        if let Some(aic_system) = compat.aic_system.into_nullable() {
1201            builder = builder.aic_system(aic_system);
1202        }
1203        if let Some(aic_backend_version) = compat.aic_backend_version.into_nullable() {
1204            builder = builder.aic_backend_version(aic_backend_version);
1205        }
1206        if let Some(aic_tp_size) = compat.aic_tp_size.into_nullable() {
1207            builder = builder.aic_tp_size(aic_tp_size);
1208        }
1209        if let Some(aic_model_path) = compat.aic_model_path.into_nullable() {
1210            builder = builder.aic_model_path(aic_model_path);
1211        }
1212        if let Some(aic_moe_tp_size) = compat.aic_moe_tp_size.into_nullable() {
1213            builder = builder.aic_moe_tp_size(aic_moe_tp_size);
1214        }
1215        if let Some(aic_moe_ep_size) = compat.aic_moe_ep_size.into_nullable() {
1216            builder = builder.aic_moe_ep_size(aic_moe_ep_size);
1217        }
1218        if let Some(aic_attention_dp_size) = compat.aic_attention_dp_size.into_nullable() {
1219            builder = builder.aic_attention_dp_size(aic_attention_dp_size);
1220        }
1221        if let Some(aic_gemm_dtype) = compat.aic_gemm_dtype.into_nullable() {
1222            builder = builder.aic_gemm_dtype(aic_gemm_dtype);
1223        }
1224        if let Some(aic_moe_dtype) = compat.aic_moe_dtype.into_nullable() {
1225            builder = builder.aic_moe_dtype(aic_moe_dtype);
1226        }
1227        if let Some(aic_fmha_dtype) = compat.aic_fmha_dtype.into_nullable() {
1228            builder = builder.aic_fmha_dtype(aic_fmha_dtype);
1229        }
1230        if let Some(aic_kv_cache_dtype) = compat.aic_kv_cache_dtype.into_nullable() {
1231            builder = builder.aic_kv_cache_dtype(aic_kv_cache_dtype);
1232        }
1233        if let Some(aic_comm_dtype) = compat.aic_comm_dtype.into_nullable() {
1234            builder = builder.aic_comm_dtype(aic_comm_dtype);
1235        }
1236        if let Some(aic_nextn) = compat.aic_nextn.into_nullable() {
1237            builder = builder.aic_nextn(aic_nextn);
1238        }
1239        if let Some(aic_nextn_accept_rates) = compat.aic_nextn_accept_rates.into_nullable() {
1240            builder = builder.aic_nextn_accept_rates(aic_nextn_accept_rates);
1241        }
1242        if let Some(aic_mtp_seed) = compat.aic_mtp_seed.into_non_null("aic_mtp_seed")? {
1243            builder = builder.aic_mtp_seed(aic_mtp_seed);
1244        }
1245        if let Some(gpu_memory_utilization) = compat.gpu_memory_utilization.into_nullable() {
1246            builder = builder.gpu_memory_utilization(gpu_memory_utilization);
1247        }
1248        if let Some(mem_fraction_static) = compat.mem_fraction_static.into_nullable() {
1249            builder = builder.mem_fraction_static(mem_fraction_static);
1250        }
1251        if let Some(free_gpu_memory_fraction) = compat.free_gpu_memory_fraction.into_nullable() {
1252            builder = builder.free_gpu_memory_fraction(free_gpu_memory_fraction);
1253        }
1254        if let Some(enable_local_indexer) = compat
1255            .enable_local_indexer
1256            .into_non_null("enable_local_indexer")?
1257        {
1258            builder = builder.enable_local_indexer(enable_local_indexer);
1259        }
1260        if let Some(bootstrap_port) = compat.bootstrap_port.into_nullable() {
1261            builder = builder.bootstrap_port(bootstrap_port);
1262        }
1263        if let Some(timeout_ms) = compat
1264            .handoff_session_timeout_ms
1265            .into_non_null("handoff_session_timeout_ms")?
1266        {
1267            builder = builder.handoff_session_timeout_ms(timeout_ms);
1268        }
1269        if let Some(kv_bytes_per_token) = compat.kv_bytes_per_token.into_nullable() {
1270            builder = builder.kv_bytes_per_token(kv_bytes_per_token);
1271        }
1272        if let Some(kv_transfer_bandwidth) = compat.kv_transfer_bandwidth.into_nullable() {
1273            builder = builder.kv_transfer_bandwidth(kv_transfer_bandwidth);
1274        }
1275        if let Some(mode) = compat
1276            .kv_transfer_timing_mode
1277            .into_non_null("kv_transfer_timing_mode")?
1278        {
1279            builder = builder.kv_transfer_timing_mode(mode.parse()?);
1280        }
1281        if let Some(num_g2_blocks) = compat.num_g2_blocks.into_nullable() {
1282            builder = builder.num_g2_blocks(num_g2_blocks);
1283        }
1284        if let Some(num_g3_blocks) = compat.num_g3_blocks.into_nullable() {
1285            builder = builder.num_g3_blocks(num_g3_blocks);
1286        }
1287        if let Some(enable_g4_storage) = compat
1288            .enable_g4_storage
1289            .into_non_null("enable_g4_storage")?
1290        {
1291            builder = builder.enable_g4_storage(enable_g4_storage);
1292        }
1293        if let Some(offload_batch_size) = compat.offload_batch_size.into_nullable() {
1294            builder = builder.offload_batch_size(offload_batch_size);
1295        }
1296        if let Some(bandwidth_g1_to_g2_gbps) = compat.bandwidth_g1_to_g2_gbps.into_nullable() {
1297            builder = builder.bandwidth_g1_to_g2_gbps(bandwidth_g1_to_g2_gbps);
1298        }
1299        if let Some(bandwidth_g2_to_g1_gbps) = compat.bandwidth_g2_to_g1_gbps.into_nullable() {
1300            builder = builder.bandwidth_g2_to_g1_gbps(bandwidth_g2_to_g1_gbps);
1301        }
1302        if let Some(bandwidth_g2_to_g3_gbps) = compat.bandwidth_g2_to_g3_gbps.into_nullable() {
1303            builder = builder.bandwidth_g2_to_g3_gbps(bandwidth_g2_to_g3_gbps);
1304        }
1305        if let Some(bandwidth_g3_to_g2_gbps) = compat.bandwidth_g3_to_g2_gbps.into_nullable() {
1306            builder = builder.bandwidth_g3_to_g2_gbps(bandwidth_g3_to_g2_gbps);
1307        }
1308        if let Some(bandwidth_g2_to_g4_gbps) = compat.bandwidth_g2_to_g4_gbps.into_nullable() {
1309            builder = builder.bandwidth_g2_to_g4_gbps(bandwidth_g2_to_g4_gbps);
1310        }
1311        if let Some(bandwidth_g4_to_g2_gbps) = compat.bandwidth_g4_to_g2_gbps.into_nullable() {
1312            builder = builder.bandwidth_g4_to_g2_gbps(bandwidth_g4_to_g2_gbps);
1313        }
1314        if let Some(reasoning) = compat.reasoning.into_nullable() {
1315            builder = builder.reasoning(reasoning);
1316        }
1317        if let Some(response_replay_trace_path) = compat.response_replay_trace_path.into_nullable()
1318        {
1319            builder = builder.response_replay_trace_path(response_replay_trace_path);
1320        }
1321        if let Some(zmq_kv_events_port) = compat.zmq_kv_events_port.into_nullable() {
1322            builder = builder.zmq_kv_events_port(zmq_kv_events_port);
1323        }
1324        if let Some(zmq_replay_port) = compat.zmq_replay_port.into_nullable() {
1325            builder = builder.zmq_replay_port(zmq_replay_port);
1326        }
1327        if let Some(preemption_mode) = compat.preemption_mode.into_non_null("preemption_mode")? {
1328            builder = builder.preemption_mode(preemption_mode.parse()?);
1329        }
1330        if let Some(router_queue_policy) = compat.router_queue_policy.into_nullable() {
1331            let router_queue_policy = router_queue_policy
1332                .map(|policy| policy.parse().map_err(|e: String| e))
1333                .transpose()?;
1334            builder = builder.router_queue_policy(router_queue_policy);
1335        }
1336        if let Some(sglang) = compat.sglang.into_nullable() {
1337            builder = builder.sglang(sglang);
1338        }
1339        if let Some(trtllm) = compat.trtllm.into_nullable() {
1340            builder = builder.trtllm(trtllm);
1341        }
1342
1343        builder
1344            .build()
1345            .map_err(|e| format!("Failed to build MockEngineArgs: {e}"))?
1346            .normalized()
1347            .map_err(|e| e.to_string())
1348    }
1349}
1350
1351impl Default for MockEngineArgs {
1352    fn default() -> MockEngineArgs {
1353        MockEngineArgsBuilder::default()
1354            .build()
1355            .expect("Failed to build default MockEngineArgs")
1356            .normalized()
1357            .expect("Failed to normalize default MockEngineArgs")
1358    }
1359}
1360
1361impl MockEngineArgs {
1362    const DEFAULT_VLLM_BLOCK_SIZE: usize = 64;
1363    const DEFAULT_SGLANG_BLOCK_SIZE: usize = 1;
1364    const DEFAULT_TRTLLM_BLOCK_SIZE: usize = 32;
1365
1366    pub fn builder() -> MockEngineArgsBuilder {
1367        MockEngineArgsBuilder::default()
1368    }
1369
1370    /// GPUs occupied by one worker (engine), derived from tensor parallelism
1371    /// and the materialized DP topology. AIC-backed replay uses
1372    /// `aic_tp_size × aic_attention_dp_size`; non-AIC replay still counts one
1373    /// GPU for every independently modeled `dp_size` rank. Used to turn
1374    /// provisioned worker-seconds into GPU-hours.
1375    pub fn aic_gpus_per_worker(&self) -> usize {
1376        self.aic_tp_size.unwrap_or(1) * self.dp_size.max(1) as usize
1377    }
1378
1379    /// Finite ownership bound for live handoff queues and sessions.
1380    ///
1381    /// An unset runnable-sequence limit is semantically unbounded, so use the
1382    /// physical KV block count as the conservative process-local bound.
1383    pub fn effective_handoff_capacity(&self) -> usize {
1384        self.max_num_seqs.unwrap_or(self.num_gpu_blocks).max(1)
1385    }
1386
1387    pub fn normalized(mut self) -> anyhow::Result<Self> {
1388        self.materialize_defaults();
1389        self.resolve_g1_backend();
1390        self.validate_config()?;
1391        Ok(self)
1392    }
1393
1394    fn materialize_defaults(&mut self) {
1395        match self.engine_type {
1396            EngineType::Vllm => {
1397                if self.block_size == 0 {
1398                    self.block_size = Self::DEFAULT_VLLM_BLOCK_SIZE;
1399                }
1400            }
1401            EngineType::Sglang => {
1402                let page_size = self.sglang.as_ref().and_then(|sglang| sglang.page_size);
1403                match (self.block_size, page_size) {
1404                    (0, None) => {
1405                        self.block_size = Self::DEFAULT_SGLANG_BLOCK_SIZE;
1406                    }
1407                    (0, Some(page_size)) => {
1408                        self.block_size = page_size;
1409                    }
1410                    (_, Some(_)) => {}
1411                    (_, None) => {}
1412                }
1413            }
1414            EngineType::Trtllm => {
1415                if self.block_size == 0 {
1416                    self.block_size = Self::DEFAULT_TRTLLM_BLOCK_SIZE;
1417                }
1418            }
1419        }
1420
1421        if self.num_g2_blocks == Some(0) {
1422            self.num_g2_blocks = None;
1423        }
1424        if self.num_g3_blocks == Some(0) {
1425            self.num_g3_blocks = None;
1426        }
1427        if self.offload_batch_size == Some(0) {
1428            self.offload_batch_size = None;
1429        }
1430    }
1431
1432    fn requires_kvbm_g1(&self) -> bool {
1433        matches!(self.engine_type, EngineType::Vllm | EngineType::Trtllm)
1434            && (self.num_g2_blocks.is_some_and(|blocks| blocks > 0)
1435                || self.num_g3_blocks.is_some_and(|blocks| blocks > 0)
1436                || self.enable_g4_storage)
1437    }
1438
1439    fn resolve_g1_backend(&mut self) {
1440        if self.g1_backend.is_none() {
1441            self.g1_backend = Some(if self.requires_kvbm_g1() {
1442                G1Backend::Kvbm
1443            } else {
1444                G1Backend::Native
1445            });
1446        }
1447    }
1448
1449    /// Return the selected backend, resolving an unset raw configuration from
1450    /// its engine and lower-tier offload settings.
1451    pub fn resolved_g1_backend(&self) -> G1Backend {
1452        self.g1_backend.unwrap_or_else(|| {
1453            if self.requires_kvbm_g1() {
1454                G1Backend::Kvbm
1455            } else {
1456                G1Backend::Native
1457            }
1458        })
1459    }
1460
1461    fn validate_config(&mut self) -> anyhow::Result<()> {
1462        self.validate()
1463            .map_err(|error| anyhow::anyhow!("Failed to validate MockEngineArgs: {error}"))?;
1464        if let Some(nextn) = self.aic_nextn {
1465            let rates = crate::common::speculative::normalize_conditional_accept_rates(
1466                nextn,
1467                self.aic_nextn_accept_rates.as_deref(),
1468            )?;
1469            self.aic_nextn_accept_rates =
1470                Some(crate::common::speculative::format_accept_rates(&rates));
1471        }
1472        Ok(())
1473    }
1474
1475    /// Scheduling policy applied by the shared vLLM scheduler core, derived
1476    /// from the engine type. TRT-LLM uses `GUARANTEED_NO_EVICT`.
1477    pub fn scheduling_policy(&self) -> SchedulingPolicy {
1478        match self.engine_type {
1479            EngineType::Trtllm => SchedulingPolicy::TrtllmGuaranteedNoEvict,
1480            EngineType::Vllm | EngineType::Sglang => SchedulingPolicy::Vllm,
1481        }
1482    }
1483
1484    pub fn is_prefill(&self) -> bool {
1485        self.worker_type == WorkerType::Prefill
1486    }
1487
1488    pub fn is_decode(&self) -> bool {
1489        self.worker_type == WorkerType::Decode
1490    }
1491
1492    pub fn needs_kv_publisher(&self) -> bool {
1493        self.enable_prefix_caching && !self.is_decode()
1494    }
1495
1496    pub fn undiscounted_aic_accept_rates(&self) -> Option<String> {
1497        crate::common::speculative::undiscounted_aic_accept_rates(self.aic_nextn)
1498    }
1499
1500    /// Create MockEngineArgs from a JSON file containing extra engine arguments
1501    pub fn from_json_file(path: &Path) -> anyhow::Result<Self> {
1502        let file_content = std::fs::read_to_string(path)?;
1503        Self::from_json_str(&file_content)
1504    }
1505
1506    pub fn from_json_str(content: &str) -> anyhow::Result<Self> {
1507        let mut deserializer = serde_json::Deserializer::from_str(content);
1508        let args = serde_path_to_error::deserialize(&mut deserializer)
1509            .map_err(|error| anyhow::anyhow!("{error}"))?;
1510        deserializer
1511            .end()
1512            .map_err(|error| anyhow::anyhow!("{error}"))?;
1513        Ok(args)
1514    }
1515}
1516
1517#[cfg(test)]
1518mod tests {
1519    use std::sync::Mutex;
1520
1521    use super::*;
1522    use serde_json::json;
1523
1524    #[derive(Default)]
1525    struct FailingRawSink {
1526        attempts: Mutex<Vec<u64>>,
1527    }
1528
1529    impl RawKvEventSink for FailingRawSink {
1530        fn publish(&self, event: RawKvEvent) -> anyhow::Result<()> {
1531            self.attempts.lock().unwrap().push(event.event.event_id);
1532            if event.event.event_id == 2 {
1533                anyhow::bail!("injected raw sink failure");
1534            }
1535            Ok(())
1536        }
1537    }
1538
1539    #[test]
1540    fn raw_sink_batch_fallback_attempts_later_events_after_failure() {
1541        let sink = FailingRawSink::default();
1542        let error = sink
1543            .publish_batch(
1544                (1..=3)
1545                    .map(|event_id| RawKvEvent {
1546                        event: KvCacheEvent {
1547                            event_id,
1548                            data: dynamo_kv_router::protocols::KvCacheEventData::Cleared,
1549                            dp_rank: 0,
1550                        },
1551                        block_token_ids: None,
1552                        storage_tier: StorageTier::Device,
1553                    })
1554                    .collect(),
1555            )
1556            .unwrap_err();
1557
1558        assert_eq!(error.to_string(), "injected raw sink failure");
1559        assert_eq!(*sink.attempts.lock().unwrap(), vec![1, 2, 3]);
1560    }
1561
1562    #[test]
1563    fn direct_request_priorities_are_backward_compatible() {
1564        let legacy = json!({
1565            "tokens": [1, 2],
1566            "max_output_tokens": 3,
1567            "uuid": null,
1568            "dp_rank": 0,
1569            "arrival_timestamp_ms": null
1570        });
1571        let request: DirectRequest = serde_json::from_value(legacy).unwrap();
1572        assert_eq!(request.priority, 0);
1573        assert_eq!(request.strict_priority, 0);
1574        assert_eq!(request.router_priorities(), (0.0, 0));
1575
1576        let rendered = serde_json::to_value(&request).unwrap();
1577        assert!(rendered.get("priority").is_none());
1578        assert!(rendered.get("strict_priority").is_none());
1579    }
1580
1581    #[test]
1582    fn direct_request_derives_router_priorities() {
1583        let negative: DirectRequest = serde_json::from_value(json!({
1584            "tokens": [1],
1585            "max_output_tokens": 1,
1586            "uuid": null,
1587            "dp_rank": 0,
1588            "arrival_timestamp_ms": null,
1589            "priority": -7,
1590            "strict_priority": 4
1591        }))
1592        .unwrap();
1593        assert_eq!(negative.router_priorities(), (0.0, 4));
1594
1595        let positive = DirectRequest {
1596            priority: 9,
1597            strict_priority: 5,
1598            ..negative
1599        };
1600        assert_eq!(positive.router_priorities(), (9.0, 5));
1601        let rendered = serde_json::to_value(&positive).unwrap();
1602        assert_eq!(rendered["priority"], 9);
1603        assert_eq!(rendered["strict_priority"], 5);
1604    }
1605
1606    #[test]
1607    fn test_mock_engine_args_json_round_trip_preserves_worker_type_and_nulls() {
1608        let args = MockEngineArgs::builder()
1609            .worker_type(WorkerType::Decode)
1610            .g1_backend(G1Backend::Native)
1611            .max_model_len(Some(32768))
1612            .max_num_seqs(None)
1613            .max_num_batched_tokens(None)
1614            .reasoning(None)
1615            .sglang(None)
1616            .build()
1617            .unwrap()
1618            .normalized()
1619            .unwrap();
1620
1621        let mut payload = serde_json::json!({
1622            "engine_type": "vllm",
1623            "num_gpu_blocks": args.num_gpu_blocks,
1624            "block_size": args.block_size,
1625            "max_num_seqs": args.max_num_seqs,
1626            "max_num_batched_tokens": args.max_num_batched_tokens,
1627            "enable_prefix_caching": args.enable_prefix_caching,
1628            "enable_chunked_prefill": args.enable_chunked_prefill,
1629            "speedup_ratio": args.speedup_ratio,
1630            "decode_speedup_ratio": args.decode_speedup_ratio,
1631            "dp_size": args.dp_size,
1632            "startup_time": args.startup_time,
1633            "worker_type": "decode",
1634            "planner_profile_data": args.planner_profile_data,
1635            "aic_backend": args.aic_backend,
1636            "aic_system": args.aic_system,
1637            "aic_backend_version": args.aic_backend_version,
1638            "aic_tp_size": args.aic_tp_size,
1639            "aic_model_path": args.aic_model_path,
1640            "enable_local_indexer": args.enable_local_indexer,
1641            "bootstrap_port": args.bootstrap_port,
1642            "handoff_session_timeout_ms": args.handoff_session_timeout_ms,
1643            "kv_bytes_per_token": args.kv_bytes_per_token,
1644            "kv_transfer_bandwidth": args.kv_transfer_bandwidth,
1645            "kv_transfer_timing_mode": "full_prompt",
1646            "num_g2_blocks": args.num_g2_blocks,
1647            "num_g3_blocks": args.num_g3_blocks,
1648            "enable_g4_storage": args.enable_g4_storage,
1649            "offload_batch_size": args.offload_batch_size,
1650            "bandwidth_g1_to_g2_gbps": args.bandwidth_g1_to_g2_gbps,
1651            "bandwidth_g2_to_g1_gbps": args.bandwidth_g2_to_g1_gbps,
1652            "bandwidth_g2_to_g3_gbps": args.bandwidth_g2_to_g3_gbps,
1653            "bandwidth_g3_to_g2_gbps": args.bandwidth_g3_to_g2_gbps,
1654            "bandwidth_g2_to_g4_gbps": args.bandwidth_g2_to_g4_gbps,
1655            "bandwidth_g4_to_g2_gbps": args.bandwidth_g4_to_g2_gbps,
1656            "reasoning": args.reasoning,
1657            "zmq_kv_events_port": args.zmq_kv_events_port,
1658            "zmq_replay_port": args.zmq_replay_port,
1659            "preemption_mode": "lifo",
1660            "router_queue_policy": args.router_queue_policy.map(|policy| policy.to_string()),
1661            "sglang": args.sglang,
1662            "has_perf_model": true,
1663        });
1664        payload["max_model_len"] = serde_json::json!(args.max_model_len);
1665        payload["g1_backend"] = serde_json::json!(args.g1_backend);
1666
1667        let restored = MockEngineArgs::from_json_str(&payload.to_string()).unwrap();
1668
1669        assert_eq!(restored.worker_type, WorkerType::Decode);
1670        assert_eq!(restored.max_model_len, Some(32768));
1671        assert_eq!(restored.max_num_seqs, None);
1672        assert_eq!(restored.max_num_batched_tokens, None);
1673        assert_eq!(restored.g1_backend, Some(G1Backend::Native));
1674        assert_eq!(
1675            restored.kv_transfer_timing_mode,
1676            KvTransferTimingMode::FullPrompt
1677        );
1678    }
1679
1680    #[test]
1681    fn test_mock_engine_args_json_omits_unset_g1_backend() {
1682        let args = MockEngineArgs::builder().build().unwrap();
1683        let serialized = serde_json::to_value(args).unwrap();
1684
1685        assert!(serialized.get("g1_backend").is_none());
1686    }
1687
1688    #[test]
1689    fn test_mock_engine_args_accepts_legacy_enum_case_and_writes_lowercase() {
1690        let args = MockEngineArgs::from_json_str(
1691            &json!({
1692                "engine_type": "VLLM",
1693                "worker_type": "Aggregated",
1694                "preemption_mode": "Lifo",
1695                "num_g2_blocks": 8,
1696            })
1697            .to_string(),
1698        )
1699        .unwrap();
1700
1701        assert_eq!(args.g1_backend, Some(G1Backend::Kvbm));
1702        let serialized = serde_json::to_value(args).unwrap();
1703        assert_eq!(serialized["engine_type"], "vllm");
1704        assert_eq!(serialized["worker_type"], "aggregated");
1705        assert_eq!(serialized["preemption_mode"], "lifo");
1706    }
1707
1708    #[test]
1709    fn test_mock_engine_args_json_accepts_aic_quant_dtypes() {
1710        let args = MockEngineArgs::from_json_str(
1711            &json!({
1712                "aic_gemm_dtype": "fp8_block",
1713                "aic_moe_dtype": "w4a16_mxfp4",
1714                "aic_fmha_dtype": "bfloat16",
1715                "aic_kv_cache_dtype": "fp8",
1716                "aic_comm_dtype": "fp8",
1717            })
1718            .to_string(),
1719        )
1720        .unwrap();
1721
1722        assert_eq!(args.aic_gemm_dtype.as_deref(), Some("fp8_block"));
1723        assert_eq!(args.aic_moe_dtype.as_deref(), Some("w4a16_mxfp4"));
1724        assert_eq!(args.aic_fmha_dtype.as_deref(), Some("bfloat16"));
1725        assert_eq!(args.aic_kv_cache_dtype.as_deref(), Some("fp8"));
1726        assert_eq!(args.aic_comm_dtype.as_deref(), Some("fp8"));
1727    }
1728
1729    #[test]
1730    fn test_mock_engine_args_json_rejects_unknown_and_invalid_types() {
1731        let unknown = MockEngineArgs::from_json_str(&json!({"unknown": true}).to_string())
1732            .expect_err("unknown fields should be rejected");
1733        assert!(
1734            unknown.to_string().contains("unknown field"),
1735            "unexpected error: {unknown}",
1736        );
1737
1738        let invalid =
1739            MockEngineArgs::from_json_str(&json!({"gpu_memory_utilization": "bad"}).to_string())
1740                .expect_err("wrongly typed fields should be rejected");
1741        assert!(
1742            invalid.to_string().contains("gpu_memory_utilization"),
1743            "unexpected error: {invalid}",
1744        );
1745
1746        let trailing = MockEngineArgs::from_json_str(r#"{"block_size": 16} true"#)
1747            .expect_err("trailing JSON should be rejected");
1748        assert!(
1749            trailing.to_string().contains("trailing characters"),
1750            "unexpected error: {trailing}",
1751        );
1752    }
1753
1754    #[test]
1755    fn test_unique_block_default_uniqueness() {
1756        // Create 10 default UniqueBlock instances
1757        let blocks: Vec<UniqueBlock> = (0..10).map(|_| UniqueBlock::default()).collect();
1758
1759        // Extract UUIDs from each block
1760        let mut uuids = Vec::new();
1761        for block in blocks {
1762            match block {
1763                UniqueBlock::PartialBlock(uuid) => uuids.push(uuid),
1764                _ => panic!("Expected UuidIdentifier variant"),
1765            }
1766        }
1767
1768        // Check that all UUIDs are unique by comparing each with every other
1769        for i in 0..uuids.len() {
1770            for j in i + 1..uuids.len() {
1771                assert_ne!(
1772                    uuids[i], uuids[j],
1773                    "UUID at index {} and {} are identical",
1774                    i, j
1775                );
1776            }
1777        }
1778    }
1779
1780    #[test]
1781    fn test_normalized_sglang_uses_page_size_alias_for_block_size() {
1782        let args = MockEngineArgs::builder()
1783            .engine_type(EngineType::Sglang)
1784            .sglang(Some(SglangArgs {
1785                page_size: Some(16),
1786                ..Default::default()
1787            }))
1788            .build()
1789            .unwrap()
1790            .normalized()
1791            .unwrap();
1792
1793        assert_eq!(args.block_size, 16);
1794    }
1795
1796    #[test]
1797    fn test_normalized_sglang_accepts_equal_block_size_and_page_size() {
1798        let args = MockEngineArgs::builder()
1799            .engine_type(EngineType::Sglang)
1800            .block_size(8)
1801            .sglang(Some(SglangArgs {
1802                page_size: Some(8),
1803                ..Default::default()
1804            }))
1805            .build()
1806            .unwrap()
1807            .normalized()
1808            .unwrap();
1809
1810        assert_eq!(args.block_size, 8);
1811    }
1812
1813    #[test]
1814    fn test_normalized_sglang_rejects_mismatched_block_size_and_page_size() {
1815        let error = MockEngineArgs::builder()
1816            .engine_type(EngineType::Sglang)
1817            .block_size(8)
1818            .sglang(Some(SglangArgs {
1819                page_size: Some(4),
1820                ..Default::default()
1821            }))
1822            .build()
1823            .unwrap()
1824            .normalized()
1825            .unwrap_err();
1826
1827        assert!(
1828            error
1829                .to_string()
1830                .contains("block_size and sglang.page_size to match"),
1831            "unexpected error: {error}",
1832        );
1833    }
1834
1835    #[test]
1836    fn test_normalized_g3_requires_g2() {
1837        let missing_g2 = MockEngineArgs::builder()
1838            .num_g3_blocks(Some(10))
1839            .kv_bytes_per_token(Some(1024))
1840            .build()
1841            .unwrap()
1842            .normalized()
1843            .unwrap_err();
1844        assert!(
1845            missing_g2.to_string().contains("requires num_g2_blocks"),
1846            "unexpected error: {missing_g2}",
1847        );
1848    }
1849
1850    #[test]
1851    fn test_normalized_g4_requires_g2() {
1852        let missing_g2 = MockEngineArgs::builder()
1853            .enable_g4_storage(true)
1854            .kv_bytes_per_token(Some(1024))
1855            .build()
1856            .unwrap()
1857            .normalized()
1858            .unwrap_err();
1859        assert!(
1860            missing_g2.to_string().contains("requires num_g2_blocks"),
1861            "unexpected error: {missing_g2}",
1862        );
1863    }
1864
1865    #[test]
1866    fn test_native_g1_accepts_both_shared_scheduler_engines() {
1867        for engine_type in [EngineType::Vllm, EngineType::Trtllm] {
1868            let args = MockEngineArgs::builder()
1869                .engine_type(engine_type)
1870                .g1_backend(G1Backend::Native)
1871                .build()
1872                .unwrap()
1873                .normalized()
1874                .unwrap_or_else(|error| {
1875                    panic!("native G1 should support {engine_type:?}: {error}")
1876                });
1877            assert_eq!(args.g1_backend, Some(G1Backend::Native));
1878        }
1879    }
1880
1881    #[test]
1882    fn test_g1_backend_defaults_to_native() {
1883        let default_args = MockEngineArgs::default();
1884        assert_eq!(default_args.g1_backend, Some(G1Backend::Native));
1885
1886        let json_args = MockEngineArgs::from_json_str("{}").unwrap();
1887        assert_eq!(json_args.g1_backend, Some(G1Backend::Native));
1888    }
1889
1890    #[test]
1891    fn test_legacy_kvbm_offload_selects_kvbm_g1() {
1892        let configs = [
1893            MockEngineArgs::builder()
1894                .num_g2_blocks(Some(8))
1895                .build()
1896                .unwrap(),
1897            MockEngineArgs::builder()
1898                .num_g2_blocks(Some(8))
1899                .num_g3_blocks(Some(16))
1900                .build()
1901                .unwrap(),
1902            MockEngineArgs::builder()
1903                .num_g2_blocks(Some(8))
1904                .enable_g4_storage(true)
1905                .build()
1906                .unwrap(),
1907        ];
1908
1909        for config in configs {
1910            assert_eq!(config.resolved_g1_backend(), G1Backend::Kvbm);
1911            let args = config.normalized().unwrap();
1912            assert_eq!(args.g1_backend, Some(G1Backend::Kvbm));
1913        }
1914    }
1915
1916    #[test]
1917    fn test_explicit_native_g1_with_offload_is_rejected() {
1918        for engine_type in [EngineType::Vllm, EngineType::Trtllm] {
1919            let error = MockEngineArgs::builder()
1920                .engine_type(engine_type)
1921                .g1_backend(G1Backend::Native)
1922                .num_g2_blocks(Some(8))
1923                .build()
1924                .unwrap()
1925                .normalized()
1926                .unwrap_err();
1927
1928            assert!(
1929                error.to_string().contains("omit g1_backend"),
1930                "unexpected error for {engine_type:?}: {error}"
1931            );
1932        }
1933    }
1934
1935    #[test]
1936    fn test_explicit_native_g1_accepts_disabled_offload() {
1937        let args = MockEngineArgs::builder()
1938            .g1_backend(G1Backend::Native)
1939            .num_g2_blocks(Some(0))
1940            .num_g3_blocks(Some(0))
1941            .build()
1942            .unwrap()
1943            .normalized()
1944            .unwrap();
1945
1946        assert_eq!(args.g1_backend, Some(G1Backend::Native));
1947        assert_eq!(args.num_g2_blocks, None);
1948        assert_eq!(args.num_g3_blocks, None);
1949    }
1950
1951    #[test]
1952    fn test_g1_backend_is_ignored_for_sglang() {
1953        for g1_backend in [G1Backend::Kvbm, G1Backend::Native] {
1954            let args = MockEngineArgs::builder()
1955                .engine_type(EngineType::Sglang)
1956                .g1_backend(g1_backend)
1957                .build()
1958                .unwrap()
1959                .normalized()
1960                .unwrap();
1961            assert_eq!(args.engine_type, EngineType::Sglang);
1962            assert_eq!(args.g1_backend, Some(g1_backend));
1963        }
1964    }
1965
1966    #[test]
1967    fn test_native_g1_accepts_mtp() {
1968        for engine_type in [EngineType::Vllm, EngineType::Trtllm] {
1969            let args = MockEngineArgs::builder()
1970                .engine_type(engine_type)
1971                .g1_backend(G1Backend::Native)
1972                .aic_nextn(Some(1))
1973                .build()
1974                .unwrap()
1975                .normalized()
1976                .unwrap_or_else(|error| {
1977                    panic!("native G1 MTP should support {engine_type:?}: {error}")
1978                });
1979            assert_eq!(args.g1_backend, Some(G1Backend::Native));
1980            assert_eq!(args.aic_nextn, Some(1));
1981        }
1982    }
1983
1984    #[test]
1985    fn test_normalized_rejects_out_of_range_aic_nextn() {
1986        // The mocker/replay JSON path must share AicPerfConfig's 1..=5 contract.
1987        for bad in [0_usize, 6, usize::MAX] {
1988            let err = MockEngineArgs::builder()
1989                .aic_nextn(Some(bad))
1990                .build()
1991                .unwrap()
1992                .normalized()
1993                .unwrap_err();
1994            assert!(
1995                err.to_string().contains("aic_nextn"),
1996                "unexpected error for nextn={bad}: {err}",
1997            );
1998        }
1999        MockEngineArgs::builder()
2000            .aic_nextn(Some(3))
2001            .build()
2002            .unwrap()
2003            .normalized()
2004            .expect("in-range aic_nextn should validate");
2005    }
2006
2007    #[test]
2008    fn test_normalized_rejects_zero_max_model_len() {
2009        let error = MockEngineArgs::builder()
2010            .max_model_len(Some(0))
2011            .build()
2012            .unwrap()
2013            .normalized()
2014            .unwrap_err();
2015
2016        assert!(
2017            error.to_string().contains("max_model_len"),
2018            "unexpected error: {error}",
2019        );
2020    }
2021
2022    #[test]
2023    fn test_mtp_defaults_and_json_round_trip() {
2024        let args = MockEngineArgs::builder()
2025            .aic_nextn(Some(3))
2026            .build()
2027            .unwrap()
2028            .normalized()
2029            .unwrap();
2030        assert_eq!(args.aic_nextn_accept_rates.as_deref(), Some("0.85,0.3,0"));
2031        assert_eq!(args.aic_mtp_seed, 42);
2032        assert_eq!(
2033            args.undiscounted_aic_accept_rates().as_deref(),
2034            Some("0,0,0")
2035        );
2036
2037        let json = serde_json::to_string(&args).unwrap();
2038        let round_trip = MockEngineArgs::from_json_str(&json).unwrap();
2039        assert_eq!(round_trip.aic_nextn, Some(3));
2040        assert_eq!(
2041            round_trip.aic_nextn_accept_rates.as_deref(),
2042            Some("0.85,0.3,0")
2043        );
2044        assert_eq!(round_trip.aic_mtp_seed, 42);
2045    }
2046
2047    #[test]
2048    fn test_mtp_rates_are_validated_before_normalization() {
2049        for rates in ["nan", "inf", "-0.1", "1.1", "bad"] {
2050            let err = MockEngineArgs::builder()
2051                .aic_nextn(Some(1))
2052                .aic_nextn_accept_rates(Some(rates.to_string()))
2053                .build()
2054                .unwrap()
2055                .normalized()
2056                .unwrap_err();
2057            assert!(
2058                err.to_string().contains("aic_nextn_accept_rates"),
2059                "unexpected error for rates={rates:?}: {err}"
2060            );
2061        }
2062    }
2063
2064    #[test]
2065    fn test_mtp_rates_are_padded_and_truncated_to_nextn() {
2066        let padded = MockEngineArgs::builder()
2067            .aic_nextn(Some(3))
2068            .aic_nextn_accept_rates(Some("1,0.5".to_string()))
2069            .build()
2070            .unwrap()
2071            .normalized()
2072            .unwrap();
2073        assert_eq!(padded.aic_nextn_accept_rates.as_deref(), Some("1,0.5,0"));
2074
2075        let truncated = MockEngineArgs::builder()
2076            .aic_nextn(Some(2))
2077            .aic_nextn_accept_rates(Some("1,0.5,0.25".to_string()))
2078            .build()
2079            .unwrap()
2080            .normalized()
2081            .unwrap();
2082        assert_eq!(truncated.aic_nextn_accept_rates.as_deref(), Some("1,0.5"));
2083    }
2084
2085    #[test]
2086    fn test_mtp_rejects_decode_speedup_ratio() {
2087        let err = MockEngineArgs::builder()
2088            .aic_nextn(Some(1))
2089            .decode_speedup_ratio(2.0)
2090            .build()
2091            .unwrap()
2092            .normalized()
2093            .unwrap_err();
2094        assert!(err.to_string().contains("decode_speedup_ratio=1.0"));
2095    }
2096
2097    #[test]
2098    fn test_normalized_zero_disables_optional_offload_knobs() {
2099        let args = MockEngineArgs::builder()
2100            .num_g2_blocks(Some(0))
2101            .num_g3_blocks(Some(0))
2102            .offload_batch_size(Some(0))
2103            .build()
2104            .unwrap()
2105            .normalized()
2106            .unwrap();
2107
2108        assert_eq!(args.num_g2_blocks, None);
2109        assert_eq!(args.num_g3_blocks, None);
2110        assert!(!args.enable_g4_storage);
2111        assert_eq!(args.offload_batch_size, None);
2112    }
2113
2114    #[test]
2115    fn test_normalized_zero_g3_does_not_require_g2_or_kv_bytes() {
2116        let args = MockEngineArgs::builder()
2117            .num_g3_blocks(Some(0))
2118            .build()
2119            .unwrap()
2120            .normalized()
2121            .unwrap();
2122
2123        assert_eq!(args.num_g3_blocks, None);
2124    }
2125
2126    #[test]
2127    fn test_normalized_g3_allows_missing_kv_bytes_for_cli_auto_compute() {
2128        let args = MockEngineArgs::builder()
2129            .num_g2_blocks(Some(10))
2130            .num_g3_blocks(Some(10))
2131            .build()
2132            .unwrap()
2133            .normalized()
2134            .unwrap();
2135
2136        assert_eq!(args.num_g2_blocks, Some(10));
2137        assert_eq!(args.num_g3_blocks, Some(10));
2138        assert_eq!(args.kv_bytes_per_token, None);
2139    }
2140
2141    #[test]
2142    fn test_normalized_g4_allows_missing_kv_bytes_for_cli_auto_compute() {
2143        let args = MockEngineArgs::builder()
2144            .num_g2_blocks(Some(10))
2145            .enable_g4_storage(true)
2146            .build()
2147            .unwrap()
2148            .normalized()
2149            .unwrap();
2150
2151        assert_eq!(args.num_g2_blocks, Some(10));
2152        assert!(args.enable_g4_storage);
2153        assert_eq!(args.kv_bytes_per_token, None);
2154    }
2155
2156    #[test]
2157    fn test_normalized_sglang_defaults_block_size_to_one() {
2158        let args = MockEngineArgs::builder()
2159            .engine_type(EngineType::Sglang)
2160            .build()
2161            .unwrap()
2162            .normalized()
2163            .unwrap();
2164
2165        assert_eq!(args.block_size, 1);
2166    }
2167
2168    #[test]
2169    fn test_from_json_file_normalizes_sglang_page_size() {
2170        let tempdir = tempfile::tempdir().unwrap();
2171        let path = tempdir.path().join("args.json");
2172        std::fs::write(
2173            &path,
2174            serde_json::to_string(&json!({
2175                "engine_type": "sglang",
2176                "sglang": {
2177                    "page_size": 32
2178                }
2179            }))
2180            .unwrap(),
2181        )
2182        .unwrap();
2183
2184        let args = MockEngineArgs::from_json_file(&path).unwrap();
2185        assert_eq!(args.block_size, 32);
2186    }
2187
2188    #[test]
2189    fn test_normalized_sglang_rejects_chunked_prefill_not_divisible_by_block_size() {
2190        let error = MockEngineArgs::builder()
2191            .engine_type(EngineType::Sglang)
2192            .block_size(4)
2193            .sglang(Some(SglangArgs {
2194                page_size: Some(4),
2195                chunked_prefill_size: Some(6),
2196                ..Default::default()
2197            }))
2198            .build()
2199            .unwrap()
2200            .normalized()
2201            .unwrap_err();
2202
2203        assert!(
2204            error
2205                .to_string()
2206                .contains("chunked_prefill_size to be divisible by block_size"),
2207            "unexpected error: {error}",
2208        );
2209    }
2210
2211    #[test]
2212    fn test_normalized_sglang_accepts_chunked_prefill_divisible_by_block_size() {
2213        let args = MockEngineArgs::builder()
2214            .engine_type(EngineType::Sglang)
2215            .block_size(4)
2216            .sglang(Some(SglangArgs {
2217                page_size: Some(4),
2218                chunked_prefill_size: Some(8),
2219                ..Default::default()
2220            }))
2221            .build()
2222            .unwrap()
2223            .normalized()
2224            .unwrap();
2225
2226        assert_eq!(args.block_size, 4);
2227    }
2228}