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 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub startup_memory_request: Option<crate::StartupMemoryRequest>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub startup_memory_plan: Option<crate::StartupMemoryPlan>,
151 pub kv_capacity: Option<usize>,
152 pub max_model_len: Option<usize>,
153 pub chunked_prefill_size: Option<usize>,
154 pub batch_decode_prof: bool,
155 pub next_batch_prof: bool,
156 pub rbd_prof: bool,
157 #[serde(default)]
158 pub profile_jsonl: Option<PathBuf>,
159 pub scheduler_trace_jsonl: Option<PathBuf>,
160 pub legacy_scheduler_trace_jsonl: Option<PathBuf>,
161 pub profile_entrypoint: Option<ProfileEntrypoint>,
162 pub profile_detail: ObservabilityProfileDetail,
163 pub unified_post_prof: bool,
164 pub prefix_cache_enabled: bool,
165 #[serde(default)]
167 pub prefix_state_cache_enabled: bool,
168 pub recurrent_state_max_slots: Option<usize>,
169 pub attention_execution_policy: AttentionExecutionPolicy,
170
171 pub model_path: Option<String>,
178 pub spec_draft: Option<String>,
179 pub spec_n: Option<usize>,
180 pub dtype: Option<String>,
181 pub metal_dtype: Option<String>,
182 pub tp: Option<usize>,
183 #[serde(default)]
184 pub vnext_checkpoint_capture: Option<VNextCheckpointCaptureConfig>,
185 #[serde(default)]
186 pub vnext_diagnostic_fault: Option<VNextDiagnosticFault>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize, Default)]
191pub struct EngineConfig {
192 #[serde(default)]
195 pub numerical_execution: crate::NumericalExecutionPolicy,
196 pub model: EngineModelConfig,
197 pub scheduler: SchedulerConfig,
198 pub sampling: SamplingConfig,
199 pub backend: BackendConfig,
200 pub kv_cache: KvCacheConfig,
201 pub memory: MemoryConfig,
202 pub batching: BatchConfig,
203 pub monitoring: MonitoringConfig,
204 #[serde(default)]
205 pub runtime: RuntimeKnobs,
206}
207
208impl EngineConfig {
209 pub fn apply_runtime_config_snapshot(
210 &mut self,
211 snapshot: &RuntimeConfigSnapshot,
212 ) -> std::result::Result<(), String> {
213 self.scheduler.apply_runtime_config_snapshot(snapshot)?;
214 if let Some(value) = runtime_config_value(snapshot, "FERRUM_KV_MAX_BLOCKS") {
215 self.kv_cache.max_blocks =
216 parse_required_positive_usize("FERRUM_KV_MAX_BLOCKS", value)?;
217 }
218 if let Some(value) = runtime_config_value(snapshot, "FERRUM_MAX_BATCHED_TOKENS") {
219 self.batching.max_num_batched_tokens =
220 parse_required_positive_usize("FERRUM_MAX_BATCHED_TOKENS", value)?;
221 }
222 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PAGED_MAX_SEQS") {
223 self.scheduler.max_running_requests =
224 parse_required_positive_usize("FERRUM_PAGED_MAX_SEQS", value)?;
225 }
226 if let Some(value) = runtime_config_value(snapshot, "FERRUM_RUNTIME_MEMORY_BUDGET_BYTES") {
227 self.memory.usable_capacity_bytes = Some(parse_required_positive_usize(
228 "FERRUM_RUNTIME_MEMORY_BUDGET_BYTES",
229 value,
230 )?);
231 }
232 if let Some(value) = runtime_config_value(snapshot, "FERRUM_BATCHED_GRAPH") {
233 self.backend.enable_cuda_graphs = parse_presence_bool(value)?;
234 }
235 if let Some(value) = runtime_config_value(snapshot, "FERRUM_REUSABLE_EXECUTION") {
236 self.backend.enable_reusable_execution = parse_presence_bool(value)?;
237 }
238 if let Some(value) = runtime_config_value(snapshot, "FERRUM_REUSABLE_EXECUTION_PREPARATION")
239 {
240 self.backend.reusable_execution_capture.preparation = value.parse()?;
241 }
242 if let Some(value) =
243 runtime_config_value(snapshot, "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS")
244 {
245 let widths =
246 parse_positive_usize_list("FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS", value)?;
247 if widths
248 .iter()
249 .any(|width| *width > MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH)
250 {
251 return Err(format!(
252 "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS: startup capture widths must be within 1..={MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH}"
253 ));
254 }
255 self.backend.reusable_execution_capture.exact_decode_widths = Some(widths);
256 }
257 if let Some(value) = runtime_config_value(
258 snapshot,
259 "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH",
260 ) {
261 let maximum = parse_required_positive_usize(
262 "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH",
263 value,
264 )?;
265 if maximum > MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH {
266 return Err(format!(
267 "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH: must be within 1..={MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH}"
268 ));
269 }
270 self.backend
271 .reusable_execution_capture
272 .maximum_automatic_exact_decode_width = maximum;
273 }
274 if let Some(value) = runtime_config_value(snapshot, "FERRUM_KV_CAPACITY") {
278 self.runtime.kv_capacity =
279 Some(parse_required_positive_usize("FERRUM_KV_CAPACITY", value)?);
280 }
281 if let Some(value) = runtime_config_value(snapshot, "FERRUM_MAX_MODEL_LEN") {
282 self.runtime.max_model_len = Some(parse_required_positive_usize(
283 "FERRUM_MAX_MODEL_LEN",
284 value,
285 )?);
286 }
287 if let Some(value) = runtime_config_value(snapshot, "FERRUM_RECURRENT_STATE_MAX_SLOTS") {
288 self.runtime.recurrent_state_max_slots = Some(parse_required_positive_usize(
289 "FERRUM_RECURRENT_STATE_MAX_SLOTS",
290 value,
291 )?);
292 }
293 if let Some(value) = runtime_config_value(snapshot, "FERRUM_ATTENTION_POLICY") {
294 self.runtime.attention_execution_policy =
295 AttentionExecutionPolicy::parse_runtime_value(value)
296 .map_err(|reason| format!("FERRUM_ATTENTION_POLICY: {reason}"))?;
297 }
298 if let Some(value) = runtime_config_value(snapshot, "FERRUM_CHUNKED_PREFILL") {
299 self.runtime.chunked_prefill_size =
300 parse_usize_env_value(value).ok().filter(|&v| v > 0);
301 }
302 self.runtime.batch_decode_prof |=
303 runtime_config_value(snapshot, "FERRUM_BATCH_DECODE_PROF").is_some();
304 self.runtime.next_batch_prof |=
305 runtime_config_value(snapshot, "FERRUM_NEXT_BATCH_PROF").is_some();
306 self.runtime.rbd_prof |= runtime_config_value(snapshot, "FERRUM_RBD_PROF").is_some();
307 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PROFILE_JSONL") {
308 self.runtime.profile_jsonl = Some(parse_path_env_value(value)?);
309 }
310 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHEDULER_TRACE_JSONL") {
311 self.runtime.scheduler_trace_jsonl = Some(parse_path_env_value(value)?);
312 }
313 if let Some(value) = runtime_config_value(snapshot, "FERRUM_LEGACY_SCHEDULER_TRACE_JSONL") {
314 self.runtime.legacy_scheduler_trace_jsonl = Some(parse_path_env_value(value)?);
315 }
316 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PROFILE_ENTRYPOINT") {
317 self.runtime.profile_entrypoint = Some(parse_profile_entrypoint(
318 "FERRUM_PROFILE_ENTRYPOINT",
319 value,
320 )?);
321 }
322 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PROFILE_DETAIL") {
323 self.runtime.profile_detail =
324 ObservabilityProfileDetail::parse(value).ok_or_else(|| {
325 format!(
326 "FERRUM_PROFILE_DETAIL: expected one of off, basic, resource, latency, kernel, debug, replay, verify, full; got {value:?}"
327 )
328 })?;
329 }
330 if let Some(value) = runtime_config_value(snapshot, "FERRUM_VNEXT_DIAGNOSTIC_FAULT") {
331 self.runtime.vnext_diagnostic_fault = Some(
332 VNextDiagnosticFault::parse_runtime_value(value)
333 .map_err(|reason| format!("FERRUM_VNEXT_DIAGNOSTIC_FAULT: {reason}"))?,
334 );
335 }
336 self.runtime.unified_post_prof |=
337 runtime_config_value(snapshot, "FERRUM_UNIFIED_POST_PROF").is_some();
338 self.runtime.prefix_cache_enabled |=
339 runtime_config_value(snapshot, "FERRUM_WHOLE_PROMPT_PREFIX_CACHE")
340 .map(|v| v == "1")
341 .unwrap_or(false);
342 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PREFIX_CACHE") {
343 self.runtime.prefix_state_cache_enabled = parse_bool_env_value(value)
344 .map_err(|reason| format!("FERRUM_PREFIX_CACHE: {reason}"))?;
345 }
346
347 if let Some(value) = runtime_config_value(snapshot, "FERRUM_MODEL_PATH") {
351 self.runtime.model_path = Some(value.to_string());
352 }
353 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SPEC_DRAFT") {
354 self.runtime.spec_draft = if value.is_empty() {
355 None
356 } else {
357 Some(value.to_string())
358 };
359 }
360 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SPEC_N") {
361 self.runtime.spec_n = value.parse::<usize>().ok();
362 }
363 if let Some(value) = runtime_config_value(snapshot, "FERRUM_DTYPE") {
364 self.runtime.dtype = Some(value.to_string());
365 }
366 if let Some(value) = runtime_config_value(snapshot, "FERRUM_METAL_DTYPE") {
367 self.runtime.metal_dtype = Some(value.to_string());
368 }
369 if let Some(value) = runtime_config_value(snapshot, "FERRUM_TP") {
370 self.runtime.tp = value.parse::<usize>().ok();
371 }
372
373 crate::install_runtime_snapshot(snapshot.clone());
378 Ok(())
379 }
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct EngineModelConfig {
384 pub model_id: ModelId,
385 pub model_info: Option<ModelInfo>,
386 pub tokenizer: TokenizerConfig,
387 #[serde(default)]
391 pub source: Option<crate::ModelSource>,
392}
393
394impl Default for EngineModelConfig {
395 fn default() -> Self {
396 Self {
397 model_id: ModelId::new("default"),
398 model_info: None,
399 tokenizer: TokenizerConfig::default(),
400 source: None,
401 }
402 }
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct SchedulerConfig {
408 pub policy: SchedulingPolicy,
410 pub max_waiting_requests: usize,
412 pub max_running_requests: usize,
414 pub enable_preemption: bool,
416 pub enable_load_balancing: bool,
418 pub fair_share_weights: HashMap<String, f32>,
420 pub enable_sla_enforcement: bool,
422 #[serde(default = "default_prompt_token_estimate")]
424 pub prompt_token_estimate: bool,
425 #[serde(default)]
427 pub prefill_first_until_active: Option<usize>,
428 #[serde(default)]
431 pub prefix_rendezvous_max_wait_ms: Option<std::num::NonZeroU64>,
432 #[serde(default)]
436 pub prefill_step_chunk: Option<usize>,
437 #[serde(default)]
439 pub active_decode_prefill_chunk: Option<usize>,
440 #[serde(default)]
442 pub scheduler_none_prof: bool,
443 #[serde(default)]
445 pub sequence_fit_policy: SequenceFitPolicy,
446}
447
448impl Default for SchedulerConfig {
449 fn default() -> Self {
450 Self {
451 policy: SchedulingPolicy::Priority,
452 max_waiting_requests: 1000,
453 max_running_requests: 32,
454 enable_preemption: true,
455 enable_load_balancing: false,
456 fair_share_weights: HashMap::new(),
457 enable_sla_enforcement: false,
458 prompt_token_estimate: default_prompt_token_estimate(),
459 prefill_first_until_active: None,
460 prefix_rendezvous_max_wait_ms: None,
461 prefill_step_chunk: None,
462 active_decode_prefill_chunk: None,
463 scheduler_none_prof: false,
464 sequence_fit_policy: SequenceFitPolicy::default(),
465 }
466 }
467}
468
469fn default_prompt_token_estimate() -> bool {
470 true
471}
472
473impl SchedulerConfig {
474 pub fn apply_runtime_config_snapshot(
475 &mut self,
476 snapshot: &RuntimeConfigSnapshot,
477 ) -> std::result::Result<(), String> {
478 if let Some(value) = runtime_config_value(snapshot, "FERRUM_PREFIX_RENDEZVOUS_MAX_WAIT_MS")
479 {
480 self.prefix_rendezvous_max_wait_ms = Some(
481 value
482 .parse::<std::num::NonZeroU64>()
483 .map_err(|error| format!("FERRUM_PREFIX_RENDEZVOUS_MAX_WAIT_MS: {error}"))?,
484 );
485 }
486 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHED_PROMPT_TOKEN_ESTIMATE") {
487 self.prompt_token_estimate = parse_bool_env_value(value)
488 .map_err(|reason| format!("FERRUM_SCHED_PROMPT_TOKEN_ESTIMATE: {reason}"))?;
489 }
490 if let Some(value) =
491 runtime_config_value(snapshot, "FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE")
492 {
493 self.prefill_first_until_active =
494 parse_optional_positive_usize("FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE", value)?;
495 }
496 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHED_PREFILL_STEP_CHUNK") {
497 self.prefill_step_chunk =
498 parse_optional_positive_usize("FERRUM_SCHED_PREFILL_STEP_CHUNK", value)?;
499 }
500 if let Some(value) = runtime_config_value(snapshot, "FERRUM_ACTIVE_DECODE_PREFILL_CHUNK") {
501 self.active_decode_prefill_chunk =
502 parse_optional_positive_usize("FERRUM_ACTIVE_DECODE_PREFILL_CHUNK", value)?;
503 }
504 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SCHED_NONE_PROF") {
505 self.scheduler_none_prof = parse_presence_bool(value)
506 .map_err(|reason| format!("FERRUM_SCHED_NONE_PROF: {reason}"))?;
507 }
508 if let Some(value) = runtime_config_value(snapshot, "FERRUM_SEQUENCE_FIT_POLICY") {
509 self.sequence_fit_policy = SequenceFitPolicy::parse_runtime_value(value)
510 .map_err(|reason| format!("FERRUM_SEQUENCE_FIT_POLICY: {reason}"))?;
511 }
512 Ok(())
513 }
514}
515
516fn runtime_config_value<'a>(snapshot: &'a RuntimeConfigSnapshot, key: &str) -> Option<&'a str> {
517 snapshot
518 .entries
519 .iter()
520 .find(|entry| entry.key == key)
521 .map(|entry| entry.effective_value.as_str())
522}
523
524fn parse_optional_positive_usize(
525 key: &str,
526 value: &str,
527) -> std::result::Result<Option<usize>, String> {
528 let parsed = parse_usize_env_value(value).map_err(|reason| format!("{key}: {reason}"))?;
529 Ok((parsed > 0).then_some(parsed))
530}
531
532fn parse_required_positive_usize(key: &str, value: &str) -> std::result::Result<usize, String> {
533 let parsed = parse_usize_env_value(value).map_err(|reason| format!("{key}: {reason}"))?;
534 if parsed == 0 {
535 Err(format!("{key}: must be greater than zero"))
536 } else {
537 Ok(parsed)
538 }
539}
540
541fn parse_positive_usize_list(key: &str, value: &str) -> std::result::Result<Vec<usize>, String> {
542 let values = value
543 .split(',')
544 .map(str::trim)
545 .map(|value| parse_required_positive_usize(key, value))
546 .collect::<std::result::Result<Vec<_>, _>>()?;
547 if values.is_empty() {
548 Err(format!("{key}: must contain at least one width"))
549 } else {
550 Ok(values)
551 }
552}
553
554fn parse_profile_entrypoint(
555 key: &str,
556 value: &str,
557) -> std::result::Result<ProfileEntrypoint, String> {
558 ProfileEntrypoint::parse(value).ok_or_else(|| {
559 format!("{key}: expected one of run, serve, bench_serve, synthetic; got {value:?}")
560 })
561}
562
563fn parse_presence_bool(value: &str) -> std::result::Result<bool, String> {
564 if value.trim().is_empty() {
565 Ok(true)
566 } else {
567 parse_bool_env_value(value)
568 }
569}
570
571#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
573pub enum SchedulingPolicy {
574 FCFS,
576 Priority,
578 FairShare,
580 SJF,
582 RoundRobin,
584 ContinuousBatch,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct KvCacheConfig {
591 pub cache_type: KvCacheType,
593 #[serde(default)]
598 pub dtype: KvCacheDtype,
599 pub block_size: usize,
601 pub max_blocks: usize,
603 pub enable_compression: bool,
605 pub compression_ratio: f32,
607 pub enable_multi_level: bool,
609 pub swap_threshold: f32,
611 pub enable_prefix_caching: bool,
613 pub prefix_cache_size: usize,
615}
616
617impl Default for KvCacheConfig {
618 fn default() -> Self {
619 Self {
624 cache_type: KvCacheType::Contiguous,
625 dtype: KvCacheDtype::default(),
626 block_size: 16,
627 max_blocks: 2048,
628 enable_compression: false,
629 compression_ratio: 0.5,
630 enable_multi_level: true,
631 swap_threshold: 0.8,
632 enable_prefix_caching: true,
633 prefix_cache_size: 100,
634 }
635 }
636}
637
638#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
640pub enum KvCacheType {
641 Contiguous,
643 Paged,
645 Tree,
647}
648
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
655#[serde(rename_all = "lowercase")]
656pub enum KvCacheDtype {
657 #[default]
659 Fp16,
660 Bf16,
663 Int8,
668 Fp8,
670}
671
672impl KvCacheDtype {
673 pub fn parse(s: &str) -> Option<Self> {
675 match s.trim().to_ascii_lowercase().as_str() {
676 "fp16" | "f16" | "float16" => Some(Self::Fp16),
677 "bf16" | "bfloat16" => Some(Self::Bf16),
678 "int8" | "i8" => Some(Self::Int8),
679 "fp8" | "f8" | "f8e4m3" | "e4m3" => Some(Self::Fp8),
680 _ => None,
681 }
682 }
683
684 pub fn as_str(&self) -> &'static str {
686 match self {
687 Self::Fp16 => "fp16",
688 Self::Bf16 => "bf16",
689 Self::Int8 => "int8",
690 Self::Fp8 => "fp8",
691 }
692 }
693}
694
695#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct MemoryConfig {
698 pub pool_size: Option<usize>,
700 #[serde(default)]
704 pub usable_capacity_bytes: Option<usize>,
705 pub enable_pooling: bool,
707 pub alignment: usize,
709 pub enable_defragmentation: bool,
711 pub defragmentation_threshold: f32,
713 pub enable_memory_stats: bool,
715 pub pressure_warning_threshold: f32,
717 pub pressure_critical_threshold: f32,
719}
720
721#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
723pub struct MemoryCapacityBudget {
724 pub capacity_bytes: u64,
725 pub usable_capacity_bytes: u64,
726 pub reserve_bytes: u64,
727}
728
729impl MemoryConfig {
730 pub fn resolve_capacity_budget(
731 &self,
732 device_capacity_bytes: u64,
733 ) -> std::result::Result<MemoryCapacityBudget, String> {
734 if device_capacity_bytes == 0 {
735 return Err("runtime device memory capacity must be greater than zero".to_string());
736 }
737 let capacity_bytes = self
738 .pool_size
739 .map(|bytes| bytes as u64)
740 .unwrap_or(device_capacity_bytes)
741 .min(device_capacity_bytes);
742 if capacity_bytes == 0 {
743 return Err("runtime memory capacity must be greater than zero".to_string());
744 }
745
746 let usable_capacity_bytes = if let Some(bytes) = self.usable_capacity_bytes {
747 let bytes = bytes as u64;
748 if bytes == 0 || bytes > capacity_bytes {
749 return Err(format!(
750 "memory.usable_capacity_bytes must be in 1..={capacity_bytes}, got {bytes}"
751 ));
752 }
753 bytes
754 } else {
755 let critical = self.pressure_critical_threshold;
756 if !critical.is_finite() || critical <= 0.0 || critical > 1.0 {
757 return Err(format!(
758 "memory.pressure_critical_threshold must be in (0, 1], got {critical}"
759 ));
760 }
761 let threshold_bytes = ((capacity_bytes as f64) * f64::from(critical)).floor() as u64;
762 capacity_bytes.saturating_sub(
763 capacity_bytes
764 .saturating_sub(threshold_bytes)
765 .min(capacity_bytes - 1),
766 )
767 };
768 Ok(MemoryCapacityBudget {
769 capacity_bytes,
770 usable_capacity_bytes,
771 reserve_bytes: capacity_bytes - usable_capacity_bytes,
772 })
773 }
774}
775
776impl Default for MemoryConfig {
777 fn default() -> Self {
778 Self {
779 pool_size: None,
780 usable_capacity_bytes: None,
781 enable_pooling: true,
782 alignment: 256,
783 enable_defragmentation: false,
784 defragmentation_threshold: 0.7,
785 enable_memory_stats: true,
786 pressure_warning_threshold: 0.8,
787 pressure_critical_threshold: 0.95,
788 }
789 }
790}
791
792pub const MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH: usize = 32;
799
800pub const DEFAULT_MAXIMUM_AUTOMATIC_EXACT_DECODE_WIDTH: usize =
808 MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH;
809
810#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
812#[serde(rename_all = "snake_case")]
813pub enum ReusableExecutionPreparationMode {
814 #[default]
816 Auto,
817 Startup,
819 OnDemand,
821}
822
823impl ReusableExecutionPreparationMode {
824 pub const fn as_runtime_value(self) -> &'static str {
825 match self {
826 Self::Auto => "auto",
827 Self::Startup => "startup",
828 Self::OnDemand => "on_demand",
829 }
830 }
831
832 pub fn resolve(self, on_demand_supported: bool) -> Result<Self, String> {
833 match (self, on_demand_supported) {
834 (Self::Auto, true) => Ok(Self::OnDemand),
835 (Self::Auto, false) => Ok(Self::Startup),
836 (Self::OnDemand, false) => Err(
837 "runtime.reusable_execution_preparation=on_demand requires a runtime declaring on-demand reusable execution support".to_owned(),
838 ),
839 (mode, _) => Ok(mode),
840 }
841 }
842}
843
844impl std::str::FromStr for ReusableExecutionPreparationMode {
845 type Err = String;
846
847 fn from_str(value: &str) -> Result<Self, Self::Err> {
848 match value.trim() {
849 "auto" => Ok(Self::Auto),
850 "startup" => Ok(Self::Startup),
851 "on_demand" => Ok(Self::OnDemand),
852 _ => {
853 Err("reusable execution preparation must be auto, startup, or on_demand".to_owned())
854 }
855 }
856 }
857}
858
859#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
869#[serde(default)]
870pub struct ReusableExecutionCaptureConfig {
871 pub preparation: ReusableExecutionPreparationMode,
873 pub exact_decode_widths: Option<Vec<usize>>,
876 pub maximum_automatic_exact_decode_width: usize,
879}
880
881impl Default for ReusableExecutionCaptureConfig {
882 fn default() -> Self {
883 Self {
884 preparation: ReusableExecutionPreparationMode::default(),
885 exact_decode_widths: None,
886 maximum_automatic_exact_decode_width: DEFAULT_MAXIMUM_AUTOMATIC_EXACT_DECODE_WIDTH,
887 }
888 }
889}
890
891#[derive(Debug, Clone, Serialize, Deserialize)]
893pub struct BackendConfig {
894 pub backend_type: BackendType,
896 pub device: Device,
898 pub dtype: DataType,
900 pub enable_optimizations: bool,
902 pub optimization_level: u8,
904 pub enable_cuda_graphs: bool,
906 #[serde(default = "default_enable_reusable_execution")]
908 pub enable_reusable_execution: bool,
909 #[serde(default)]
913 pub reusable_execution_capture: ReusableExecutionCaptureConfig,
914 pub enable_kernel_fusion: bool,
916 pub backend_options: HashMap<String, serde_json::Value>,
918}
919
920impl Default for BackendConfig {
921 fn default() -> Self {
922 Self {
923 backend_type: BackendType::Candle,
924 device: Device::CPU,
925 dtype: DataType::FP16,
926 enable_optimizations: true,
927 optimization_level: 2,
928 enable_cuda_graphs: false,
929 enable_reusable_execution: default_enable_reusable_execution(),
930 reusable_execution_capture: ReusableExecutionCaptureConfig::default(),
931 enable_kernel_fusion: true,
932 backend_options: HashMap::new(),
933 }
934 }
935}
936
937const fn default_enable_reusable_execution() -> bool {
938 true
939}
940
941#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
943pub enum BackendType {
944 Candle,
946 OnnxRuntime,
948 TensorRT,
950 Custom,
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize)]
956pub struct TokenizerConfig {
957 pub tokenizer_type: TokenizerType,
959 pub tokenizer_path: Option<String>,
961 pub enable_fast: bool,
963 pub add_special_tokens: bool,
965 pub truncation: Option<TruncationConfig>,
967 pub padding: Option<PaddingConfig>,
969}
970
971impl Default for TokenizerConfig {
972 fn default() -> Self {
973 Self {
974 tokenizer_type: TokenizerType::BPE,
975 tokenizer_path: None,
976 enable_fast: true,
977 add_special_tokens: true,
978 truncation: None,
979 padding: None,
980 }
981 }
982}
983
984#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
986pub enum TokenizerType {
987 BPE,
989 WordPiece,
991 SentencePiece,
993 Tiktoken,
995 Custom,
997}
998
999#[derive(Debug, Clone, Serialize, Deserialize)]
1001pub struct TruncationConfig {
1002 pub max_length: usize,
1004 pub strategy: TruncationStrategy,
1006}
1007
1008#[derive(Debug, Clone, Serialize, Deserialize)]
1010pub enum TruncationStrategy {
1011 TruncateStart,
1013 TruncateEnd,
1015 TruncateBoth,
1017}
1018
1019#[derive(Debug, Clone, Serialize, Deserialize)]
1021pub struct PaddingConfig {
1022 pub strategy: PaddingStrategy,
1024 pub token_id: u32,
1026 pub target_length: Option<usize>,
1028}
1029
1030#[derive(Debug, Clone, Serialize, Deserialize)]
1032pub enum PaddingStrategy {
1033 None,
1035 MaxLength,
1037 FixedLength,
1039}
1040
1041#[derive(Debug, Clone, Serialize, Deserialize)]
1045pub struct SecurityConfig {
1046 pub enable_auth: bool,
1048 pub api_keys: Vec<String>,
1050 pub enable_rate_limiting: bool,
1052 pub rate_limit_rpm: u32,
1054 pub enable_content_filter: bool,
1056 pub max_prompt_length: usize,
1058 pub enable_prompt_validation: bool,
1060 pub allowed_extensions: Vec<String>,
1062}
1063
1064impl Default for SecurityConfig {
1065 fn default() -> Self {
1066 Self {
1067 enable_auth: false,
1068 api_keys: vec![],
1069 enable_rate_limiting: true,
1070 rate_limit_rpm: 60,
1071 enable_content_filter: false,
1072 max_prompt_length: 32768,
1073 enable_prompt_validation: true,
1074 allowed_extensions: vec!["txt".to_string(), "json".to_string()],
1075 }
1076 }
1077}
1078
1079#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1080pub struct SamplingConfig {
1081 pub default_params: SamplingParams,
1082 pub presets: SamplingPresets,
1083 pub enable_custom_processors: bool,
1084}
1085
1086#[derive(Debug, Clone, Serialize, Deserialize)]
1087pub struct MonitoringConfig {
1088 pub enable_metrics: bool,
1089 pub enable_tracing: bool,
1090 pub export_interval: Duration,
1091}
1092
1093impl Default for MonitoringConfig {
1094 fn default() -> Self {
1095 Self {
1096 enable_metrics: true,
1097 enable_tracing: true,
1098 export_interval: Duration::from_secs(5),
1099 }
1100 }
1101}
1102
1103#[derive(Debug, Clone, Serialize, Deserialize)]
1104pub struct BatchConfig {
1105 pub max_batch_size: usize,
1106 pub max_wait_ms: u64,
1107 pub enable_dynamic: bool,
1108 pub enable_continuous: bool,
1109 #[serde(default = "BatchConfig::default_max_num_batched_tokens")]
1116 pub max_num_batched_tokens: usize,
1117}
1118
1119impl BatchConfig {
1120 fn default_max_num_batched_tokens() -> usize {
1121 2048
1122 }
1123}
1124
1125impl Default for BatchConfig {
1126 fn default() -> Self {
1127 Self {
1128 max_batch_size: 32,
1129 max_wait_ms: 8,
1130 enable_dynamic: true,
1131 enable_continuous: false,
1132 max_num_batched_tokens: Self::default_max_num_batched_tokens(),
1133 }
1134 }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use super::*;
1140
1141 #[test]
1142 fn prefix_state_cache_canonical_boolean_overrides_and_missing_key_preserves() {
1143 let mut config = EngineConfig::default();
1144 for (value, expected) in [("1", true), ("0", false), ("true", true), ("false", false)] {
1145 config
1146 .apply_runtime_config_snapshot(&RuntimeConfigSnapshot::from_env_vars([(
1147 "FERRUM_PREFIX_CACHE",
1148 value,
1149 )]))
1150 .unwrap();
1151 assert_eq!(config.runtime.prefix_state_cache_enabled, expected);
1152 assert!(!config.runtime.prefix_cache_enabled);
1153 config
1154 .apply_runtime_config_snapshot(&RuntimeConfigSnapshot::default())
1155 .unwrap();
1156 assert_eq!(config.runtime.prefix_state_cache_enabled, expected);
1157 }
1158 assert!(config
1159 .apply_runtime_config_snapshot(&RuntimeConfigSnapshot::from_env_vars([(
1160 "FERRUM_PREFIX_CACHE",
1161 "invalid"
1162 ),]))
1163 .is_err());
1164 }
1165
1166 #[test]
1167 fn prefix_state_cache_is_independent_of_whole_prompt_flag_and_defaults_on_old_wire() {
1168 let mut config = EngineConfig::default();
1169 config
1170 .apply_runtime_config_snapshot(&RuntimeConfigSnapshot::from_env_vars([(
1171 "FERRUM_WHOLE_PROMPT_PREFIX_CACHE",
1172 "1",
1173 )]))
1174 .unwrap();
1175 assert!(config.runtime.prefix_cache_enabled);
1176 assert!(!config.runtime.prefix_state_cache_enabled);
1177 config.runtime.prefix_state_cache_enabled = true;
1178 config
1179 .apply_runtime_config_snapshot(&RuntimeConfigSnapshot::from_env_vars([(
1180 "FERRUM_PREFIX_CACHE",
1181 "false",
1182 )]))
1183 .unwrap();
1184 assert!(config.runtime.prefix_cache_enabled);
1185 assert!(!config.runtime.prefix_state_cache_enabled);
1186 let mut old = serde_json::to_value(&config.runtime).unwrap();
1187 old.as_object_mut()
1188 .unwrap()
1189 .remove("prefix_state_cache_enabled");
1190 let restored: RuntimeKnobs = serde_json::from_value(old).unwrap();
1191 assert!(!restored.prefix_state_cache_enabled);
1192 assert!(restored.prefix_cache_enabled);
1193 }
1194
1195 #[test]
1196 fn scheduler_keeps_immediate_fit_default_until_full_input_policy_is_gated() {
1197 assert_eq!(
1198 SchedulerConfig::default().sequence_fit_policy,
1199 SequenceFitPolicy::ImmediateOnly
1200 );
1201 }
1202
1203 #[test]
1204 fn sequence_fit_policy_uses_canonical_product_values() {
1205 assert_eq!(
1206 serde_json::to_string(&SequenceFitPolicy::FullInputMustFit).unwrap(),
1207 "\"full-input-must-fit\""
1208 );
1209 assert_eq!(
1210 serde_json::from_str::<SequenceFitPolicy>("\"immediate-only\"").unwrap(),
1211 SequenceFitPolicy::ImmediateOnly
1212 );
1213 }
1214
1215 #[test]
1216 fn diagnostic_fault_uses_one_canonical_product_value() {
1217 assert_eq!(
1218 VNextDiagnosticFault::parse_runtime_value("prefill_resource_after_submit_once")
1219 .unwrap(),
1220 VNextDiagnosticFault::PrefillResourceAfterSubmitOnce
1221 );
1222 assert_eq!(
1223 VNextDiagnosticFault::PrefillResourceAfterSubmitOnce.as_runtime_value(),
1224 "prefill-resource-after-submit-once"
1225 );
1226 assert!(VNextDiagnosticFault::parse_runtime_value("resource-failure").is_err());
1227 }
1228
1229 #[test]
1230 fn engine_config_applies_typed_diagnostic_fault() {
1231 let mut config = EngineConfig::default();
1232 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1233 "FERRUM_VNEXT_DIAGNOSTIC_FAULT",
1234 "prefill-resource-after-submit-once",
1235 )]);
1236
1237 config
1238 .apply_runtime_config_snapshot(&snapshot)
1239 .expect("runtime config should apply");
1240
1241 assert_eq!(
1242 config.runtime.vnext_diagnostic_fault,
1243 Some(VNextDiagnosticFault::PrefillResourceAfterSubmitOnce)
1244 );
1245 }
1246
1247 #[test]
1248 fn engine_config_rejects_unknown_diagnostic_fault() {
1249 let mut config = EngineConfig::default();
1250 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1251 "FERRUM_VNEXT_DIAGNOSTIC_FAULT",
1252 "resource-failure",
1253 )]);
1254
1255 let error = config
1256 .apply_runtime_config_snapshot(&snapshot)
1257 .expect_err("unknown diagnostic fault must fail closed");
1258
1259 assert!(error.contains("FERRUM_VNEXT_DIAGNOSTIC_FAULT"));
1260 }
1261
1262 #[test]
1263 fn scheduler_deserialization_without_fit_policy_keeps_legacy_default() {
1264 let mut serialized = serde_json::to_value(SchedulerConfig::default()).unwrap();
1265 serialized
1266 .as_object_mut()
1267 .unwrap()
1268 .remove("sequence_fit_policy");
1269
1270 let scheduler: SchedulerConfig = serde_json::from_value(serialized).unwrap();
1271
1272 assert_eq!(
1273 scheduler.sequence_fit_policy,
1274 SequenceFitPolicy::ImmediateOnly
1275 );
1276 }
1277
1278 #[test]
1279 fn checkpoint_capture_deserialization_keeps_decode_capture_disabled_by_default() {
1280 let capture: VNextCheckpointCaptureConfig = serde_json::from_value(serde_json::json!({
1281 "output_dir": "capture",
1282 "value_ids": ["value.output.logits"],
1283 "maximum_prefill_waves": 1
1284 }))
1285 .unwrap();
1286
1287 assert_eq!(capture.maximum_decode_waves, 0);
1288 assert!(!capture.capture_product_output);
1289 }
1290
1291 #[test]
1292 fn engine_config_applies_typed_sequence_fit_policy() {
1293 let mut config = EngineConfig::default();
1294 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1295 "FERRUM_SEQUENCE_FIT_POLICY",
1296 "full-input-must-fit",
1297 )]);
1298
1299 config
1300 .apply_runtime_config_snapshot(&snapshot)
1301 .expect("runtime config should apply");
1302
1303 assert_eq!(
1304 config.scheduler.sequence_fit_policy,
1305 SequenceFitPolicy::FullInputMustFit
1306 );
1307 }
1308
1309 #[test]
1310 fn engine_config_rejects_unknown_sequence_fit_policy() {
1311 let mut config = EngineConfig::default();
1312 let snapshot = RuntimeConfigSnapshot::from_env_vars([(
1313 "FERRUM_SEQUENCE_FIT_POLICY",
1314 "reserve-everything",
1315 )]);
1316
1317 let error = config
1318 .apply_runtime_config_snapshot(&snapshot)
1319 .expect_err("unknown fit policy must fail closed");
1320
1321 assert!(error.contains("FERRUM_SEQUENCE_FIT_POLICY"));
1322 }
1323
1324 #[test]
1325 fn engine_config_applies_recurrent_state_max_slots_runtime_key() {
1326 let mut config = EngineConfig::default();
1327 let snapshot =
1328 RuntimeConfigSnapshot::from_env_vars([("FERRUM_RECURRENT_STATE_MAX_SLOTS", "16")]);
1329
1330 config
1331 .apply_runtime_config_snapshot(&snapshot)
1332 .expect("runtime config should apply");
1333
1334 assert_eq!(config.runtime.recurrent_state_max_slots, Some(16));
1335 }
1336
1337 #[test]
1338 fn engine_config_does_not_apply_removed_qwen35_slot_alias() {
1339 let mut config = EngineConfig::default();
1340 let snapshot =
1341 RuntimeConfigSnapshot::from_env_vars([("FERRUM_QWEN35_LINEAR_STATE_MAX_SLOTS", "16")]);
1342
1343 config
1344 .apply_runtime_config_snapshot(&snapshot)
1345 .expect("runtime config should apply");
1346
1347 assert_eq!(config.runtime.recurrent_state_max_slots, None);
1348 }
1349
1350 #[test]
1351 fn engine_config_uses_generic_recurrent_state_slots_when_removed_alias_is_present() {
1352 let mut config = EngineConfig::default();
1353 let snapshot = RuntimeConfigSnapshot::from_env_vars([
1354 ("FERRUM_RECURRENT_STATE_MAX_SLOTS", "8"),
1355 ("FERRUM_QWEN35_LINEAR_STATE_MAX_SLOTS", "16"),
1356 ]);
1357
1358 config
1359 .apply_runtime_config_snapshot(&snapshot)
1360 .expect("runtime config should apply");
1361
1362 assert_eq!(config.runtime.recurrent_state_max_slots, Some(8));
1363 }
1364
1365 #[test]
1366 fn engine_config_applies_profile_entrypoint_runtime_key() {
1367 let mut config = EngineConfig::default();
1368 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_ENTRYPOINT", "run")]);
1369
1370 config
1371 .apply_runtime_config_snapshot(&snapshot)
1372 .expect("runtime config should apply");
1373
1374 assert_eq!(
1375 config.runtime.profile_entrypoint,
1376 Some(ProfileEntrypoint::Run)
1377 );
1378 }
1379
1380 #[test]
1381 fn engine_config_applies_typed_profile_detail_runtime_key() {
1382 let mut config = EngineConfig::default();
1383 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "full")]);
1384
1385 config
1386 .apply_runtime_config_snapshot(&snapshot)
1387 .expect("runtime config should apply");
1388
1389 assert_eq!(
1390 config.runtime.profile_detail,
1391 ObservabilityProfileDetail::Full
1392 );
1393 }
1394
1395 #[test]
1396 fn engine_config_applies_typed_latency_profile_detail_runtime_key() {
1397 let mut config = EngineConfig::default();
1398 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "latency")]);
1399
1400 config
1401 .apply_runtime_config_snapshot(&snapshot)
1402 .expect("latency profile detail should apply");
1403
1404 assert_eq!(
1405 config.runtime.profile_detail,
1406 ObservabilityProfileDetail::Latency
1407 );
1408 }
1409
1410 #[test]
1411 fn engine_config_applies_typed_replay_profile_detail_runtime_key() {
1412 let mut config = EngineConfig::default();
1413 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "replay")]);
1414
1415 config
1416 .apply_runtime_config_snapshot(&snapshot)
1417 .expect("runtime config should apply");
1418
1419 assert_eq!(
1420 config.runtime.profile_detail,
1421 ObservabilityProfileDetail::Replay
1422 );
1423 }
1424
1425 #[test]
1426 fn engine_config_applies_typed_verification_profile_detail_runtime_key() {
1427 let mut config = EngineConfig::default();
1428 let snapshot = RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "verify")]);
1429
1430 config
1431 .apply_runtime_config_snapshot(&snapshot)
1432 .expect("runtime config should apply");
1433
1434 assert_eq!(
1435 config.runtime.profile_detail,
1436 ObservabilityProfileDetail::Verify
1437 );
1438 }
1439
1440 #[test]
1441 fn engine_config_applies_typed_profile_jsonl_runtime_key() {
1442 let mut config = EngineConfig::default();
1443 let snapshot =
1444 RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_JSONL", "/tmp/profile.jsonl")]);
1445
1446 config
1447 .apply_runtime_config_snapshot(&snapshot)
1448 .expect("runtime config should apply");
1449
1450 assert_eq!(
1451 config.runtime.profile_jsonl.as_deref(),
1452 Some(std::path::Path::new("/tmp/profile.jsonl"))
1453 );
1454 }
1455
1456 #[test]
1457 fn engine_config_rejects_unknown_profile_detail() {
1458 let mut config = EngineConfig::default();
1459 let snapshot =
1460 RuntimeConfigSnapshot::from_env_vars([("FERRUM_PROFILE_DETAIL", "everything")]);
1461
1462 let error = config
1463 .apply_runtime_config_snapshot(&snapshot)
1464 .expect_err("unknown profile detail must fail closed");
1465
1466 assert!(error.contains("FERRUM_PROFILE_DETAIL"));
1467 }
1468}