Skip to main content

ferrum_cli/commands/
serve.rs

1//! Serve command - Start the HTTP inference server
2
3use 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    /// Model to serve (default: from config)
27    #[arg(value_name = "MODEL")]
28    pub model: Option<String>,
29
30    /// Model to serve (default: from config)
31    #[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    /// Public OpenAI-compatible model names. The first name is primary and
43    /// additional names are aliases for the same loaded model.
44    #[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    /// Enable model reasoning by default when a request omits
53    /// `chat_template_kwargs.enable_thinking`.
54    #[arg(long, conflicts_with = "disable_thinking")]
55    pub enable_thinking: bool,
56
57    /// Disable model reasoning by default when a request omits
58    /// `chat_template_kwargs.enable_thinking`.
59    #[arg(long, conflicts_with = "enable_thinking")]
60    pub disable_thinking: bool,
61
62    /// Host to bind to
63    #[arg(long)]
64    pub host: Option<String>,
65
66    /// Port to listen on
67    #[arg(short, long)]
68    pub port: Option<u16>,
69
70    /// Number of TTS concurrent slots (default: 2)
71    #[arg(long, default_value = "2")]
72    pub tts_slots: usize,
73
74    /// Backend: auto, cpu, metal, cuda.
75    #[arg(long, default_value = "auto")]
76    pub backend: String,
77
78    /// CUDA GPU ids to use, comma-separated. Multi-GPU requests select
79    /// layer-split for supported Llama-family safetensors models.
80    #[arg(long, value_name = "IDS")]
81    pub gpu_devices: Option<String>,
82
83    /// Layer-split decode pipeline mode for multi-GPU CUDA serving.
84    #[arg(long, value_enum)]
85    pub layer_split_pipeline_mode: Option<crate::layer_split_pipeline::LayerSplitPipelineModeArg>,
86
87    /// Speculative decoding: draft model id (same family as target).
88    /// Example: `--spec-draft qwen3:0.6b` when serving `qwen3:4b`.
89    /// The draft model must share the tokenizer + vocabulary.
90    #[arg(long, value_name = "MODEL")]
91    pub spec_draft: Option<String>,
92
93    /// Number of speculative tokens per draft forward pass (default: 4).
94    /// Only active when --spec-draft is set.
95    #[arg(long, default_value = "4")]
96    pub spec_tokens: usize,
97
98    /// Fraction of GPU memory ferrum is allowed to use (mirrors vLLM's
99    /// `--gpu-memory-utilization`). Auto-sizes the KV pool to fit
100    /// weights + scratch + KV inside `total_mem * util`. Default 0.9.
101    /// Set 1.0 for an exclusive GPU; lower if you share the card.
102    #[arg(long, default_value = "0.9")]
103    pub gpu_memory_utilization: f32,
104
105    /// Exact device-wide memory budget available to runtime weights and
106    /// dynamic resources. This is the same typed ceiling used by `run`.
107    #[arg(long, value_name = "BYTES")]
108    pub runtime_memory_budget_bytes: Option<std::num::NonZeroUsize>,
109
110    /// vLLM-compatible alias for `FERRUM_MAX_MODEL_LEN`.
111    #[arg(long, value_name = "N")]
112    pub max_model_len: Option<usize>,
113
114    /// vLLM-compatible alias for `FERRUM_PAGED_MAX_SEQS`.
115    #[arg(long, value_name = "N")]
116    pub max_num_seqs: Option<usize>,
117
118    /// vLLM-compatible alias for `FERRUM_MAX_BATCHED_TOKENS`.
119    #[arg(long, value_name = "N")]
120    pub max_num_batched_tokens: Option<usize>,
121
122    /// Sequence fit gate used before prefill admission.
123    #[arg(long, value_enum)]
124    pub sequence_fit_policy: Option<crate::commands::SequenceFitPolicyArg>,
125
126    /// Prefer prefilling until this many requests are active before early decodes.
127    #[arg(long, value_name = "N")]
128    pub scheduler_prefill_first_until_active: Option<usize>,
129
130    /// Cap per-request scheduler prefill chunks before they enter the engine.
131    #[arg(long, value_name = "N")]
132    pub scheduler_prefill_step_chunk: Option<usize>,
133
134    /// Cap prefill chunks while decode requests are active.
135    #[arg(long, value_name = "N")]
136    pub scheduler_active_decode_prefill_chunk: Option<usize>,
137
138    /// Enable prefix caching (`FERRUM_PREFIX_CACHE=1`).
139    #[arg(
140        long,
141        conflicts_with_all = ["no_enable_prefix_caching", "disable_prefix_cache"]
142    )]
143    pub enable_prefix_caching: bool,
144
145    /// Disable prefix caching (`FERRUM_PREFIX_CACHE=0`).
146    #[arg(long, conflicts_with = "enable_prefix_cache")]
147    pub no_enable_prefix_caching: bool,
148
149    /// Enable prefix cache (`FERRUM_PREFIX_CACHE=1`).
150    #[arg(
151        long,
152        conflicts_with_all = ["no_enable_prefix_caching", "disable_prefix_cache"]
153    )]
154    pub enable_prefix_cache: bool,
155
156    /// Disable prefix cache (`FERRUM_PREFIX_CACHE=0`).
157    #[arg(long, conflicts_with_all = ["enable_prefix_caching", "enable_prefix_cache"])]
158    pub disable_prefix_cache: bool,
159
160    /// Session cache mode (`off` or `memory`).
161    #[arg(long, value_name = "MODE", value_parser = ["off", "memory"])]
162    pub session_cache: Option<String>,
163
164    /// Maximum in-memory session cache entries.
165    #[arg(long, value_name = "N")]
166    pub session_cache_max_entries: Option<usize>,
167
168    /// Approximate maximum tokens retained per session.
169    #[arg(long, value_name = "N")]
170    pub session_cache_max_tokens: Option<usize>,
171
172    /// KV cache element dtype (Dim 5 polymorphism point). Accepts
173    /// `fp16`, `bf16`, `int8`, `fp8`. Default `fp16`. INT8 / FP8
174    /// require model wire-up; today only the kernel + type layer ships.
175    /// Override via `FERRUM_KV_DTYPE` env var.
176    #[arg(long, value_name = "DTYPE")]
177    pub kv_dtype: Option<String>,
178
179    /// Per-sequence KV token capacity (`FERRUM_KV_CAPACITY`).
180    #[arg(long, value_name = "N")]
181    pub kv_capacity: Option<usize>,
182
183    /// Global KV block budget (`FERRUM_KV_MAX_BLOCKS`).
184    #[arg(long, value_name = "N")]
185    pub kv_max_blocks: Option<usize>,
186
187    /// Use GPU argmax for greedy decoding (`FERRUM_GREEDY_ARGMAX=1`).
188    #[arg(long, conflicts_with = "disable_greedy_argmax")]
189    pub greedy_argmax: bool,
190
191    /// Disable GPU argmax for greedy decoding (`FERRUM_GREEDY_ARGMAX=0`).
192    #[arg(long, conflicts_with = "greedy_argmax")]
193    pub disable_greedy_argmax: bool,
194
195    /// Enable legacy Llama/Gemma batched decode CUDA graph replay.
196    #[arg(long, conflicts_with = "disable_batched_graph")]
197    pub batched_graph: bool,
198
199    /// Disable legacy Llama/Gemma batched decode CUDA graph replay.
200    #[arg(long, conflicts_with = "batched_graph")]
201    pub disable_batched_graph: bool,
202
203    /// Enable vNext reusable device-program preparation.
204    #[arg(long, conflicts_with = "disable_reusable_execution")]
205    pub reusable_execution: bool,
206
207    /// Disable vNext reusable device-program preparation.
208    #[arg(long, conflicts_with = "reusable_execution")]
209    pub disable_reusable_execution: bool,
210
211    /// Enable Llama/Gemma unified decode CUDA graph replay.
212    #[arg(long, conflicts_with = "disable_unified_graph")]
213    pub unified_graph: bool,
214
215    /// Disable Llama/Gemma unified decode CUDA graph replay.
216    #[arg(long, conflicts_with = "unified_graph")]
217    pub disable_unified_graph: bool,
218
219    /// Capture only Llama/Gemma unified transformer layers in CUDA graph replay.
220    #[arg(long, conflicts_with = "disable_unified_graph_layers_only")]
221    pub unified_graph_layers_only: bool,
222
223    /// Disable layers-only unified CUDA graph capture scope.
224    #[arg(long, conflicts_with = "unified_graph_layers_only")]
225    pub disable_unified_graph_layers_only: bool,
226
227    /// Capture unified layers plus final packing; leave lm_head eager.
228    #[arg(long, conflicts_with = "disable_unified_graph_lm_head_eager")]
229    pub unified_graph_lm_head_eager: bool,
230
231    /// Disable lm-head-eager unified CUDA graph capture scope.
232    #[arg(long, conflicts_with = "unified_graph_lm_head_eager")]
233    pub disable_unified_graph_lm_head_eager: bool,
234
235    /// Named startup/runtime preset, for example
236    /// `m3_qwen3_30b_a3b_int4`.
237    #[arg(long, value_name = "PRESET")]
238    pub runtime_preset: Option<String>,
239
240    /// Write the startup effective runtime config JSON artifact.
241    #[arg(long, value_name = "PATH")]
242    pub effective_config_json: Option<PathBuf>,
243
244    /// Write the startup auto-config decision trace JSONL artifact.
245    #[arg(long, value_name = "PATH")]
246    pub decision_trace_jsonl: Option<PathBuf>,
247
248    /// Generate a synthetic/no-weight observability vertical-slice artifact and exit.
249    #[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    /// Write native structured profile events to this JSONL path.
256    #[arg(long, value_name = "PATH")]
257    pub profile_jsonl: Option<PathBuf>,
258
259    /// Product observability detail level.
260    #[arg(long, value_enum, default_value_t = crate::observability_product::ProfileDetailArg::Off)]
261    pub profile_detail: crate::observability_product::ProfileDetailArg,
262
263    /// Inject one typed vNext diagnostic fault. Requires a latency profile.
264    #[arg(long, value_enum)]
265    pub vnext_diagnostic_fault: Option<crate::commands::VNextDiagnosticFaultArg>,
266
267    /// Write product memory profile events to this JSONL path.
268    #[arg(long, value_name = "PATH")]
269    pub memory_profile_jsonl: Option<PathBuf>,
270
271    /// Write scheduler iteration trace events to this JSONL path.
272    #[arg(long, value_name = "PATH")]
273    pub scheduler_trace_jsonl: Option<PathBuf>,
274
275    /// Write a sanitized request/replay bundle to this directory.
276    #[arg(long, value_name = "DIR")]
277    pub request_dump_dir: Option<PathBuf>,
278
279    /// Product observability sampling rate for resource lifecycle events.
280    #[arg(long, default_value_t = crate::observability_product::default_profile_sample_rate())]
281    pub profile_sample_rate: f64,
282
283    /// Git commit stamped into native structured profile events.
284    #[arg(long, value_name = "SHA")]
285    pub profile_commit_sha: Option<String>,
286
287    /// Runtime environment hash stamped into native structured profile events.
288    #[arg(long, value_name = "SHA256")]
289    pub profile_env_hash: Option<String>,
290
291    /// Model label stamped into native structured profile events.
292    #[arg(long, value_name = "MODEL")]
293    pub profile_model: Option<String>,
294
295    /// Concurrency stamped into native structured profile events.
296    #[arg(long, value_name = "N")]
297    pub profile_concurrency: Option<u32>,
298
299    /// Runtime flags/config JSON object embedded in native profile events.
300    #[arg(long, value_name = "JSON")]
301    pub profile_runtime_flags_json: Option<String>,
302
303    /// Startup-loaded LoRA adapter, formatted as NAME=PATH. May be repeated.
304    #[arg(long = "lora", value_name = "NAME=PATH")]
305    pub lora: Vec<String>,
306
307    /// Public model id template for LoRA adapters. Supports <base> and <name>.
308    #[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    // Reject malformed adapter specs before selecting a device or resolving,
389    // downloading, and preparing the base model.
390    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    // Resolve model
405    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    // Select the requested device before model/cache resolution. Explicit
454    // backend requests must fail closed instead of doing model work and then
455    // silently running on CPU.
456    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
477    print_banner();
478
479    // `run` and `serve` share one source decision. This covers exact GGUF
480    // aliases/files, local model directories, HF cache hits, and resumable HF
481    // download without an entrypoint-specific fallback chain.
482    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    // Speculative decoding draft model: resolve the draft path and pass it
594    // through EngineConfig backend options. Validates that the draft model is
595    // actually cached before the target load kicks in.
596    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    // Detect architecture to choose engine type. For GGUF we skip
660    // ConfigManager::load_from_path (which expects HF safetensors layout)
661    // and route directly to the continuous-batching LLM engine — the
662    // engine's LlmExecutorFactory uses WeightFormat::detect() to route GGUF.
663    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    // Materialize the multi-GPU layer-split plan. The safetensors path
679    // gets the layer count from ModelDefinition; the GGUF path reads it
680    // from the file header — without this the placeholder plan
681    // (`layers=auto`) reaches the engine and is rejected.
682    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    // Preserve the historical non-preset serve default for Qwen3-MoE, but
724    // route it through the typed startup snapshot instead of a hidden
725    // process-wide env mutation. M3 explicit or model-inferred presets have
726    // already materialized the same graph-clean defaults above.
727    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            &current_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    // GPU-memory auto-sizing must run after the model source resolves.
841    // HF-cache models are not `local_dir_path`, but they still need the same
842    // KV block sizing as direct local safetensors directories.
843    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    // Create server config
1119    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    // Write PID file for stop command
1155    let pid_file = std::env::temp_dir().join("ferrum.pid");
1156    std::fs::write(&pid_file, std::process::id().to_string()).ok();
1157
1158    // Keep the server future alive while stop requests graceful HTTP drain.
1159    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    // Clean up PID file
1200    std::fs::remove_file(&pid_file).ok();
1201
1202    // PLAYBOOK § 1.5: Rust statics don't drop on exit, so the global
1203    // TraceWriter's buffered events would be lost. Force-flush on
1204    // ctrl_c-driven shutdown (matches bench / bench-serve exit paths).
1205    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
2262// `find_cached_model` and `detect_format` previously lived here as forks
2263// of the `run.rs` versions. They moved to `crate::source_resolver` so the
2264// HF cache walk + format detection have a single source of truth across
2265// `run` / `serve` / `bench`. Use `crate::source_resolver::find_cached_model`
2266// / `crate::source_resolver::detect_format` directly.
2267
2268fn 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(&current, 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}