metrale_bench/benchmarks/default_tier_boot/
memory.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-10-01: What a server reports about its memory (`GET /memory`), and the pure arithmetic
4//! the default-tier boot check derives from it. The server fills [`MemoryReport`]; the driver
5//! reads it back, so the one type is the wire contract on both sides.
6//!
7//! Owner: bench, default-tier-boot.
8//! Invariants:
9//! - A reading the server could not take is `None`, serialised as JSON `null`; [`footprint`]
10//!   refuses to derive from one rather than substituting a number.
11//! - Pure: no I/O.
12
13use std::collections::BTreeMap;
14
15use crate::result::Verdict;
16
17/// 2026-10-01: Bytes per MiB, the unit of every byte-valued metric this check emits.
18pub const MIB: f64 = 1024.0 * 1024.0;
19
20/// 2026-10-01: The body of `GET /memory`. Byte counts unless the name says otherwise.
21#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub struct MemoryReport {
23    /// 2026-10-01: Blocks in the model's main paged KV pool.
24    pub kv_blocks: usize,
25    /// 2026-10-01: Bytes of one block index across every layer, K and V.
26    pub kv_block_bytes: u64,
27    /// 2026-10-01: The slot count the model was built with.
28    pub max_batch_size: usize,
29    /// 2026-10-01: Total device memory times `--gpu-memory-utilization`.
30    pub budget_bytes: u64,
31    /// 2026-10-01: The GPU backend's allocation ledger at request time; `None` on a backend
32    /// without one.
33    pub ledger_live_bytes: Option<u64>,
34    /// 2026-10-01: Host `MemAvailable`, sampled once before the GPU backend initialised.
35    pub mem_available_at_start_bytes: Option<u64>,
36    /// 2026-10-01: Host `MemAvailable` at request time.
37    pub mem_available_now_bytes: Option<u64>,
38    /// 2026-10-01: The server process's `RssAnon` at request time.
39    pub rss_anon_bytes: Option<u64>,
40}
41
42/// 2026-10-01: The figures the check reports, derived from one [`MemoryReport`].
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct Footprint {
45    pub kv_blocks: usize,
46    pub max_batch_size: usize,
47    pub budget_mib: f64,
48    /// 2026-10-01: `MemAvailable` at start minus now, minus the process's anonymous RSS: on a
49    /// unified-memory box, what the serve holds on the device.
50    pub device_footprint_mib: f64,
51    /// 2026-10-01: Footprint minus budget; negative is within budget.
52    pub footprint_over_budget_mib: f64,
53    pub ledger_live_mib: Option<f64>,
54}
55
56/// 2026-10-01: Derive the [`Footprint`]. The subtraction is done in whole bytes, so a footprint
57/// exactly at the budget reports 0, not a rounding residue. Errors, naming the fields, when a
58/// host reading is missing.
59pub fn footprint(report: &MemoryReport) -> Result<Footprint, String> {
60    let (Some(start), Some(now), Some(rss)) = (
61        report.mem_available_at_start_bytes,
62        report.mem_available_now_bytes,
63        report.rss_anon_bytes,
64    ) else {
65        let missing: Vec<&str> = [
66            (
67                "mem_available_at_start_bytes",
68                report.mem_available_at_start_bytes,
69            ),
70            ("mem_available_now_bytes", report.mem_available_now_bytes),
71            ("rss_anon_bytes", report.rss_anon_bytes),
72        ]
73        .into_iter()
74        .filter(|(_, v)| v.is_none())
75        .map(|(k, _)| k)
76        .collect();
77        return Err(format!(
78            "/memory reported null for {} — the server could not read the host's memory \
79             counters, so there is no device footprint to measure",
80            missing.join(", ")
81        ));
82    };
83    let device = i128::from(start) - i128::from(now) - i128::from(rss);
84    let over = device - i128::from(report.budget_bytes);
85    Ok(Footprint {
86        kv_blocks: report.kv_blocks,
87        max_batch_size: report.max_batch_size,
88        budget_mib: report.budget_bytes as f64 / MIB,
89        device_footprint_mib: device as f64 / MIB,
90        footprint_over_budget_mib: over as f64 / MIB,
91        ledger_live_mib: report.ledger_live_bytes.map(|b| b as f64 / MIB),
92    })
93}
94
95impl Footprint {
96    /// 2026-10-01: The record's metric keys. `ledger_live_mib` only when the backend has a
97    /// ledger.
98    pub fn metrics(&self, m: &mut BTreeMap<String, f64>) {
99        m.insert("kv_blocks".to_string(), self.kv_blocks as f64);
100        m.insert("max_batch_size".to_string(), self.max_batch_size as f64);
101        m.insert("budget_mib".to_string(), self.budget_mib);
102        m.insert(
103            "device_footprint_mib".to_string(),
104            self.device_footprint_mib,
105        );
106        m.insert(
107            "footprint_over_budget_mib".to_string(),
108            self.footprint_over_budget_mib,
109        );
110        if let Some(ledger) = self.ledger_live_mib {
111            m.insert("ledger_live_mib".to_string(), ledger);
112        }
113    }
114}
115
116/// 2026-10-01: The run verdict. A missing reading fails as inconclusive; a measured run is
117/// informational, because no bounds are declared for this check yet.
118pub fn verdict_for(result: &Result<Footprint, String>) -> Verdict {
119    match result {
120        Err(why) => Verdict::fail(format!("INCONCLUSIVE: {why}")),
121        Ok(f) => Verdict::info(format!(
122            "{} KV blocks at {} slots; device footprint {:.0} MiB against a {:.0} MiB budget \
123             ({:+.0} MiB) — informational, no bounds are declared yet",
124            f.kv_blocks,
125            f.max_batch_size,
126            f.device_footprint_mib,
127            f.budget_mib,
128            f.footprint_over_budget_mib,
129        )),
130    }
131}