Skip to main content

kernel/profiles/
fit.rs

1//! Whether a model fits in a machine's memory: a coarse runs-well / tight-fit /
2//! too-large verdict from the model's footprint and the total RAM.
3
4/// How well a model is expected to run given available memory.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum FitVerdict {
7    /// Comfortable — well under the runs-well fraction of memory.
8    RunsWell,
9    /// Fits, but with little headroom.
10    TightFit,
11    /// Won't fit comfortably.
12    TooLarge,
13}
14
15/// A fit verdict plus the memory the model is estimated to require.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct FitAssessment {
18    /// The verdict.
19    pub verdict: FitVerdict,
20    /// The estimated required bytes (footprint × overhead).
21    pub required_bytes: i64,
22}
23
24impl FitVerdict {
25    /// Weights need working memory beyond the raw footprint.
26    const MEMORY_OVERHEAD_FACTOR: f64 = 1.25;
27    /// Below this share of memory a model runs well.
28    const RUNS_WELL_FRACTION: f64 = 0.75;
29    /// Below this share it's a tight fit; at or above it's too large.
30    const TIGHT_FIT_FRACTION: f64 = 0.95;
31
32    /// Assess a `footprint_mb` model against `total_memory_bytes`. Returns `None`
33    /// when the footprint is unknown/non-positive or the memory total is zero.
34    pub fn assess(footprint_mb: Option<i64>, total_memory_bytes: u64) -> Option<FitAssessment> {
35        let footprint_mb = footprint_mb.filter(|&mb| mb > 0)?;
36        if total_memory_bytes == 0 {
37            return None;
38        }
39        let required_bytes =
40            (footprint_mb as f64 * (1i64 << 20) as f64 * Self::MEMORY_OVERHEAD_FACTOR) as i64;
41        let share = required_bytes as f64 / total_memory_bytes as f64;
42        let verdict = if share < Self::RUNS_WELL_FRACTION {
43            FitVerdict::RunsWell
44        } else if share < Self::TIGHT_FIT_FRACTION {
45            FitVerdict::TightFit
46        } else {
47            FitVerdict::TooLarge
48        };
49        Some(FitAssessment {
50            verdict,
51            required_bytes,
52        })
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    const GIB: u64 = 1 << 30;
61
62    #[test]
63    fn an_unknown_or_empty_footprint_is_unassessable() {
64        assert!(FitVerdict::assess(None, 16 * GIB).is_none());
65        assert!(FitVerdict::assess(Some(0), 16 * GIB).is_none());
66        assert!(FitVerdict::assess(Some(-5), 16 * GIB).is_none());
67        assert!(FitVerdict::assess(Some(1000), 0).is_none());
68    }
69
70    #[test]
71    fn the_verdict_tracks_the_memory_share() {
72        // ~1 GiB footprint × 1.25 = ~1.25 GiB of 16 GiB → well under 0.75 → runs well.
73        let assessment = FitVerdict::assess(Some(1024), 16 * GIB).unwrap();
74        assert_eq!(assessment.verdict, FitVerdict::RunsWell);
75
76        // 12 GiB × 1.25 = 15 GiB of 16 GiB → share 0.9375 → tight fit.
77        let tight = FitVerdict::assess(Some(12 * 1024), 16 * GIB).unwrap();
78        assert_eq!(tight.verdict, FitVerdict::TightFit);
79
80        // 16 GiB × 1.25 = 20 GiB of 16 GiB → share 1.25 → too large.
81        let too_large = FitVerdict::assess(Some(16 * 1024), 16 * GIB).unwrap();
82        assert_eq!(too_large.verdict, FitVerdict::TooLarge);
83    }
84
85    #[test]
86    fn required_bytes_includes_the_overhead_factor() {
87        let assessment = FitVerdict::assess(Some(1024), 64 * GIB).unwrap();
88        // 1024 MiB × 1.25 = 1280 MiB.
89        assert_eq!(assessment.required_bytes, 1280 * (1 << 20));
90    }
91}