ferrum-types 0.8.4

Shared type definitions for the Ferrum LLM inference engine
Documentation
use ferrum_types::*;

#[test]
fn engine_config_default_sane() {
    let cfg = EngineConfig::default();
    assert_eq!(cfg.model.model_id.as_str(), "default");
    assert!(cfg.batching.max_batch_size >= 1);
    assert_eq!(cfg.kv_cache.max_blocks, 2048);
    assert_eq!(cfg.batching.max_num_batched_tokens, 2048);
    assert!(cfg.scheduler.prompt_token_estimate);
    assert!(cfg.monitoring.enable_metrics);
    assert_eq!(cfg.memory.usable_capacity_bytes, None);
}

#[test]
fn memory_config_resolves_exact_usable_capacity_without_float_rounding() {
    let mut memory = MemoryConfig::default();
    memory.usable_capacity_bytes = Some(777);

    let budget = memory.resolve_capacity_budget(1_000).unwrap();

    assert_eq!(budget.capacity_bytes, 1_000);
    assert_eq!(budget.usable_capacity_bytes, 777);
    assert_eq!(budget.reserve_bytes, 223);
}

#[test]
fn memory_config_rejects_exact_usable_capacity_above_pool_ceiling() {
    let mut memory = MemoryConfig::default();
    memory.pool_size = Some(512);
    memory.usable_capacity_bytes = Some(513);

    let error = memory.resolve_capacity_budget(1_000).unwrap_err();

    assert!(error.contains("1..=512"));
}

#[test]
fn memory_config_preserves_threshold_based_default_budget() {
    let memory = MemoryConfig::default();

    let budget = memory.resolve_capacity_budget(1_000).unwrap();

    assert_eq!(budget.capacity_bytes, 1_000);
    // Preserve the historical f32 threshold behavior exactly. Callers that
    // need byte-exact capacity use `usable_capacity_bytes` instead.
    assert_eq!(budget.usable_capacity_bytes, 949);
    assert_eq!(budget.reserve_bytes, 51);
}

#[test]
fn memory_config_missing_exact_budget_uses_backward_compatible_default() {
    let mut value = serde_json::to_value(MemoryConfig::default()).unwrap();
    value
        .as_object_mut()
        .unwrap()
        .remove("usable_capacity_bytes");

    let memory: MemoryConfig = serde_json::from_value(value).unwrap();

    assert_eq!(memory.usable_capacity_bytes, None);
}

#[test]
fn engine_config_applies_runtime_snapshot() {
    let snapshot = RuntimeConfigSnapshot::from_entries([
        RuntimeConfigEntry::new("FERRUM_KV_MAX_BLOCKS", "4096", RuntimeConfigSource::Env),
        RuntimeConfigEntry::new(
            "FERRUM_MAX_BATCHED_TOKENS",
            "8192",
            RuntimeConfigSource::Env,
        ),
        RuntimeConfigEntry::new("FERRUM_PAGED_MAX_SEQS", "7", RuntimeConfigSource::Env),
        RuntimeConfigEntry::new(
            "FERRUM_RUNTIME_MEMORY_BUDGET_BYTES",
            "12345",
            RuntimeConfigSource::Cli,
        ),
        RuntimeConfigEntry::new(
            "FERRUM_SCHED_PROMPT_TOKEN_ESTIMATE",
            "1",
            RuntimeConfigSource::Env,
        ),
        RuntimeConfigEntry::new("FERRUM_BATCHED_GRAPH", "1", RuntimeConfigSource::Cli),
        RuntimeConfigEntry::new("FERRUM_REUSABLE_EXECUTION", "0", RuntimeConfigSource::Cli),
        RuntimeConfigEntry::new(
            "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS",
            "1,3,7",
            RuntimeConfigSource::ConfigFile,
        ),
        RuntimeConfigEntry::new(
            "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH",
            "24",
            RuntimeConfigSource::ConfigFile,
        ),
        RuntimeConfigEntry::new(
            "FERRUM_SCHEDULER_TRACE_JSONL",
            "/tmp/sched.jsonl",
            RuntimeConfigSource::Env,
        ),
    ]);
    let mut cfg = EngineConfig::default();

    cfg.apply_runtime_config_snapshot(&snapshot).unwrap();

    assert_eq!(cfg.kv_cache.max_blocks, 4096);
    assert_eq!(cfg.batching.max_num_batched_tokens, 8192);
    assert_eq!(cfg.scheduler.max_running_requests, 7);
    assert_eq!(cfg.memory.usable_capacity_bytes, Some(12_345));
    assert!(cfg.scheduler.prompt_token_estimate);
    assert!(cfg.backend.enable_cuda_graphs);
    assert!(!cfg.backend.enable_reusable_execution);
    assert_eq!(
        cfg.backend.reusable_execution_capture.exact_decode_widths,
        Some(vec![1, 3, 7])
    );
    assert_eq!(
        cfg.backend
            .reusable_execution_capture
            .maximum_automatic_exact_decode_width,
        24
    );
    assert_eq!(
        cfg.runtime.scheduler_trace_jsonl.as_deref(),
        Some(std::path::Path::new("/tmp/sched.jsonl"))
    );
}

#[test]
fn engine_config_rejects_empty_reusable_exact_width_list() {
    let snapshot = RuntimeConfigSnapshot::from_entries([RuntimeConfigEntry::new(
        "FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS",
        "",
        RuntimeConfigSource::ConfigFile,
    )]);
    let mut cfg = EngineConfig::default();

    let error = cfg.apply_runtime_config_snapshot(&snapshot).unwrap_err();

    assert!(error.contains("FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS"));
}

#[test]
fn engine_config_rejects_reusable_capture_widths_above_independent_hard_bound() {
    for (key, value) in [
        ("FERRUM_REUSABLE_EXECUTION_EXACT_DECODE_WIDTHS", "1,32,33"),
        (
            "FERRUM_REUSABLE_EXECUTION_MAX_AUTOMATIC_EXACT_DECODE_WIDTH",
            "33",
        ),
    ] {
        let snapshot = RuntimeConfigSnapshot::from_entries([RuntimeConfigEntry::new(
            key,
            value,
            RuntimeConfigSource::ConfigFile,
        )]);
        let mut cfg = EngineConfig::default();

        let error = cfg.apply_runtime_config_snapshot(&snapshot).unwrap_err();

        assert!(error.contains(key));
        assert!(error.contains("1..=32"));
    }
}

#[test]
fn engine_config_rejects_invalid_runtime_snapshot() {
    let snapshot = RuntimeConfigSnapshot::from_entries([RuntimeConfigEntry::new(
        "FERRUM_KV_MAX_BLOCKS",
        "0",
        RuntimeConfigSource::Env,
    )]);
    let mut cfg = EngineConfig::default();

    let err = cfg.apply_runtime_config_snapshot(&snapshot).unwrap_err();

    assert!(err.contains("FERRUM_KV_MAX_BLOCKS"));
}

#[test]
fn engine_config_applies_build_composition_knobs() {
    // FERRUM_MODEL_PATH / SPEC_* / DTYPE / METAL_DTYPE / TP used to be read
    // straight from env by builder.rs and registry.rs. They now land in
    // EngineConfig.runtime here; the builder/registry read the typed field.
    let snapshot = RuntimeConfigSnapshot::from_entries([
        RuntimeConfigEntry::new(
            "FERRUM_MODEL_PATH",
            "/models/target",
            RuntimeConfigSource::Env,
        ),
        RuntimeConfigEntry::new(
            "FERRUM_SPEC_DRAFT",
            "/models/draft",
            RuntimeConfigSource::Env,
        ),
        RuntimeConfigEntry::new("FERRUM_SPEC_N", "8", RuntimeConfigSource::Env),
        RuntimeConfigEntry::new("FERRUM_DTYPE", "fp32", RuntimeConfigSource::Env),
        RuntimeConfigEntry::new("FERRUM_METAL_DTYPE", "fp16", RuntimeConfigSource::Env),
        RuntimeConfigEntry::new("FERRUM_TP", "4", RuntimeConfigSource::Env),
    ]);
    let mut cfg = EngineConfig::default();

    cfg.apply_runtime_config_snapshot(&snapshot).unwrap();

    assert_eq!(cfg.runtime.model_path.as_deref(), Some("/models/target"));
    assert_eq!(cfg.runtime.spec_draft.as_deref(), Some("/models/draft"));
    assert_eq!(cfg.runtime.spec_n, Some(8));
    assert_eq!(cfg.runtime.dtype.as_deref(), Some("fp32"));
    assert_eq!(cfg.runtime.metal_dtype.as_deref(), Some("fp16"));
    assert_eq!(cfg.runtime.tp, Some(4));
}

#[test]
fn engine_config_build_knobs_ignore_empty_spec_draft() {
    // Empty FERRUM_SPEC_DRAFT must resolve to None (disabled), matching the
    // old builder env parse that treated "" as "no draft".
    let snapshot = RuntimeConfigSnapshot::from_entries([
        RuntimeConfigEntry::new("FERRUM_SPEC_DRAFT", "", RuntimeConfigSource::Env),
        RuntimeConfigEntry::new("FERRUM_SPEC_N", "not-a-number", RuntimeConfigSource::Env),
    ]);
    let mut cfg = EngineConfig::default();

    cfg.apply_runtime_config_snapshot(&snapshot).unwrap();

    assert_eq!(cfg.runtime.spec_draft, None);
    assert_eq!(cfg.runtime.spec_n, None);
}

#[test]
fn scheduler_config_default_sane() {
    let cfg = SchedulerConfig::default();
    assert!(matches!(cfg.policy, SchedulingPolicy::Priority));
    assert!(cfg.max_waiting_requests >= 1);
    assert!(cfg.prompt_token_estimate);
    assert_eq!(cfg.prefill_first_until_active, None);
    assert_eq!(cfg.active_decode_prefill_chunk, None);
}

#[test]
fn scheduler_config_missing_prompt_token_estimate_uses_default_true() {
    let mut value = serde_json::to_value(SchedulerConfig::default()).unwrap();
    value
        .as_object_mut()
        .unwrap()
        .remove("prompt_token_estimate");

    let cfg: SchedulerConfig = serde_json::from_value(value).unwrap();

    assert!(cfg.prompt_token_estimate);
}

#[test]
fn scheduler_config_applies_runtime_snapshot() {
    let snapshot = RuntimeConfigSnapshot::from_entries([
        RuntimeConfigEntry::new(
            "FERRUM_SCHED_PROMPT_TOKEN_ESTIMATE",
            "1",
            RuntimeConfigSource::Env,
        ),
        RuntimeConfigEntry::new(
            "FERRUM_SCHED_PREFILL_FIRST_UNTIL_ACTIVE",
            "4",
            RuntimeConfigSource::Env,
        ),
        RuntimeConfigEntry::new(
            "FERRUM_ACTIVE_DECODE_PREFILL_CHUNK",
            "64",
            RuntimeConfigSource::Env,
        ),
        RuntimeConfigEntry::new("FERRUM_SCHED_NONE_PROF", "", RuntimeConfigSource::Env),
    ]);
    let mut cfg = SchedulerConfig::default();

    cfg.apply_runtime_config_snapshot(&snapshot).unwrap();

    assert!(cfg.prompt_token_estimate);
    assert_eq!(cfg.prefill_first_until_active, Some(4));
    assert_eq!(cfg.active_decode_prefill_chunk, Some(64));
    assert!(cfg.scheduler_none_prof);
}

#[test]
fn scheduler_config_rejects_invalid_runtime_snapshot() {
    let snapshot = RuntimeConfigSnapshot::from_entries([RuntimeConfigEntry::new(
        "FERRUM_ACTIVE_DECODE_PREFILL_CHUNK",
        "not-a-number",
        RuntimeConfigSource::Env,
    )]);
    let mut cfg = SchedulerConfig::default();

    let err = cfg.apply_runtime_config_snapshot(&snapshot).unwrap_err();

    assert!(err.contains("FERRUM_ACTIVE_DECODE_PREFILL_CHUNK"));
}

#[test]
fn kv_cache_config_default_sane() {
    let cfg = KvCacheConfig::default();
    assert!(matches!(cfg.cache_type, KvCacheType::Contiguous));
    assert!(cfg.block_size > 0);
}

#[test]
fn memory_config_default_sane() {
    let cfg = MemoryConfig::default();
    assert!(cfg.alignment >= 1);
}

#[test]
fn backend_config_default_sane() {
    let cfg = BackendConfig::default();
    assert!(matches!(cfg.backend_type, BackendType::Candle));
    assert!(cfg.enable_optimizations);
    assert!(cfg.enable_reusable_execution);
    assert_eq!(cfg.reusable_execution_capture.exact_decode_widths, None);
    assert_eq!(
        cfg.reusable_execution_capture
            .maximum_automatic_exact_decode_width,
        DEFAULT_MAXIMUM_AUTOMATIC_EXACT_DECODE_WIDTH
    );
    assert_eq!(
        DEFAULT_MAXIMUM_AUTOMATIC_EXACT_DECODE_WIDTH,
        MAXIMUM_REUSABLE_EXECUTION_STARTUP_CAPTURE_WIDTH
    );
}

#[test]
fn backend_config_without_capture_policy_uses_backward_compatible_default() {
    let mut serialized = serde_json::to_value(BackendConfig::default()).unwrap();
    serialized
        .as_object_mut()
        .unwrap()
        .remove("reusable_execution_capture");

    let cfg: BackendConfig = serde_json::from_value(serialized).unwrap();

    assert_eq!(
        cfg.reusable_execution_capture,
        ReusableExecutionCaptureConfig::default()
    );
}

#[test]
fn reusable_execution_capture_partial_config_keeps_automatic_safety_default() {
    let capture: ReusableExecutionCaptureConfig = serde_json::from_value(serde_json::json!({
        "exact_decode_widths": [1, 2, 4, 8, 16, 24, 32]
    }))
    .unwrap();

    assert_eq!(
        capture.exact_decode_widths,
        Some(vec![1, 2, 4, 8, 16, 24, 32])
    );
    assert_eq!(
        capture.maximum_automatic_exact_decode_width,
        DEFAULT_MAXIMUM_AUTOMATIC_EXACT_DECODE_WIDTH
    );
}

#[test]
fn reusable_execution_capture_explicit_policy_round_trips() {
    let expected = ReusableExecutionCaptureConfig {
        exact_decode_widths: Some(vec![1, 3, 7, 32]),
        maximum_automatic_exact_decode_width: 16,
    };

    let serialized = serde_json::to_value(&expected).unwrap();
    let actual: ReusableExecutionCaptureConfig = serde_json::from_value(serialized).unwrap();

    assert_eq!(actual, expected);
}

#[test]
fn sampling_presets_contains_expected() {
    let presets = SamplingPresets::default();
    assert!(presets.presets.contains_key("greedy"));
    assert!(presets.presets.get("creative").is_some());
    assert!(presets.presets.get("precise").is_some());
}