1use crate::{
4 parse_bool_env_value, parse_path_env_value, parse_usize_env_value, AttentionExecutionPolicy,
5 DataType, Device, ModelId, ModelInfo, ObservabilityProfileDetail, ProfileEntrypoint,
6 RuntimeConfigSnapshot, SamplingParams, SamplingPresets, TokenId,
7};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use std::{collections::HashMap, path::PathBuf, time::Duration};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "kebab-case")]
18pub enum SequenceFitPolicy {
19 FullInputMustFit,
20 ImmediateOnly,
21}
22
23impl SequenceFitPolicy {
24 pub const fn as_runtime_value(self) -> &'static str {
25 match self {
26 Self::FullInputMustFit => "full-input-must-fit",
27 Self::ImmediateOnly => "immediate-only",
28 }
29 }
30
31 pub fn parse_runtime_value(raw: &str) -> std::result::Result<Self, String> {
32 match raw.trim().to_ascii_lowercase().replace('_', "-").as_str() {
33 "full-input-must-fit" => Ok(Self::FullInputMustFit),
34 "immediate-only" => Ok(Self::ImmediateOnly),
35 _ => Err(format!(
36 "expected full-input-must-fit or immediate-only; got {raw:?}"
37 )),
38 }
39 }
40}
41
42impl Default for SequenceFitPolicy {
43 fn default() -> Self {
44 Self::ImmediateOnly
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "kebab-case")]
52pub enum VNextDiagnosticFault {
53 PrefillResourceAfterSubmitOnce,
54}
55
56impl VNextDiagnosticFault {
57 pub const fn as_runtime_value(self) -> &'static str {
58 match self {
59 Self::PrefillResourceAfterSubmitOnce => "prefill-resource-after-submit-once",
60 }
61 }
62
63 pub fn parse_runtime_value(raw: &str) -> std::result::Result<Self, String> {
64 match raw.trim().to_ascii_lowercase().replace('_', "-").as_str() {
65 "prefill-resource-after-submit-once" => Ok(Self::PrefillResourceAfterSubmitOnce),
66 _ => Err(format!(
67 "expected prefill-resource-after-submit-once; got {raw:?}"
68 )),
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub struct VNextCheckpointCaptureConfig {
77 pub output_dir: PathBuf,
78 pub value_ids: Vec<String>,
79 pub maximum_prefill_waves: usize,
80 #[serde(default)]
81 pub maximum_decode_waves: usize,
82 #[serde(default)]
85 pub capture_product_output: bool,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub teacher_forcing: Option<VNextTeacherForcingConfig>,
92}
93
94pub const MAX_VNEXT_TEACHER_FORCED_TOKENS: usize = 512;
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct VNextTeacherForcingConfig {
103 token_ids: Vec<TokenId>,
104}
105
106impl VNextTeacherForcingConfig {
107 pub fn new(token_ids: Vec<TokenId>) -> std::result::Result<Self, String> {
108 let value = Self { token_ids };
109 value.validate()?;
110 Ok(value)
111 }
112
113 pub fn validate(&self) -> std::result::Result<(), String> {
114 if self.token_ids.is_empty() || self.token_ids.len() > MAX_VNEXT_TEACHER_FORCED_TOKENS {
115 return Err(format!(
116 "vNext checkpoint teacher forcing requires 1..={MAX_VNEXT_TEACHER_FORCED_TOKENS} tokens"
117 ));
118 }
119 Ok(())
120 }
121
122 pub fn token_ids(&self) -> &[TokenId] {
123 &self.token_ids
124 }
125
126 pub fn token_count(&self) -> usize {
127 self.token_ids.len()
128 }
129
130 pub fn token_ids_sha256(&self) -> String {
133 let mut digest = Sha256::new();
134 for token in &self.token_ids {
135 digest.update(token.get().to_le_bytes());
136 }
137 format!("{:x}", digest.finalize())
138 }
139}
140
141#[derive(Debug, Clone, Default, Serialize, Deserialize)]
146pub struct RuntimeKnobs {
147 pub kv_capacity: Option<usize>,
148 pub max_model_len: Option<usize>,
149 pub chunked_prefill_size: Option<usize>,
150 pub batch_decode_prof: bool,
151 pub next_batch_prof: bool,
152 pub rbd_prof: bool,
153 #[serde(default)]
154 pub profile_jsonl: Option<PathBuf>,
155 pub scheduler_trace_jsonl: Option<PathBuf>,
156 pub legacy_scheduler_trace_jsonl: Option<PathBuf>,
157 pub profile_entrypoint: Option<ProfileEntrypoint>,
158 pub profile_detail: ObservabilityProfileDetail,
159 pub unified_post_prof: bool,
160 pub prefix_cache_enabled: bool,
161 pub recurrent_state_max_slots: Option<usize>,
162 pub attention_execution_policy: AttentionExecutionPolicy,
163
164 pub model_path: Option<String>,
171 pub spec_draft: Option<String>,
172 pub spec_n: Option<usize>,
173 pub dtype: Option<String>,
174 pub metal_dtype: Option<String>,
175 pub tp: Option<usize>,
176 #[serde(default)]
177 pub vnext_checkpoint_capture: Option<VNextCheckpointCaptureConfig>,
178 #[serde(default)]
179 pub vnext_diagnostic_fault: Option<VNextDiagnosticFault>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, Default)]
184pub struct EngineConfig {
185 pub model: EngineModelConfig,
186 pub scheduler: SchedulerConfig,
187 pub sampling: SamplingConfig,
188 pub backend: BackendConfig,
189 pub kv_cache: KvCacheConfig,
190 pub memory: MemoryConfig,
191 pub batching: BatchConfig,
192 pub monitoring: MonitoringConfig,
193 #[serde(default)]
194 pub runtime: RuntimeKnobs,
195}
196
197impl EngineConfig {
198 pub fn apply_runtime_config_snapshot(
199 &mut self,
200 snapshot: &RuntimeConfigSnapshot,
201 ) -> std::result::Result<(), String> {
202 self.scheduler.apply_runtime_config_snapshot(snapshot)?;
203 if let Some(value) = runtime_config_value(snapshot, "FERRUM_KV_MAX_BLOCKS") {
204 self.kv_cache.max_blocks =
205 parse_required_positive_usize("FERRUM_KV_MAX_BLOCKS", value)?;
206 }
207 if let Some(value) = runtime_config_value(snapshot, "FERRUM_MAX_BATCHED_TOKENS") {
208 self.batching.max_num_batched_tokens =
209 parse_required_positive_usize("FERRUM_MAX_BATCHED_TOKENS", value)?;
210 }
211 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PAGED_MAX_SEQS") {
212 self.scheduler.max_running_requests =
213 parse_required_positive_usize("FERRUM_PAGED_MAX_SEQS", value)?;
214 }
215 if let Some(value) = runtime_config_value(snapshot, "FERRUM_RUNTIME_MEMORY_BUDGET_BYTES") {
216 self.memory.usable_capacity_bytes = Some(parse_required_positive_usize(
217 "FERRUM_RUNTIME_MEMORY_BUDGET_BYTES",
218 value,
219 )?);
220 }
221 if let Some(value) = runtime_config_value(snapshot, "FERRUM_BATCHED_GRAPH") {
222 self.backend.enable_cuda_graphs = parse_presence_bool(value)?;
223 }
224 if let Some(value) = runtime_config_value(snapshot, "FERRUM_REUSABLE_EXECUTION") {
225 self.backend.enable_reusable_execution = parse_presence_bool(value)?;
226 }
227 if let Some(value) =
228 runtime_config_value(snapshot, "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS")
229 {
230 let widths =
231 parse_positive_usize_list("FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS", value)?;
232 if widths
233 .iter()
234 .any(|width| *width > MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH)
235 {
236 return Err(format!(
237 "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS: startup capture widths must be within 1..={MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH}"
238 ));
239 }
240 self.backend.reusable_execution_capture.exact_decode_widths = Some(widths);
241 }
242 if let Some(value) = runtime_config_value(
243 snapshot,
244 "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH",
245 ) {
246 let maximum = parse_required_positive_usize(
247 "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH",
248 value,
249 )?;
250 if maximum > MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH {
251 return Err(format!(
252 "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH: must be within 1..={MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH}"
253 ));
254 }
255 self.backend
256 .reusable_execution_capture
257 .maximum_automatic_exact_decode_width = maximum;
258 }
259 if let Some(value) = runtime_config_value(snapshot, "FERRUM_KV_CAPACITY") {
263 self.runtime.kv_capacity =
264 Some(parse_required_positive_usize("FERRUM_KV_CAPACITY", value)?);
265 }
266 if let Some(value) = runtime_config_value(snapshot, "FERRUM_MAX_MODEL_LEN") {
267 self.runtime.max_model_len = Some(parse_required_positive_usize(
268 "FERRUM_MAX_MODEL_LEN",
269 value,
270 )?);
271 }
272 if let Some(value) = runtime_config_value(snapshot, "FERRUM_RECURRENT_STATE_MAX_SLOTS") {
273 self.runtime.recurrent_state_max_slots = Some(parse_required_positive_usize(
274 "FERRUM_RECURRENT_STATE_MAX_SLOTS",
275 value,
276 )?);
277 }
278 if let Some(value) = runtime_config_value(snapshot, "FERRUM_ATTENTION_POLICY") {
279 self.runtime.attention_execution_policy =
280 AttentionExecutionPolicy::parse_runtime_value(value)
281 .map_err(|reason| format!("FERRUM_ATTENTION_POLICY: {reason}"))?;
282 }
283 if let Some(value) = runtime_config_value(snapshot, "FERRUM_CHUNKED_PREFILL") {
284 self.runtime.chunked_prefill_size =
285 parse_usize_env_value(value).ok().filter(|&v| v > 0);
286 }
287 self.runtime.batch_decode_prof |=
288 runtime_config_value(snapshot, "FERRUM_BATCH_DECODE_PROF").is_some();
289 self.runtime.next_batch_prof |=
290 runtime_config_value(snapshot, "FERRUM_NEXT_BATCH_PROF").is_some();
291 self.runtime.rbd_prof |= runtime_config_value(snapshot, "FERRUM_RBD_PROF").is_some();
292 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PROFILE_JSONL") {
293 self.runtime.profile_jsonl = Some(parse_path_env_value(value)?);
294 }
295 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHEDULER_TRACE_JSONL") {
296 self.runtime.scheduler_trace_jsonl = Some(parse_path_env_value(value)?);
297 }
298 if let Some(value) = runtime_config_value(snapshot, "FERRUM_LEGACY_SCHEDULER_TRACE_JSONL") {
299 self.runtime.legacy_scheduler_trace_jsonl = Some(parse_path_env_value(value)?);
300 }
301 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PROFILE_ENTRYPOINT") {
302 self.runtime.profile_entrypoint = Some(parse_profile_entrypoint(
303 "FERRUM_PROFILE_ENTRYPOINT",
304 value,
305 )?);
306 }
307 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PROFILE_DETAIL") {
308 self.runtime.profile_detail =
309 ObservabilityProfileDetail::parse(value).ok_or_else(|| {
310 format!(
311 "FERRUM_PROFILE_DETAIL: expected one of off, basic, resource, latency, kernel, debug, replay, verify, full; got {value:?}"
312 )
313 })?;
314 }
315 if let Some(value) = runtime_config_value(snapshot, "FERRUM_VNEXT_DIAGNOSTIC_FAULT") {
316 self.runtime.vnext_diagnostic_fault = Some(
317 VNextDiagnosticFault::parse_runtime_value(value)
318 .map_err(|reason| format!("FERRUM_VNEXT_DIAGNOSTIC_FAULT: {reason}"))?,
319 );
320 }
321 self.runtime.unified_post_prof |=
322 runtime_config_value(snapshot, "FERRUM_UNIFIED_POST_PROF").is_some();
323 self.runtime.prefix_cache_enabled |=
324 runtime_config_value(snapshot, "FERRUM_WHOLE_PROMPT_PREFIX_CACHE")
325 .map(|v| v == "1")
326 .unwrap_or(false);
327
328 if let Some(value) = runtime_config_value(snapshot, "FERRUM_MODEL_PATH") {
332 self.runtime.model_path = Some(value.to_string());
333 }
334 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SPEC_DRAFT") {
335 self.runtime.spec_draft = if value.is_empty() {
336 None
337 } else {
338 Some(value.to_string())
339 };
340 }
341 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SPEC_N") {
342 self.runtime.spec_n = value.parse::<usize>().ok();
343 }
344 if let Some(value) = runtime_config_value(snapshot, "FERRUM_DTYPE") {
345 self.runtime.dtype = Some(value.to_string());
346 }
347 if let Some(value) = runtime_config_value(snapshot, "FERRUM_METAL_DTYPE") {
348 self.runtime.metal_dtype = Some(value.to_string());
349 }
350 if let Some(value) = runtime_config_value(snapshot, "FERRUM_TP") {
351 self.runtime.tp = value.parse::<usize>().ok();
352 }
353
354 crate::install_runtime_snapshot(snapshot.clone());
359 Ok(())
360 }
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct EngineModelConfig {
365 pub model_id: ModelId,
366 pub model_info: Option<ModelInfo>,
367 pub tokenizer: TokenizerConfig,
368 #[serde(default)]
372 pub source: Option<crate::ModelSource>,
373}
374
375impl Default for EngineModelConfig {
376 fn default() -> Self {
377 Self {
378 model_id: ModelId::new("default"),
379 model_info: None,
380 tokenizer: TokenizerConfig::default(),
381 source: None,
382 }
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct SchedulerConfig {
389 pub policy: SchedulingPolicy,
391 pub max_waiting_requests: usize,
393 pub max_running_requests: usize,
395 pub enable_preemption: bool,
397 pub enable_load_balancing: bool,
399 pub fair_share_weights: HashMap<String, f32>,
401 pub enable_sla_enforcement: bool,
403 #[serde(default = "default_prompt_token_estimate")]
405 pub prompt_token_estimate: bool,
406 #[serde(default)]
408 pub prefill_first_until_active: Option<usize>,
409 #[serde(default)]
413 pub prefill_step_chunk: Option<usize>,
414 #[serde(default)]
416 pub active_decode_prefill_chunk: Option<usize>,
417 #[serde(default)]
419 pub scheduler_none_prof: bool,
420 #[serde(default)]
422 pub sequence_fit_policy: SequenceFitPolicy,
423}
424
425impl Default for SchedulerConfig {
426 fn default() -> Self {
427 Self {
428 policy: SchedulingPolicy::Priority,
429 max_waiting_requests: 1000,
430 max_running_requests: 32,
431 enable_preemption: true,
432 enable_load_balancing: false,
433 fair_share_weights: HashMap::new(),
434 enable_sla_enforcement: false,
435 prompt_token_estimate: default_prompt_token_estimate(),
436 prefill_first_until_active: None,
437 prefill_step_chunk: None,
438 active_decode_prefill_chunk: None,
439 scheduler_none_prof: false,
440 sequence_fit_policy: SequenceFitPolicy::default(),
441 }
442 }
443}
444
445fn default_prompt_token_estimate() -> bool {
446 true
447}
448
449impl SchedulerConfig {
450 pub fn apply_runtime_config_snapshot(
451 &mut self,
452 snapshot: &RuntimeConfigSnapshot,
453 ) -> std::result::Result<(), String> {
454 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHED_PROMPT_TOKEN_ESTIMATE") {
455 self.prompt_token_estimate = parse_bool_env_value(value)
456 .map_err(|reason| format!("FERRUM_SCHED_PROMPT_TOKEN_ESTIMATE: {reason}"))?;
457 }
458 if let Some(value) =
459 runtime_config_value(snapshot, "FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE")
460 {
461 self.prefill_first_until_active =
462 parse_optional_positive_usize("FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE", value)?;
463 }
464 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHED_PREFILL_STEP_CHUNK") {
465 self.prefill_step_chunk =
466 parse_optional_positive_usize("FERRUM_SCHED_PREFILL_STEP_CHUNK", value)?;
467 }
468 if let Some(value) = runtime_config_value(snapshot, "FERRUM_ACTIVE_DECODE_PREFILL_CHUNK") {
469 self.active_decode_prefill_chunk =
470 parse_optional_positive_usize("FERRUM_ACTIVE_DECODE_PREFILL_CHUNK", value)?;
471 }
472 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHED_NONE_PROF") {
473 self.scheduler_none_prof = parse_presence_bool(value)
474 .map_err(|reason| format!("FERRUM_SCHED_NONE_PROF: {reason}"))?;
475 }
476 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SEQUENCE_FIT_POLICY") {
477 self.sequence_fit_policy = SequenceFitPolicy::parse_runtime_value(value)
478 .map_err(|reason| format!("FERRUM_SEQUENCE_FIT_POLICY: {reason}"))?;
479 }
480 Ok(())
481 }
482}
483
484fn runtime_config_value<'a>(snapshot: &'a RuntimeConfigSnapshot, key: &str) -> Option<&'a str> {
485 snapshot
486 .entries
487 .iter()
488 .find(|entry| entry.key == key)
489 .map(|entry| entry.effective_value.as_str())
490}
491
492fn parse_optional_positive_usize(
493 key: &str,
494 value: &str,
495) -> std::result::Result<Option<usize>, String> {
496 let parsed = parse_usize_env_value(value).map_err(|reason| format!("{key}: {reason}"))?;
497 Ok((parsed > 0).then_some(parsed))
498}
499
500fn parse_required_positive_usize(key: &str, value: &str) -> std::result::Result<usize, String> {
501 let parsed = parse_usize_env_value(value).map_err(|reason| format!("{key}: {reason}"))?;
502 if parsed == 0 {
503 Err(format!("{key}: must be greater than zero"))
504 } else {
505 Ok(parsed)
506 }
507}
508
509fn parse_positive_usize_list(key: &str, value: &str) -> std::result::Result<Vec<usize>, String> {
510 let values = value
511 .split(',')
512 .map(str::trim)
513 .map(|value| parse_required_positive_usize(key, value))
514 .collect::<std::result::Result<Vec<_>, _>>()?;
515 if values.is_empty() {
516 Err(format!("{key}: must contain at least one width"))
517 } else {
518 Ok(values)
519 }
520}
521
522fn parse_profile_entrypoint(
523 key: &str,
524 value: &str,
525) -> std::result::Result<ProfileEntrypoint, String> {
526 ProfileEntrypoint::parse(value).ok_or_else(|| {
527 format!("{key}: expected one of run, serve, bench_serve, synthetic; got {value:?}")
528 })
529}
530
531fn parse_presence_bool(value: &str) -> std::result::Result<bool, String> {
532 if value.trim().is_empty() {
533 Ok(true)
534 } else {
535 parse_bool_env_value(value)
536 }
537}
538
539#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
541pub enum SchedulingPolicy {
542 FCFS,
544 Priority,
546 FairShare,
548 SJF,
550 RoundRobin,
552 ContinuousBatch,
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct KvCacheConfig {
559 pub cache_type: KvCacheType,
561 #[serde(default)]
566 pub dtype: KvCacheDtype,
567 pub block_size: usize,
569 pub max_blocks: usize,
571 pub enable_compression: bool,
573 pub compression_ratio: f32,
575 pub enable_multi_level: bool,
577 pub swap_threshold: f32,
579 pub enable_prefix_caching: bool,
581 pub prefix_cache_size: usize,
583}
584
585impl Default for KvCacheConfig {
586 fn default() -> Self {
587 Self {
592 cache_type: KvCacheType::Contiguous,
593 dtype: KvCacheDtype::default(),
594 block_size: 16,
595 max_blocks: 2048,
596 enable_compression: false,
597 compression_ratio: 0.5,
598 enable_multi_level: true,
599 swap_threshold: 0.8,
600 enable_prefix_caching: true,
601 prefix_cache_size: 100,
602 }
603 }
604}
605
606#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
608pub enum KvCacheType {
609 Contiguous,
611 Paged,
613 Tree,
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
623#[serde(rename_all = "lowercase")]
624pub enum KvCacheDtype {
625 #[default]
627 Fp16,
628 Bf16,
631 Int8,
637 Fp8,
639}
640
641impl KvCacheDtype {
642 pub fn parse(s: &str) -> Option<Self> {
644 match s.trim().to_ascii_lowercase().as_str() {
645 "fp16" | "f16" | "float16" => Some(Self::Fp16),
646 "bf16" | "bfloat16" => Some(Self::Bf16),
647 "int8" | "i8" => Some(Self::Int8),
648 "fp8" | "f8" | "f8e4m3" | "e4m3" => Some(Self::Fp8),
649 _ => None,
650 }
651 }
652
653 pub fn as_str(&self) -> &'static str {
655 match self {
656 Self::Fp16 => "fp16",
657 Self::Bf16 => "bf16",
658 Self::Int8 => "int8",
659 Self::Fp8 => "fp8",
660 }
661 }
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize)]
666pub struct MemoryConfig {
667 pub pool_size: Option<usize>,
669 #[serde(default)]
673 pub usable_capacity_bytes: Option<usize>,
674 pub enable_pooling: bool,
676 pub alignment: usize,
678 pub enable_defragmentation: bool,
680 pub defragmentation_threshold: f32,
682 pub enable_memory_stats: bool,
684 pub pressure_warning_threshold: f32,
686 pub pressure_critical_threshold: f32,
688}
689
690#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
692pub struct MemoryCapacityBudget {
693 pub capacity_bytes: u64,
694 pub usable_capacity_bytes: u64,
695 pub reserve_bytes: u64,
696}
697
698impl MemoryConfig {
699 pub fn resolve_capacity_budget(
700 &self,
701 device_capacity_bytes: u64,
702 ) -> std::result::Result<MemoryCapacityBudget, String> {
703 if device_capacity_bytes == 0 {
704 return Err("runtime device memory capacity must be greater than zero".to_string());
705 }
706 let capacity_bytes = self
707 .pool_size
708 .map(|bytes| bytes as u64)
709 .unwrap_or(device_capacity_bytes)
710 .min(device_capacity_bytes);
711 if capacity_bytes == 0 {
712 return Err("runtime memory capacity must be greater than zero".to_string());
713 }
714
715 let usable_capacity_bytes = if let Some(bytes) = self.usable_capacity_bytes {
716 let bytes = bytes as u64;
717 if bytes == 0 || bytes > capacity_bytes {
718 return Err(format!(
719 "memory.usable_capacity_bytes must be in 1..={capacity_bytes}, got {bytes}"
720 ));
721 }
722 bytes
723 } else {
724 let critical = self.pressure_critical_threshold;
725 if !critical.is_finite() || critical <= 0.0 || critical > 1.0 {
726 return Err(format!(
727 "memory.pressure_critical_threshold must be in (0, 1], got {critical}"
728 ));
729 }
730 let threshold_bytes = ((capacity_bytes as f64) * f64::from(critical)).floor() as u64;
731 capacity_bytes.saturating_sub(
732 capacity_bytes
733 .saturating_sub(threshold_bytes)
734 .min(capacity_bytes - 1),
735 )
736 };
737 Ok(MemoryCapacityBudget {
738 capacity_bytes,
739 usable_capacity_bytes,
740 reserve_bytes: capacity_bytes - usable_capacity_bytes,
741 })
742 }
743}
744
745impl Default for MemoryConfig {
746 fn default() -> Self {
747 Self {
748 pool_size: None,
749 usable_capacity_bytes: None,
750 enable_pooling: true,
751 alignment: 256,
752 enable_defragmentation: false,
753 defragmentation_threshold: 0.7,
754 enable_memory_stats: true,
755 pressure_warning_threshold: 0.8,
756 pressure_critical_threshold: 0.95,
757 }
758 }
759}
760
761pub const MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH: usize = 32;
768
769pub const DEFAULT_MAXIMUM_AUTOMATIC_EXACT_DECODE_WIDTH: usize =
777 MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH;
778
779#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
789#[serde(default)]
790pub struct ReusableExecutionCaptureConfig {
791 pub exact_decode_widths: Option<Vec<usize>>,
794 pub maximum_automatic_exact_decode_width: usize,
797}
798
799impl Default for ReusableExecutionCaptureConfig {
800 fn default() -> Self {
801 Self {
802 exact_decode_widths: None,
803 maximum_automatic_exact_decode_width: DEFAULT_MAXIMUM_AUTOMATIC_EXACT_DECODE_WIDTH,
804 }
805 }
806}
807
808#[derive(Debug, Clone, Serialize, Deserialize)]
810pub struct BackendConfig {
811 pub backend_type: BackendType,
813 pub device: Device,
815 pub dtype: DataType,
817 pub enable_optimizations: bool,
819 pub optimization_level: u8,
821 pub enable_cuda_graphs: bool,
823 #[serde(default = "default_enable_reusable_execution")]
825 pub enable_reusable_execution: bool,
826 #[serde(default)]
830 pub reusable_execution_capture: ReusableExecutionCaptureConfig,
831 pub enable_kernel_fusion: bool,
833 pub backend_options: HashMap<String, serde_json::Value>,
835}
836
837impl Default for BackendConfig {
838 fn default() -> Self {
839 Self {
840 backend_type: BackendType::Candle,
841 device: Device::CPU,
842 dtype: DataType::FP16,
843 enable_optimizations: true,
844 optimization_level: 2,
845 enable_cuda_graphs: false,
846 enable_reusable_execution: default_enable_reusable_execution(),
847 reusable_execution_capture: ReusableExecutionCaptureConfig::default(),
848 enable_kernel_fusion: true,
849 backend_options: HashMap::new(),
850 }
851 }
852}
853
854const fn default_enable_reusable_execution() -> bool {
855 true
856}
857
858#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
860pub enum BackendType {
861 Candle,
863 OnnxRuntime,
865 TensorRT,
867 Custom,
869}
870
871#[derive(Debug, Clone, Serialize, Deserialize)]
873pub struct TokenizerConfig {
874 pub tokenizer_type: TokenizerType,
876 pub tokenizer_path: Option<String>,
878 pub enable_fast: bool,
880 pub add_special_tokens: bool,
882 pub truncation: Option<TruncationConfig>,
884 pub padding: Option<PaddingConfig>,
886}
887
888impl Default for TokenizerConfig {
889 fn default() -> Self {
890 Self {
891 tokenizer_type: TokenizerType::BPE,
892 tokenizer_path: None,
893 enable_fast: true,
894 add_special_tokens: true,
895 truncation: None,
896 padding: None,
897 }
898 }
899}
900
901#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
903pub enum TokenizerType {
904 BPE,
906 WordPiece,
908 SentencePiece,
910 Tiktoken,
912 Custom,
914}
915
916#[derive(Debug, Clone, Serialize, Deserialize)]
918pub struct TruncationConfig {
919 pub max_length: usize,
921 pub strategy: TruncationStrategy,
923}
924
925#[derive(Debug, Clone, Serialize, Deserialize)]
927pub enum TruncationStrategy {
928 TruncateStart,
930 TruncateEnd,
932 TruncateBoth,
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize)]
938pub struct PaddingConfig {
939 pub strategy: PaddingStrategy,
941 pub token_id: u32,
943 pub target_length: Option<usize>,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize)]
949pub enum PaddingStrategy {
950 None,
952 MaxLength,
954 FixedLength,
956}
957
958#[derive(Debug, Clone, Serialize, Deserialize)]
962pub struct SecurityConfig {
963 pub enable_auth: bool,
965 pub api_keys: Vec<String>,
967 pub enable_rate_limiting: bool,
969 pub rate_limit_rpm: u32,
971 pub enable_content_filter: bool,
973 pub max_prompt_length: usize,
975 pub enable_prompt_validation: bool,
977 pub allowed_extensions: Vec<String>,
979}
980
981impl Default for SecurityConfig {
982 fn default() -> Self {
983 Self {
984 enable_auth: false,
985 api_keys: vec![],
986 enable_rate_limiting: true,
987 rate_limit_rpm: 60,
988 enable_content_filter: false,
989 max_prompt_length: 32768,
990 enable_prompt_validation: true,
991 allowed_extensions: vec!["txt".to_string(), "json".to_string()],
992 }
993 }
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize, Default)]
997pub struct SamplingConfig {
998 pub default_params: SamplingParams,
999 pub presets: SamplingPresets,
1000 pub enable_custom_processors: bool,
1001}
1002
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004pub struct MonitoringConfig {
1005 pub enable_metrics: bool,
1006 pub enable_tracing: bool,
1007 pub export_interval: Duration,
1008}
1009
1010impl Default for MonitoringConfig {
1011 fn default() -> Self {
1012 Self {
1013 enable_metrics: true,
1014 enable_tracing: true,
1015 export_interval: Duration::from_secs(5),
1016 }
1017 }
1018}
1019
1020#[derive(Debug, Clone, Serialize, Deserialize)]
1021pub struct BatchConfig {
1022 pub max_batch_size: usize,
1023 pub max_wait_ms: u64,
1024 pub enable_dynamic: bool,
1025 pub enable_continuous: bool,
1026 #[serde(default = "BatchConfig::default_max_num_batched_tokens")]
1033 pub max_num_batched_tokens: usize,
1034}
1035
1036impl BatchConfig {
1037 fn default_max_num_batched_tokens() -> usize {
1038 2048
1039 }
1040}
1041
1042impl Default for BatchConfig {
1043 fn default() -> Self {
1044 Self {
1045 max_batch_size: 32,
1046 max_wait_ms: 8,
1047 enable_dynamic: true,
1048 enable_continuous: false,
1049 max_num_batched_tokens: Self::default_max_num_batched_tokens(),
1050 }
1051 }
1052}
1053
1054#[cfg(test)]
1055mod tests {
1056 use super::*;
1057
1058 #[test]
1059 fn scheduler_keeps_immediate_fit_default_until_full_input_policy_is_gated() {
1060 assert_eq!(
1061 SchedulerConfig::default().sequence_fit_policy,
1062 SequenceFitPolicy::ImmediateOnly
1063 );
1064 }
1065
1066 #[test]
1067 fn sequence_fit_policy_uses_canonical_product_values() {
1068 assert_eq!(
1069 serde_json::to_string(&SequenceFitPolicy::FullInputMustFit).unwrap(),
1070 "\"full-input-must-fit\""
1071 );
1072 assert_eq!(
1073 serde_json::from_str::<SequenceFitPolicy>("\"immediate-only\"").unwrap(),
1074 SequenceFitPolicy::ImmediateOnly
1075 );
1076 }
1077
1078 #[test]
1079 fn diagnostic_fault_uses_one_canonical_product_value() {
1080 assert_eq!(
1081 VNextDiagnosticFault::parse_runtime_value("prefill_resource_after_submit_once")
1082 .unwrap(),
1083 VNextDiagnosticFault::PrefillResourceAfterSubmitOnce
1084 );
1085 assert_eq!(
1086 VNextDiagnosticFault::PrefillResourceAfterSubmitOnce.as_runtime_value(),
1087 "prefill-resource-after-submit-once"
1088 );
1089 assert!(VNextDiagnosticFault::parse_runtime_value("resource-failure").is_err());
1090 }
1091
1092 #[test]
1093 fn engine_config_applies_typed_diagnostic_fault() {
1094 let mut config = EngineConfig::default();
1095 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1096 "FERRUM_VNEXT_DIAGNOSTIC_FAULT",
1097 "prefill-resource-after-submit-once",
1098 )]);
1099
1100 config
1101 .apply_runtime_config_snapshot(&snapshot)
1102 .expect("runtime config should apply");
1103
1104 assert_eq!(
1105 config.runtime.vnext_diagnostic_fault,
1106 Some(VNextDiagnosticFault::PrefillResourceAfterSubmitOnce)
1107 );
1108 }
1109
1110 #[test]
1111 fn engine_config_rejects_unknown_diagnostic_fault() {
1112 let mut config = EngineConfig::default();
1113 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1114 "FERRUM_VNEXT_DIAGNOSTIC_FAULT",
1115 "resource-failure",
1116 )]);
1117
1118 let error = config
1119 .apply_runtime_config_snapshot(&snapshot)
1120 .expect_err("unknown diagnostic fault must fail closed");
1121
1122 assert!(error.contains("FERRUM_VNEXT_DIAGNOSTIC_FAULT"));
1123 }
1124
1125 #[test]
1126 fn scheduler_deserialization_without_fit_policy_keeps_legacy_default() {
1127 let mut serialized = serde_json::to_value(SchedulerConfig::default()).unwrap();
1128 serialized
1129 .as_object_mut()
1130 .unwrap()
1131 .remove("sequence_fit_policy");
1132
1133 let scheduler: SchedulerConfig = serde_json::from_value(serialized).unwrap();
1134
1135 assert_eq!(
1136 scheduler.sequence_fit_policy,
1137 SequenceFitPolicy::ImmediateOnly
1138 );
1139 }
1140
1141 #[test]
1142 fn checkpoint_capture_deserialization_keeps_decode_capture_disabled_by_default() {
1143 let capture: VNextCheckpointCaptureConfig = serde_json::from_value(serde_json::json!({
1144 "output_dir": "capture",
1145 "value_ids": ["value.output.logits"],
1146 "maximum_prefill_waves": 1
1147 }))
1148 .unwrap();
1149
1150 assert_eq!(capture.maximum_decode_waves, 0);
1151 assert!(!capture.capture_product_output);
1152 }
1153
1154 #[test]
1155 fn engine_config_applies_typed_sequence_fit_policy() {
1156 let mut config = EngineConfig::default();
1157 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1158 "FERRUM_SEQUENCE_FIT_POLICY",
1159 "full-input-must-fit",
1160 )]);
1161
1162 config
1163 .apply_runtime_config_snapshot(&snapshot)
1164 .expect("runtime config should apply");
1165
1166 assert_eq!(
1167 config.scheduler.sequence_fit_policy,
1168 SequenceFitPolicy::FullInputMustFit
1169 );
1170 }
1171
1172 #[test]
1173 fn engine_config_rejects_unknown_sequence_fit_policy() {
1174 let mut config = EngineConfig::default();
1175 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1176 "FERRUM_SEQUENCE_FIT_POLICY",
1177 "reserve-everything",
1178 )]);
1179
1180 let error = config
1181 .apply_runtime_config_snapshot(&snapshot)
1182 .expect_err("unknown fit policy must fail closed");
1183
1184 assert!(error.contains("FERRUM_SEQUENCE_FIT_POLICY"));
1185 }
1186
1187 #[test]
1188 fn engine_config_applies_recurrent_state_max_slots_runtime_key() {
1189 let mut config = EngineConfig::default();
1190 let snapshot =
1191 RuntimeConfigSnapshot::from_env_vars([("FERRUM_RECURRENT_STATE_MAX_SLOTS", "16")]);
1192
1193 config
1194 .apply_runtime_config_snapshot(&snapshot)
1195 .expect("runtime config should apply");
1196
1197 assert_eq!(config.runtime.recurrent_state_max_slots, Some(16));
1198 }
1199
1200 #[test]
1201 fn engine_config_does_not_apply_removed_qwen35_slot_alias() {
1202 let mut config = EngineConfig::default();
1203 let snapshot =
1204 RuntimeConfigSnapshot::from_env_vars([("FERRUM_QWEN35_LINEAR_STATE_MAX_SLOTS", "16")]);
1205
1206 config
1207 .apply_runtime_config_snapshot(&snapshot)
1208 .expect("runtime config should apply");
1209
1210 assert_eq!(config.runtime.recurrent_state_max_slots, None);
1211 }
1212
1213 #[test]
1214 fn engine_config_uses_generic_recurrent_state_slots_when_removed_alias_is_present() {
1215 let mut config = EngineConfig::default();
1216 let snapshot = RuntimeConfigSnapshot::from_env_vars([
1217 ("FERRUM_RECURRENT_STATE_MAX_SLOTS", "8"),
1218 ("FERRUM_QWEN35_LINEAR_STATE_MAX_SLOTS", "16"),
1219 ]);
1220
1221 config
1222 .apply_runtime_config_snapshot(&snapshot)
1223 .expect("runtime config should apply");
1224
1225 assert_eq!(config.runtime.recurrent_state_max_slots, Some(8));
1226 }
1227
1228 #[test]
1229 fn engine_config_applies_profile_entrypoint_runtime_key() {
1230 let mut config = EngineConfig::default();
1231 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_ENTRYPOINT", "run")]);
1232
1233 config
1234 .apply_runtime_config_snapshot(&snapshot)
1235 .expect("runtime config should apply");
1236
1237 assert_eq!(
1238 config.runtime.profile_entrypoint,
1239 Some(ProfileEntrypoint::Run)
1240 );
1241 }
1242
1243 #[test]
1244 fn engine_config_applies_typed_profile_detail_runtime_key() {
1245 let mut config = EngineConfig::default();
1246 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "full")]);
1247
1248 config
1249 .apply_runtime_config_snapshot(&snapshot)
1250 .expect("runtime config should apply");
1251
1252 assert_eq!(
1253 config.runtime.profile_detail,
1254 ObservabilityProfileDetail::Full
1255 );
1256 }
1257
1258 #[test]
1259 fn engine_config_applies_typed_latency_profile_detail_runtime_key() {
1260 let mut config = EngineConfig::default();
1261 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "latency")]);
1262
1263 config
1264 .apply_runtime_config_snapshot(&snapshot)
1265 .expect("latency profile detail should apply");
1266
1267 assert_eq!(
1268 config.runtime.profile_detail,
1269 ObservabilityProfileDetail::Latency
1270 );
1271 }
1272
1273 #[test]
1274 fn engine_config_applies_typed_replay_profile_detail_runtime_key() {
1275 let mut config = EngineConfig::default();
1276 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "replay")]);
1277
1278 config
1279 .apply_runtime_config_snapshot(&snapshot)
1280 .expect("runtime config should apply");
1281
1282 assert_eq!(
1283 config.runtime.profile_detail,
1284 ObservabilityProfileDetail::Replay
1285 );
1286 }
1287
1288 #[test]
1289 fn engine_config_applies_typed_verification_profile_detail_runtime_key() {
1290 let mut config = EngineConfig::default();
1291 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "verify")]);
1292
1293 config
1294 .apply_runtime_config_snapshot(&snapshot)
1295 .expect("runtime config should apply");
1296
1297 assert_eq!(
1298 config.runtime.profile_detail,
1299 ObservabilityProfileDetail::Verify
1300 );
1301 }
1302
1303 #[test]
1304 fn engine_config_applies_typed_profile_jsonl_runtime_key() {
1305 let mut config = EngineConfig::default();
1306 let snapshot =
1307 RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_JSONL", "/tmp/profile.jsonl")]);
1308
1309 config
1310 .apply_runtime_config_snapshot(&snapshot)
1311 .expect("runtime config should apply");
1312
1313 assert_eq!(
1314 config.runtime.profile_jsonl.as_deref(),
1315 Some(std::path::Path::new("/tmp/profile.jsonl"))
1316 );
1317 }
1318
1319 #[test]
1320 fn engine_config_rejects_unknown_profile_detail() {
1321 let mut config = EngineConfig::default();
1322 let snapshot =
1323 RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "everything")]);
1324
1325 let error = config
1326 .apply_runtime_config_snapshot(&snapshot)
1327 .expect_err("unknown profile detail must fail closed");
1328
1329 assert!(error.contains("FERRUM_PROFILE_DETAIL"));
1330 }
1331}