1use 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#[derive(Clone, Debug)]
20pub struct G1;
21
22#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
28pub enum MockerEvictionBackend {
29 Lru,
30 MultiLru,
31 #[default]
32 Lineage,
33}
34
35#[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
49pub 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 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#[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
89pub trait RawKvEventSink: Send + Sync {
91 fn publish(&self, event: RawKvEvent) -> anyhow::Result<()>;
92
93 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#[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 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 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#[derive(Debug, Clone, Default)]
193pub struct ForwardPassSnapshot {
194 pub version: u32,
199 pub worker_id: String,
200 pub dp_rank: u32,
201 pub counter_id: u64,
202 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 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 pub wall_time_secs: f64,
219}
220
221pub trait FpmSink: Send + Sync {
224 fn publish(&self, snapshot: ForwardPassSnapshot) -> anyhow::Result<()>;
225}
226
227#[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#[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 #[serde(default, skip_serializing_if = "is_zero_i32")]
289 pub priority: i32,
290 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct PrefillCost {
315 pub new_blocks: usize,
316 pub new_tokens: usize,
317 pub cached_tokens: usize,
320 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#[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 pub completed: bool,
346 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
357#[serde(rename_all = "lowercase")]
358pub enum PreemptionMode {
359 #[default]
361 Lifo,
362 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
382#[serde(rename_all = "lowercase")]
383pub enum EngineType {
384 #[default]
386 Vllm,
387 Sglang,
389 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
414pub enum SchedulingPolicy {
415 #[default]
418 Vllm,
419 TrtllmGuaranteedNoEvict,
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
426#[serde(rename_all = "lowercase")]
427pub enum WorkerType {
428 #[default]
430 Aggregated,
431 Prefill,
433 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
454#[serde(rename_all = "snake_case")]
455pub enum KvTransferTimingMode {
456 #[default]
458 FullPrompt,
459 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#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize, Validate, Default)]
514pub struct SglangArgs {
515 pub schedule_policy: Option<String>,
517 #[validate(range(min = 1))]
519 pub page_size: Option<usize>,
520 #[validate(range(min = 1))]
522 pub max_prefill_tokens: Option<usize>,
523 #[validate(range(min = 1))]
525 pub chunked_prefill_size: Option<usize>,
526 #[validate(range(min = 1))]
528 pub clip_max_new_tokens: Option<usize>,
529 #[validate(range(min = 0.0, max = 1.0))]
531 pub schedule_conservativeness: Option<f64>,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, Validate, Default)]
539pub struct TrtllmArgs {
540 pub capacity_scheduler_policy: Option<String>,
543}
544
545#[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#[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 #[builder(default = "EngineType::Vllm")]
675 pub engine_type: EngineType,
676
677 #[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 #[builder(default = "None")]
691 #[validate(range(min = 1))]
692 pub max_model_len: Option<usize>,
693
694 #[builder(default = Some(256))]
696 #[validate(range(min = 1))]
697 pub max_num_seqs: Option<usize>,
698
699 #[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 #[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 #[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 #[builder(default = "None")]
735 #[validate(range(min = 0.0))]
736 pub startup_time: Option<f64>,
737
738 #[builder(default = "WorkerType::Aggregated")]
740 pub worker_type: WorkerType,
741
742 #[builder(default = "None")]
744 pub planner_profile_data: Option<PathBuf>,
745
746 #[serde(skip)]
748 #[builder(default = "Arc::new(PerfModel::default())")]
749 pub perf_model: Arc<PerfModel>,
750
751 #[serde(skip)]
755 #[builder(default = "None")]
756 pub aic_backend: Option<String>,
757
758 #[serde(skip)]
760 #[builder(default = "None")]
761 pub aic_system: Option<String>,
762
763 #[serde(skip)]
766 #[builder(default = "None")]
767 pub aic_backend_version: Option<String>,
768
769 #[serde(skip)]
772 #[builder(default = "None")]
773 pub aic_tp_size: Option<usize>,
774
775 #[serde(skip)]
777 #[builder(default = "None")]
778 pub aic_model_path: Option<String>,
779
780 #[serde(skip)]
783 #[builder(default = "None")]
784 pub aic_moe_tp_size: Option<usize>,
785
786 #[serde(skip)]
789 #[builder(default = "None")]
790 pub aic_moe_ep_size: Option<usize>,
791
792 #[serde(skip)]
796 #[builder(default = "None")]
797 pub aic_attention_dp_size: Option<usize>,
798
799 #[serde(skip)]
801 #[builder(default = "None")]
802 pub aic_gemm_dtype: Option<String>,
803
804 #[serde(skip)]
806 #[builder(default = "None")]
807 pub aic_moe_dtype: Option<String>,
808
809 #[serde(skip)]
811 #[builder(default = "None")]
812 pub aic_fmha_dtype: Option<String>,
813
814 #[serde(skip)]
816 #[builder(default = "None")]
817 pub aic_kv_cache_dtype: Option<String>,
818
819 #[serde(skip)]
821 #[builder(default = "None")]
822 pub aic_comm_dtype: Option<String>,
823
824 #[builder(default = "None")]
828 #[validate(range(min = 1, max = 5))]
829 pub aic_nextn: Option<usize>,
830
831 #[builder(default = "None")]
834 pub aic_nextn_accept_rates: Option<String>,
835
836 #[builder(default = "42")]
839 pub aic_mtp_seed: u64,
840
841 #[builder(default = "None")]
843 #[validate(range(min = 0.0, max = 1.0))]
844 pub gpu_memory_utilization: Option<f64>,
845
846 #[builder(default = "None")]
848 #[validate(range(min = 0.0, max = 1.0))]
849 pub mem_fraction_static: Option<f64>,
850
851 #[builder(default = "None")]
857 #[validate(range(min = 0.0, max = 1.0))]
858 pub free_gpu_memory_fraction: Option<f64>,
859
860 #[builder(default = "false")]
862 pub enable_local_indexer: bool,
863
864 #[builder(default = "None")]
868 pub bootstrap_port: Option<u16>,
869
870 #[builder(default = "300_000")]
872 #[validate(range(min = 1))]
873 pub handoff_session_timeout_ms: u64,
874
875 #[builder(default = "None")]
878 pub kv_bytes_per_token: Option<usize>,
879
880 #[builder(default = "None")]
884 #[validate(range(min = 0.0))]
885 pub kv_transfer_bandwidth: Option<f64>,
886
887 #[builder(default = "KvTransferTimingMode::FullPrompt")]
890 pub kv_transfer_timing_mode: KvTransferTimingMode,
891
892 #[builder(default = "None")]
897 #[validate(range(min = 1))]
898 pub num_g2_blocks: Option<usize>,
899
900 #[builder(default = "None")]
903 #[validate(range(min = 1))]
904 pub num_g3_blocks: Option<usize>,
905
906 #[builder(default = "false")]
909 pub enable_g4_storage: bool,
910
911 #[builder(default = "None")]
916 #[validate(range(min = 1))]
917 pub offload_batch_size: Option<usize>,
918
919 #[builder(default = "None")]
924 #[validate(range(min = 0.0))]
925 pub bandwidth_g1_to_g2_gbps: Option<f64>,
926
927 #[builder(default = "None")]
931 #[validate(range(min = 0.0))]
932 pub bandwidth_g2_to_g1_gbps: Option<f64>,
933
934 #[builder(default = "None")]
936 #[validate(range(min = 0.0))]
937 pub bandwidth_g2_to_g3_gbps: Option<f64>,
938
939 #[builder(default = "None")]
941 #[validate(range(min = 0.0))]
942 pub bandwidth_g3_to_g2_gbps: Option<f64>,
943
944 #[builder(default = "None")]
946 #[validate(range(min = 0.0))]
947 pub bandwidth_g2_to_g4_gbps: Option<f64>,
948
949 #[builder(default = "None")]
951 #[validate(range(min = 0.0))]
952 pub bandwidth_g4_to_g2_gbps: Option<f64>,
953
954 #[builder(default = "None")]
957 pub reasoning: Option<ReasoningConfig>,
958
959 #[builder(default = "None")]
963 pub response_replay_trace_path: Option<PathBuf>,
964
965 #[builder(default = "None")]
969 pub zmq_kv_events_port: Option<u16>,
970
971 #[builder(default = "None")]
976 pub zmq_replay_port: Option<u16>,
977
978 #[builder(default)]
981 pub preemption_mode: PreemptionMode,
982
983 #[builder(default = "None")]
985 pub router_queue_policy: Option<RouterQueuePolicy>,
986
987 #[builder(default = "None")]
989 pub sglang: Option<SglangArgs>,
990
991 #[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 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 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 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 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 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 let blocks: Vec<UniqueBlock> = (0..10).map(|_| UniqueBlock::default()).collect();
1758
1759 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 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 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}