Skip to main content

gglib_core/
cache_config.rs

1//! KV-cache configuration types: quantized cache types and the host-RAM
2//! prompt cache setting.
3//!
4//! Kept as a standalone, low-complexity module (not folded into
5//! `server_config`) so cache-related config resolution has one home.
6
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10
11// =============================================================================
12// Host-RAM prompt cache budget setting (`--cache-ram`)
13// =============================================================================
14
15/// How to determine the host-RAM prompt cache budget (`--cache-ram`).
16///
17/// Deliberately a two-state enum rather than `Option<u64>`: benchmark
18/// launches (which must never gain a prompt cache — it would perturb
19/// throughput measurements and RAM footprint) pass `ExplicitMb(0)`, which
20/// unambiguously disables the cache rather than leaving it to an implicit
21/// "no value" state.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum CacheRamSetting {
24    /// Compute a budget from system RAM, model size, and the KV estimate.
25    /// The default variant — every launch surface auto-sizes unless it
26    /// opts out.
27    #[default]
28    Auto,
29    /// Use exactly this MiB value. `0` disables the cache.
30    ExplicitMb(u64),
31}
32
33// =============================================================================
34// KV cache quantization type (`--cache-type-k` / `--cache-type-v`)
35// =============================================================================
36
37/// A llama.cpp KV-cache element type, as accepted by `--cache-type-k` /
38/// `--cache-type-v`.
39///
40/// Quantized K types are supported unconditionally by llama.cpp; quantized V
41/// types additionally require Flash Attention to be active (llama.cpp
42/// hard-errors at startup otherwise — see `resolve_kv_cache_types` in
43/// `gglib-runtime` for the escape hatches).
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum KvCacheType {
46    F32,
47    F16,
48    Bf16,
49    Q8_0,
50    Q5_1,
51    Q5_0,
52    Q4_1,
53    Q4_0,
54}
55
56impl KvCacheType {
57    /// Every accepted type, in the order they should be offered to a user.
58    ///
59    /// The one list: [`FromStr`] parses against it, its error message names
60    /// it, and the CLI builds `--cache-type-k`/`-v`'s possible values from
61    /// it. Adding a variant here is enough to make it parse, appear in
62    /// `--help` and appear in shell completions.
63    pub const ALL: &'static [Self] = &[
64        Self::F32,
65        Self::F16,
66        Self::Bf16,
67        Self::Q8_0,
68        Self::Q5_1,
69        Self::Q5_0,
70        Self::Q4_1,
71        Self::Q4_0,
72    ];
73
74    /// The value passed on the command line (matches llama.cpp's own
75    /// `ggml_type_name`).
76    #[must_use]
77    pub const fn as_llama_arg(self) -> &'static str {
78        match self {
79            Self::F32 => "f32",
80            Self::F16 => "f16",
81            Self::Bf16 => "bf16",
82            Self::Q8_0 => "q8_0",
83            Self::Q5_1 => "q5_1",
84            Self::Q5_0 => "q5_0",
85            Self::Q4_1 => "q4_1",
86            Self::Q4_0 => "q4_0",
87        }
88    }
89
90    /// `(block_bytes, block_elems)` — the on-disk/in-memory layout ggml uses
91    /// for this type. Unquantized types are a trivial one-element block;
92    /// quantized types pack `block_elems` values into `block_bytes` bytes
93    /// (a shared scale/min plus packed sub-byte values).
94    #[must_use]
95    pub const fn block_layout(self) -> (u64, u64) {
96        match self {
97            Self::F32 => (4, 1),
98            Self::F16 | Self::Bf16 => (2, 1),
99            Self::Q8_0 => (34, 32),
100            Self::Q5_1 => (24, 32),
101            Self::Q5_0 => (22, 32),
102            Self::Q4_1 => (20, 32),
103            Self::Q4_0 => (18, 32),
104        }
105    }
106
107    /// Estimated bytes to store `elems` elements at this type, rounding up
108    /// to whole blocks (a partial trailing block still costs a full block).
109    #[must_use]
110    pub const fn bytes_for_elems(self, elems: u64) -> u64 {
111        let (block_bytes, block_elems) = self.block_layout();
112        elems.div_ceil(block_elems) * block_bytes
113    }
114}
115
116impl FromStr for KvCacheType {
117    type Err = String;
118
119    fn from_str(s: &str) -> Result<Self, Self::Err> {
120        let normalized = s.trim().to_ascii_lowercase();
121        Self::ALL
122            .iter()
123            .find(|t| t.as_llama_arg() == normalized)
124            .copied()
125            .ok_or_else(|| {
126                let expected: Vec<&str> = Self::ALL.iter().map(|t| t.as_llama_arg()).collect();
127                format!(
128                    "unknown KV cache type {normalized:?} (expected one of: {})",
129                    expected.join(", ")
130                )
131            })
132    }
133}
134
135/// Default K cache type: quantized to roughly halve the KV footprint versus
136/// `f16`, doubling how much conversation history the RAM/disk prompt caches
137/// can hold.
138pub const DEFAULT_CACHE_TYPE_K: KvCacheType = KvCacheType::Q8_0;
139
140/// Default V cache type. Same rationale as [`DEFAULT_CACHE_TYPE_K`]; see the
141/// module docs on the Flash Attention requirement for quantized V.
142pub const DEFAULT_CACHE_TYPE_V: KvCacheType = KvCacheType::Q8_0;
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn as_llama_arg_matches_ggml_type_names() {
150        assert_eq!(KvCacheType::F16.as_llama_arg(), "f16");
151        assert_eq!(KvCacheType::Q8_0.as_llama_arg(), "q8_0");
152    }
153
154    #[test]
155    fn from_str_round_trips_through_as_llama_arg() {
156        for t in [
157            KvCacheType::F32,
158            KvCacheType::F16,
159            KvCacheType::Bf16,
160            KvCacheType::Q8_0,
161            KvCacheType::Q5_1,
162            KvCacheType::Q5_0,
163            KvCacheType::Q4_1,
164            KvCacheType::Q4_0,
165        ] {
166            assert_eq!(KvCacheType::from_str(t.as_llama_arg()), Ok(t));
167        }
168    }
169
170    #[test]
171    fn from_str_is_case_insensitive_and_trims() {
172        assert_eq!(KvCacheType::from_str(" Q8_0 "), Ok(KvCacheType::Q8_0));
173        assert_eq!(KvCacheType::from_str("F16"), Ok(KvCacheType::F16));
174    }
175
176    #[test]
177    fn from_str_rejects_unknown_type() {
178        assert!(KvCacheType::from_str("q2_k").is_err());
179    }
180
181    #[test]
182    fn f16_bytes_for_elems_is_two_bytes_per_element() {
183        assert_eq!(KvCacheType::F16.bytes_for_elems(1000), 2000);
184    }
185
186    #[test]
187    fn q8_0_bytes_for_elems_matches_ggml_block_layout() {
188        // Exactly one block: 32 elements -> 34 bytes.
189        assert_eq!(KvCacheType::Q8_0.bytes_for_elems(32), 34);
190        // Two full blocks: 64 elements -> 68 bytes.
191        assert_eq!(KvCacheType::Q8_0.bytes_for_elems(64), 68);
192    }
193
194    #[test]
195    fn bytes_for_elems_rounds_up_a_partial_trailing_block() {
196        // 33 elements needs 2 blocks of 32 at q8_0 -> 68 bytes, not 34+.
197        assert_eq!(KvCacheType::Q8_0.bytes_for_elems(33), 68);
198    }
199
200    #[test]
201    fn q8_0_is_smaller_than_f16_for_the_same_element_count() {
202        let elems = 65_536;
203        assert!(KvCacheType::Q8_0.bytes_for_elems(elems) < KvCacheType::F16.bytes_for_elems(elems));
204    }
205
206    #[test]
207    fn cache_ram_setting_default_is_auto() {
208        assert_eq!(CacheRamSetting::default(), CacheRamSetting::Auto);
209    }
210}