metrale_storage/
config.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `HighSpeedSwapConfig`, the settings of the high-speed swap, and
4//! their range checks. `HighSpeedSwap::new_on_stream` runs
5//! `validate_and_prepare` before it builds anything.
6//!
7//! Owner: metrale-storage high-speed swap.
8//! Invariants: none beyond the types.
9
10use anyhow::{Result, bail};
11use serde::Deserialize;
12use std::path::PathBuf;
13
14#[derive(Clone, Debug, Deserialize)]
15pub struct HighSpeedSwapConfig {
16    /// 2026-09-25: Directory of the per-layer KV files.
17    pub dir: PathBuf,
18    /// 2026-09-25: Disk budget in bytes; the server passes
19    /// `--high-speed-swap-gb` × 2^30. `validate` requires it to be non-zero,
20    /// and nothing in this crate reads it otherwise.
21    pub bytes: u64,
22    /// 2026-09-25: Scratch-pool slot count; also the attention tile capacity
23    /// and the eviction policy's capacity.
24    pub resident_blocks: u32,
25    /// 2026-09-25: Predictor low-rank dimension r.
26    pub rank: u32,
27    /// 2026-09-25: io_uring queue depth; the portable backend ignores it.
28    pub qd: u32,
29    /// 2026-09-25: Capture the per-layer body in a CUDA graph and replay it.
30    /// The server sets it unless `--no-high-speed-swap-graph` is given.
31    pub graph: bool,
32    /// 2026-09-25: Seed of the predictor's projection matrix.
33    pub projection_seed: u64,
34}
35
36impl HighSpeedSwapConfig {
37    /// 2026-09-25: Range checks: `bytes` and `resident_blocks` non-zero, `rank`
38    /// in 1..=128, `qd` in 1..=64. The directory is not checked here.
39    pub fn validate(&self) -> Result<()> {
40        if self.bytes == 0 {
41            bail!("--high-speed-swap-bytes must be > 0");
42        }
43        if self.resident_blocks == 0 {
44            bail!("--high-speed-swap-resident-blocks must be > 0");
45        }
46        if self.rank == 0 || self.rank > 128 {
47            bail!(
48                "--high-speed-swap-rank must be in 1..=128, got {}",
49                self.rank
50            );
51        }
52        if self.qd == 0 || self.qd > 64 {
53            bail!("--high-speed-swap-qd must be in 1..=64, got {}", self.qd);
54        }
55        Ok(())
56    }
57
58    /// 2026-09-25: `validate`, then create the directory if it is missing.
59    pub fn validate_and_prepare(&self) -> Result<()> {
60        self.validate()?;
61        std::fs::create_dir_all(&self.dir)
62            .map_err(|e| anyhow::anyhow!("create {}: {e}", self.dir.display()))?;
63        // 2026-09-25: The server checks the directory against the
64        // `--swap-space-gb` path (`validate_head_high_speed_swap`).
65        Ok(())
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    fn cfg() -> HighSpeedSwapConfig {
74        HighSpeedSwapConfig {
75            dir: PathBuf::from("/tmp/metrale-hss-cfg"),
76            bytes: 64 << 30,
77            resident_blocks: 8192,
78            rank: 32,
79            qd: 8,
80            graph: true,
81            projection_seed: 0xCAFE_F00D,
82        }
83    }
84
85    #[test]
86    fn happy_path() {
87        cfg().validate().unwrap();
88    }
89
90    #[test]
91    fn rejects_zero_bytes() {
92        let mut c = cfg();
93        c.bytes = 0;
94        assert!(c.validate().is_err());
95    }
96
97    #[test]
98    fn rejects_zero_resident_blocks() {
99        let mut c = cfg();
100        c.resident_blocks = 0;
101        assert!(c.validate().is_err());
102    }
103
104    #[test]
105    fn rejects_out_of_range_rank() {
106        let mut c = cfg();
107        c.rank = 0;
108        assert!(c.validate().is_err());
109        c.rank = 129;
110        assert!(c.validate().is_err());
111    }
112
113    #[test]
114    fn rejects_out_of_range_qd() {
115        let mut c = cfg();
116        c.qd = 0;
117        assert!(c.validate().is_err());
118        c.qd = 65;
119        assert!(c.validate().is_err());
120    }
121}