Skip to main content

qualia_core_db/inference/lab/
device_roof.rs

1//! Device peak calibration — real roofline anchors (plan L1.2).
2
3use std::time::Instant;
4
5use crate::device_benchmark::benchmark_devices;
6
7#[derive(Debug, Clone)]
8pub struct DeviceRoof {
9    pub gemv_n: usize,
10    pub best_label: String,
11    pub best_backend: String,
12    pub gemv_ms: f64,
13    pub gemv_gflops: f64,
14    pub upload_gbps: f64,
15    pub balance_flop_per_byte: f64,
16    pub notes: String,
17}
18
19/// Calibrate using the existing multi-circuit GEMV passport bench + derived balance point.
20pub fn calibrate_device_roof(gemv_n: usize) -> DeviceRoof {
21    let n = gemv_n.max(256).min(4096);
22    let matrix = benchmark_devices(n);
23    let best = matrix.best();
24    let (label, backend, ms, gflops, up) = match best {
25        Some(c) => (
26            c.label.clone(),
27            c.backend.clone(),
28            c.ms_per_gemv,
29            c.gflops,
30            c.upload_gbps,
31        ),
32        None => ("none".into(), "none".into(), f64::INFINITY, 0.0, 0.0),
33    };
34    // Rough balance: if we measured G FLOP/s and U GB/s, balance ≈ G / U (FLOP/byte).
35    // upload_gbps is host→device; for in-pool compute use gflops as primary signal.
36    let balance = if up.is_finite() && up > 0.1 {
37        // GFLOP/s / (GB/s) = FLOP/byte
38        (gflops.max(0.01)) / up
39    } else {
40        // CPU in-pool: treat as compute-leaning default
41        20.0
42    };
43    DeviceRoof {
44        gemv_n: n,
45        best_label: label,
46        best_backend: backend,
47        gemv_ms: ms,
48        gemv_gflops: gflops,
49        upload_gbps: up,
50        balance_flop_per_byte: balance,
51        notes: format!(
52            "from benchmark_devices({n}); use balance_flop_per_byte for schedule classification"
53        ),
54    }
55}
56
57impl DeviceRoof {
58    pub fn format_report(&self) -> String {
59        format!(
60            "Device roof calibration\n  gemv_n:        {}\n  best:          {} [{}]\n  gemv_ms:       {:.4}\n  gemv_gflops:   {:.2}\n  upload_gbps:   {:.2}\n  balance FLOP/B:{:.2}\n  {}\n",
61            self.gemv_n,
62            self.best_label,
63            self.best_backend,
64            self.gemv_ms,
65            self.gemv_gflops,
66            if self.upload_gbps.is_finite() {
67                self.upload_gbps
68            } else {
69                -1.0
70            },
71            self.balance_flop_per_byte,
72            self.notes
73        )
74    }
75}
76
77/// Quick CPU Q4 dequant·dot micro for intensity reference (no GPU).
78pub fn cpu_q4_intensity_probe(n_in: usize, n_out: usize) -> (f64, f64) {
79    use crate::ggml_quants::{
80        dequantize_row_into, q4k_block_to_soa, BLOCK_Q4K_SOA_BYTES, GGML_TYPE_Q4_K_SOA,
81    };
82    let n_in = n_in.max(256) & !255; // multiple of 256
83    let n_out = n_out.max(1).min(64);
84    let mut stock = [0u8; 144];
85    stock[0] = 0x00;
86    stock[1] = 0x3c;
87    for i in 4..144 {
88        stock[i] = (i as u8).wrapping_mul(17);
89    }
90    let mut soa = [0u8; BLOCK_Q4K_SOA_BYTES];
91    let _ = q4k_block_to_soa(&stock, &mut soa);
92    let n_blocks = n_in / 256;
93    let mut weight = Vec::with_capacity(n_out * n_blocks * BLOCK_Q4K_SOA_BYTES);
94    for _ in 0..n_out {
95        for _ in 0..n_blocks {
96            weight.extend_from_slice(&soa);
97        }
98    }
99    let x: Vec<f32> = (0..n_in).map(|i| (i as f32) * 0.001).collect();
100    let mut row = vec![0.0f32; n_in];
101    let t0 = Instant::now();
102    let mut acc = 0.0f64;
103    for r in 0..n_out {
104        let off = r * n_blocks * BLOCK_Q4K_SOA_BYTES;
105        dequantize_row_into(
106            &weight[off..off + n_blocks * BLOCK_Q4K_SOA_BYTES],
107            GGML_TYPE_Q4_K_SOA,
108            n_in,
109            &mut row,
110        )
111        .ok();
112        acc += row
113            .iter()
114            .zip(x.iter())
115            .map(|(a, b)| (*a as f64) * (*b as f64))
116            .sum::<f64>();
117    }
118    let secs = t0.elapsed().as_secs_f64().max(1e-9);
119    let flops = (n_in as f64) * (n_out as f64) * 2.0; // mul+add
120    let bytes = (weight.len() + n_in * 4 + n_out * 4) as f64;
121    let gflops = (flops / secs) / 1e9;
122    let intensity = flops / bytes;
123    let _ = acc;
124    (gflops, intensity)
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn cpu_intensity_positive() {
133        let (g, i) = cpu_q4_intensity_probe(256, 4);
134        assert!(g > 0.0);
135        assert!(i > 0.0);
136    }
137}