1use crate::config::CliConfig;
4use crate::runtime_env::runtime_snapshot_value;
5use clap::Args;
6use colored::*;
7use ferrum_bench_core::{ProfileMetadata, ProfileSinkConfig};
8use ferrum_models::source::ModelFormat;
9use ferrum_server::{AxumServer, HttpServer, ServedModelKind, ServedModelRegistry, ServerConfig};
10use ferrum_types::{
11 CompiledKernelFeatures, CompiledNativeOperatorArtifact, FerrumConfigBuilder, FerrumError,
12 HardwareCapabilities, ModelCapabilities, ResolvedFerrumConfig, Result, RuntimeConfigEntry,
13 RuntimeConfigSnapshot, RuntimeConfigSource, WorkloadProfile, M3_QWEN3_30B_A3B_INT4_PRESET,
14 QWEN25_72B_GPTQ_INT4_2X4090_LAYER_SPLIT_PRESET,
15};
16use std::collections::HashSet;
17use std::path::Path;
18use std::path::PathBuf;
19use std::process::Command;
20use std::sync::Arc;
21use std::time::Duration;
22use tokio::signal;
23
24#[derive(Args)]
25pub struct ServeCommand {
26 #[arg(value_name = "MODEL")]
28 pub model: Option<String>,
29
30 #[arg(
32 short = 'm',
33 long = "model",
34 value_name = "MODEL",
35 conflicts_with = "model"
36 )]
37 pub model_option: Option<String>,
38
39 #[command(flatten)]
40 pub product_sources: crate::source_resolver::ProductSourceArgs,
41
42 #[arg(
45 long = "served-model-name",
46 value_name = "NAME",
47 value_delimiter = ',',
48 action = clap::ArgAction::Append
49 )]
50 pub served_model_name: Vec<String>,
51
52 #[arg(long, conflicts_with = "disable_thinking")]
55 pub enable_thinking: bool,
56
57 #[arg(long, conflicts_with = "enable_thinking")]
60 pub disable_thinking: bool,
61
62 #[arg(long)]
64 pub host: Option<String>,
65
66 #[arg(short, long)]
68 pub port: Option<u16>,
69
70 #[arg(long, default_value = "2")]
72 pub tts_slots: usize,
73
74 #[arg(long, default_value = "auto")]
76 pub backend: String,
77
78 #[arg(long, value_name = "IDS")]
81 pub gpu_devices: Option<String>,
82
83 #[arg(long, value_enum)]
85 pub layer_split_pipeline_mode: Option<crate::layer_split_pipeline::LayerSplitPipelineModeArg>,
86
87 #[arg(long, value_name = "MODEL")]
91 pub spec_draft: Option<String>,
92
93 #[arg(long, default_value = "4")]
96 pub spec_tokens: usize,
97
98 #[arg(long, default_value = "0.9")]
103 pub gpu_memory_utilization: f32,
104
105 #[arg(long, value_name = "BYTES")]
108 pub runtime_memory_budget_bytes: Option<std::num::NonZeroUsize>,
109
110 #[arg(long, value_name = "N")]
112 pub max_model_len: Option<usize>,
113
114 #[arg(long, value_name = "N")]
116 pub max_num_seqs: Option<usize>,
117
118 #[arg(long, value_name = "N")]
120 pub max_num_batched_tokens: Option<usize>,
121
122 #[arg(long, value_enum)]
124 pub sequence_fit_policy: Option<crate::commands::SequenceFitPolicyArg>,
125
126 #[arg(long, value_name = "N")]
128 pub scheduler_prefill_first_until_active: Option<usize>,
129
130 #[arg(long, value_name = "N")]
132 pub scheduler_prefill_step_chunk: Option<usize>,
133
134 #[arg(long, value_name = "N")]
136 pub scheduler_active_decode_prefill_chunk: Option<usize>,
137
138 #[arg(
140 long,
141 conflicts_with_all = ["no_enable_prefix_caching", "disable_prefix_cache"]
142 )]
143 pub enable_prefix_caching: bool,
144
145 #[arg(long, conflicts_with = "enable_prefix_cache")]
147 pub no_enable_prefix_caching: bool,
148
149 #[arg(
151 long,
152 conflicts_with_all = ["no_enable_prefix_caching", "disable_prefix_cache"]
153 )]
154 pub enable_prefix_cache: bool,
155
156 #[arg(long, conflicts_with_all = ["enable_prefix_caching", "enable_prefix_cache"])]
158 pub disable_prefix_cache: bool,
159
160 #[arg(long, value_name = "MODE", value_parser = ["off", "memory"])]
162 pub session_cache: Option<String>,
163
164 #[arg(long, value_name = "N")]
166 pub session_cache_max_entries: Option<usize>,
167
168 #[arg(long, value_name = "N")]
170 pub session_cache_max_tokens: Option<usize>,
171
172 #[arg(long, value_name = "DTYPE")]
177 pub kv_dtype: Option<String>,
178
179 #[arg(long, value_name = "N")]
181 pub kv_capacity: Option<usize>,
182
183 #[arg(long, value_name = "N")]
185 pub kv_max_blocks: Option<usize>,
186
187 #[arg(long, conflicts_with = "disable_greedy_argmax")]
189 pub greedy_argmax: bool,
190
191 #[arg(long, conflicts_with = "greedy_argmax")]
193 pub disable_greedy_argmax: bool,
194
195 #[arg(long, conflicts_with = "disable_batched_graph")]
197 pub batched_graph: bool,
198
199 #[arg(long, conflicts_with = "batched_graph")]
201 pub disable_batched_graph: bool,
202
203 #[arg(long, conflicts_with = "disable_reusable_execution")]
205 pub reusable_execution: bool,
206
207 #[arg(long, conflicts_with = "reusable_execution")]
209 pub disable_reusable_execution: bool,
210
211 #[arg(long, conflicts_with = "disable_unified_graph")]
213 pub unified_graph: bool,
214
215 #[arg(long, conflicts_with = "unified_graph")]
217 pub disable_unified_graph: bool,
218
219 #[arg(long, conflicts_with = "disable_unified_graph_layers_only")]
221 pub unified_graph_layers_only: bool,
222
223 #[arg(long, conflicts_with = "unified_graph_layers_only")]
225 pub disable_unified_graph_layers_only: bool,
226
227 #[arg(long, conflicts_with = "disable_unified_graph_lm_head_eager")]
229 pub unified_graph_lm_head_eager: bool,
230
231 #[arg(long, conflicts_with = "unified_graph_lm_head_eager")]
233 pub disable_unified_graph_lm_head_eager: bool,
234
235 #[arg(long, value_name = "PRESET")]
238 pub runtime_preset: Option<String>,
239
240 #[arg(long, value_name = "PATH")]
242 pub effective_config_json: Option<PathBuf>,
243
244 #[arg(long, value_name = "PATH")]
246 pub decision_trace_jsonl: Option<PathBuf>,
247
248 #[arg(long, value_name = "DIR")]
250 pub observability_vertical_slice_out: Option<PathBuf>,
251
252 #[command(flatten)]
253 pub vnext_checkpoint: crate::commands::vnext_checkpoint::VNextCheckpointArgs,
254
255 #[arg(long, value_name = "PATH")]
257 pub profile_jsonl: Option<PathBuf>,
258
259 #[arg(long, value_enum, default_value_t = crate::observability_product::ProfileDetailArg::Off)]
261 pub profile_detail: crate::observability_product::ProfileDetailArg,
262
263 #[arg(long, value_enum)]
265 pub vnext_diagnostic_fault: Option<crate::commands::VNextDiagnosticFaultArg>,
266
267 #[arg(long, value_name = "PATH")]
269 pub memory_profile_jsonl: Option<PathBuf>,
270
271 #[arg(long, value_name = "PATH")]
273 pub scheduler_trace_jsonl: Option<PathBuf>,
274
275 #[arg(long, value_name = "DIR")]
277 pub request_dump_dir: Option<PathBuf>,
278
279 #[arg(long, default_value_t = crate::observability_product::default_profile_sample_rate())]
281 pub profile_sample_rate: f64,
282
283 #[arg(long, value_name = "SHA")]
285 pub profile_commit_sha: Option<String>,
286
287 #[arg(long, value_name = "SHA256")]
289 pub profile_env_hash: Option<String>,
290
291 #[arg(long, value_name = "MODEL")]
293 pub profile_model: Option<String>,
294
295 #[arg(long, value_name = "N")]
297 pub profile_concurrency: Option<u32>,
298
299 #[arg(long, value_name = "JSON")]
301 pub profile_runtime_flags_json: Option<String>,
302
303 #[arg(long = "lora", value_name = "NAME=PATH")]
305 pub lora: Vec<String>,
306
307 #[arg(long, value_name = "TEMPLATE", default_value = "<base>:<name>")]
309 pub lora_model_id_template: String,
310}
311
312pub async fn execute(cmd: ServeCommand, config: CliConfig) -> Result<()> {
313 let ServeCommand {
314 model,
315 model_option,
316 product_sources,
317 served_model_name,
318 enable_thinking,
319 disable_thinking,
320 host,
321 port,
322 tts_slots,
323 backend,
324 gpu_devices,
325 layer_split_pipeline_mode,
326 spec_draft,
327 spec_tokens,
328 gpu_memory_utilization,
329 runtime_memory_budget_bytes,
330 max_model_len,
331 max_num_seqs,
332 max_num_batched_tokens,
333 sequence_fit_policy,
334 scheduler_prefill_first_until_active,
335 scheduler_prefill_step_chunk,
336 scheduler_active_decode_prefill_chunk,
337 enable_prefix_caching,
338 no_enable_prefix_caching,
339 enable_prefix_cache,
340 disable_prefix_cache,
341 session_cache,
342 session_cache_max_entries,
343 session_cache_max_tokens,
344 kv_dtype,
345 kv_capacity,
346 kv_max_blocks,
347 greedy_argmax,
348 disable_greedy_argmax,
349 batched_graph,
350 disable_batched_graph,
351 reusable_execution,
352 disable_reusable_execution,
353 unified_graph,
354 disable_unified_graph,
355 unified_graph_layers_only,
356 disable_unified_graph_layers_only,
357 unified_graph_lm_head_eager,
358 disable_unified_graph_lm_head_eager,
359 runtime_preset,
360 effective_config_json,
361 decision_trace_jsonl,
362 observability_vertical_slice_out,
363 vnext_checkpoint,
364 profile_jsonl,
365 profile_detail,
366 vnext_diagnostic_fault,
367 memory_profile_jsonl,
368 scheduler_trace_jsonl,
369 request_dump_dir,
370 profile_sample_rate,
371 profile_commit_sha,
372 profile_env_hash,
373 profile_model,
374 profile_concurrency,
375 profile_runtime_flags_json,
376 lora,
377 lora_model_id_template,
378 } = cmd;
379
380 let default_enable_thinking = if enable_thinking {
381 Some(true)
382 } else if disable_thinking {
383 Some(false)
384 } else {
385 None
386 };
387
388 let lora_specs = parse_lora_specs(&lora)?;
391
392 if let Some(out_dir) = observability_vertical_slice_out.as_ref() {
393 crate::observability_vertical_slice::write_observability_vertical_slice(
394 ferrum_types::ProfileEntrypoint::Serve,
395 out_dir,
396 )?;
397 println!(
398 "OBSERVABILITY VERTICAL SLICE ARTIFACT: {}",
399 out_dir.display()
400 );
401 return Ok(());
402 }
403
404 let model_name = model
406 .or(model_option)
407 .or_else(|| {
408 config
409 .models
410 .default_model
411 .clone()
412 .filter(|model| !model.trim().is_empty())
413 })
414 .ok_or_else(|| {
415 FerrumError::config(crate::source_resolver::first_success_model_help(
416 "serve --model",
417 ))
418 })?;
419 let serve_start = std::time::Instant::now();
420 let product_observability = crate::observability_product::ProductObservabilityConfig::new(
421 ferrum_types::ProfileEntrypoint::Serve,
422 &model_name,
423 profile_jsonl.as_ref(),
424 profile_detail,
425 memory_profile_jsonl.as_ref(),
426 scheduler_trace_jsonl.as_ref(),
427 request_dump_dir.as_ref(),
428 profile_sample_rate,
429 );
430 let memory_sampler = crate::memory_profile::ProcessMemorySampler;
431 let product_memory_enabled = product_observability.enabled();
432 let process_start_sample = product_memory_enabled
433 .then(|| memory_sampler.sample())
434 .flatten();
435 let process_start_memory = process_start_sample
436 .clone()
437 .map(crate::memory_profile::ProcessMemoryObservation::from_sample);
438 if product_observability.synthetic_no_weight_enabled() {
439 let written = crate::observability_product::write_synthetic_product_observability(
440 &product_observability,
441 )?;
442 println!(
443 "OBSERVABILITY PRODUCT ARTIFACTS: {}",
444 written
445 .iter()
446 .map(|path| path.display().to_string())
447 .collect::<Vec<_>>()
448 .join(",")
449 );
450 return Ok(());
451 }
452
453 let mut device = super::run::select_device(&backend)?;
457 let mut gpu_selection =
458 crate::gpu_devices::resolve_cuda_gpu_devices(gpu_devices.as_deref(), &device)?;
459 if let Some(selection) = &gpu_selection {
460 device = selection.primary_device();
461 println!(
462 "{} {} ({})",
463 "CUDA GPUs:".dimmed(),
464 selection.selected_csv(),
465 selection.selected_distributed_strategy
466 );
467 }
468 let backend_initialized_sample = product_memory_enabled
469 .then(|| memory_sampler.sample())
470 .flatten();
471 let backend_initialized_memory = serve_process_memory_observation_between(
472 process_start_sample.clone(),
473 backend_initialized_sample.clone(),
474 );
475
476 print_banner();
478
479 let cache_dir = crate::source_resolver::hf_cache_dir(&config);
483 let resolved = crate::source_resolver::resolve_model_source_with_product_sources(
484 &model_name,
485 &cache_dir,
486 crate::source_resolver::DownloadPolicy::AutoDownload,
487 None,
488 &product_sources,
489 )
490 .await?;
491 let product_input = resolved.into_product_engine_input();
492 let requested_model = product_input.requested_model.clone();
493 let model_id = product_input.public_model_id.clone();
494 let source = product_input.source;
495 let product_engine_config = product_input.engine_config;
496 let model_sources = product_input.model_sources;
497 let prepared_model = model_sources
498 .as_ref()
499 .map(crate::source_resolver::prepare_registered_product_model)
500 .transpose()?
501 .flatten();
502 let vnext_plan_owns_context_capacity = prepared_model.is_some();
503 let model_chat_template = match prepared_model.as_deref() {
504 Some(prepared) => Some(crate::source_resolver::load_prepared_product_chat_template(
505 prepared,
506 )?),
507 None => match model_sources.as_deref() {
508 Some(sources) => crate::source_resolver::load_product_chat_template(sources),
509 None => crate::source_resolver::load_model_chat_template(&source.local_path),
510 },
511 };
512 let product_source_identity = prepared_model
513 .as_deref()
514 .map(|prepared| {
515 crate::source_resolver::prepared_product_source_identity(
516 prepared,
517 &requested_model,
518 &model_id,
519 model_chat_template.as_ref(),
520 )
521 })
522 .transpose()?;
523 let requested_public_model_name = matches!(
524 product_engine_config.model.source.as_ref(),
525 Some(ferrum_types::ModelSource::HuggingFace { .. })
526 )
527 .then_some(model_name.as_str());
528 let served_model_names =
529 effective_served_model_names(&model_id, requested_public_model_name, served_model_name)?;
530 let primary_served_model_name = served_model_names
531 .first()
532 .expect("effective served model names are non-empty")
533 .clone();
534 let gguf_path = (source.format == ModelFormat::GGUF).then(|| source.local_path.clone());
535 println!("{} {}", "Model:".dimmed(), model_id.cyan());
536 println!("{} {}", "Path:".dimmed(), source.local_path.display());
537
538 let config_runtime_entries = config.runtime.runtime_config_entries();
539 let configured_runtime_preset = runtime_preset
540 .as_deref()
541 .map(|preset| (preset, RuntimeConfigSource::Cli))
542 .or_else(|| {
543 config
544 .runtime
545 .preset
546 .as_deref()
547 .map(|preset| (preset, RuntimeConfigSource::ConfigFile))
548 });
549 let selected_runtime_preset_name =
550 configured_runtime_preset.map(|(preset, _source)| preset.to_string());
551 let preset_runtime_entries = match configured_runtime_preset {
552 Some((preset, source)) => runtime_preset_entries(preset, source)?,
553 None => Vec::new(),
554 };
555 let mut non_env_runtime_entries = preset_runtime_entries;
556 non_env_runtime_entries.extend(config_runtime_entries);
557 let mut non_env_runtime_entries =
558 RuntimeConfigSnapshot::from_entries(non_env_runtime_entries).entries;
559 let mut materialized_runtime_keys =
560 crate::runtime_env::materialize_runtime_env_defaults(&non_env_runtime_entries);
561
562 let startup_lora_adapters = if lora_specs.is_empty() {
563 Vec::new()
564 } else {
565 ferrum_models::load_startup_lora_adapters(
566 &primary_served_model_name,
567 Some(&lora_model_id_template),
568 &lora_specs,
569 )?
570 };
571 for adapter in &startup_lora_adapters {
572 println!(
573 "{} {} -> {} ({})",
574 "LoRA:".dimmed(),
575 adapter.name.cyan(),
576 adapter.public_model_id.cyan(),
577 adapter.path.display()
578 );
579 }
580
581 let host = host.unwrap_or_else(|| config.server.host.clone());
582 let port = port.unwrap_or(config.server.port);
583 let kv_runtime_snapshot = RuntimeConfigSnapshot::capture_current();
584 let env_kv_dtype = runtime_snapshot_value(&kv_runtime_snapshot, "FERRUM_KV_DTYPE");
585 let effective_kv_dtype = resolve_effective_kv_dtype(
586 kv_dtype.as_deref(),
587 env_kv_dtype,
588 config.runtime.kv_dtype.as_deref(),
589 );
590
591 let engine_model_path = source.local_path.to_string_lossy().to_string();
592
593 let mut engine_spec_draft_path = None;
597 if let Some(ref draft_name) = spec_draft {
598 if gguf_path.is_some() {
599 return Err(ferrum_types::FerrumError::unsupported(
600 "Speculative decoding is not yet wired through the GGUF path",
601 ));
602 }
603 let draft_id = crate::source_resolver::resolve_model_alias(draft_name);
604 println!("{} {}", "Draft model:".dimmed(), draft_id.cyan());
605 let cache_dir = crate::source_resolver::hf_cache_dir(&config);
606 let draft_source = crate::source_resolver::find_cached_model(&cache_dir, &draft_id)
607 .ok_or_else(|| {
608 eprintln!(
609 "{} Draft model '{}' not in HF cache. Run: ferrum pull {}",
610 "Error:".red().bold(),
611 draft_id,
612 draft_name
613 );
614 ferrum_types::FerrumError::model("Draft model not found")
615 })?;
616 engine_spec_draft_path = Some(draft_source.local_path.to_string_lossy().to_string());
617 println!(
618 "{} {} tokens / verify pass",
619 "Speculative decoding:".dimmed(),
620 spec_tokens
621 );
622 }
623
624 println!("{} {:?}", "Device:".dimmed(), device);
625 let serve_profile_entries = crate::source_resolver::serve_profile_runtime_entries(
626 &source.local_path,
627 &device,
628 vnext_plan_owns_context_capacity,
629 &RuntimeConfigSnapshot::capture_current(),
630 RuntimeConfigSource::Default,
631 );
632 if !serve_profile_entries.is_empty() {
633 non_env_runtime_entries.extend(serve_profile_entries.clone());
634 non_env_runtime_entries =
635 RuntimeConfigSnapshot::from_entries(non_env_runtime_entries).entries;
636 materialized_runtime_keys.extend(crate::runtime_env::materialize_runtime_env_defaults(
637 &serve_profile_entries,
638 ));
639 materialized_runtime_keys.sort();
640 materialized_runtime_keys.dedup();
641 }
642 let metal_moe_entries = crate::source_resolver::metal_gguf_moe_correctness_entries(
643 &source.local_path,
644 &device,
645 &RuntimeConfigSnapshot::capture_current(),
646 RuntimeConfigSource::Default,
647 );
648 if !metal_moe_entries.is_empty() {
649 non_env_runtime_entries.extend(metal_moe_entries.clone());
650 non_env_runtime_entries =
651 RuntimeConfigSnapshot::from_entries(non_env_runtime_entries).entries;
652 materialized_runtime_keys.extend(crate::runtime_env::materialize_runtime_env_defaults(
653 &metal_moe_entries,
654 ));
655 materialized_runtime_keys.sort();
656 materialized_runtime_keys.dedup();
657 }
658
659 println!();
664 let model_definition: Option<ferrum_models::ModelDefinition> = if prepared_model.is_some() {
665 None
666 } else if let Some(sources) = model_sources.as_deref() {
667 let mut config_manager = ferrum_models::ConfigManager::new();
668 Some(config_manager.load_from_bytes(sources.config_json())?)
669 } else if gguf_path.is_some() {
670 None
671 } else {
672 let mut config_manager = ferrum_models::ConfigManager::new();
673 Some(config_manager.load_from_path(&source.local_path).await?)
674 };
675 let arch_for_dispatch = model_definition
676 .as_ref()
677 .map(|model_def| model_def.architecture);
678 let model_layer_count = if let Some(prepared) = prepared_model.as_ref() {
683 Some(prepared.descriptor().layer_count())
684 } else if let Some(definition) = model_definition.as_ref() {
685 Some(definition.num_hidden_layers)
686 } else if let (Some(selection), Some(p)) = (gpu_selection.as_ref(), gguf_path.as_ref()) {
687 if selection.selected_layer_split_plan.is_some() {
688 Some(ferrum_models::gguf_config::gguf_num_layers(p)?)
689 } else {
690 None
691 }
692 } else {
693 None
694 };
695 if let (Some(selection), Some(layer_count)) = (gpu_selection.as_mut(), model_layer_count) {
696 if selection.apply_model_layer_count(layer_count)? {
697 if let Some(plan) = selection.selected_layer_split_plan.as_deref() {
698 println!("{}", format!("CUDA layer split plan: {plan}").dimmed());
699 }
700 }
701 }
702 let mut selected_runtime_preset_name = selected_runtime_preset_name;
703 if selected_runtime_preset_name.is_none() {
704 let inferred_preset = infer_runtime_preset_for_startup(
705 arch_for_dispatch,
706 model_definition.as_ref(),
707 gpu_selection.as_ref(),
708 );
709 if let Some(preset) = inferred_preset {
710 selected_runtime_preset_name = Some(preset.to_string());
711 let mut inferred_entries =
712 runtime_preset_entries(preset, RuntimeConfigSource::Default)?;
713 inferred_entries.extend(non_env_runtime_entries);
714 non_env_runtime_entries = RuntimeConfigSnapshot::from_entries(inferred_entries).entries;
715 materialized_runtime_keys.extend(crate::runtime_env::materialize_runtime_env_defaults(
716 &non_env_runtime_entries,
717 ));
718 materialized_runtime_keys.sort();
719 materialized_runtime_keys.dedup();
720 }
721 }
722
723 if selected_runtime_preset_name.is_none()
728 && arch_for_dispatch == Some(ferrum_models::Architecture::Qwen3Moe)
729 {
730 let current_runtime = merge_runtime_config_sources(
731 non_env_runtime_entries.clone(),
732 RuntimeConfigSnapshot::capture_current(),
733 Vec::new(),
734 );
735 let mut legacy_entries = crate::runtime_env::moe_graph_default_entries(
736 ¤t_runtime,
737 RuntimeConfigSource::Default,
738 );
739 legacy_entries.extend(non_env_runtime_entries);
740 non_env_runtime_entries = RuntimeConfigSnapshot::from_entries(legacy_entries).entries;
741 materialized_runtime_keys.extend(crate::runtime_env::materialize_runtime_env_defaults(
742 &non_env_runtime_entries,
743 ));
744 materialized_runtime_keys.sort();
745 materialized_runtime_keys.dedup();
746 }
747
748 let mut startup_cli_runtime_entries = serve_cli_runtime_entries(
749 kv_dtype.as_deref(),
750 kv_capacity,
751 kv_max_blocks,
752 max_model_len,
753 max_num_seqs,
754 max_num_batched_tokens,
755 runtime_memory_budget_bytes.map(std::num::NonZeroUsize::get),
756 scheduler_prefill_first_until_active,
757 scheduler_prefill_step_chunk,
758 scheduler_active_decode_prefill_chunk,
759 greedy_argmax_cli_override(greedy_argmax, disable_greedy_argmax),
760 prefix_cache_cli_override(
761 enable_prefix_caching,
762 no_enable_prefix_caching,
763 enable_prefix_cache,
764 disable_prefix_cache,
765 ),
766 session_cache.as_deref(),
767 session_cache_max_entries,
768 session_cache_max_tokens,
769 profile_jsonl.as_ref(),
770 scheduler_trace_jsonl.as_ref(),
771 profile_commit_sha.as_deref(),
772 profile_env_hash.as_deref(),
773 profile_model.as_deref(),
774 profile_concurrency,
775 profile_runtime_flags_json.as_deref(),
776 layer_split_pipeline_mode,
777 );
778 startup_cli_runtime_entries.push(RuntimeConfigEntry::new(
779 "FERRUM_PROFILE_DETAIL",
780 profile_detail.as_str(),
781 RuntimeConfigSource::Cli,
782 ));
783 push_cli_runtime_entry(
784 &mut startup_cli_runtime_entries,
785 "FERRUM_VNEXT_DIAGNOSTIC_FAULT",
786 vnext_diagnostic_fault.map(crate::commands::VNextDiagnosticFaultArg::as_runtime_value),
787 );
788 push_sequence_fit_policy_cli_entry(&mut startup_cli_runtime_entries, sequence_fit_policy);
789 if let Some(enabled) = batched_graph_cli_override(batched_graph, disable_batched_graph) {
790 startup_cli_runtime_entries.push(RuntimeConfigEntry::new(
791 "FERRUM_BATCHED_GRAPH",
792 if enabled { "1" } else { "0" },
793 RuntimeConfigSource::Cli,
794 ));
795 }
796 if let Some(enabled) =
797 batched_graph_cli_override(reusable_execution, disable_reusable_execution)
798 {
799 startup_cli_runtime_entries.push(RuntimeConfigEntry::new(
800 "FERRUM_REUSABLE_EXECUTION",
801 if enabled { "1" } else { "0" },
802 RuntimeConfigSource::Cli,
803 ));
804 }
805 if let Some(enabled) = batched_graph_cli_override(unified_graph, disable_unified_graph) {
806 startup_cli_runtime_entries.push(RuntimeConfigEntry::new(
807 "FERRUM_UNIFIED_GRAPH",
808 if enabled { "1" } else { "0" },
809 RuntimeConfigSource::Cli,
810 ));
811 }
812 if let Some(enabled) =
813 batched_graph_cli_override(unified_graph_layers_only, disable_unified_graph_layers_only)
814 {
815 startup_cli_runtime_entries.push(RuntimeConfigEntry::new(
816 "FERRUM_UNIFIED_GRAPH_LAYERS_ONLY",
817 if enabled { "1" } else { "0" },
818 RuntimeConfigSource::Cli,
819 ));
820 }
821 if let Some(enabled) = batched_graph_cli_override(
822 unified_graph_lm_head_eager,
823 disable_unified_graph_lm_head_eager,
824 ) {
825 startup_cli_runtime_entries.push(RuntimeConfigEntry::new(
826 "FERRUM_UNIFIED_GRAPH_LM_HEAD_EAGER",
827 if enabled { "1" } else { "0" },
828 RuntimeConfigSource::Cli,
829 ));
830 }
831 if let Some(selection) = &gpu_selection {
832 startup_cli_runtime_entries.extend(selection.runtime_config_entries());
833 }
834 if !startup_cli_runtime_entries.is_empty() {
835 crate::runtime_env::materialize_runtime_env_effective(
836 &RuntimeConfigSnapshot::from_entries(startup_cli_runtime_entries.clone()),
837 );
838 }
839 let autosize_env_before = RuntimeConfigSnapshot::capture_current();
840 crate::gpu_mem_autosize::apply_auto_size(&source.local_path, gpu_memory_utilization);
844 let autosize_runtime_entries = runtime_entries_changed_by_snapshot(
845 &autosize_env_before,
846 &RuntimeConfigSnapshot::capture_current(),
847 SERVE_AUTOSIZE_RUNTIME_KEYS,
848 RuntimeConfigSource::MemoryProfile,
849 );
850 let autosize_runtime_keys: Vec<String> = autosize_runtime_entries
851 .iter()
852 .map(|entry| entry.key.clone())
853 .collect();
854 startup_cli_runtime_entries.retain(|entry| !autosize_runtime_keys.contains(&entry.key));
855 materialized_runtime_keys.extend(
856 autosize_runtime_entries
857 .iter()
858 .map(|entry| entry.key.clone()),
859 );
860 non_env_runtime_entries.extend(autosize_runtime_entries);
861 non_env_runtime_entries = RuntimeConfigSnapshot::from_entries(non_env_runtime_entries).entries;
862 materialized_runtime_keys.sort();
863 materialized_runtime_keys.dedup();
864 let typed_model_capabilities = prepared_model
865 .as_ref()
866 .map(|prepared| prepared.model_capabilities())
867 .transpose()?;
868 let startup_auto_config = startup_auto_config(
869 &device,
870 typed_model_capabilities,
871 if prepared_model.is_some() {
872 ferrum_types::ExecutionResourceAuthority::PlanRuntime
873 } else {
874 ferrum_types::ExecutionResourceAuthority::LegacyEngine
875 },
876 arch_for_dispatch,
877 model_definition.as_ref(),
878 model_weight_bytes_from_path(&source.local_path),
879 selected_runtime_preset_name.as_deref(),
880 non_env_runtime_entries,
881 materialized_runtime_keys,
882 startup_cli_runtime_entries,
883 )?;
884 crate::runtime_env::materialize_runtime_env_effective(&startup_auto_config.runtime_config);
885 write_startup_config_artifacts(
886 &startup_auto_config,
887 product_source_identity.as_ref(),
888 effective_config_json.as_deref(),
889 decision_trace_jsonl.as_deref(),
890 )?;
891 let native_profile_jsonl = if product_observability.unified_product_profile_enabled() {
892 None
893 } else {
894 profile_jsonl.clone()
895 };
896 configure_profile_sink(
897 native_profile_jsonl,
898 ProfileSinkCliFields {
899 commit_sha: profile_commit_sha,
900 env_hash: profile_env_hash,
901 model: profile_model,
902 concurrency: profile_concurrency,
903 runtime_flags_json: profile_runtime_flags_json,
904 },
905 &startup_auto_config,
906 &model_id,
907 )?;
908
909 let lora_server_models: Vec<ferrum_server::LoraAdapterModel> = startup_lora_adapters
910 .iter()
911 .map(|adapter| {
912 ferrum_server::LoraAdapterModel::new(
913 adapter.name.clone(),
914 adapter.public_model_id.clone(),
915 adapter.path.display().to_string(),
916 )
917 })
918 .collect();
919 let served_model_kind = match arch_for_dispatch {
920 Some(ferrum_models::Architecture::Clip) => ServedModelKind::Embedding,
921 Some(ferrum_models::Architecture::Whisper) => ServedModelKind::Transcription,
922 Some(ferrum_models::Architecture::Qwen3TTS) => ServedModelKind::Speech,
923 _ => ServedModelKind::Llm,
924 };
925 if vnext_checkpoint.teacher_token_file.is_some() {
926 return Err(FerrumError::unsupported(
927 "vNext checkpoint teacher forcing is supported only by one-shot ferrum run",
928 ));
929 }
930 let vnext_checkpoint_capture = vnext_checkpoint.to_config()?;
931 if vnext_checkpoint_capture.is_some() && served_model_kind != ServedModelKind::Llm {
932 return Err(FerrumError::unsupported(
933 "vNext checkpoint capture is only supported for causal language models",
934 ));
935 }
936 let served_model_registry = ServedModelRegistry::try_new(
937 model_id.clone(),
938 served_model_kind,
939 served_model_names,
940 lora_server_models,
941 )
942 .map_err(|error| FerrumError::config(error.to_string()))?;
943
944 let mut cache_allocated_status = None;
945 let server = match arch_for_dispatch {
946 Some(ferrum_models::Architecture::Clip) => {
947 println!("{}", "Initializing CLIP embedding engine...".dimmed());
948 let candle_device = candle_core::Device::Cpu;
949 let executor = ferrum_models::ClipModelExecutor::from_path(
950 &source.local_path.to_string_lossy(),
951 candle_device,
952 candle_core::DType::F32,
953 )?;
954 let tokenizer = crate::commands::embed::load_tokenizer(&source.local_path)?;
955 let mut engine_config = product_engine_config;
956 engine_config.sampling.default_params = ferrum_server::default_chat_sampling_params();
957 engine_config.backend.device = device;
958 if let Some(selection) = &gpu_selection {
959 selection.insert_backend_options(&mut engine_config.backend.backend_options);
960 }
961 let engine: Arc<dyn ferrum_engine::EmbedEngine + Send + Sync> = Arc::new(
962 ferrum_engine::embedding_engine::EmbeddingEngine::new(executor, engine_config)
963 .with_tokenizer(tokenizer),
964 );
965 AxumServer::from_embed(engine)
966 }
967 Some(ferrum_models::Architecture::Whisper) => {
968 println!("{}", "Initializing Whisper ASR engine...".dimmed());
969 let candle_device = to_candle_device(&device)?;
970 let executor = ferrum_models::WhisperModelExecutor::from_path(
971 &source.local_path.to_string_lossy(),
972 candle_device,
973 candle_core::DType::F32,
974 )?;
975 let mut engine_config = product_engine_config;
976 engine_config.backend.device = device;
977 if let Some(selection) = &gpu_selection {
978 selection.insert_backend_options(&mut engine_config.backend.backend_options);
979 }
980 let engine: Arc<dyn ferrum_engine::TranscribeEngine + Send + Sync> = Arc::new(
981 ferrum_engine::transcription_engine::TranscriptionEngine::new(
982 executor,
983 engine_config,
984 ),
985 );
986 AxumServer::from_transcribe(engine)
987 }
988 Some(ferrum_models::Architecture::Qwen3TTS) => {
989 let n_slots = tts_slots.max(1);
990 println!(
991 "{} ({} slot{})",
992 "Initializing Qwen3-TTS engine...".dimmed(),
993 n_slots,
994 if n_slots > 1 { "s" } else { "" }
995 );
996 let model_path = source.local_path.to_string_lossy().to_string();
997 let mut executors = Vec::with_capacity(n_slots);
998 for i in 0..n_slots {
999 let candle_device = to_candle_device(&device)?;
1000 let executor = ferrum_models::TtsModelExecutor::from_path(
1001 &model_path,
1002 candle_device,
1003 candle_core::DType::F32,
1004 )?;
1005 if i == 0 {
1006 println!(" Slot 0 loaded");
1007 } else {
1008 println!(" Slot {} loaded", i);
1009 }
1010 executors.push(executor);
1011 }
1012 let engine: Arc<dyn ferrum_engine::TtsEngine + Send + Sync> =
1013 Arc::new(ferrum_engine::tts_engine::TtsService::new_multi(
1014 executors,
1015 ferrum_types::ModelId(model_id.clone()),
1016 ));
1017 AxumServer::from_tts(engine)
1018 }
1019 _ => {
1020 println!(
1021 "{}",
1022 "Initializing engine (continuous batching)...".dimmed()
1023 );
1024 let mut engine_config = product_engine_config;
1025 engine_config.kv_cache.cache_type = serve_kv_cache_type_for_device(&device);
1026 engine_config.backend.device = device;
1027 engine_config.scheduler.policy = ferrum_types::SchedulingPolicy::ContinuousBatch;
1028 engine_config
1029 .apply_runtime_config_snapshot(&startup_auto_config.runtime_config)
1030 .map_err(ferrum_types::FerrumError::config)?;
1031 engine_config.runtime.vnext_checkpoint_capture = vnext_checkpoint_capture;
1032 engine_config.backend.backend_options.insert(
1033 "model_path".to_string(),
1034 serde_json::Value::String(engine_model_path.clone()),
1035 );
1036 if let Some(selection) = &gpu_selection {
1037 selection.insert_backend_options(&mut engine_config.backend.backend_options);
1038 }
1039 crate::layer_split_pipeline::insert_backend_option_from_runtime(
1040 &startup_auto_config.runtime_config,
1041 &mut engine_config.backend.backend_options,
1042 )?;
1043 if let Some(draft_path) = engine_spec_draft_path.as_ref() {
1044 engine_config.backend.backend_options.insert(
1045 "spec_draft".to_string(),
1046 serde_json::Value::String(draft_path.clone()),
1047 );
1048 engine_config.backend.backend_options.insert(
1049 "spec_n".to_string(),
1050 serde_json::Value::Number(serde_json::Number::from(spec_tokens)),
1051 );
1052 }
1053 super::run::apply_kv_dtype_override(&mut engine_config, effective_kv_dtype)?;
1054 let engine: Arc<dyn ferrum_engine::LlmInferenceEngine + Send + Sync> =
1055 Arc::from(match (prepared_model, model_sources) {
1056 (Some(prepared), _) => {
1057 ferrum_engine::create_prepared_product_engine(engine_config, prepared)
1058 .await?
1059 }
1060 (None, Some(sources)) => {
1061 ferrum_engine::create_product_engine(engine_config, sources).await?
1062 }
1063 (None, None) => ferrum_engine::create_default_engine(engine_config).await?,
1064 });
1065 if product_memory_enabled {
1066 cache_allocated_status = Some(engine.status().await);
1067 }
1068 AxumServer::from_llm(engine).with_prompt_template(model_chat_template)
1069 }
1070 }
1071 .with_auto_config(startup_auto_config)
1072 .with_default_enable_thinking(default_enable_thinking);
1073 let model_loaded_sample = product_memory_enabled
1074 .then(|| memory_sampler.sample())
1075 .flatten();
1076 let model_loaded_memory = serve_process_memory_observation_between(
1077 backend_initialized_sample
1078 .clone()
1079 .or_else(|| process_start_sample.clone()),
1080 model_loaded_sample.clone(),
1081 );
1082 let model_loaded_duration_us = serve_start
1083 .elapsed()
1084 .as_micros()
1085 .try_into()
1086 .unwrap_or(u64::MAX);
1087 let profile_run_done_sample = product_memory_enabled
1088 .then(|| memory_sampler.sample())
1089 .flatten();
1090 let profile_run_done_memory = serve_process_memory_observation_between(
1091 model_loaded_sample.clone(),
1092 profile_run_done_sample.clone(),
1093 );
1094 let cache_allocated_sample = product_memory_enabled
1095 .then(|| memory_sampler.sample())
1096 .flatten();
1097 let cache_allocated_memory = serve_process_memory_observation_between(
1098 profile_run_done_sample
1099 .clone()
1100 .or_else(|| model_loaded_sample.clone()),
1101 cache_allocated_sample.clone(),
1102 );
1103 let server = server.with_served_model_registry(served_model_registry);
1104 crate::observability_product::write_actual_serve_startup_observability(
1105 &product_observability,
1106 model_loaded_duration_us,
1107 model_loaded_memory.clone(),
1108 actual_serve_startup_memory_stages(
1109 product_memory_enabled,
1110 process_start_memory.clone(),
1111 backend_initialized_memory.clone(),
1112 profile_run_done_memory.clone(),
1113 cache_allocated_memory.clone(),
1114 cache_allocated_status.clone(),
1115 ),
1116 )?;
1117
1118 let server_config = ServerConfig {
1120 host: host.clone(),
1121 port,
1122 request_dump_dir: request_dump_dir.clone(),
1123 profile_jsonl: product_observability
1124 .unified_product_profile_enabled()
1125 .then(|| profile_jsonl.clone())
1126 .flatten(),
1127 profile_detail: product_observability.profile_detail,
1128 memory_profile_jsonl: product_observability
1129 .unified_product_profile_enabled()
1130 .then(|| memory_profile_jsonl.clone())
1131 .flatten(),
1132 ..Default::default()
1133 };
1134
1135 println!();
1136 println!(
1137 "{} {} {}",
1138 "🚀".green(),
1139 "Server running at".green().bold(),
1140 format!("http://{}:{}", host, port).cyan().bold()
1141 );
1142 println!();
1143 println!("Endpoints:");
1144 println!(" POST /v1/chat/completions - OpenAI-compatible chat");
1145 println!(" POST /v1/audio/transcriptions - Speech-to-text (Whisper)");
1146 println!(" POST /v1/audio/speech - Text-to-speech (TTS)");
1147 println!(" POST /v1/embeddings - Text/image embeddings");
1148 println!(" GET /v1/models - List models");
1149 println!(" GET /health - Health check");
1150 println!();
1151 println!("{}", "Press Ctrl+C to stop.".dimmed());
1152 println!();
1153
1154 let pid_file = std::env::temp_dir().join("ferrum.pid");
1156 std::fs::write(&pid_file, std::process::id().to_string()).ok();
1157
1158 let server = Arc::new(server);
1160 let mut server_task = {
1161 let server = Arc::clone(&server);
1162 let server_config = server_config.clone();
1163 tokio::spawn(async move { server.start(&server_config).await })
1164 };
1165 let shutdown_timeout = Duration::from_secs(30);
1166 let serve_result: Result<()> = tokio::select! {
1167 joined = &mut server_task => {
1168 let start_result = match joined {
1169 Ok(result) => result,
1170 Err(error) => Err(FerrumError::internal(format!(
1171 "serve task failed: {error}"
1172 ))),
1173 };
1174 let stop_result = server.stop(shutdown_timeout).await;
1175 start_result.and(stop_result)
1176 }
1177 _ = serve_shutdown_signal() => {
1178 println!();
1179 println!("{}", "Shutting down...".yellow());
1180 let stop_result = server.stop(shutdown_timeout).await;
1181 let start_result = match tokio::time::timeout(shutdown_timeout, &mut server_task).await {
1182 Ok(Ok(result)) => result,
1183 Ok(Err(error)) => Err(FerrumError::internal(format!(
1184 "serve task failed during shutdown: {error}"
1185 ))),
1186 Err(_) => {
1187 server_task.abort();
1188 let _ = server_task.await;
1189 Err(FerrumError::internal(format!(
1190 "serve task did not stop within {} ms",
1191 shutdown_timeout.as_millis()
1192 )))
1193 }
1194 };
1195 stop_result.and(start_result)
1196 }
1197 };
1198
1199 std::fs::remove_file(&pid_file).ok();
1201
1202 ferrum_bench_core::trace::flush_global_trace();
1206 ferrum_bench_core::flush_global_profile();
1207 let shutdown_after = product_memory_enabled
1208 .then(|| memory_sampler.sample())
1209 .flatten();
1210 let shutdown_memory = serve_process_memory_observation_between(
1211 model_loaded_sample
1212 .clone()
1213 .or_else(|| backend_initialized_sample.clone())
1214 .or_else(|| process_start_sample.clone()),
1215 shutdown_after,
1216 );
1217 crate::observability_product::append_actual_serve_memory_stage_observability(
1218 &product_observability,
1219 crate::observability_product::ActualMemoryStageObservation::new(
1220 "actual_serve_shutdown",
1221 "shutdown",
1222 None,
1223 shutdown_memory,
1224 ),
1225 )?;
1226
1227 serve_result?;
1228 Ok(())
1229}
1230
1231async fn serve_shutdown_signal() {
1232 #[cfg(unix)]
1233 {
1234 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
1235 Ok(mut terminate) => {
1236 tokio::select! {
1237 _ = signal::ctrl_c() => {}
1238 _ = terminate.recv() => {}
1239 }
1240 }
1241 Err(_) => {
1242 let _ = signal::ctrl_c().await;
1243 }
1244 }
1245 }
1246 #[cfg(not(unix))]
1247 {
1248 let _ = signal::ctrl_c().await;
1249 }
1250}
1251
1252fn serve_process_memory_observation_between(
1253 before: Option<crate::memory_profile::ProcessMemorySample>,
1254 after: Option<crate::memory_profile::ProcessMemorySample>,
1255) -> Option<crate::memory_profile::ProcessMemoryObservation> {
1256 after.map(|after| crate::memory_profile::ProcessMemoryObservation::from_samples(before, after))
1257}
1258
1259fn actual_serve_startup_memory_stages(
1260 enabled: bool,
1261 process_start_memory: Option<crate::memory_profile::ProcessMemoryObservation>,
1262 backend_initialized_memory: Option<crate::memory_profile::ProcessMemoryObservation>,
1263 profile_run_done_memory: Option<crate::memory_profile::ProcessMemoryObservation>,
1264 cache_allocated_memory: Option<crate::memory_profile::ProcessMemoryObservation>,
1265 cache_allocated_status: Option<ferrum_types::EngineStatus>,
1266) -> Vec<crate::observability_product::ActualMemoryStageObservation> {
1267 if !enabled {
1268 return Vec::new();
1269 }
1270 let profile_run_done = crate::observability_product::ActualMemoryStageObservation::new(
1271 "actual_serve_profile_run_done",
1272 "profile_run_done",
1273 None,
1274 profile_run_done_memory,
1275 )
1276 .with_profile_run_status(
1277 false,
1278 "not_configured",
1279 "product_basic_profile_does_not_execute_extra_warmup",
1280 );
1281 let mut cache_allocated = crate::observability_product::ActualMemoryStageObservation::new(
1282 "actual_serve_cache_allocated",
1283 "cache_allocated",
1284 None,
1285 cache_allocated_memory,
1286 );
1287 if let Some(status) = cache_allocated_status.as_ref() {
1288 cache_allocated = cache_allocated.with_engine_cache_status(status);
1289 }
1290 vec![
1291 crate::observability_product::ActualMemoryStageObservation::new(
1292 "actual_serve_process_start",
1293 "process_start",
1294 None,
1295 process_start_memory,
1296 ),
1297 crate::observability_product::ActualMemoryStageObservation::new(
1298 "actual_serve_backend_initialized",
1299 "backend_initialized",
1300 None,
1301 backend_initialized_memory,
1302 ),
1303 profile_run_done,
1304 cache_allocated,
1305 ]
1306}
1307
1308fn print_banner() {
1309 println!();
1310 println!("{}", " ______ ".bright_red());
1311 println!("{}", " | ____| ".bright_red());
1312 println!("{}", " | |__ ___ _ __ _ __ _ _ _ __ ___ ".bright_red());
1313 println!("{}", " | __/ _ \\ '__| '__| | | | '_ ` _ \\ ".bright_red());
1314 println!("{}", " | | | __/ | | | | |_| | | | | | ".bright_red());
1315 println!("{}", " |_| \\___|_| |_| \\__,_|_| |_| |_|".bright_red());
1316 println!();
1317 println!(" {}", "🦀 Rust LLM Inference Server".bright_cyan().bold());
1318 println!(
1319 " {}",
1320 format!("Version {}", env!("CARGO_PKG_VERSION")).dimmed()
1321 );
1322 println!();
1323}
1324
1325fn parse_lora_specs(values: &[String]) -> Result<Vec<ferrum_models::StartupLoraSpec>> {
1326 let mut specs = Vec::with_capacity(values.len());
1327 for value in values {
1328 let (name, path) = value.split_once('=').ok_or_else(|| {
1329 ferrum_types::FerrumError::config(format!(
1330 "invalid --lora value {value:?}; expected NAME=PATH"
1331 ))
1332 })?;
1333 if name.is_empty() || path.is_empty() {
1334 return Err(ferrum_types::FerrumError::config(format!(
1335 "invalid --lora value {value:?}; expected non-empty NAME=PATH"
1336 )));
1337 }
1338 specs.push(ferrum_models::StartupLoraSpec {
1339 name: name.to_string(),
1340 path: PathBuf::from(shellexpand::tilde(path).to_string()),
1341 });
1342 }
1343 Ok(specs)
1344}
1345
1346fn startup_auto_config(
1347 device: &ferrum_types::Device,
1348 typed_model_capabilities: Option<ModelCapabilities>,
1349 execution_resource_authority: ferrum_types::ExecutionResourceAuthority,
1350 architecture: Option<ferrum_models::Architecture>,
1351 model_definition: Option<&ferrum_models::ModelDefinition>,
1352 model_weight_bytes: Option<u64>,
1353 runtime_preset: Option<&str>,
1354 non_env_runtime_entries: Vec<RuntimeConfigEntry>,
1355 materialized_runtime_keys: Vec<String>,
1356 cli_runtime_entries: Vec<RuntimeConfigEntry>,
1357) -> Result<ResolvedFerrumConfig> {
1358 let mut env_snapshot = RuntimeConfigSnapshot::capture_current();
1359 env_snapshot = remove_materialized_config_env_entries(env_snapshot, &materialized_runtime_keys);
1360 let runtime_config =
1361 merge_runtime_config_sources(non_env_runtime_entries, env_snapshot, cli_runtime_entries);
1362 let hardware = hardware_capabilities_for_device(device);
1363 let model = typed_model_capabilities
1364 .or_else(|| {
1365 model_definition.map(|definition| {
1366 model_capabilities_from_definition_with_weight_bytes_for_hardware(
1367 definition,
1368 model_weight_bytes,
1369 &hardware,
1370 )
1371 })
1372 })
1373 .unwrap_or_else(ModelCapabilities::unknown);
1374 let workload = match runtime_preset {
1375 Some(M3_QWEN3_30B_A3B_INT4_PRESET) => WorkloadProfile::m3_qwen3_30b_a3b_int4(),
1376 Some(QWEN25_72B_GPTQ_INT4_2X4090_LAYER_SPLIT_PRESET) => {
1377 WorkloadProfile::qwen25_72b_gptq_int4_2x4090_layer_split()
1378 }
1379 Some(other) => {
1380 return Err(ferrum_types::FerrumError::config(format!(
1381 "unknown runtime preset: {other}"
1382 )));
1383 }
1384 None => match infer_runtime_preset_for_startup(architecture, model_definition, None) {
1385 Some(M3_QWEN3_30B_A3B_INT4_PRESET) => WorkloadProfile::m3_qwen3_30b_a3b_int4(),
1386 _ => WorkloadProfile::serving_default_for_hardware(&hardware),
1387 },
1388 };
1389
1390 FerrumConfigBuilder::new(runtime_config)
1391 .with_model_capabilities(model)
1392 .with_hardware_capabilities(hardware)
1393 .with_workload_profile(workload)
1394 .with_execution_resource_authority(execution_resource_authority)
1395 .resolve()
1396 .map_err(|err| ferrum_types::FerrumError::config(format!("invalid auto config: {err}")))
1397}
1398
1399pub(crate) fn merge_runtime_config_sources(
1400 config_file_entries: Vec<RuntimeConfigEntry>,
1401 env_snapshot: RuntimeConfigSnapshot,
1402 cli_entries: Vec<RuntimeConfigEntry>,
1403) -> RuntimeConfigSnapshot {
1404 let mut runtime_config = RuntimeConfigSnapshot::from_entries(config_file_entries);
1405 for entry in env_snapshot.entries {
1406 runtime_config.upsert_entry(entry);
1407 }
1408 for entry in cli_entries {
1409 runtime_config.upsert_entry(entry);
1410 }
1411 runtime_config
1412}
1413
1414fn remove_materialized_config_env_entries(
1415 mut env_snapshot: RuntimeConfigSnapshot,
1416 materialized_config_runtime_keys: &[String],
1417) -> RuntimeConfigSnapshot {
1418 env_snapshot
1419 .entries
1420 .retain(|entry| !materialized_config_runtime_keys.contains(&entry.key));
1421 env_snapshot
1422}
1423
1424pub(crate) const SERVE_AUTOSIZE_RUNTIME_KEYS: &[&str] = &[
1425 "FERRUM_MAX_BATCHED_TOKENS",
1426 "FERRUM_KV_MAX_BLOCKS",
1427 "FERRUM_PAGED_MAX_SEQS",
1428 "FERRUM_KV_CAPACITY",
1429];
1430
1431pub(crate) fn runtime_entries_changed_by_snapshot(
1432 before: &RuntimeConfigSnapshot,
1433 after: &RuntimeConfigSnapshot,
1434 keys: &[&str],
1435 source: RuntimeConfigSource,
1436) -> Vec<RuntimeConfigEntry> {
1437 keys.iter()
1438 .filter_map(|key| {
1439 let before_value = runtime_snapshot_value(before, key);
1440 let after_value = runtime_snapshot_value(after, key);
1441 match (before_value, after_value) {
1442 (None, Some(value)) => Some(RuntimeConfigEntry::new(*key, value, source)),
1443 (Some(before), Some(after)) if before != after => {
1444 Some(RuntimeConfigEntry::new(*key, after, source))
1445 }
1446 _ => None,
1447 }
1448 })
1449 .collect()
1450}
1451
1452pub(crate) fn runtime_preset_entries(
1453 preset: &str,
1454 source: RuntimeConfigSource,
1455) -> Result<Vec<RuntimeConfigEntry>> {
1456 let pairs: &[(&str, &str)] = match preset {
1457 M3_QWEN3_30B_A3B_INT4_PRESET => &[
1458 ("FERRUM_BACKEND", "cuda"),
1459 ("FERRUM_MOE_DEVICE_ROUTE", "1"),
1460 ("FERRUM_MOE_STREAMS", "4"),
1461 ("FERRUM_GREEDY_ARGMAX", "1"),
1462 ("FERRUM_KV_MAX_BLOCKS", "2048"),
1463 ("FERRUM_PAGED_MAX_SEQS", "32"),
1464 ("FERRUM_KV_CAPACITY", "512"),
1465 ("FERRUM_MOE_GRAPH", "0"),
1466 ("FERRUM_VLLM_MOE", "1"),
1467 ("FERRUM_VLLM_MOE_PAIR_IDS", "1"),
1468 ("FERRUM_ATTENTION_POLICY", "native-adaptive"),
1469 ("FERRUM_PREFIX_CACHE", "0"),
1470 ],
1471 QWEN25_72B_GPTQ_INT4_2X4090_LAYER_SPLIT_PRESET => &[
1472 ("FERRUM_BACKEND", "cuda"),
1473 ("FERRUM_LAYER_SPLIT_PIPELINE_MODE", "batch"),
1474 ("FERRUM_MAX_MODEL_LEN", "4096"),
1475 ("FERRUM_KV_MAX_BLOCKS", "1024"),
1476 ("FERRUM_KV_CAPACITY", "1024"),
1477 ("FERRUM_PAGED_MAX_SEQS", "16"),
1478 ("FERRUM_MAX_BATCHED_TOKENS", "1536"),
1479 ("FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE", "16"),
1480 ],
1481 other => {
1482 return Err(ferrum_types::FerrumError::config(format!(
1483 "unknown runtime preset: {other}"
1484 )));
1485 }
1486 };
1487 Ok(pairs
1488 .iter()
1489 .map(|(key, value)| RuntimeConfigEntry::new(*key, *value, source))
1490 .collect())
1491}
1492
1493fn serve_cli_runtime_entries(
1494 kv_dtype: Option<&str>,
1495 kv_capacity: Option<usize>,
1496 kv_max_blocks: Option<usize>,
1497 max_model_len: Option<usize>,
1498 max_num_seqs: Option<usize>,
1499 max_num_batched_tokens: Option<usize>,
1500 runtime_memory_budget_bytes: Option<usize>,
1501 scheduler_prefill_first_until_active: Option<usize>,
1502 scheduler_prefill_step_chunk: Option<usize>,
1503 scheduler_active_decode_prefill_chunk: Option<usize>,
1504 greedy_argmax: Option<bool>,
1505 prefix_cache: Option<bool>,
1506 session_cache: Option<&str>,
1507 session_cache_max_entries: Option<usize>,
1508 session_cache_max_tokens: Option<usize>,
1509 profile_jsonl: Option<&PathBuf>,
1510 scheduler_trace_jsonl: Option<&PathBuf>,
1511 profile_commit_sha: Option<&str>,
1512 profile_env_hash: Option<&str>,
1513 profile_model: Option<&str>,
1514 profile_concurrency: Option<u32>,
1515 profile_runtime_flags_json: Option<&str>,
1516 layer_split_pipeline_mode: Option<crate::layer_split_pipeline::LayerSplitPipelineModeArg>,
1517) -> Vec<RuntimeConfigEntry> {
1518 let mut entries = Vec::new();
1519 push_cli_runtime_entry(&mut entries, "FERRUM_KV_DTYPE", kv_dtype);
1520 push_cli_runtime_usize(&mut entries, "FERRUM_KV_CAPACITY", kv_capacity);
1521 push_cli_runtime_usize(&mut entries, "FERRUM_KV_MAX_BLOCKS", kv_max_blocks);
1522 push_cli_runtime_usize(&mut entries, "FERRUM_MAX_MODEL_LEN", max_model_len);
1523 push_cli_runtime_usize(&mut entries, "FERRUM_PAGED_MAX_SEQS", max_num_seqs);
1524 push_cli_runtime_usize(
1525 &mut entries,
1526 "FERRUM_MAX_BATCHED_TOKENS",
1527 max_num_batched_tokens,
1528 );
1529 push_cli_runtime_usize(
1530 &mut entries,
1531 "FERRUM_RUNTIME_MEMORY_BUDGET_BYTES",
1532 runtime_memory_budget_bytes,
1533 );
1534 push_cli_runtime_usize(
1535 &mut entries,
1536 "FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE",
1537 scheduler_prefill_first_until_active,
1538 );
1539 push_cli_runtime_usize(
1540 &mut entries,
1541 "FERRUM_SCHED_PREFILL_STEP_CHUNK",
1542 scheduler_prefill_step_chunk,
1543 );
1544 push_cli_runtime_usize(
1545 &mut entries,
1546 "FERRUM_ACTIVE_DECODE_PREFILL_CHUNK",
1547 scheduler_active_decode_prefill_chunk,
1548 );
1549 if let Some(enabled) = greedy_argmax {
1550 entries.push(RuntimeConfigEntry::new(
1551 "FERRUM_GREEDY_ARGMAX",
1552 if enabled { "1" } else { "0" },
1553 RuntimeConfigSource::Cli,
1554 ));
1555 }
1556 if let Some(enabled) = prefix_cache {
1557 entries.push(RuntimeConfigEntry::new(
1558 "FERRUM_PREFIX_CACHE_REQUESTED",
1559 if enabled { "1" } else { "0" },
1560 RuntimeConfigSource::Cli,
1561 ));
1562 entries.push(RuntimeConfigEntry::new(
1563 "FERRUM_PREFIX_CACHE_PRODUCT",
1564 if enabled { "1" } else { "0" },
1565 RuntimeConfigSource::Cli,
1566 ));
1567 entries.push(RuntimeConfigEntry::new(
1568 "FERRUM_PREFIX_CACHE",
1569 if enabled { "1" } else { "0" },
1570 RuntimeConfigSource::Cli,
1571 ));
1572 }
1573 push_cli_runtime_entry(&mut entries, "FERRUM_SESSION_CACHE", session_cache);
1574 push_cli_runtime_usize(
1575 &mut entries,
1576 "FERRUM_SESSION_CACHE_MAX_ENTRIES",
1577 session_cache_max_entries,
1578 );
1579 push_cli_runtime_usize(
1580 &mut entries,
1581 "FERRUM_SESSION_CACHE_MAX_TOKENS",
1582 session_cache_max_tokens,
1583 );
1584 if let Some(path) = profile_jsonl {
1585 entries.push(RuntimeConfigEntry::new(
1586 "FERRUM_PROFILE_JSONL",
1587 path.to_string_lossy().to_string(),
1588 RuntimeConfigSource::Cli,
1589 ));
1590 }
1591 if let Some(path) = scheduler_trace_jsonl {
1592 entries.push(RuntimeConfigEntry::new(
1593 "FERRUM_SCHEDULER_TRACE_JSONL",
1594 path.to_string_lossy().to_string(),
1595 RuntimeConfigSource::Cli,
1596 ));
1597 }
1598 if profile_jsonl.is_some() || scheduler_trace_jsonl.is_some() {
1599 entries.push(RuntimeConfigEntry::new(
1600 "FERRUM_PROFILE_ENTRYPOINT",
1601 "serve",
1602 RuntimeConfigSource::Cli,
1603 ));
1604 }
1605 push_cli_runtime_entry(
1606 &mut entries,
1607 "FERRUM_PROFILE_COMMIT_SHA",
1608 profile_commit_sha,
1609 );
1610 push_cli_runtime_entry(&mut entries, "FERRUM_PROFILE_ENV_HASH", profile_env_hash);
1611 push_cli_runtime_entry(&mut entries, "FERRUM_PROFILE_MODEL", profile_model);
1612 if let Some(concurrency) = profile_concurrency {
1613 entries.push(RuntimeConfigEntry::new(
1614 "FERRUM_PROFILE_CONCURRENCY",
1615 concurrency.to_string(),
1616 RuntimeConfigSource::Cli,
1617 ));
1618 }
1619 push_cli_runtime_entry(
1620 &mut entries,
1621 "FERRUM_PROFILE_RUNTIME_FLAGS_JSON",
1622 profile_runtime_flags_json,
1623 );
1624 crate::layer_split_pipeline::push_cli_runtime_entry(&mut entries, layer_split_pipeline_mode);
1625 entries
1626}
1627
1628fn prefix_cache_cli_override(
1629 enable_vllm: bool,
1630 disable_vllm: bool,
1631 enable_product: bool,
1632 disable_product: bool,
1633) -> Option<bool> {
1634 if enable_vllm || enable_product {
1635 Some(true)
1636 } else if disable_vllm || disable_product {
1637 Some(false)
1638 } else {
1639 None
1640 }
1641}
1642
1643fn greedy_argmax_cli_override(enable: bool, disable: bool) -> Option<bool> {
1644 if enable {
1645 Some(true)
1646 } else if disable {
1647 Some(false)
1648 } else {
1649 None
1650 }
1651}
1652
1653fn batched_graph_cli_override(enable: bool, disable: bool) -> Option<bool> {
1654 if enable {
1655 Some(true)
1656 } else if disable {
1657 Some(false)
1658 } else {
1659 None
1660 }
1661}
1662
1663fn resolve_effective_kv_dtype<'a>(
1664 cli_arg: Option<&'a str>,
1665 env_value: Option<&'a str>,
1666 config_file_value: Option<&'a str>,
1667) -> Option<&'a str> {
1668 cli_arg.or(env_value).or(config_file_value)
1669}
1670
1671fn push_cli_runtime_entry(entries: &mut Vec<RuntimeConfigEntry>, key: &str, value: Option<&str>) {
1672 if let Some(value) = value.filter(|value| !value.trim().is_empty()) {
1673 entries.push(RuntimeConfigEntry::new(
1674 key,
1675 value.to_string(),
1676 RuntimeConfigSource::Cli,
1677 ));
1678 }
1679}
1680
1681fn push_sequence_fit_policy_cli_entry(
1682 entries: &mut Vec<RuntimeConfigEntry>,
1683 policy: Option<crate::commands::SequenceFitPolicyArg>,
1684) {
1685 push_cli_runtime_entry(
1686 entries,
1687 "FERRUM_SEQUENCE_FIT_POLICY",
1688 policy.map(crate::commands::SequenceFitPolicyArg::as_runtime_value),
1689 );
1690}
1691
1692fn push_cli_runtime_usize(entries: &mut Vec<RuntimeConfigEntry>, key: &str, value: Option<usize>) {
1693 if let Some(value) = value {
1694 entries.push(RuntimeConfigEntry::new(
1695 key,
1696 value.to_string(),
1697 RuntimeConfigSource::Cli,
1698 ));
1699 }
1700}
1701
1702fn serve_kv_cache_type_for_device(device: &ferrum_types::Device) -> ferrum_types::KvCacheType {
1703 match device {
1704 ferrum_types::Device::CPU => ferrum_types::KvCacheType::Contiguous,
1705 _ => ferrum_types::KvCacheType::Paged,
1706 }
1707}
1708
1709fn effective_served_model_names(
1710 default_model_id: &str,
1711 requested_model: Option<&str>,
1712 requested_names: Vec<String>,
1713) -> Result<Vec<String>> {
1714 let names = if requested_names.is_empty() {
1715 let mut names = Vec::with_capacity(2);
1716 if let Some(requested_model) = requested_model {
1717 names.push(requested_model.to_string());
1718 }
1719 if names.first().map(String::as_str) != Some(default_model_id) {
1720 names.push(default_model_id.to_string());
1721 }
1722 names
1723 } else {
1724 requested_names
1725 };
1726 let mut seen = HashSet::with_capacity(names.len());
1727 for name in &names {
1728 if name.is_empty() || name.trim() != name {
1729 return Err(FerrumError::config(
1730 "--served-model-name values must be non-empty and have no surrounding whitespace",
1731 ));
1732 }
1733 if !seen.insert(name.clone()) {
1734 return Err(FerrumError::config(format!(
1735 "duplicate --served-model-name value: {name}"
1736 )));
1737 }
1738 }
1739 Ok(names)
1740}
1741
1742pub(crate) fn write_startup_config_artifacts(
1743 auto_config: &ResolvedFerrumConfig,
1744 resolution_evidence: Option<&ferrum_interfaces::vnext::ProductModelSourceIdentity>,
1745 effective_config_json: Option<&std::path::Path>,
1746 decision_trace_jsonl: Option<&std::path::Path>,
1747) -> Result<()> {
1748 if let Some(path) = effective_config_json {
1749 if let Some(parent) = path.parent() {
1750 std::fs::create_dir_all(parent)
1751 .map_err(|err| ferrum_types::FerrumError::io(err.to_string()))?;
1752 }
1753 let mut document = auto_config.effective_config_document();
1754 if let Some(evidence) = resolution_evidence {
1755 let object = document.as_object_mut().ok_or_else(|| {
1756 ferrum_types::FerrumError::serialization(
1757 "effective startup config document must be an object",
1758 )
1759 })?;
1760 object.insert(
1761 "resolution_evidence".to_owned(),
1762 serde_json::to_value(evidence)
1763 .map_err(|err| ferrum_types::FerrumError::serialization(err.to_string()))?,
1764 );
1765 }
1766 let bytes = serde_json::to_vec_pretty(&document)
1767 .map_err(|err| ferrum_types::FerrumError::serialization(err.to_string()))?;
1768 std::fs::write(path, [bytes.as_slice(), b"\n"].concat())
1769 .map_err(|err| ferrum_types::FerrumError::io(err.to_string()))?;
1770 }
1771 if let Some(path) = decision_trace_jsonl {
1772 if let Some(parent) = path.parent() {
1773 std::fs::create_dir_all(parent)
1774 .map_err(|err| ferrum_types::FerrumError::io(err.to_string()))?;
1775 }
1776 let trace = auto_config
1777 .decision_trace_jsonl()
1778 .map_err(|err| ferrum_types::FerrumError::serialization(err.to_string()))?;
1779 std::fs::write(path, trace)
1780 .map_err(|err| ferrum_types::FerrumError::io(err.to_string()))?;
1781 }
1782 Ok(())
1783}
1784
1785struct ProfileSinkCliFields {
1786 commit_sha: Option<String>,
1787 env_hash: Option<String>,
1788 model: Option<String>,
1789 concurrency: Option<u32>,
1790 runtime_flags_json: Option<String>,
1791}
1792
1793fn configure_profile_sink(
1794 profile_jsonl: Option<PathBuf>,
1795 fields: ProfileSinkCliFields,
1796 auto_config: &ResolvedFerrumConfig,
1797 model_id: &str,
1798) -> Result<()> {
1799 let Some(path) = profile_jsonl else {
1800 return Ok(());
1801 };
1802
1803 let runtime_flags = match fields.runtime_flags_json {
1804 Some(json) => {
1805 let value = serde_json::from_str::<serde_json::Value>(&json).map_err(|err| {
1806 ferrum_types::FerrumError::config(format!(
1807 "invalid --profile-runtime-flags-json: {err}"
1808 ))
1809 })?;
1810 if !value.is_object() {
1811 return Err(ferrum_types::FerrumError::config(
1812 "--profile-runtime-flags-json must be a JSON object",
1813 ));
1814 }
1815 value
1816 }
1817 None => auto_config.effective_config_document(),
1818 };
1819
1820 let env_hash = match fields.env_hash {
1821 Some(value) if value.starts_with("sha256:") => value,
1822 Some(_) => {
1823 return Err(ferrum_types::FerrumError::config(
1824 "--profile-env-hash must start with sha256:",
1825 ))
1826 }
1827 None => auto_config.runtime_env_hash(),
1828 };
1829
1830 let metadata = ProfileMetadata {
1831 commit_sha: fields.commit_sha.filter(|value| !value.trim().is_empty()),
1832 env_hash,
1833 model: fields
1834 .model
1835 .filter(|value| !value.trim().is_empty())
1836 .unwrap_or_else(|| model_id.to_string()),
1837 concurrency: fields
1838 .concurrency
1839 .filter(|value| *value > 0)
1840 .unwrap_or_else(|| auto_config.workload_profile.target_concurrency.max(1) as u32),
1841 runtime_flags,
1842 };
1843 let profile_config = ProfileSinkConfig::enabled(path, metadata);
1844 ferrum_bench_core::configure_global_profile(profile_config.clone())
1845 .map_err(|err| ferrum_types::FerrumError::io(err.to_string()))?;
1846 ferrum_kernels::configure_native_profile_sink(&profile_config)
1847 .map_err(|err| ferrum_types::FerrumError::io(err.to_string()))?;
1848 Ok(())
1849}
1850
1851#[cfg(test)]
1852pub(crate) fn model_capabilities_from_definition_with_weight_bytes(
1853 definition: &ferrum_models::ModelDefinition,
1854 model_weight_bytes: Option<u64>,
1855) -> ModelCapabilities {
1856 ferrum_models::legacy_capabilities::from_definition_with_weight_bytes(
1857 definition,
1858 model_weight_bytes,
1859 )
1860}
1861
1862pub(crate) fn model_capabilities_from_definition_with_weight_bytes_for_hardware(
1863 definition: &ferrum_models::ModelDefinition,
1864 model_weight_bytes: Option<u64>,
1865 hardware: &HardwareCapabilities,
1866) -> ModelCapabilities {
1867 ferrum_models::legacy_capabilities::from_definition_with_weight_bytes_for_hardware(
1868 definition,
1869 model_weight_bytes,
1870 hardware,
1871 )
1872}
1873
1874pub(crate) fn model_weight_bytes_from_path(path: &Path) -> Option<u64> {
1875 if path.is_file() {
1876 return std::fs::metadata(path)
1877 .ok()
1878 .map(|metadata| metadata.len())
1879 .filter(|value| *value > 0);
1880 }
1881 if !path.is_dir() {
1882 return None;
1883 }
1884 let mut total = 0u64;
1885 for entry in std::fs::read_dir(path).ok()?.flatten() {
1886 let entry_path = entry.path();
1887 let is_weight = entry_path
1888 .extension()
1889 .and_then(|value| value.to_str())
1890 .map(|ext| ext == "safetensors" || ext == "bin")
1891 .unwrap_or(false);
1892 if !is_weight {
1893 continue;
1894 }
1895 if let Ok(metadata) = std::fs::metadata(&entry_path) {
1896 total = total.saturating_add(metadata.len());
1897 }
1898 }
1899 (total > 0).then_some(total)
1900}
1901
1902pub(crate) fn hardware_capabilities_for_device(
1903 device: &ferrum_types::Device,
1904) -> HardwareCapabilities {
1905 let features = compiled_kernel_features();
1906 match device {
1907 ferrum_types::Device::CUDA(id) => {
1908 cuda_hardware_capabilities(features, probe_cuda_device(*id as usize))
1909 }
1910 ferrum_types::Device::ROCm(_) => HardwareCapabilities {
1911 backend: "rocm".to_string(),
1912 supported_dtypes: vec!["fp16".to_string(), "fp32".to_string()],
1913 supported_kv_dtypes: vec!["fp16".to_string()],
1914 compiled_features: features,
1915 ..HardwareCapabilities::unknown()
1916 },
1917 #[cfg(any(target_os = "macos", target_os = "ios"))]
1918 ferrum_types::Device::Metal => HardwareCapabilities {
1919 backend: "metal".to_string(),
1920 supported_dtypes: vec!["fp16".to_string(), "fp32".to_string()],
1921 supported_kv_dtypes: vec!["fp16".to_string()],
1922 compiled_features: features,
1923 ..HardwareCapabilities::unknown()
1924 },
1925 ferrum_types::Device::CPU => HardwareCapabilities {
1926 backend: "cpu".to_string(),
1927 supported_dtypes: vec!["fp32".to_string()],
1928 supported_kv_dtypes: vec!["fp16".to_string()],
1929 compiled_features: features,
1930 ..HardwareCapabilities::unknown()
1931 },
1932 }
1933}
1934
1935#[derive(Debug, Clone, Default, PartialEq, Eq)]
1936struct CudaDeviceProbe {
1937 name: Option<String>,
1938 cuda_runtime: Option<String>,
1939 compute_capability: Option<String>,
1940 vram_bytes: Option<u64>,
1941 sm_count: Option<u32>,
1942}
1943
1944fn cuda_hardware_capabilities(
1945 features: CompiledKernelFeatures,
1946 probe: CudaDeviceProbe,
1947) -> HardwareCapabilities {
1948 HardwareCapabilities {
1949 backend: "cuda".to_string(),
1950 cuda_runtime: probe.cuda_runtime,
1951 compute_capability: probe.compute_capability,
1952 vram_bytes: probe.vram_bytes,
1953 sm_count: probe.sm_count,
1954 supported_dtypes: vec!["fp16".to_string(), "fp32".to_string()],
1955 supported_kv_dtypes: vec![
1956 "fp16".to_string(),
1957 "bf16".to_string(),
1958 "int8".to_string(),
1959 "fp8".to_string(),
1960 ],
1961 graph_support: cfg!(feature = "cuda"),
1962 compiled_features: features,
1963 }
1964}
1965
1966fn probe_cuda_device(device_id: usize) -> CudaDeviceProbe {
1967 let mut probe = run_nvidia_smi_query(device_id, "name,compute_cap,memory.total")
1968 .and_then(|output| parse_nvidia_smi_gpu_query(&output))
1969 .unwrap_or_default();
1970 probe.cuda_runtime = probe_cuda_runtime_version();
1971 probe.sm_count = run_nvidia_smi_query(device_id, "multiprocessor_count")
1972 .and_then(|output| parse_first_u32(&output))
1973 .or_else(|| probe.name.as_deref().and_then(infer_sm_count_from_gpu_name));
1974 probe
1975}
1976
1977fn run_nvidia_smi_query(device_id: usize, query: &str) -> Option<String> {
1978 let output = Command::new("nvidia-smi")
1979 .args([
1980 format!("--query-gpu={query}"),
1981 "--format=csv,noheader,nounits".to_string(),
1982 "-i".to_string(),
1983 device_id.to_string(),
1984 ])
1985 .output()
1986 .ok()?;
1987 output
1988 .status
1989 .success()
1990 .then(|| String::from_utf8_lossy(&output.stdout).to_string())
1991}
1992
1993fn probe_cuda_runtime_version() -> Option<String> {
1994 run_command_stdout("nvcc", &["--version"])
1995 .and_then(|output| parse_nvcc_cuda_release(&output))
1996 .or_else(|| {
1997 run_command_stdout("nvidia-smi", &[])
1998 .and_then(|output| parse_nvidia_smi_cuda_version(&output))
1999 })
2000}
2001
2002fn run_command_stdout(command: &str, args: &[&str]) -> Option<String> {
2003 let output = Command::new(command).args(args).output().ok()?;
2004 output
2005 .status
2006 .success()
2007 .then(|| String::from_utf8_lossy(&output.stdout).to_string())
2008}
2009
2010fn parse_nvidia_smi_gpu_query(output: &str) -> Option<CudaDeviceProbe> {
2011 let line = output.lines().find(|line| !line.trim().is_empty())?;
2012 let fields = line.split(',').map(str::trim).collect::<Vec<_>>();
2013 if fields.len() < 3 {
2014 return None;
2015 }
2016 let name = non_empty_probe_value(fields[0]).map(str::to_string);
2017 let compute_capability = non_empty_probe_value(fields[1]).map(str::to_string);
2018 let vram_bytes = parse_memory_total_bytes(fields[2]);
2019 Some(CudaDeviceProbe {
2020 name,
2021 compute_capability,
2022 vram_bytes,
2023 ..CudaDeviceProbe::default()
2024 })
2025}
2026
2027fn parse_memory_total_bytes(raw: &str) -> Option<u64> {
2028 let lower = raw.trim().to_ascii_lowercase();
2029 let numeric = lower
2030 .trim_end_matches("mib")
2031 .trim_end_matches("mb")
2032 .trim_end_matches("gib")
2033 .trim_end_matches("gb")
2034 .trim();
2035 let value = numeric.parse::<f64>().ok()?;
2036 let multiplier = if lower.contains("gib") || lower.contains("gb") {
2037 1024.0 * 1024.0 * 1024.0
2038 } else {
2039 1024.0 * 1024.0
2040 };
2041 Some((value * multiplier).round() as u64)
2042}
2043
2044fn parse_first_u32(output: &str) -> Option<u32> {
2045 output
2046 .lines()
2047 .find_map(|line| non_empty_probe_value(line)?.parse::<u32>().ok())
2048}
2049
2050fn parse_nvcc_cuda_release(output: &str) -> Option<String> {
2051 let marker = "release ";
2052 let start = output.find(marker)? + marker.len();
2053 parse_version_prefix(&output[start..])
2054}
2055
2056fn parse_nvidia_smi_cuda_version(output: &str) -> Option<String> {
2057 let marker = "CUDA Version:";
2058 let start = output.find(marker)? + marker.len();
2059 parse_version_prefix(output[start..].trim())
2060}
2061
2062fn parse_version_prefix(raw: &str) -> Option<String> {
2063 let version = raw
2064 .chars()
2065 .take_while(|ch| ch.is_ascii_digit() || *ch == '.')
2066 .collect::<String>();
2067 (!version.is_empty()).then_some(version)
2068}
2069
2070fn non_empty_probe_value(raw: &str) -> Option<&str> {
2071 let value = raw.trim();
2072 if value.is_empty() || value.eq_ignore_ascii_case("n/a") {
2073 None
2074 } else {
2075 Some(value)
2076 }
2077}
2078
2079fn infer_sm_count_from_gpu_name(name: &str) -> Option<u32> {
2080 let normalized = name.to_ascii_lowercase();
2081 if normalized.contains("rtx 4090") {
2082 Some(128)
2083 } else {
2084 None
2085 }
2086}
2087
2088fn compiled_kernel_features() -> CompiledKernelFeatures {
2089 let fa2_native = ferrum_kernels::native_ops::compiled_fa2_native_operator_artifact();
2090 let native_operator_artifacts =
2091 ferrum_kernels::native_ops::compiled_native_operator_artifacts().to_vec();
2092 let has_v2_fa2 = native_operator_artifacts
2093 .iter()
2094 .any(|artifact| artifact.operator == ferrum_kernels::native_ops::FA2_NATIVE_OPERATOR);
2095 CompiledKernelFeatures {
2096 cuda: cfg!(feature = "cuda"),
2097 vllm_paged_attn: cfg!(feature = "vllm-paged-attn-v2"),
2098 vllm_moe_marlin: cfg!(feature = "vllm-moe-marlin"),
2099 cuda_graph: cfg!(feature = "cuda"),
2100 greedy_argmax: cfg!(feature = "cuda") || cfg!(feature = "metal"),
2101 fa2_source: false,
2102 fa2_direct_ffi: cfg!(feature = "cuda"),
2103 fa2_native_operator_artifact: fa2_native.is_some() || has_v2_fa2,
2104 fa2_native_operator_artifact_metadata: fa2_native.map(|artifact| {
2105 CompiledNativeOperatorArtifact {
2106 manifest_path: artifact.manifest_path.to_string(),
2107 artifact_path: artifact.artifact_path.to_string(),
2108 source_package_sha256: artifact.source_package_sha256.to_string(),
2109 inputs_sha256: artifact.inputs_sha256.to_string(),
2110 binary_sha256: artifact.binary_sha256.to_string(),
2111 }
2112 }),
2113 native_operator_artifacts,
2114 }
2115}
2116
2117#[derive(Clone, Copy)]
2118struct RuntimePresetInferenceRule {
2119 preset: &'static str,
2120 architecture: ferrum_models::Architecture,
2121 quantization: Option<&'static str>,
2122 exact_hidden_size: Option<usize>,
2123 min_hidden_size: Option<usize>,
2124 exact_hidden_layers: Option<usize>,
2125 min_hidden_layers: Option<usize>,
2126 kv_heads: Option<usize>,
2127 num_experts: Option<u64>,
2128 experts_per_token: Option<u64>,
2129 distributed_strategy: Option<&'static str>,
2130 gpu_count: Option<usize>,
2131}
2132
2133const RUNTIME_PRESET_INFERENCE_RULES: &[RuntimePresetInferenceRule] = &[
2134 RuntimePresetInferenceRule {
2135 preset: M3_QWEN3_30B_A3B_INT4_PRESET,
2136 architecture: ferrum_models::Architecture::Qwen3Moe,
2137 quantization: None,
2138 exact_hidden_size: Some(2048),
2139 min_hidden_size: None,
2140 exact_hidden_layers: None,
2141 min_hidden_layers: Some(40),
2142 kv_heads: Some(4),
2143 num_experts: Some(128),
2144 experts_per_token: Some(8),
2145 distributed_strategy: None,
2146 gpu_count: None,
2147 },
2148 RuntimePresetInferenceRule {
2149 preset: QWEN25_72B_GPTQ_INT4_2X4090_LAYER_SPLIT_PRESET,
2150 architecture: ferrum_models::Architecture::Qwen2,
2151 quantization: Some("gptq_int4"),
2152 exact_hidden_size: None,
2153 min_hidden_size: Some(8192),
2154 exact_hidden_layers: Some(80),
2155 min_hidden_layers: None,
2156 kv_heads: Some(8),
2157 num_experts: None,
2158 experts_per_token: None,
2159 distributed_strategy: Some("layer_split"),
2160 gpu_count: Some(2),
2161 },
2162];
2163
2164fn infer_runtime_preset_for_startup(
2165 architecture: Option<ferrum_models::Architecture>,
2166 model_definition: Option<&ferrum_models::ModelDefinition>,
2167 gpu_selection: Option<&crate::gpu_devices::GpuDeviceSelection>,
2168) -> Option<&'static str> {
2169 let definition = model_definition?;
2170 RUNTIME_PRESET_INFERENCE_RULES
2171 .iter()
2172 .find(|rule| rule.matches(architecture, definition, gpu_selection))
2173 .map(|rule| rule.preset)
2174}
2175
2176impl RuntimePresetInferenceRule {
2177 fn matches(
2178 &self,
2179 architecture: Option<ferrum_models::Architecture>,
2180 definition: &ferrum_models::ModelDefinition,
2181 gpu_selection: Option<&crate::gpu_devices::GpuDeviceSelection>,
2182 ) -> bool {
2183 if architecture != Some(self.architecture) {
2184 return false;
2185 }
2186 if self.quantization.is_some()
2187 && ferrum_models::legacy_capabilities::quantization_from_definition(definition)
2188 .as_deref()
2189 != self.quantization
2190 {
2191 return false;
2192 }
2193 if self
2194 .exact_hidden_size
2195 .is_some_and(|value| definition.hidden_size != value)
2196 {
2197 return false;
2198 }
2199 if self
2200 .min_hidden_size
2201 .is_some_and(|value| definition.hidden_size < value)
2202 {
2203 return false;
2204 }
2205 if self
2206 .exact_hidden_layers
2207 .is_some_and(|value| definition.num_hidden_layers != value)
2208 {
2209 return false;
2210 }
2211 if self
2212 .min_hidden_layers
2213 .is_some_and(|value| definition.num_hidden_layers < value)
2214 {
2215 return false;
2216 }
2217 if self
2218 .kv_heads
2219 .is_some_and(|value| definition.num_key_value_heads != Some(value))
2220 {
2221 return false;
2222 }
2223 if self.num_experts.is_some()
2224 && definition
2225 .extra_params
2226 .get("num_experts")
2227 .and_then(|value| value.as_u64())
2228 != self.num_experts
2229 {
2230 return false;
2231 }
2232 if self.experts_per_token.is_some()
2233 && definition
2234 .extra_params
2235 .get("num_experts_per_tok")
2236 .and_then(|value| value.as_u64())
2237 != self.experts_per_token
2238 {
2239 return false;
2240 }
2241 if self.distributed_strategy.is_some() || self.gpu_count.is_some() {
2242 let Some(selection) = gpu_selection else {
2243 return false;
2244 };
2245 if self
2246 .distributed_strategy
2247 .is_some_and(|value| selection.selected_distributed_strategy != value)
2248 {
2249 return false;
2250 }
2251 if self
2252 .gpu_count
2253 .is_some_and(|value| selection.selected_gpu_devices.len() != value)
2254 {
2255 return false;
2256 }
2257 }
2258 true
2259 }
2260}
2261
2262fn to_candle_device(device: &ferrum_types::Device) -> ferrum_types::Result<candle_core::Device> {
2269 match device {
2270 #[cfg(all(target_os = "macos", feature = "metal"))]
2271 ferrum_types::Device::Metal => candle_core::Device::new_metal(0)
2272 .map_err(|error| ferrum_types::FerrumError::device(error.to_string())),
2273 #[cfg(feature = "candle-cuda-compat")]
2274 ferrum_types::Device::CUDA(id) => candle_core::Device::new_cuda(*id as usize)
2275 .map_err(|error| ferrum_types::FerrumError::device(error.to_string())),
2276 ferrum_types::Device::CUDA(_) => Err(ferrum_types::FerrumError::unsupported(
2277 "this Candle-backed server architecture requires the candle-cuda-compat feature",
2278 )),
2279 ferrum_types::Device::ROCm(_) => Err(ferrum_types::FerrumError::unsupported(
2280 "ROCm is not supported",
2281 )),
2282 _ => Ok(candle_core::Device::Cpu),
2283 }
2284}
2285
2286#[cfg(test)]
2287mod tests {
2288 use super::*;
2289
2290 #[test]
2291 fn serve_exposes_typed_diagnostic_fault() {
2292 use clap::Parser;
2293
2294 #[derive(Parser)]
2295 struct TestCli {
2296 #[command(flatten)]
2297 serve: ServeCommand,
2298 }
2299
2300 let parsed = TestCli::parse_from([
2301 "ferrum",
2302 "--model",
2303 "Qwen/Qwen3.5-4B",
2304 "--vnext-diagnostic-fault",
2305 "prefill-resource-after-submit-once",
2306 ]);
2307
2308 assert_eq!(
2309 parsed.serve.vnext_diagnostic_fault,
2310 Some(crate::commands::VNextDiagnosticFaultArg::PrefillResourceAfterSubmitOnce)
2311 );
2312 }
2313
2314 #[test]
2315 fn serve_rejects_removed_qwen35_flag() {
2316 use clap::Parser;
2317
2318 #[derive(Parser)]
2319 struct TestCli {
2320 #[command(flatten)]
2321 serve: ServeCommand,
2322 }
2323
2324 let error =
2325 match TestCli::try_parse_from(["ferrum", "--model", "qwen3.5", "--qwen35-reference"]) {
2326 Ok(_) => panic!("product CLI exposed the legacy Qwen3.5 reference adapter"),
2327 Err(error) => error,
2328 };
2329
2330 assert!(error.to_string().contains("--qwen35-reference"));
2331 }
2332
2333 #[test]
2334 fn serve_parses_public_model_aliases() {
2335 use clap::Parser;
2336
2337 #[derive(Parser)]
2338 struct TestCli {
2339 #[command(flatten)]
2340 serve: ServeCommand,
2341 }
2342
2343 let parsed = TestCli::parse_from([
2344 "ferrum",
2345 "--model",
2346 "Qwen/Qwen3.5-4B",
2347 "--served-model-name",
2348 "ferrum,qwen35",
2349 "--port",
2350 "8001",
2351 ]);
2352
2353 assert_eq!(parsed.serve.served_model_name, ["ferrum", "qwen35"]);
2354 }
2355
2356 #[test]
2357 fn served_model_names_default_and_reject_ambiguity() {
2358 assert_eq!(
2359 effective_served_model_names("Qwen/Qwen3.5-4B", Some("Qwen/Qwen3.5-4B"), vec![])
2360 .unwrap(),
2361 ["Qwen/Qwen3.5-4B"]
2362 );
2363 assert_eq!(
2364 effective_served_model_names("Qwen3.5-4B-Q4_K_M", Some("qwen3.5:4b-q4_k_m"), vec![])
2365 .unwrap(),
2366 ["qwen3.5:4b-q4_k_m", "Qwen3.5-4B-Q4_K_M"]
2367 );
2368 assert!(effective_served_model_names(
2369 "Qwen/Qwen3.5-4B",
2370 Some("qwen3.5:4b"),
2371 vec!["same".to_string(), "same".to_string()]
2372 )
2373 .is_err());
2374 assert!(effective_served_model_names(
2375 "Qwen/Qwen3.5-4B",
2376 Some("qwen3.5:4b"),
2377 vec![" ferrum".to_string()]
2378 )
2379 .is_err());
2380 }
2381
2382 #[test]
2383 fn served_model_names_do_not_expose_non_hugging_face_source_names() {
2384 assert_eq!(
2385 effective_served_model_names("local-model", None, vec![]).unwrap(),
2386 ["local-model"]
2387 );
2388 }
2389
2390 #[test]
2391 fn serve_cli_runtime_entries_are_cli_sourced_and_classified() {
2392 let mut entries = serve_cli_runtime_entries(
2393 Some("int8"),
2394 Some(1024),
2395 Some(4096),
2396 Some(4096),
2397 Some(64),
2398 Some(2048),
2399 Some(12_345),
2400 Some(8),
2401 Some(16),
2402 Some(24),
2403 Some(true),
2404 Some(false),
2405 Some("memory"),
2406 Some(16),
2407 Some(1024),
2408 Some(&PathBuf::from("/tmp/profile.jsonl")),
2409 Some(&PathBuf::from("/tmp/scheduler-trace.jsonl")),
2410 Some("abc123"),
2411 Some("sha256:test"),
2412 Some("Qwen/Qwen3-30B-A3B-GPTQ-Int4"),
2413 Some(32),
2414 Some("{\"schema_version\":1}"),
2415 Some(crate::layer_split_pipeline::LayerSplitPipelineModeArg::Batch),
2416 );
2417 push_sequence_fit_policy_cli_entry(
2418 &mut entries,
2419 Some(crate::commands::SequenceFitPolicyArg::FullInputMustFit),
2420 );
2421 let snapshot = RuntimeConfigSnapshot::from_entries(entries);
2422 let entry = |key: &str| {
2423 snapshot
2424 .entries
2425 .iter()
2426 .find(|entry| entry.key == key)
2427 .unwrap_or_else(|| panic!("missing {key}"))
2428 };
2429
2430 assert_eq!(entry("FERRUM_KV_DTYPE").effective_value, "int8");
2431 assert_eq!(entry("FERRUM_KV_DTYPE").source, RuntimeConfigSource::Cli);
2432 assert!(entry("FERRUM_KV_DTYPE")
2433 .affects
2434 .contains(&ferrum_types::RuntimeConfigEffect::Correctness));
2435 assert_eq!(entry("FERRUM_MAX_MODEL_LEN").effective_value, "4096");
2436 assert_eq!(entry("FERRUM_KV_CAPACITY").effective_value, "1024");
2437 assert_eq!(entry("FERRUM_KV_MAX_BLOCKS").effective_value, "4096");
2438 assert_eq!(
2439 entry("FERRUM_KV_MAX_BLOCKS").source,
2440 RuntimeConfigSource::Cli
2441 );
2442 assert_eq!(entry("FERRUM_PAGED_MAX_SEQS").effective_value, "64");
2443 assert_eq!(entry("FERRUM_MAX_BATCHED_TOKENS").effective_value, "2048");
2444 assert_eq!(
2445 entry("FERRUM_RUNTIME_MEMORY_BUDGET_BYTES").effective_value,
2446 "12345"
2447 );
2448 assert!(entry("FERRUM_RUNTIME_MEMORY_BUDGET_BYTES")
2449 .affects
2450 .contains(&ferrum_types::RuntimeConfigEffect::Memory));
2451 assert!(entry("FERRUM_RUNTIME_MEMORY_BUDGET_BYTES")
2452 .affects
2453 .contains(&ferrum_types::RuntimeConfigEffect::Correctness));
2454 assert_eq!(
2455 entry("FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE").effective_value,
2456 "8"
2457 );
2458 assert_eq!(
2459 entry("FERRUM_SCHED_PREFILL_STEP_CHUNK").effective_value,
2460 "16"
2461 );
2462 assert_eq!(
2463 entry("FERRUM_ACTIVE_DECODE_PREFILL_CHUNK").effective_value,
2464 "24"
2465 );
2466 assert_eq!(entry("FERRUM_GREEDY_ARGMAX").effective_value, "1");
2467 assert_eq!(entry("FERRUM_PREFIX_CACHE").effective_value, "0");
2468 assert_eq!(entry("FERRUM_SESSION_CACHE").effective_value, "memory");
2469 assert_eq!(
2470 entry("FERRUM_SESSION_CACHE_MAX_ENTRIES").effective_value,
2471 "16"
2472 );
2473 assert_eq!(
2474 entry("FERRUM_SESSION_CACHE_MAX_TOKENS").effective_value,
2475 "1024"
2476 );
2477 assert_eq!(
2478 entry("FERRUM_MAX_MODEL_LEN").source,
2479 RuntimeConfigSource::Cli
2480 );
2481 assert_eq!(
2482 entry("FERRUM_PROFILE_JSONL").effective_value,
2483 "/tmp/profile.jsonl"
2484 );
2485 assert_eq!(
2486 entry("FERRUM_SCHEDULER_TRACE_JSONL").effective_value,
2487 "/tmp/scheduler-trace.jsonl"
2488 );
2489 assert_eq!(entry("FERRUM_PROFILE_ENTRYPOINT").effective_value, "serve");
2490 assert_eq!(
2491 entry("FERRUM_PROFILE_ENV_HASH").effective_value,
2492 "sha256:test"
2493 );
2494 assert_eq!(entry("FERRUM_PROFILE_CONCURRENCY").effective_value, "32");
2495 assert_eq!(
2496 entry("FERRUM_SEQUENCE_FIT_POLICY").effective_value,
2497 "full-input-must-fit"
2498 );
2499 assert_eq!(
2500 entry("FERRUM_SEQUENCE_FIT_POLICY").source,
2501 RuntimeConfigSource::Cli
2502 );
2503 assert!(entry("FERRUM_SEQUENCE_FIT_POLICY")
2504 .affects
2505 .contains(&ferrum_types::RuntimeConfigEffect::Memory));
2506 assert!(entry("FERRUM_SEQUENCE_FIT_POLICY")
2507 .affects
2508 .contains(&ferrum_types::RuntimeConfigEffect::Correctness));
2509 assert_eq!(
2510 entry(crate::layer_split_pipeline::LAYER_SPLIT_PIPELINE_MODE_KEY).effective_value,
2511 "batch"
2512 );
2513 assert!(entry("FERRUM_PROFILE_JSONL")
2514 .affects
2515 .contains(&ferrum_types::RuntimeConfigEffect::Diagnostics));
2516 assert!(entry("FERRUM_SCHEDULER_TRACE_JSONL")
2517 .affects
2518 .contains(&ferrum_types::RuntimeConfigEffect::Diagnostics));
2519 }
2520
2521 #[test]
2522 fn cpu_serve_uses_contiguous_kv_cache() {
2523 assert!(matches!(
2524 serve_kv_cache_type_for_device(&ferrum_types::Device::CPU),
2525 ferrum_types::KvCacheType::Contiguous
2526 ));
2527 }
2528
2529 #[cfg(any(all(target_os = "macos", feature = "metal"), feature = "cuda"))]
2530 #[test]
2531 fn accelerator_serve_uses_paged_kv_cache() {
2532 #[cfg(all(target_os = "macos", feature = "metal"))]
2533 let device = ferrum_types::Device::Metal;
2534 #[cfg(all(feature = "cuda", not(all(target_os = "macos", feature = "metal"))))]
2535 let device = ferrum_types::Device::CUDA(0);
2536
2537 assert!(matches!(
2538 serve_kv_cache_type_for_device(&device),
2539 ferrum_types::KvCacheType::Paged
2540 ));
2541 }
2542
2543 #[test]
2544 fn serve_runtime_snapshot_prefers_cli_over_config_file() {
2545 let config_entries = crate::config::RuntimeCliConfig {
2546 kv_dtype: Some("fp16".to_string()),
2547 sequence_fit_policy: Some(ferrum_types::SequenceFitPolicy::ImmediateOnly),
2548 ..Default::default()
2549 }
2550 .runtime_config_entries();
2551 let mut cli_entries = serve_cli_runtime_entries(
2552 Some("int8"),
2553 None,
2554 None,
2555 None,
2556 None,
2557 None,
2558 None,
2559 None,
2560 None,
2561 None,
2562 None,
2563 None,
2564 None,
2565 None,
2566 None,
2567 None,
2568 None,
2569 None,
2570 None,
2571 None,
2572 None,
2573 None,
2574 None,
2575 );
2576 push_sequence_fit_policy_cli_entry(
2577 &mut cli_entries,
2578 Some(crate::commands::SequenceFitPolicyArg::FullInputMustFit),
2579 );
2580
2581 let snapshot = merge_runtime_config_sources(
2582 config_entries,
2583 RuntimeConfigSnapshot::default(),
2584 cli_entries,
2585 );
2586 let kv = snapshot
2587 .entries
2588 .iter()
2589 .find(|entry| entry.key == "FERRUM_KV_DTYPE")
2590 .unwrap();
2591 assert_eq!(kv.effective_value, "int8");
2592 assert_eq!(kv.source, RuntimeConfigSource::Cli);
2593 let sequence_fit = snapshot
2594 .entries
2595 .iter()
2596 .find(|entry| entry.key == "FERRUM_SEQUENCE_FIT_POLICY")
2597 .unwrap();
2598 assert_eq!(sequence_fit.effective_value, "full-input-must-fit");
2599 assert_eq!(sequence_fit.source, RuntimeConfigSource::Cli);
2600 }
2601
2602 #[test]
2603 fn serve_runtime_snapshot_applies_recurrent_state_slots_to_engine_config() {
2604 let config_entries = crate::config::RuntimeCliConfig {
2605 recurrent_state_max_slots: Some(16),
2606 ..Default::default()
2607 }
2608 .runtime_config_entries();
2609 let snapshot = merge_runtime_config_sources(
2610 config_entries,
2611 RuntimeConfigSnapshot::default(),
2612 Vec::new(),
2613 );
2614 let entry = snapshot
2615 .entries
2616 .iter()
2617 .find(|entry| entry.key == "FERRUM_RECURRENT_STATE_MAX_SLOTS")
2618 .expect("missing recurrent state slot entry");
2619 let mut engine_config = ferrum_types::EngineConfig::default();
2620
2621 engine_config
2622 .apply_runtime_config_snapshot(&snapshot)
2623 .expect("serve runtime config should apply to engine config");
2624
2625 assert_eq!(entry.effective_value, "16");
2626 assert_eq!(entry.source, RuntimeConfigSource::ConfigFile);
2627 assert_eq!(engine_config.runtime.recurrent_state_max_slots, Some(16));
2628 }
2629
2630 #[test]
2631 fn vllm_compat_runtime_flags_follow_existing_precedence() {
2632 let config_entries = crate::config::RuntimeCliConfig {
2633 max_model_len: Some(1024),
2634 paged_max_seqs: Some(2),
2635 max_batched_tokens: Some(128),
2636 prefix_cache: Some(false),
2637 ..Default::default()
2638 }
2639 .runtime_config_entries();
2640 let env_snapshot = RuntimeConfigSnapshot::from_entries([
2641 RuntimeConfigEntry::new("FERRUM_MAX_MODEL_LEN", "2048", RuntimeConfigSource::Env),
2642 RuntimeConfigEntry::new("FERRUM_PAGED_MAX_SEQS", "4", RuntimeConfigSource::Env),
2643 RuntimeConfigEntry::new("FERRUM_MAX_BATCHED_TOKENS", "256", RuntimeConfigSource::Env),
2644 RuntimeConfigEntry::new("FERRUM_PREFIX_CACHE", "1", RuntimeConfigSource::Env),
2645 ]);
2646
2647 let env_over_config =
2648 merge_runtime_config_sources(config_entries.clone(), env_snapshot.clone(), Vec::new());
2649 fn entry<'a>(snapshot: &'a RuntimeConfigSnapshot, key: &str) -> &'a RuntimeConfigEntry {
2650 snapshot
2651 .entries
2652 .iter()
2653 .find(|entry| entry.key == key)
2654 .unwrap_or_else(|| panic!("missing {key}"))
2655 }
2656 assert_eq!(
2657 entry(&env_over_config, "FERRUM_MAX_MODEL_LEN").effective_value,
2658 "2048"
2659 );
2660 assert_eq!(
2661 entry(&env_over_config, "FERRUM_PREFIX_CACHE").source,
2662 RuntimeConfigSource::Env
2663 );
2664
2665 let cli_entries = serve_cli_runtime_entries(
2666 None,
2667 Some(1024),
2668 None,
2669 Some(4096),
2670 Some(8),
2671 Some(512),
2672 None,
2673 Some(8),
2674 Some(16),
2675 Some(32),
2676 Some(false),
2677 Some(false),
2678 None,
2679 None,
2680 None,
2681 None,
2682 None,
2683 None,
2684 None,
2685 None,
2686 None,
2687 None,
2688 None,
2689 );
2690 let cli_over_env = merge_runtime_config_sources(config_entries, env_snapshot, cli_entries);
2691 assert_eq!(
2692 entry(&cli_over_env, "FERRUM_MAX_MODEL_LEN").effective_value,
2693 "4096"
2694 );
2695 assert_eq!(
2696 entry(&cli_over_env, "FERRUM_KV_CAPACITY").effective_value,
2697 "1024"
2698 );
2699 assert_eq!(
2700 entry(&cli_over_env, "FERRUM_PAGED_MAX_SEQS").effective_value,
2701 "8"
2702 );
2703 assert_eq!(
2704 entry(&cli_over_env, "FERRUM_MAX_BATCHED_TOKENS").effective_value,
2705 "512"
2706 );
2707 assert_eq!(
2708 entry(&cli_over_env, "FERRUM_ACTIVE_DECODE_PREFILL_CHUNK").effective_value,
2709 "32"
2710 );
2711 assert_eq!(
2712 entry(&cli_over_env, "FERRUM_PREFIX_CACHE").effective_value,
2713 "0"
2714 );
2715 assert_eq!(
2716 entry(&cli_over_env, "FERRUM_PREFIX_CACHE").source,
2717 RuntimeConfigSource::Cli
2718 );
2719 }
2720
2721 #[test]
2722 fn autosize_snapshot_diff_marks_new_and_changed_values_as_memory_profile() {
2723 let before = RuntimeConfigSnapshot::from_entries([
2724 RuntimeConfigEntry::new("FERRUM_KV_MAX_BLOCKS", "2048", RuntimeConfigSource::Env),
2725 RuntimeConfigEntry::new("FERRUM_MOE_GRAPH", "1", RuntimeConfigSource::Env),
2726 RuntimeConfigEntry::new("FERRUM_KV_CAPACITY", "512", RuntimeConfigSource::Cli),
2727 ]);
2728 let after = RuntimeConfigSnapshot::from_entries([
2729 RuntimeConfigEntry::new("FERRUM_KV_MAX_BLOCKS", "2048", RuntimeConfigSource::Env),
2730 RuntimeConfigEntry::new("FERRUM_MOE_GRAPH", "1", RuntimeConfigSource::Env),
2731 RuntimeConfigEntry::new("FERRUM_KV_CAPACITY", "256", RuntimeConfigSource::Env),
2732 RuntimeConfigEntry::new(
2733 "FERRUM_MAX_BATCHED_TOKENS",
2734 "2048",
2735 RuntimeConfigSource::Env,
2736 ),
2737 RuntimeConfigEntry::new("FERRUM_PAGED_MAX_SEQS", "32", RuntimeConfigSource::Env),
2738 ]);
2739
2740 let entries = runtime_entries_changed_by_snapshot(
2741 &before,
2742 &after,
2743 SERVE_AUTOSIZE_RUNTIME_KEYS,
2744 RuntimeConfigSource::MemoryProfile,
2745 );
2746 let snapshot = RuntimeConfigSnapshot::from_entries(entries);
2747 let entry = |key: &str| {
2748 snapshot
2749 .entries
2750 .iter()
2751 .find(|entry| entry.key == key)
2752 .unwrap_or_else(|| panic!("missing {key}"))
2753 };
2754
2755 assert_eq!(snapshot.entries.len(), 3);
2756 assert_eq!(
2757 entry("FERRUM_MAX_BATCHED_TOKENS").source,
2758 RuntimeConfigSource::MemoryProfile
2759 );
2760 assert_eq!(entry("FERRUM_PAGED_MAX_SEQS").effective_value, "32");
2761 assert_eq!(entry("FERRUM_KV_CAPACITY").effective_value, "256");
2762 assert_eq!(
2763 entry("FERRUM_KV_CAPACITY").source,
2764 RuntimeConfigSource::MemoryProfile
2765 );
2766 assert!(snapshot
2767 .entries
2768 .iter()
2769 .all(|entry| entry.key != "FERRUM_KV_MAX_BLOCKS"));
2770 }
2771
2772 #[test]
2773 fn materialized_autosize_entries_keep_memory_profile_source() {
2774 let autosize_entries = vec![RuntimeConfigEntry::new(
2775 "FERRUM_MAX_BATCHED_TOKENS",
2776 "2048",
2777 RuntimeConfigSource::MemoryProfile,
2778 )];
2779 let materialized_keys = autosize_entries
2780 .iter()
2781 .map(|entry| entry.key.clone())
2782 .collect::<Vec<_>>();
2783 let env_snapshot = RuntimeConfigSnapshot::from_entries([
2784 RuntimeConfigEntry::new(
2785 "FERRUM_MAX_BATCHED_TOKENS",
2786 "2048",
2787 RuntimeConfigSource::Env,
2788 ),
2789 RuntimeConfigEntry::new("FERRUM_KV_DTYPE", "fp16", RuntimeConfigSource::Env),
2790 ]);
2791 let env_snapshot = remove_materialized_config_env_entries(env_snapshot, &materialized_keys);
2792 let snapshot = merge_runtime_config_sources(autosize_entries, env_snapshot, Vec::new());
2793 let entry = |key: &str| {
2794 snapshot
2795 .entries
2796 .iter()
2797 .find(|entry| entry.key == key)
2798 .unwrap_or_else(|| panic!("missing {key}"))
2799 };
2800
2801 assert_eq!(
2802 entry("FERRUM_MAX_BATCHED_TOKENS").source,
2803 RuntimeConfigSource::MemoryProfile
2804 );
2805 assert_eq!(entry("FERRUM_KV_DTYPE").source, RuntimeConfigSource::Env);
2806 }
2807
2808 #[test]
2809 fn nvidia_smi_gpu_query_parser_extracts_cuda_hardware_fields() {
2810 let probe = parse_nvidia_smi_gpu_query("NVIDIA GeForce RTX 4090, 8.9, 24564\n").unwrap();
2811
2812 assert_eq!(probe.name.as_deref(), Some("NVIDIA GeForce RTX 4090"));
2813 assert_eq!(probe.compute_capability.as_deref(), Some("8.9"));
2814 assert_eq!(probe.vram_bytes, Some(24564 * 1024 * 1024));
2815 }
2816
2817 #[test]
2818 fn nvidia_smi_gpu_query_parser_handles_units_and_empty_values() {
2819 let probe = parse_nvidia_smi_gpu_query("N/A, N/A, 24 GiB\n").unwrap();
2820
2821 assert_eq!(probe.name, None);
2822 assert_eq!(probe.compute_capability, None);
2823 assert_eq!(probe.vram_bytes, Some(24 * 1024 * 1024 * 1024));
2824 }
2825
2826 #[test]
2827 fn cuda_runtime_version_parsers_accept_nvcc_and_nvidia_smi_output() {
2828 let nvcc = "Cuda compilation tools, release 12.8, V12.8.93";
2829 let smi = "| NVIDIA-SMI 570.86.15 Driver Version: 570.86.15 CUDA Version: 12.8 |";
2830
2831 assert_eq!(parse_nvcc_cuda_release(nvcc).as_deref(), Some("12.8"));
2832 assert_eq!(parse_nvidia_smi_cuda_version(smi).as_deref(), Some("12.8"));
2833 assert_eq!(parse_first_u32("128\n").unwrap(), 128);
2834 assert_eq!(
2835 infer_sm_count_from_gpu_name("NVIDIA GeForce RTX 4090"),
2836 Some(128)
2837 );
2838 }
2839
2840 #[test]
2841 fn cuda_hardware_capabilities_uses_runtime_probe_values() {
2842 let hardware = cuda_hardware_capabilities(
2843 CompiledKernelFeatures {
2844 cuda: true,
2845 cuda_graph: true,
2846 ..CompiledKernelFeatures::default()
2847 },
2848 CudaDeviceProbe {
2849 name: Some("NVIDIA GeForce RTX 4090".to_string()),
2850 cuda_runtime: Some("12.8".to_string()),
2851 compute_capability: Some("8.9".to_string()),
2852 vram_bytes: Some(24 * 1024 * 1024 * 1024),
2853 sm_count: Some(128),
2854 },
2855 );
2856
2857 assert_eq!(hardware.backend, "cuda");
2858 assert_eq!(hardware.cuda_runtime.as_deref(), Some("12.8"));
2859 assert_eq!(hardware.compute_capability.as_deref(), Some("8.9"));
2860 assert_eq!(hardware.vram_bytes, Some(24 * 1024 * 1024 * 1024));
2861 assert_eq!(hardware.sm_count, Some(128));
2862 assert!(hardware.supported_kv_dtypes.contains(&"int8".to_string()));
2863 assert!(hardware.compiled_features.cuda);
2864 }
2865
2866 #[test]
2867 fn m3_runtime_preset_entries_are_cli_sourced_defaults() {
2868 let entries =
2869 runtime_preset_entries(M3_QWEN3_30B_A3B_INT4_PRESET, RuntimeConfigSource::Cli).unwrap();
2870 let snapshot = RuntimeConfigSnapshot::from_entries(entries);
2871 let entry = |key: &str| {
2872 snapshot
2873 .entries
2874 .iter()
2875 .find(|entry| entry.key == key)
2876 .unwrap_or_else(|| panic!("missing {key}"))
2877 };
2878
2879 assert_eq!(entry("FERRUM_BACKEND").effective_value, "cuda");
2880 assert_eq!(entry("FERRUM_MOE_GRAPH").effective_value, "0");
2881 assert_eq!(entry("FERRUM_VLLM_MOE").effective_value, "1");
2882 assert_eq!(entry("FERRUM_VLLM_MOE_PAIR_IDS").effective_value, "1");
2883 assert_eq!(
2884 entry("FERRUM_ATTENTION_POLICY").effective_value,
2885 "native-adaptive"
2886 );
2887 assert_eq!(entry("FERRUM_KV_CAPACITY").effective_value, "512");
2888 assert_eq!(entry("FERRUM_PREFIX_CACHE").effective_value, "0");
2889 assert_eq!(entry("FERRUM_BACKEND").source, RuntimeConfigSource::Cli);
2890 assert_eq!(snapshot.entries.len(), 12);
2891 }
2892
2893 #[test]
2894 fn m3_runtime_preset_entries_can_be_default_sourced_for_model_inference() {
2895 let entries =
2896 runtime_preset_entries(M3_QWEN3_30B_A3B_INT4_PRESET, RuntimeConfigSource::Default)
2897 .unwrap();
2898 let snapshot = RuntimeConfigSnapshot::from_entries(entries);
2899 let entry = |key: &str| {
2900 snapshot
2901 .entries
2902 .iter()
2903 .find(|entry| entry.key == key)
2904 .unwrap_or_else(|| panic!("missing {key}"))
2905 };
2906
2907 assert_eq!(entry("FERRUM_MOE_GRAPH").effective_value, "0");
2908 assert_eq!(entry("FERRUM_VLLM_MOE").effective_value, "1");
2909 assert_eq!(entry("FERRUM_KV_CAPACITY").effective_value, "512");
2910 assert_eq!(
2911 entry("FERRUM_MOE_GRAPH").source,
2912 RuntimeConfigSource::Default
2913 );
2914 assert_eq!(
2915 entry("FERRUM_VLLM_MOE").source,
2916 RuntimeConfigSource::Default
2917 );
2918 }
2919
2920 fn qwen25_72b_gptq_definition() -> ferrum_models::ModelDefinition {
2921 let mut definition = ferrum_models::ModelDefinition {
2922 architecture: ferrum_models::Architecture::Qwen2,
2923 hidden_size: 8192,
2924 num_hidden_layers: 80,
2925 num_key_value_heads: Some(8),
2926 ..Default::default()
2927 };
2928 definition.extra_params = serde_json::json!({
2929 "quantization_config": {
2930 "bits": 4,
2931 "quant_method": "gptq"
2932 }
2933 });
2934 definition
2935 }
2936
2937 #[test]
2938 fn model_capabilities_prefer_measured_weight_bytes_from_model_source() {
2939 let mut definition = ferrum_models::ModelDefinition {
2940 architecture: ferrum_models::Architecture::Qwen3Moe,
2941 hidden_size: 2048,
2942 intermediate_size: 512,
2943 num_hidden_layers: 40,
2944 num_attention_heads: 16,
2945 num_key_value_heads: Some(2),
2946 max_position_embeddings: 262144,
2947 ..Default::default()
2948 };
2949 definition.extra_params = serde_json::json!({
2950 "head_dim": 256,
2951 "num_experts": 256,
2952 "num_experts_per_tok": 8,
2953 "moe_intermediate_size": 512,
2954 "shared_expert_intermediate_size": 512,
2955 "quantization_config": {
2956 "bits": 4,
2957 "quant_method": "gptq"
2958 }
2959 });
2960
2961 let capabilities =
2962 model_capabilities_from_definition_with_weight_bytes(&definition, Some(19_123_456_789));
2963
2964 assert_eq!(capabilities.estimated_weight_bytes, Some(19_123_456_789));
2965 }
2966
2967 #[test]
2968 fn model_weight_bytes_from_path_sums_local_weight_files() {
2969 let dir = std::env::temp_dir().join(format!(
2970 "ferrum-weight-bytes-test-{}-{}",
2971 std::process::id(),
2972 std::thread::current().name().unwrap_or("unnamed")
2973 ));
2974 let _ = std::fs::remove_dir_all(&dir);
2975 std::fs::create_dir_all(&dir).expect("create temp model dir");
2976 std::fs::write(dir.join("model-00001-of-00002.safetensors"), vec![0u8; 7])
2977 .expect("write safetensors shard");
2978 std::fs::write(dir.join("model-00002-of-00002.safetensors"), vec![0u8; 11])
2979 .expect("write safetensors shard");
2980 std::fs::write(dir.join("tokenizer.json"), vec![0u8; 101]).expect("write non-weight file");
2981
2982 let result = model_weight_bytes_from_path(&dir);
2983 let _ = std::fs::remove_dir_all(&dir);
2984
2985 assert_eq!(result, Some(18));
2986 }
2987
2988 fn two_gpu_layer_split_selection() -> crate::gpu_devices::GpuDeviceSelection {
2989 crate::gpu_devices::GpuDeviceSelection {
2990 raw_cli_value: "0,1".to_string(),
2991 requested_gpu_devices: vec![0, 1],
2992 selected_gpu_devices: vec![0, 1],
2993 cuda_device_count: 2,
2994 selected_distributed_strategy: "layer_split".to_string(),
2995 selected_layer_split_plan: Some(
2996 "stage0:cuda:0:layers=0-39;stage1:cuda:1:layers=40-79".to_string(),
2997 ),
2998 selected_layer_split_stages: None,
2999 }
3000 }
3001
3002 #[test]
3003 fn qwen25_layer_split_runtime_preset_entries_are_default_sourced() {
3004 let entries = runtime_preset_entries(
3005 QWEN25_72B_GPTQ_INT4_2X4090_LAYER_SPLIT_PRESET,
3006 RuntimeConfigSource::Default,
3007 )
3008 .unwrap();
3009 let snapshot = RuntimeConfigSnapshot::from_entries(entries);
3010 let entry = |key: &str| {
3011 snapshot
3012 .entries
3013 .iter()
3014 .find(|entry| entry.key == key)
3015 .unwrap_or_else(|| panic!("missing {key}"))
3016 };
3017
3018 assert_eq!(
3019 entry(crate::layer_split_pipeline::LAYER_SPLIT_PIPELINE_MODE_KEY).effective_value,
3020 "batch"
3021 );
3022 assert_eq!(entry("FERRUM_MAX_MODEL_LEN").effective_value, "4096");
3023 assert_eq!(entry("FERRUM_KV_MAX_BLOCKS").effective_value, "1024");
3024 assert_eq!(entry("FERRUM_KV_CAPACITY").effective_value, "1024");
3025 assert_eq!(entry("FERRUM_PAGED_MAX_SEQS").effective_value, "16");
3026 assert_eq!(entry("FERRUM_MAX_BATCHED_TOKENS").effective_value, "1536");
3027 assert_eq!(
3028 entry("FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE").effective_value,
3029 "16"
3030 );
3031 assert_eq!(
3032 entry("FERRUM_PAGED_MAX_SEQS").source,
3033 RuntimeConfigSource::Default
3034 );
3035 }
3036
3037 #[test]
3038 fn runtime_preset_inference_uses_capability_rules() {
3039 let definition = qwen25_72b_gptq_definition();
3040 let selection = two_gpu_layer_split_selection();
3041 assert_eq!(
3042 infer_runtime_preset_for_startup(
3043 Some(ferrum_models::Architecture::Qwen2),
3044 Some(&definition),
3045 Some(&selection),
3046 ),
3047 Some(QWEN25_72B_GPTQ_INT4_2X4090_LAYER_SPLIT_PRESET)
3048 );
3049
3050 let mut one_gpu = selection.clone();
3051 one_gpu.selected_gpu_devices = vec![0];
3052 one_gpu.selected_distributed_strategy = "single_gpu".to_string();
3053 assert_eq!(
3054 infer_runtime_preset_for_startup(
3055 Some(ferrum_models::Architecture::Qwen2),
3056 Some(&definition),
3057 Some(&one_gpu),
3058 ),
3059 None
3060 );
3061 }
3062
3063 #[test]
3064 fn qwen3_moe_serve_defaults_are_typed_default_entries() {
3065 let entries = crate::runtime_env::moe_graph_default_entries(
3066 &RuntimeConfigSnapshot::default(),
3067 RuntimeConfigSource::Default,
3068 );
3069 let snapshot = RuntimeConfigSnapshot::from_entries(entries);
3070 let entry = |key: &str| {
3071 snapshot
3072 .entries
3073 .iter()
3074 .find(|entry| entry.key == key)
3075 .unwrap_or_else(|| panic!("missing {key}"))
3076 };
3077
3078 assert_eq!(entry("FERRUM_MOE_GRAPH").effective_value, "0");
3079 assert_eq!(
3080 entry("FERRUM_MOE_GRAPH").source,
3081 RuntimeConfigSource::Default
3082 );
3083 assert_eq!(snapshot.entries.len(), 1);
3084 }
3085
3086 #[test]
3087 fn qwen3_moe_serve_defaults_keep_config_file_overrides() {
3088 let config_entries = crate::config::RuntimeCliConfig {
3089 moe_graph: Some(false),
3090 vllm_moe: Some(false),
3091 ..Default::default()
3092 }
3093 .runtime_config_entries();
3094 let current = RuntimeConfigSnapshot::from_entries(config_entries.clone());
3095 let mut entries =
3096 crate::runtime_env::moe_graph_default_entries(¤t, RuntimeConfigSource::Default);
3097 entries.extend(config_entries);
3098 let snapshot = RuntimeConfigSnapshot::from_entries(entries);
3099 let entry = |key: &str| {
3100 snapshot
3101 .entries
3102 .iter()
3103 .find(|entry| entry.key == key)
3104 .unwrap_or_else(|| panic!("missing {key}"))
3105 };
3106
3107 assert_eq!(entry("FERRUM_MOE_GRAPH").effective_value, "0");
3108 assert_eq!(
3109 entry("FERRUM_MOE_GRAPH").source,
3110 RuntimeConfigSource::ConfigFile
3111 );
3112 assert_eq!(entry("FERRUM_VLLM_MOE").effective_value, "0");
3113 assert_eq!(
3114 entry("FERRUM_VLLM_MOE").source,
3115 RuntimeConfigSource::ConfigFile
3116 );
3117 }
3118
3119 #[test]
3120 fn model_inferred_m3_preset_keeps_config_file_overrides() {
3121 let mut inferred_entries =
3122 runtime_preset_entries(M3_QWEN3_30B_A3B_INT4_PRESET, RuntimeConfigSource::Default)
3123 .unwrap();
3124 inferred_entries.extend(
3125 crate::config::RuntimeCliConfig {
3126 prefix_cache: Some(true),
3127 kv_max_blocks: Some(4096),
3128 ..Default::default()
3129 }
3130 .runtime_config_entries(),
3131 );
3132 let snapshot = RuntimeConfigSnapshot::from_entries(inferred_entries);
3133 let entry = |key: &str| {
3134 snapshot
3135 .entries
3136 .iter()
3137 .find(|entry| entry.key == key)
3138 .unwrap_or_else(|| panic!("missing {key}"))
3139 };
3140
3141 assert_eq!(entry("FERRUM_PREFIX_CACHE").effective_value, "1");
3142 assert_eq!(
3143 entry("FERRUM_PREFIX_CACHE").source,
3144 RuntimeConfigSource::ConfigFile
3145 );
3146 assert_eq!(entry("FERRUM_KV_MAX_BLOCKS").effective_value, "4096");
3147 assert_eq!(
3148 entry("FERRUM_KV_MAX_BLOCKS").source,
3149 RuntimeConfigSource::ConfigFile
3150 );
3151 assert_eq!(
3152 entry("FERRUM_MOE_GRAPH").source,
3153 RuntimeConfigSource::Default
3154 );
3155 }
3156
3157 #[test]
3158 fn materialized_inferred_m3_entries_keep_default_source() {
3159 let inferred_entries =
3160 runtime_preset_entries(M3_QWEN3_30B_A3B_INT4_PRESET, RuntimeConfigSource::Default)
3161 .unwrap();
3162 let materialized_keys = inferred_entries
3163 .iter()
3164 .map(|entry| entry.key.clone())
3165 .collect::<Vec<_>>();
3166 let env_snapshot = RuntimeConfigSnapshot::from_entries([
3167 RuntimeConfigEntry::new("FERRUM_MOE_GRAPH", "1", RuntimeConfigSource::Env),
3168 RuntimeConfigEntry::new("FERRUM_VLLM_MOE", "1", RuntimeConfigSource::Env),
3169 ]);
3170 let env_snapshot = remove_materialized_config_env_entries(env_snapshot, &materialized_keys);
3171 let snapshot = merge_runtime_config_sources(inferred_entries, env_snapshot, Vec::new());
3172 let entry = |key: &str| {
3173 snapshot
3174 .entries
3175 .iter()
3176 .find(|entry| entry.key == key)
3177 .unwrap_or_else(|| panic!("missing {key}"))
3178 };
3179
3180 assert_eq!(
3181 entry("FERRUM_MOE_GRAPH").source,
3182 RuntimeConfigSource::Default
3183 );
3184 assert_eq!(
3185 entry("FERRUM_VLLM_MOE").source,
3186 RuntimeConfigSource::Default
3187 );
3188 }
3189
3190 #[test]
3191 fn runtime_config_fields_override_preset_defaults_before_env() {
3192 let preset_entries =
3193 runtime_preset_entries(M3_QWEN3_30B_A3B_INT4_PRESET, RuntimeConfigSource::Cli).unwrap();
3194 let config_entries = crate::config::RuntimeCliConfig {
3195 prefix_cache: Some(true),
3196 kv_max_blocks: Some(4096),
3197 ..Default::default()
3198 }
3199 .runtime_config_entries();
3200 let mut non_env_entries = preset_entries;
3201 non_env_entries.extend(config_entries);
3202 let snapshot = RuntimeConfigSnapshot::from_entries(non_env_entries);
3203 let entry = |key: &str| {
3204 snapshot
3205 .entries
3206 .iter()
3207 .find(|entry| entry.key == key)
3208 .unwrap_or_else(|| panic!("missing {key}"))
3209 };
3210
3211 assert_eq!(entry("FERRUM_PREFIX_CACHE").effective_value, "1");
3212 assert_eq!(
3213 entry("FERRUM_PREFIX_CACHE").source,
3214 RuntimeConfigSource::ConfigFile
3215 );
3216 assert_eq!(entry("FERRUM_KV_MAX_BLOCKS").effective_value, "4096");
3217 assert_eq!(
3218 entry("FERRUM_KV_MAX_BLOCKS").source,
3219 RuntimeConfigSource::ConfigFile
3220 );
3221 assert_eq!(entry("FERRUM_VLLM_MOE").effective_value, "1");
3222 assert_eq!(entry("FERRUM_VLLM_MOE").source, RuntimeConfigSource::Cli);
3223 }
3224
3225 #[test]
3226 fn serve_runtime_snapshot_prefers_env_over_config_file() {
3227 let config_entries = crate::config::RuntimeCliConfig {
3228 kv_dtype: Some("fp16".to_string()),
3229 ..Default::default()
3230 }
3231 .runtime_config_entries();
3232 let env_snapshot = RuntimeConfigSnapshot::from_entries([RuntimeConfigEntry::new(
3233 "FERRUM_KV_DTYPE",
3234 "int8",
3235 RuntimeConfigSource::Env,
3236 )]);
3237
3238 let snapshot = merge_runtime_config_sources(config_entries, env_snapshot, Vec::new());
3239 let kv = snapshot
3240 .entries
3241 .iter()
3242 .find(|entry| entry.key == "FERRUM_KV_DTYPE")
3243 .unwrap();
3244 assert_eq!(kv.effective_value, "int8");
3245 assert_eq!(kv.source, RuntimeConfigSource::Env);
3246 }
3247
3248 #[test]
3249 fn materialized_config_env_entries_keep_config_file_source() {
3250 let config_entries = crate::config::RuntimeCliConfig {
3251 prefix_cache: Some(true),
3252 ..Default::default()
3253 }
3254 .runtime_config_entries();
3255 let env_snapshot = RuntimeConfigSnapshot::from_entries([RuntimeConfigEntry::new(
3256 "FERRUM_PREFIX_CACHE",
3257 "1",
3258 RuntimeConfigSource::Env,
3259 )]);
3260 let env_snapshot = remove_materialized_config_env_entries(
3261 env_snapshot,
3262 &[String::from("FERRUM_PREFIX_CACHE")],
3263 );
3264
3265 let snapshot = merge_runtime_config_sources(config_entries, env_snapshot, Vec::new());
3266 let prefix_cache = snapshot
3267 .entries
3268 .iter()
3269 .find(|entry| entry.key == "FERRUM_PREFIX_CACHE")
3270 .unwrap();
3271 assert_eq!(prefix_cache.effective_value, "1");
3272 assert_eq!(prefix_cache.source, RuntimeConfigSource::ConfigFile);
3273 }
3274
3275 #[test]
3276 fn serve_runtime_snapshot_prefers_cli_over_env() {
3277 let env_snapshot = RuntimeConfigSnapshot::from_entries([RuntimeConfigEntry::new(
3278 "FERRUM_KV_DTYPE",
3279 "int8",
3280 RuntimeConfigSource::Env,
3281 )]);
3282 let cli_entries = serve_cli_runtime_entries(
3283 Some("bf16"),
3284 None,
3285 None,
3286 None,
3287 None,
3288 None,
3289 None,
3290 None,
3291 None,
3292 None,
3293 None,
3294 None,
3295 None,
3296 None,
3297 None,
3298 None,
3299 None,
3300 None,
3301 None,
3302 None,
3303 None,
3304 None,
3305 None,
3306 );
3307
3308 let snapshot = merge_runtime_config_sources(Vec::new(), env_snapshot, cli_entries);
3309 let kv = snapshot
3310 .entries
3311 .iter()
3312 .find(|entry| entry.key == "FERRUM_KV_DTYPE")
3313 .unwrap();
3314 assert_eq!(kv.effective_value, "bf16");
3315 assert_eq!(kv.source, RuntimeConfigSource::Cli);
3316 }
3317
3318 #[test]
3319 fn prefix_cache_vllm_and_product_aliases_resolve_identically() {
3320 assert_eq!(
3321 prefix_cache_cli_override(true, false, false, false),
3322 Some(true)
3323 );
3324 assert_eq!(
3325 prefix_cache_cli_override(false, false, true, false),
3326 Some(true)
3327 );
3328 assert_eq!(
3329 prefix_cache_cli_override(false, true, false, false),
3330 Some(false)
3331 );
3332 assert_eq!(
3333 prefix_cache_cli_override(false, false, false, true),
3334 Some(false)
3335 );
3336
3337 let enabled_entries = serve_cli_runtime_entries(
3338 None,
3339 None,
3340 None,
3341 None,
3342 None,
3343 None,
3344 None,
3345 None,
3346 None,
3347 None,
3348 None,
3349 prefix_cache_cli_override(true, false, false, false),
3350 None,
3351 None,
3352 None,
3353 None,
3354 None,
3355 None,
3356 None,
3357 None,
3358 None,
3359 None,
3360 None,
3361 );
3362 let product_enabled_entries = serve_cli_runtime_entries(
3363 None,
3364 None,
3365 None,
3366 None,
3367 None,
3368 None,
3369 None,
3370 None,
3371 None,
3372 None,
3373 None,
3374 prefix_cache_cli_override(false, false, true, false),
3375 None,
3376 None,
3377 None,
3378 None,
3379 None,
3380 None,
3381 None,
3382 None,
3383 None,
3384 None,
3385 None,
3386 );
3387 let disabled_entries = serve_cli_runtime_entries(
3388 None,
3389 None,
3390 None,
3391 None,
3392 None,
3393 None,
3394 None,
3395 None,
3396 None,
3397 None,
3398 None,
3399 prefix_cache_cli_override(false, true, false, false),
3400 None,
3401 None,
3402 None,
3403 None,
3404 None,
3405 None,
3406 None,
3407 None,
3408 None,
3409 None,
3410 None,
3411 );
3412 let product_disabled_entries = serve_cli_runtime_entries(
3413 None,
3414 None,
3415 None,
3416 None,
3417 None,
3418 None,
3419 None,
3420 None,
3421 None,
3422 None,
3423 None,
3424 prefix_cache_cli_override(false, false, false, true),
3425 None,
3426 None,
3427 None,
3428 None,
3429 None,
3430 None,
3431 None,
3432 None,
3433 None,
3434 None,
3435 None,
3436 );
3437
3438 assert_eq!(enabled_entries, product_enabled_entries);
3439 assert_eq!(disabled_entries, product_disabled_entries);
3440 }
3441
3442 #[test]
3443 fn batched_graph_cli_override_records_flag_state() {
3444 assert_eq!(batched_graph_cli_override(true, false), Some(true));
3445 assert_eq!(batched_graph_cli_override(false, true), Some(false));
3446 assert_eq!(batched_graph_cli_override(false, false), None);
3447 }
3448
3449 #[test]
3450 fn effective_kv_dtype_precedence_is_cli_env_config() {
3451 assert_eq!(
3452 resolve_effective_kv_dtype(Some("bf16"), Some("int8"), Some("fp16")),
3453 Some("bf16")
3454 );
3455 assert_eq!(
3456 resolve_effective_kv_dtype(None, Some("int8"), Some("fp16")),
3457 Some("int8")
3458 );
3459 assert_eq!(
3460 resolve_effective_kv_dtype(None, None, Some("fp16")),
3461 Some("fp16")
3462 );
3463 assert_eq!(resolve_effective_kv_dtype(None, None, None), None);
3464 }
3465}