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 #[serde(default)]
188 pub numerical_execution: crate::NumericalExecutionPolicy,
189 pub model: EngineModelConfig,
190 pub scheduler: SchedulerConfig,
191 pub sampling: SamplingConfig,
192 pub backend: BackendConfig,
193 pub kv_cache: KvCacheConfig,
194 pub memory: MemoryConfig,
195 pub batching: BatchConfig,
196 pub monitoring: MonitoringConfig,
197 #[serde(default)]
198 pub runtime: RuntimeKnobs,
199}
200
201impl EngineConfig {
202 pub fn apply_runtime_config_snapshot(
203 &mut self,
204 snapshot: &RuntimeConfigSnapshot,
205 ) -> std::result::Result<(), String> {
206 self.scheduler.apply_runtime_config_snapshot(snapshot)?;
207 if let Some(value) = runtime_config_value(snapshot, "FERRUM_KV_MAX_BLOCKS") {
208 self.kv_cache.max_blocks =
209 parse_required_positive_usize("FERRUM_KV_MAX_BLOCKS", value)?;
210 }
211 if let Some(value) = runtime_config_value(snapshot, "FERRUM_MAX_BATCHED_TOKENS") {
212 self.batching.max_num_batched_tokens =
213 parse_required_positive_usize("FERRUM_MAX_BATCHED_TOKENS", value)?;
214 }
215 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PAGED_MAX_SEQS") {
216 self.scheduler.max_running_requests =
217 parse_required_positive_usize("FERRUM_PAGED_MAX_SEQS", value)?;
218 }
219 if let Some(value) = runtime_config_value(snapshot, "FERRUM_RUNTIME_MEMORY_BUDGET_BYTES") {
220 self.memory.usable_capacity_bytes = Some(parse_required_positive_usize(
221 "FERRUM_RUNTIME_MEMORY_BUDGET_BYTES",
222 value,
223 )?);
224 }
225 if let Some(value) = runtime_config_value(snapshot, "FERRUM_BATCHED_GRAPH") {
226 self.backend.enable_cuda_graphs = parse_presence_bool(value)?;
227 }
228 if let Some(value) = runtime_config_value(snapshot, "FERRUM_REUSABLE_EXECUTION") {
229 self.backend.enable_reusable_execution = parse_presence_bool(value)?;
230 }
231 if let Some(value) = runtime_config_value(snapshot, "FERRUM_REUSABLE_EXECUTION_PREPARATION")
232 {
233 self.backend.reusable_execution_capture.preparation = value.parse()?;
234 }
235 if let Some(value) =
236 runtime_config_value(snapshot, "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS")
237 {
238 let widths =
239 parse_positive_usize_list("FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS", value)?;
240 if widths
241 .iter()
242 .any(|width| *width > MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH)
243 {
244 return Err(format!(
245 "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS: startup capture widths must be within 1..={MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH}"
246 ));
247 }
248 self.backend.reusable_execution_capture.exact_decode_widths = Some(widths);
249 }
250 if let Some(value) = runtime_config_value(
251 snapshot,
252 "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH",
253 ) {
254 let maximum = parse_required_positive_usize(
255 "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH",
256 value,
257 )?;
258 if maximum > MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH {
259 return Err(format!(
260 "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH: must be within 1..={MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH}"
261 ));
262 }
263 self.backend
264 .reusable_execution_capture
265 .maximum_automatic_exact_decode_width = maximum;
266 }
267 if let Some(value) = runtime_config_value(snapshot, "FERRUM_KV_CAPACITY") {
271 self.runtime.kv_capacity =
272 Some(parse_required_positive_usize("FERRUM_KV_CAPACITY", value)?);
273 }
274 if let Some(value) = runtime_config_value(snapshot, "FERRUM_MAX_MODEL_LEN") {
275 self.runtime.max_model_len = Some(parse_required_positive_usize(
276 "FERRUM_MAX_MODEL_LEN",
277 value,
278 )?);
279 }
280 if let Some(value) = runtime_config_value(snapshot, "FERRUM_RECURRENT_STATE_MAX_SLOTS") {
281 self.runtime.recurrent_state_max_slots = Some(parse_required_positive_usize(
282 "FERRUM_RECURRENT_STATE_MAX_SLOTS",
283 value,
284 )?);
285 }
286 if let Some(value) = runtime_config_value(snapshot, "FERRUM_ATTENTION_POLICY") {
287 self.runtime.attention_execution_policy =
288 AttentionExecutionPolicy::parse_runtime_value(value)
289 .map_err(|reason| format!("FERRUM_ATTENTION_POLICY: {reason}"))?;
290 }
291 if let Some(value) = runtime_config_value(snapshot, "FERRUM_CHUNKED_PREFILL") {
292 self.runtime.chunked_prefill_size =
293 parse_usize_env_value(value).ok().filter(|&v| v > 0);
294 }
295 self.runtime.batch_decode_prof |=
296 runtime_config_value(snapshot, "FERRUM_BATCH_DECODE_PROF").is_some();
297 self.runtime.next_batch_prof |=
298 runtime_config_value(snapshot, "FERRUM_NEXT_BATCH_PROF").is_some();
299 self.runtime.rbd_prof |= runtime_config_value(snapshot, "FERRUM_RBD_PROF").is_some();
300 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PROFILE_JSONL") {
301 self.runtime.profile_jsonl = Some(parse_path_env_value(value)?);
302 }
303 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHEDULER_TRACE_JSONL") {
304 self.runtime.scheduler_trace_jsonl = Some(parse_path_env_value(value)?);
305 }
306 if let Some(value) = runtime_config_value(snapshot, "FERRUM_LEGACY_SCHEDULER_TRACE_JSONL") {
307 self.runtime.legacy_scheduler_trace_jsonl = Some(parse_path_env_value(value)?);
308 }
309 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PROFILE_ENTRYPOINT") {
310 self.runtime.profile_entrypoint = Some(parse_profile_entrypoint(
311 "FERRUM_PROFILE_ENTRYPOINT",
312 value,
313 )?);
314 }
315 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PROFILE_DETAIL") {
316 self.runtime.profile_detail =
317 ObservabilityProfileDetail::parse(value).ok_or_else(|| {
318 format!(
319 "FERRUM_PROFILE_DETAIL: expected one of off, basic, resource, latency, kernel, debug, replay, verify, full; got {value:?}"
320 )
321 })?;
322 }
323 if let Some(value) = runtime_config_value(snapshot, "FERRUM_VNEXT_DIAGNOSTIC_FAULT") {
324 self.runtime.vnext_diagnostic_fault = Some(
325 VNextDiagnosticFault::parse_runtime_value(value)
326 .map_err(|reason| format!("FERRUM_VNEXT_DIAGNOSTIC_FAULT: {reason}"))?,
327 );
328 }
329 self.runtime.unified_post_prof |=
330 runtime_config_value(snapshot, "FERRUM_UNIFIED_POST_PROF").is_some();
331 self.runtime.prefix_cache_enabled |=
332 runtime_config_value(snapshot, "FERRUM_WHOLE_PROMPT_PREFIX_CACHE")
333 .map(|v| v == "1")
334 .unwrap_or(false);
335
336 if let Some(value) = runtime_config_value(snapshot, "FERRUM_MODEL_PATH") {
340 self.runtime.model_path = Some(value.to_string());
341 }
342 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SPEC_DRAFT") {
343 self.runtime.spec_draft = if value.is_empty() {
344 None
345 } else {
346 Some(value.to_string())
347 };
348 }
349 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SPEC_N") {
350 self.runtime.spec_n = value.parse::<usize>().ok();
351 }
352 if let Some(value) = runtime_config_value(snapshot, "FERRUM_DTYPE") {
353 self.runtime.dtype = Some(value.to_string());
354 }
355 if let Some(value) = runtime_config_value(snapshot, "FERRUM_METAL_DTYPE") {
356 self.runtime.metal_dtype = Some(value.to_string());
357 }
358 if let Some(value) = runtime_config_value(snapshot, "FERRUM_TP") {
359 self.runtime.tp = value.parse::<usize>().ok();
360 }
361
362 crate::install_runtime_snapshot(snapshot.clone());
367 Ok(())
368 }
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct EngineModelConfig {
373 pub model_id: ModelId,
374 pub model_info: Option<ModelInfo>,
375 pub tokenizer: TokenizerConfig,
376 #[serde(default)]
380 pub source: Option<crate::ModelSource>,
381}
382
383impl Default for EngineModelConfig {
384 fn default() -> Self {
385 Self {
386 model_id: ModelId::new("default"),
387 model_info: None,
388 tokenizer: TokenizerConfig::default(),
389 source: None,
390 }
391 }
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct SchedulerConfig {
397 pub policy: SchedulingPolicy,
399 pub max_waiting_requests: usize,
401 pub max_running_requests: usize,
403 pub enable_preemption: bool,
405 pub enable_load_balancing: bool,
407 pub fair_share_weights: HashMap<String, f32>,
409 pub enable_sla_enforcement: bool,
411 #[serde(default = "default_prompt_token_estimate")]
413 pub prompt_token_estimate: bool,
414 #[serde(default)]
416 pub prefill_first_until_active: Option<usize>,
417 #[serde(default)]
421 pub prefill_step_chunk: Option<usize>,
422 #[serde(default)]
424 pub active_decode_prefill_chunk: Option<usize>,
425 #[serde(default)]
427 pub scheduler_none_prof: bool,
428 #[serde(default)]
430 pub sequence_fit_policy: SequenceFitPolicy,
431}
432
433impl Default for SchedulerConfig {
434 fn default() -> Self {
435 Self {
436 policy: SchedulingPolicy::Priority,
437 max_waiting_requests: 1000,
438 max_running_requests: 32,
439 enable_preemption: true,
440 enable_load_balancing: false,
441 fair_share_weights: HashMap::new(),
442 enable_sla_enforcement: false,
443 prompt_token_estimate: default_prompt_token_estimate(),
444 prefill_first_until_active: None,
445 prefill_step_chunk: None,
446 active_decode_prefill_chunk: None,
447 scheduler_none_prof: false,
448 sequence_fit_policy: SequenceFitPolicy::default(),
449 }
450 }
451}
452
453fn default_prompt_token_estimate() -> bool {
454 true
455}
456
457impl SchedulerConfig {
458 pub fn apply_runtime_config_snapshot(
459 &mut self,
460 snapshot: &RuntimeConfigSnapshot,
461 ) -> std::result::Result<(), String> {
462 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHED_PROMPT_TOKEN_ESTIMATE") {
463 self.prompt_token_estimate = parse_bool_env_value(value)
464 .map_err(|reason| format!("FERRUM_SCHED_PROMPT_TOKEN_ESTIMATE: {reason}"))?;
465 }
466 if let Some(value) =
467 runtime_config_value(snapshot, "FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE")
468 {
469 self.prefill_first_until_active =
470 parse_optional_positive_usize("FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE", value)?;
471 }
472 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHED_PREFILL_STEP_CHUNK") {
473 self.prefill_step_chunk =
474 parse_optional_positive_usize("FERRUM_SCHED_PREFILL_STEP_CHUNK", value)?;
475 }
476 if let Some(value) = runtime_config_value(snapshot, "FERRUM_ACTIVE_DECODE_PREFILL_CHUNK") {
477 self.active_decode_prefill_chunk =
478 parse_optional_positive_usize("FERRUM_ACTIVE_DECODE_PREFILL_CHUNK", value)?;
479 }
480 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHED_NONE_PROF") {
481 self.scheduler_none_prof = parse_presence_bool(value)
482 .map_err(|reason| format!("FERRUM_SCHED_NONE_PROF: {reason}"))?;
483 }
484 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SEQUENCE_FIT_POLICY") {
485 self.sequence_fit_policy = SequenceFitPolicy::parse_runtime_value(value)
486 .map_err(|reason| format!("FERRUM_SEQUENCE_FIT_POLICY: {reason}"))?;
487 }
488 Ok(())
489 }
490}
491
492fn runtime_config_value<'a>(snapshot: &'a RuntimeConfigSnapshot, key: &str) -> Option<&'a str> {
493 snapshot
494 .entries
495 .iter()
496 .find(|entry| entry.key == key)
497 .map(|entry| entry.effective_value.as_str())
498}
499
500fn parse_optional_positive_usize(
501 key: &str,
502 value: &str,
503) -> std::result::Result<Option<usize>, String> {
504 let parsed = parse_usize_env_value(value).map_err(|reason| format!("{key}: {reason}"))?;
505 Ok((parsed > 0).then_some(parsed))
506}
507
508fn parse_required_positive_usize(key: &str, value: &str) -> std::result::Result<usize, String> {
509 let parsed = parse_usize_env_value(value).map_err(|reason| format!("{key}: {reason}"))?;
510 if parsed == 0 {
511 Err(format!("{key}: must be greater than zero"))
512 } else {
513 Ok(parsed)
514 }
515}
516
517fn parse_positive_usize_list(key: &str, value: &str) -> std::result::Result<Vec<usize>, String> {
518 let values = value
519 .split(',')
520 .map(str::trim)
521 .map(|value| parse_required_positive_usize(key, value))
522 .collect::<std::result::Result<Vec<_>, _>>()?;
523 if values.is_empty() {
524 Err(format!("{key}: must contain at least one width"))
525 } else {
526 Ok(values)
527 }
528}
529
530fn parse_profile_entrypoint(
531 key: &str,
532 value: &str,
533) -> std::result::Result<ProfileEntrypoint, String> {
534 ProfileEntrypoint::parse(value).ok_or_else(|| {
535 format!("{key}: expected one of run, serve, bench_serve, synthetic; got {value:?}")
536 })
537}
538
539fn parse_presence_bool(value: &str) -> std::result::Result<bool, String> {
540 if value.trim().is_empty() {
541 Ok(true)
542 } else {
543 parse_bool_env_value(value)
544 }
545}
546
547#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
549pub enum SchedulingPolicy {
550 FCFS,
552 Priority,
554 FairShare,
556 SJF,
558 RoundRobin,
560 ContinuousBatch,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize)]
566pub struct KvCacheConfig {
567 pub cache_type: KvCacheType,
569 #[serde(default)]
574 pub dtype: KvCacheDtype,
575 pub block_size: usize,
577 pub max_blocks: usize,
579 pub enable_compression: bool,
581 pub compression_ratio: f32,
583 pub enable_multi_level: bool,
585 pub swap_threshold: f32,
587 pub enable_prefix_caching: bool,
589 pub prefix_cache_size: usize,
591}
592
593impl Default for KvCacheConfig {
594 fn default() -> Self {
595 Self {
600 cache_type: KvCacheType::Contiguous,
601 dtype: KvCacheDtype::default(),
602 block_size: 16,
603 max_blocks: 2048,
604 enable_compression: false,
605 compression_ratio: 0.5,
606 enable_multi_level: true,
607 swap_threshold: 0.8,
608 enable_prefix_caching: true,
609 prefix_cache_size: 100,
610 }
611 }
612}
613
614#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
616pub enum KvCacheType {
617 Contiguous,
619 Paged,
621 Tree,
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
631#[serde(rename_all = "lowercase")]
632pub enum KvCacheDtype {
633 #[default]
635 Fp16,
636 Bf16,
639 Int8,
645 Fp8,
647}
648
649impl KvCacheDtype {
650 pub fn parse(s: &str) -> Option<Self> {
652 match s.trim().to_ascii_lowercase().as_str() {
653 "fp16" | "f16" | "float16" => Some(Self::Fp16),
654 "bf16" | "bfloat16" => Some(Self::Bf16),
655 "int8" | "i8" => Some(Self::Int8),
656 "fp8" | "f8" | "f8e4m3" | "e4m3" => Some(Self::Fp8),
657 _ => None,
658 }
659 }
660
661 pub fn as_str(&self) -> &'static str {
663 match self {
664 Self::Fp16 => "fp16",
665 Self::Bf16 => "bf16",
666 Self::Int8 => "int8",
667 Self::Fp8 => "fp8",
668 }
669 }
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize)]
674pub struct MemoryConfig {
675 pub pool_size: Option<usize>,
677 #[serde(default)]
681 pub usable_capacity_bytes: Option<usize>,
682 pub enable_pooling: bool,
684 pub alignment: usize,
686 pub enable_defragmentation: bool,
688 pub defragmentation_threshold: f32,
690 pub enable_memory_stats: bool,
692 pub pressure_warning_threshold: f32,
694 pub pressure_critical_threshold: f32,
696}
697
698#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
700pub struct MemoryCapacityBudget {
701 pub capacity_bytes: u64,
702 pub usable_capacity_bytes: u64,
703 pub reserve_bytes: u64,
704}
705
706impl MemoryConfig {
707 pub fn resolve_capacity_budget(
708 &self,
709 device_capacity_bytes: u64,
710 ) -> std::result::Result<MemoryCapacityBudget, String> {
711 if device_capacity_bytes == 0 {
712 return Err("runtime device memory capacity must be greater than zero".to_string());
713 }
714 let capacity_bytes = self
715 .pool_size
716 .map(|bytes| bytes as u64)
717 .unwrap_or(device_capacity_bytes)
718 .min(device_capacity_bytes);
719 if capacity_bytes == 0 {
720 return Err("runtime memory capacity must be greater than zero".to_string());
721 }
722
723 let usable_capacity_bytes = if let Some(bytes) = self.usable_capacity_bytes {
724 let bytes = bytes as u64;
725 if bytes == 0 || bytes > capacity_bytes {
726 return Err(format!(
727 "memory.usable_capacity_bytes must be in 1..={capacity_bytes}, got {bytes}"
728 ));
729 }
730 bytes
731 } else {
732 let critical = self.pressure_critical_threshold;
733 if !critical.is_finite() || critical <= 0.0 || critical > 1.0 {
734 return Err(format!(
735 "memory.pressure_critical_threshold must be in (0, 1], got {critical}"
736 ));
737 }
738 let threshold_bytes = ((capacity_bytes as f64) * f64::from(critical)).floor() as u64;
739 capacity_bytes.saturating_sub(
740 capacity_bytes
741 .saturating_sub(threshold_bytes)
742 .min(capacity_bytes - 1),
743 )
744 };
745 Ok(MemoryCapacityBudget {
746 capacity_bytes,
747 usable_capacity_bytes,
748 reserve_bytes: capacity_bytes - usable_capacity_bytes,
749 })
750 }
751}
752
753impl Default for MemoryConfig {
754 fn default() -> Self {
755 Self {
756 pool_size: None,
757 usable_capacity_bytes: None,
758 enable_pooling: true,
759 alignment: 256,
760 enable_defragmentation: false,
761 defragmentation_threshold: 0.7,
762 enable_memory_stats: true,
763 pressure_warning_threshold: 0.8,
764 pressure_critical_threshold: 0.95,
765 }
766 }
767}
768
769pub const MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH: usize = 32;
776
777pub const DEFAULT_MAXIMUM_AUTOMATIC_EXACT_DECODE_WIDTH: usize =
785 MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH;
786
787#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
789#[serde(rename_all = "snake_case")]
790pub enum ReusableExecutionPreparationMode {
791 #[default]
793 Auto,
794 Startup,
796 OnDemand,
798}
799
800impl ReusableExecutionPreparationMode {
801 pub const fn as_runtime_value(self) -> &'static str {
802 match self {
803 Self::Auto => "auto",
804 Self::Startup => "startup",
805 Self::OnDemand => "on_demand",
806 }
807 }
808
809 pub fn resolve(self, on_demand_supported: bool) -> Result<Self, String> {
810 match (self, on_demand_supported) {
811 (Self::Auto, true) => Ok(Self::OnDemand),
812 (Self::Auto, false) => Ok(Self::Startup),
813 (Self::OnDemand, false) => Err(
814 "runtime.reusable_execution_preparation=on_demand requires a runtime declaring on-demand reusable execution support".to_owned(),
815 ),
816 (mode, _) => Ok(mode),
817 }
818 }
819}
820
821impl std::str::FromStr for ReusableExecutionPreparationMode {
822 type Err = String;
823
824 fn from_str(value: &str) -> Result<Self, Self::Err> {
825 match value.trim() {
826 "auto" => Ok(Self::Auto),
827 "startup" => Ok(Self::Startup),
828 "on_demand" => Ok(Self::OnDemand),
829 _ => {
830 Err("reusable execution preparation must be auto, startup, or on_demand".to_owned())
831 }
832 }
833 }
834}
835
836#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
846#[serde(default)]
847pub struct ReusableExecutionCaptureConfig {
848 pub preparation: ReusableExecutionPreparationMode,
850 pub exact_decode_widths: Option<Vec<usize>>,
853 pub maximum_automatic_exact_decode_width: usize,
856}
857
858impl Default for ReusableExecutionCaptureConfig {
859 fn default() -> Self {
860 Self {
861 preparation: ReusableExecutionPreparationMode::default(),
862 exact_decode_widths: None,
863 maximum_automatic_exact_decode_width: DEFAULT_MAXIMUM_AUTOMATIC_EXACT_DECODE_WIDTH,
864 }
865 }
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize)]
870pub struct BackendConfig {
871 pub backend_type: BackendType,
873 pub device: Device,
875 pub dtype: DataType,
877 pub enable_optimizations: bool,
879 pub optimization_level: u8,
881 pub enable_cuda_graphs: bool,
883 #[serde(default = "default_enable_reusable_execution")]
885 pub enable_reusable_execution: bool,
886 #[serde(default)]
890 pub reusable_execution_capture: ReusableExecutionCaptureConfig,
891 pub enable_kernel_fusion: bool,
893 pub backend_options: HashMap<String, serde_json::Value>,
895}
896
897impl Default for BackendConfig {
898 fn default() -> Self {
899 Self {
900 backend_type: BackendType::Candle,
901 device: Device::CPU,
902 dtype: DataType::FP16,
903 enable_optimizations: true,
904 optimization_level: 2,
905 enable_cuda_graphs: false,
906 enable_reusable_execution: default_enable_reusable_execution(),
907 reusable_execution_capture: ReusableExecutionCaptureConfig::default(),
908 enable_kernel_fusion: true,
909 backend_options: HashMap::new(),
910 }
911 }
912}
913
914const fn default_enable_reusable_execution() -> bool {
915 true
916}
917
918#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
920pub enum BackendType {
921 Candle,
923 OnnxRuntime,
925 TensorRT,
927 Custom,
929}
930
931#[derive(Debug, Clone, Serialize, Deserialize)]
933pub struct TokenizerConfig {
934 pub tokenizer_type: TokenizerType,
936 pub tokenizer_path: Option<String>,
938 pub enable_fast: bool,
940 pub add_special_tokens: bool,
942 pub truncation: Option<TruncationConfig>,
944 pub padding: Option<PaddingConfig>,
946}
947
948impl Default for TokenizerConfig {
949 fn default() -> Self {
950 Self {
951 tokenizer_type: TokenizerType::BPE,
952 tokenizer_path: None,
953 enable_fast: true,
954 add_special_tokens: true,
955 truncation: None,
956 padding: None,
957 }
958 }
959}
960
961#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
963pub enum TokenizerType {
964 BPE,
966 WordPiece,
968 SentencePiece,
970 Tiktoken,
972 Custom,
974}
975
976#[derive(Debug, Clone, Serialize, Deserialize)]
978pub struct TruncationConfig {
979 pub max_length: usize,
981 pub strategy: TruncationStrategy,
983}
984
985#[derive(Debug, Clone, Serialize, Deserialize)]
987pub enum TruncationStrategy {
988 TruncateStart,
990 TruncateEnd,
992 TruncateBoth,
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize)]
998pub struct PaddingConfig {
999 pub strategy: PaddingStrategy,
1001 pub token_id: u32,
1003 pub target_length: Option<usize>,
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1009pub enum PaddingStrategy {
1010 None,
1012 MaxLength,
1014 FixedLength,
1016}
1017
1018#[derive(Debug, Clone, Serialize, Deserialize)]
1022pub struct SecurityConfig {
1023 pub enable_auth: bool,
1025 pub api_keys: Vec<String>,
1027 pub enable_rate_limiting: bool,
1029 pub rate_limit_rpm: u32,
1031 pub enable_content_filter: bool,
1033 pub max_prompt_length: usize,
1035 pub enable_prompt_validation: bool,
1037 pub allowed_extensions: Vec<String>,
1039}
1040
1041impl Default for SecurityConfig {
1042 fn default() -> Self {
1043 Self {
1044 enable_auth: false,
1045 api_keys: vec![],
1046 enable_rate_limiting: true,
1047 rate_limit_rpm: 60,
1048 enable_content_filter: false,
1049 max_prompt_length: 32768,
1050 enable_prompt_validation: true,
1051 allowed_extensions: vec!["txt".to_string(), "json".to_string()],
1052 }
1053 }
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1057pub struct SamplingConfig {
1058 pub default_params: SamplingParams,
1059 pub presets: SamplingPresets,
1060 pub enable_custom_processors: bool,
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize)]
1064pub struct MonitoringConfig {
1065 pub enable_metrics: bool,
1066 pub enable_tracing: bool,
1067 pub export_interval: Duration,
1068}
1069
1070impl Default for MonitoringConfig {
1071 fn default() -> Self {
1072 Self {
1073 enable_metrics: true,
1074 enable_tracing: true,
1075 export_interval: Duration::from_secs(5),
1076 }
1077 }
1078}
1079
1080#[derive(Debug, Clone, Serialize, Deserialize)]
1081pub struct BatchConfig {
1082 pub max_batch_size: usize,
1083 pub max_wait_ms: u64,
1084 pub enable_dynamic: bool,
1085 pub enable_continuous: bool,
1086 #[serde(default = "BatchConfig::default_max_num_batched_tokens")]
1093 pub max_num_batched_tokens: usize,
1094}
1095
1096impl BatchConfig {
1097 fn default_max_num_batched_tokens() -> usize {
1098 2048
1099 }
1100}
1101
1102impl Default for BatchConfig {
1103 fn default() -> Self {
1104 Self {
1105 max_batch_size: 32,
1106 max_wait_ms: 8,
1107 enable_dynamic: true,
1108 enable_continuous: false,
1109 max_num_batched_tokens: Self::default_max_num_batched_tokens(),
1110 }
1111 }
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use super::*;
1117
1118 #[test]
1119 fn scheduler_keeps_immediate_fit_default_until_full_input_policy_is_gated() {
1120 assert_eq!(
1121 SchedulerConfig::default().sequence_fit_policy,
1122 SequenceFitPolicy::ImmediateOnly
1123 );
1124 }
1125
1126 #[test]
1127 fn sequence_fit_policy_uses_canonical_product_values() {
1128 assert_eq!(
1129 serde_json::to_string(&SequenceFitPolicy::FullInputMustFit).unwrap(),
1130 "\"full-input-must-fit\""
1131 );
1132 assert_eq!(
1133 serde_json::from_str::<SequenceFitPolicy>("\"immediate-only\"").unwrap(),
1134 SequenceFitPolicy::ImmediateOnly
1135 );
1136 }
1137
1138 #[test]
1139 fn diagnostic_fault_uses_one_canonical_product_value() {
1140 assert_eq!(
1141 VNextDiagnosticFault::parse_runtime_value("prefill_resource_after_submit_once")
1142 .unwrap(),
1143 VNextDiagnosticFault::PrefillResourceAfterSubmitOnce
1144 );
1145 assert_eq!(
1146 VNextDiagnosticFault::PrefillResourceAfterSubmitOnce.as_runtime_value(),
1147 "prefill-resource-after-submit-once"
1148 );
1149 assert!(VNextDiagnosticFault::parse_runtime_value("resource-failure").is_err());
1150 }
1151
1152 #[test]
1153 fn engine_config_applies_typed_diagnostic_fault() {
1154 let mut config = EngineConfig::default();
1155 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1156 "FERRUM_VNEXT_DIAGNOSTIC_FAULT",
1157 "prefill-resource-after-submit-once",
1158 )]);
1159
1160 config
1161 .apply_runtime_config_snapshot(&snapshot)
1162 .expect("runtime config should apply");
1163
1164 assert_eq!(
1165 config.runtime.vnext_diagnostic_fault,
1166 Some(VNextDiagnosticFault::PrefillResourceAfterSubmitOnce)
1167 );
1168 }
1169
1170 #[test]
1171 fn engine_config_rejects_unknown_diagnostic_fault() {
1172 let mut config = EngineConfig::default();
1173 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1174 "FERRUM_VNEXT_DIAGNOSTIC_FAULT",
1175 "resource-failure",
1176 )]);
1177
1178 let error = config
1179 .apply_runtime_config_snapshot(&snapshot)
1180 .expect_err("unknown diagnostic fault must fail closed");
1181
1182 assert!(error.contains("FERRUM_VNEXT_DIAGNOSTIC_FAULT"));
1183 }
1184
1185 #[test]
1186 fn scheduler_deserialization_without_fit_policy_keeps_legacy_default() {
1187 let mut serialized = serde_json::to_value(SchedulerConfig::default()).unwrap();
1188 serialized
1189 .as_object_mut()
1190 .unwrap()
1191 .remove("sequence_fit_policy");
1192
1193 let scheduler: SchedulerConfig = serde_json::from_value(serialized).unwrap();
1194
1195 assert_eq!(
1196 scheduler.sequence_fit_policy,
1197 SequenceFitPolicy::ImmediateOnly
1198 );
1199 }
1200
1201 #[test]
1202 fn checkpoint_capture_deserialization_keeps_decode_capture_disabled_by_default() {
1203 let capture: VNextCheckpointCaptureConfig = serde_json::from_value(serde_json::json!({
1204 "output_dir": "capture",
1205 "value_ids": ["value.output.logits"],
1206 "maximum_prefill_waves": 1
1207 }))
1208 .unwrap();
1209
1210 assert_eq!(capture.maximum_decode_waves, 0);
1211 assert!(!capture.capture_product_output);
1212 }
1213
1214 #[test]
1215 fn engine_config_applies_typed_sequence_fit_policy() {
1216 let mut config = EngineConfig::default();
1217 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1218 "FERRUM_SEQUENCE_FIT_POLICY",
1219 "full-input-must-fit",
1220 )]);
1221
1222 config
1223 .apply_runtime_config_snapshot(&snapshot)
1224 .expect("runtime config should apply");
1225
1226 assert_eq!(
1227 config.scheduler.sequence_fit_policy,
1228 SequenceFitPolicy::FullInputMustFit
1229 );
1230 }
1231
1232 #[test]
1233 fn engine_config_rejects_unknown_sequence_fit_policy() {
1234 let mut config = EngineConfig::default();
1235 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1236 "FERRUM_SEQUENCE_FIT_POLICY",
1237 "reserve-everything",
1238 )]);
1239
1240 let error = config
1241 .apply_runtime_config_snapshot(&snapshot)
1242 .expect_err("unknown fit policy must fail closed");
1243
1244 assert!(error.contains("FERRUM_SEQUENCE_FIT_POLICY"));
1245 }
1246
1247 #[test]
1248 fn engine_config_applies_recurrent_state_max_slots_runtime_key() {
1249 let mut config = EngineConfig::default();
1250 let snapshot =
1251 RuntimeConfigSnapshot::from_env_vars([("FERRUM_RECURRENT_STATE_MAX_SLOTS", "16")]);
1252
1253 config
1254 .apply_runtime_config_snapshot(&snapshot)
1255 .expect("runtime config should apply");
1256
1257 assert_eq!(config.runtime.recurrent_state_max_slots, Some(16));
1258 }
1259
1260 #[test]
1261 fn engine_config_does_not_apply_removed_qwen35_slot_alias() {
1262 let mut config = EngineConfig::default();
1263 let snapshot =
1264 RuntimeConfigSnapshot::from_env_vars([("FERRUM_QWEN35_LINEAR_STATE_MAX_SLOTS", "16")]);
1265
1266 config
1267 .apply_runtime_config_snapshot(&snapshot)
1268 .expect("runtime config should apply");
1269
1270 assert_eq!(config.runtime.recurrent_state_max_slots, None);
1271 }
1272
1273 #[test]
1274 fn engine_config_uses_generic_recurrent_state_slots_when_removed_alias_is_present() {
1275 let mut config = EngineConfig::default();
1276 let snapshot = RuntimeConfigSnapshot::from_env_vars([
1277 ("FERRUM_RECURRENT_STATE_MAX_SLOTS", "8"),
1278 ("FERRUM_QWEN35_LINEAR_STATE_MAX_SLOTS", "16"),
1279 ]);
1280
1281 config
1282 .apply_runtime_config_snapshot(&snapshot)
1283 .expect("runtime config should apply");
1284
1285 assert_eq!(config.runtime.recurrent_state_max_slots, Some(8));
1286 }
1287
1288 #[test]
1289 fn engine_config_applies_profile_entrypoint_runtime_key() {
1290 let mut config = EngineConfig::default();
1291 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_ENTRYPOINT", "run")]);
1292
1293 config
1294 .apply_runtime_config_snapshot(&snapshot)
1295 .expect("runtime config should apply");
1296
1297 assert_eq!(
1298 config.runtime.profile_entrypoint,
1299 Some(ProfileEntrypoint::Run)
1300 );
1301 }
1302
1303 #[test]
1304 fn engine_config_applies_typed_profile_detail_runtime_key() {
1305 let mut config = EngineConfig::default();
1306 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "full")]);
1307
1308 config
1309 .apply_runtime_config_snapshot(&snapshot)
1310 .expect("runtime config should apply");
1311
1312 assert_eq!(
1313 config.runtime.profile_detail,
1314 ObservabilityProfileDetail::Full
1315 );
1316 }
1317
1318 #[test]
1319 fn engine_config_applies_typed_latency_profile_detail_runtime_key() {
1320 let mut config = EngineConfig::default();
1321 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "latency")]);
1322
1323 config
1324 .apply_runtime_config_snapshot(&snapshot)
1325 .expect("latency profile detail should apply");
1326
1327 assert_eq!(
1328 config.runtime.profile_detail,
1329 ObservabilityProfileDetail::Latency
1330 );
1331 }
1332
1333 #[test]
1334 fn engine_config_applies_typed_replay_profile_detail_runtime_key() {
1335 let mut config = EngineConfig::default();
1336 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "replay")]);
1337
1338 config
1339 .apply_runtime_config_snapshot(&snapshot)
1340 .expect("runtime config should apply");
1341
1342 assert_eq!(
1343 config.runtime.profile_detail,
1344 ObservabilityProfileDetail::Replay
1345 );
1346 }
1347
1348 #[test]
1349 fn engine_config_applies_typed_verification_profile_detail_runtime_key() {
1350 let mut config = EngineConfig::default();
1351 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "verify")]);
1352
1353 config
1354 .apply_runtime_config_snapshot(&snapshot)
1355 .expect("runtime config should apply");
1356
1357 assert_eq!(
1358 config.runtime.profile_detail,
1359 ObservabilityProfileDetail::Verify
1360 );
1361 }
1362
1363 #[test]
1364 fn engine_config_applies_typed_profile_jsonl_runtime_key() {
1365 let mut config = EngineConfig::default();
1366 let snapshot =
1367 RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_JSONL", "/tmp/profile.jsonl")]);
1368
1369 config
1370 .apply_runtime_config_snapshot(&snapshot)
1371 .expect("runtime config should apply");
1372
1373 assert_eq!(
1374 config.runtime.profile_jsonl.as_deref(),
1375 Some(std::path::Path::new("/tmp/profile.jsonl"))
1376 );
1377 }
1378
1379 #[test]
1380 fn engine_config_rejects_unknown_profile_detail() {
1381 let mut config = EngineConfig::default();
1382 let snapshot =
1383 RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "everything")]);
1384
1385 let error = config
1386 .apply_runtime_config_snapshot(&snapshot)
1387 .expect_err("unknown profile detail must fail closed");
1388
1389 assert!(error.contains("FERRUM_PROFILE_DETAIL"));
1390 }
1391}