Skip to main content

clawft_kernel/
calibration.rs

1//! Boot-time ECC benchmarking and capability advertisement (Phase K3c).
2//!
3//! This module is compiled only when the `ecc` feature is enabled.
4//! It provides [`run_calibration`], which exercises the HNSW index and
5//! causal graph with synthetic data, measures per-tick latency, and
6//! returns an [`EccCalibration`] that other modules (CognitiveTick,
7//! cluster advertisement) use to auto-tune cadence and decide which
8//! subsystems are feasible on this hardware.
9
10use std::time::Instant;
11
12use serde::{Deserialize, Serialize};
13
14use crate::causal::{CausalEdgeType, CausalGraph};
15use crate::hnsw_service::HnswService;
16
17// ---------------------------------------------------------------------------
18// EccCalibrationConfig
19// ---------------------------------------------------------------------------
20
21/// Tuning knobs for the calibration run.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct EccCalibrationConfig {
24    /// Number of synthetic ticks to execute during calibration.
25    pub calibration_ticks: u32,
26    /// Initial tick interval in milliseconds (may be auto-raised).
27    pub tick_interval_ms: u32,
28    /// Target compute-time / tick-interval ratio (e.g. 0.3 = 30%).
29    pub tick_budget_ratio: f32,
30    /// Dimensionality of synthetic test vectors.
31    pub vector_dimensions: usize,
32}
33
34impl Default for EccCalibrationConfig {
35    fn default() -> Self {
36        Self {
37            calibration_ticks: 30,
38            tick_interval_ms: 50,
39            tick_budget_ratio: 0.3,
40            vector_dimensions: 384,
41        }
42    }
43}
44
45// ---------------------------------------------------------------------------
46// EccCalibration
47// ---------------------------------------------------------------------------
48
49/// Results of a boot-time calibration run.
50///
51/// Consumed by `CognitiveTick` (for cadence), by `cluster.rs` (for
52/// capability advertisement), and optionally by ExoChain (logged as an
53/// `ecc.boot.calibration` chain event).
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct EccCalibration {
56    /// Median per-tick latency in microseconds.
57    pub compute_p50_us: u64,
58    /// 95th-percentile per-tick latency in microseconds.
59    pub compute_p95_us: u64,
60    /// Effective tick interval after auto-adjustment (ms).
61    pub tick_interval_ms: u32,
62    /// Ratio of p95 compute time to tick interval.
63    pub headroom_ratio: f32,
64    /// Number of HNSW vectors inserted during calibration.
65    pub hnsw_vector_count: u32,
66    /// Number of causal edges created during calibration.
67    pub causal_edge_count: u32,
68    /// Whether spectral analysis is feasible on this hardware.
69    pub spectral_capable: bool,
70    /// Unix timestamp (seconds) at which calibration completed.
71    pub calibrated_at: u64,
72}
73
74// ---------------------------------------------------------------------------
75// Deterministic pseudo-random vector generation
76// ---------------------------------------------------------------------------
77
78/// Generate a deterministic f32 vector using a simple LCG, avoiding the
79/// `rand` crate dependency.
80fn pseudo_random_vector(seed: u64, dims: usize) -> Vec<f32> {
81    let mut state = seed
82        .wrapping_mul(6364136223846793005)
83        .wrapping_add(1442695040888963407);
84    (0..dims)
85        .map(|_| {
86            state = state
87                .wrapping_mul(6364136223846793005)
88                .wrapping_add(1442695040888963407);
89            // Map to [-1.0, 1.0]
90            ((state >> 33) as f32) / (u32::MAX as f32 / 2.0) - 1.0
91        })
92        .collect()
93}
94
95// ---------------------------------------------------------------------------
96// run_calibration
97// ---------------------------------------------------------------------------
98
99/// Execute a calibration run against the provided HNSW index and causal
100/// graph.
101///
102/// This inserts `config.calibration_ticks` synthetic vectors into HNSW,
103/// searches each one, creates causal edges linking consecutive ticks,
104/// and hashes each vector with BLAKE3 (simulating a Merkle commit).
105/// After collecting per-tick timings it computes p50/p95 percentiles,
106/// decides the effective tick interval, checks spectral feasibility,
107/// and cleans up all synthetic data.
108pub fn run_calibration(
109    hnsw: &HnswService,
110    causal: &CausalGraph,
111    config: &EccCalibrationConfig,
112) -> EccCalibration {
113    let n = config.calibration_ticks as usize;
114    assert!(n > 0, "calibration_ticks must be > 0");
115
116    // Pre-generate all test vectors.
117    let vectors: Vec<Vec<f32>> = (0..n)
118        .map(|i| pseudo_random_vector(i as u64, config.vector_dimensions))
119        .collect();
120
121    // Pre-create causal graph nodes so that link() can find them.
122    let node_ids: Vec<u64> = (0..n)
123        .map(|i| causal.add_node(format!("cal_{i}"), serde_json::json!({})))
124        .collect();
125
126    // Run synthetic ticks and collect per-tick timings.
127    let mut timings_us: Vec<u64> = Vec::with_capacity(n);
128
129    for i in 0..n {
130        let start = Instant::now();
131
132        // 1. HNSW insert
133        let id = format!("cal_{i}");
134        hnsw.insert(id, vectors[i].clone(), serde_json::json!({}));
135
136        // 2. HNSW search (k=10)
137        let _results = hnsw.search(&vectors[i], 10);
138
139        // 3. Causal edge: link tick i-1 -> tick i
140        if i > 0 {
141            causal.link(
142                node_ids[i - 1],
143                node_ids[i],
144                CausalEdgeType::Follows,
145                1.0,
146                0,
147                0,
148            );
149        }
150
151        // 4. BLAKE3 hash (Merkle commit simulation)
152        let vec_bytes: Vec<u8> = vectors[i]
153            .iter()
154            .flat_map(|f| f.to_le_bytes())
155            .collect();
156        let _hash = blake3::hash(&vec_bytes);
157
158        let elapsed = start.elapsed().as_micros() as u64;
159        timings_us.push(elapsed);
160    }
161
162    // Compute percentiles.
163    timings_us.sort_unstable();
164    let p50 = timings_us[n / 2];
165    let p95 = timings_us[n * 95 / 100];
166
167    // Record counts before cleanup.
168    let hnsw_vector_count = n as u32;
169    let causal_edge_count = if n > 1 { (n - 1) as u32 } else { 0 };
170
171    // Clean up synthetic data.
172    hnsw.clear();
173    causal.clear();
174
175    // Auto-adjust tick interval: ensure at least config.tick_interval_ms,
176    // but raise it if p95 / budget_ratio exceeds the configured value.
177    let p95_ms = p95 as f32 / 1000.0;
178    let required_ms = (p95_ms / config.tick_budget_ratio) as u32;
179    let tick_interval_ms = config.tick_interval_ms.max(required_ms);
180
181    // Headroom ratio: what fraction of the tick interval does p95 consume?
182    let headroom_ratio = if tick_interval_ms > 0 {
183        p95_ms / (tick_interval_ms as f32)
184    } else {
185        1.0
186    };
187
188    // Spectral feasibility: p95 under 10ms means we can afford spectral
189    // analysis within the tick budget.
190    let spectral_capable = p95 < 10_000;
191
192    // Unix timestamp (seconds).
193    let calibrated_at = std::time::SystemTime::now()
194        .duration_since(std::time::UNIX_EPOCH)
195        .map(|d| d.as_secs())
196        .unwrap_or(0);
197
198    EccCalibration {
199        compute_p50_us: p50,
200        compute_p95_us: p95,
201        tick_interval_ms,
202        headroom_ratio,
203        hnsw_vector_count,
204        causal_edge_count,
205        spectral_capable,
206        calibrated_at,
207    }
208}
209
210// ---------------------------------------------------------------------------
211// Tests
212// ---------------------------------------------------------------------------
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::hnsw_service::{HnswService, HnswServiceConfig};
218
219    fn make_hnsw() -> HnswService {
220        HnswService::new(HnswServiceConfig::default())
221    }
222
223    fn make_causal() -> CausalGraph {
224        CausalGraph::new()
225    }
226
227    fn small_config() -> EccCalibrationConfig {
228        EccCalibrationConfig {
229            calibration_ticks: 10,
230            tick_interval_ms: 50,
231            tick_budget_ratio: 0.3,
232            vector_dimensions: 16,
233        }
234    }
235
236    // 1. default_config
237    #[test]
238    fn default_config() {
239        let cfg = EccCalibrationConfig::default();
240        assert_eq!(cfg.calibration_ticks, 30);
241        assert_eq!(cfg.tick_interval_ms, 50);
242        assert!((cfg.tick_budget_ratio - 0.3).abs() < f32::EPSILON);
243        assert_eq!(cfg.vector_dimensions, 384);
244    }
245
246    // 2. pseudo_random_vector_deterministic
247    #[test]
248    fn pseudo_random_vector_deterministic() {
249        let a = pseudo_random_vector(42, 8);
250        let b = pseudo_random_vector(42, 8);
251        assert_eq!(a, b, "same seed must produce identical vectors");
252
253        let c = pseudo_random_vector(99, 8);
254        assert_ne!(a, c, "different seeds must produce different vectors");
255    }
256
257    // 3. pseudo_random_vector_correct_dimensions
258    #[test]
259    fn pseudo_random_vector_correct_dimensions() {
260        for dims in [1, 16, 128, 384] {
261            let v = pseudo_random_vector(0, dims);
262            assert_eq!(v.len(), dims);
263        }
264    }
265
266    // 4. calibration_basic
267    #[test]
268    fn calibration_basic() {
269        let hnsw = make_hnsw();
270        let causal = make_causal();
271        let cfg = small_config();
272
273        let cal = run_calibration(&hnsw, &causal, &cfg);
274
275        assert!(cal.compute_p50_us > 0, "p50 must be positive");
276        assert!(cal.compute_p95_us > 0, "p95 must be positive");
277        assert!(cal.tick_interval_ms > 0, "tick interval must be positive");
278        assert!(cal.headroom_ratio > 0.0, "headroom must be positive");
279        assert_eq!(cal.hnsw_vector_count, 10);
280        assert_eq!(cal.causal_edge_count, 9);
281        assert!(cal.calibrated_at > 0, "timestamp must be set");
282    }
283
284    // 5. calibration_cleans_up
285    #[test]
286    fn calibration_cleans_up() {
287        let hnsw = make_hnsw();
288        let causal = make_causal();
289        let cfg = small_config();
290
291        let _cal = run_calibration(&hnsw, &causal, &cfg);
292
293        assert!(
294            hnsw.is_empty(),
295            "HNSW store must be empty after calibration cleanup"
296        );
297        assert_eq!(
298            causal.node_count(),
299            0,
300            "causal graph must be empty after calibration cleanup"
301        );
302        assert_eq!(
303            causal.edge_count(),
304            0,
305            "causal graph edges must be zero after calibration cleanup"
306        );
307    }
308
309    // 6. calibration_p50_less_than_p95
310    #[test]
311    fn calibration_p50_less_than_p95() {
312        let hnsw = make_hnsw();
313        let causal = make_causal();
314        let cfg = small_config();
315
316        let cal = run_calibration(&hnsw, &causal, &cfg);
317
318        assert!(
319            cal.compute_p50_us <= cal.compute_p95_us,
320            "p50 ({}) must be <= p95 ({})",
321            cal.compute_p50_us,
322            cal.compute_p95_us,
323        );
324    }
325
326    // 7. calibration_spectral_capable
327    #[test]
328    fn calibration_spectral_capable() {
329        let hnsw = make_hnsw();
330        let causal = make_causal();
331        // Very small workload should be fast enough for spectral.
332        let cfg = EccCalibrationConfig {
333            calibration_ticks: 5,
334            tick_interval_ms: 50,
335            tick_budget_ratio: 0.3,
336            vector_dimensions: 4,
337        };
338
339        let cal = run_calibration(&hnsw, &causal, &cfg);
340
341        assert!(
342            cal.spectral_capable,
343            "a trivial calibration run should report spectral capable (p95={}us)",
344            cal.compute_p95_us,
345        );
346    }
347
348    // 8. calibration_tick_interval_auto_adjusted
349    #[test]
350    fn calibration_tick_interval_auto_adjusted() {
351        let hnsw = make_hnsw();
352        let causal = make_causal();
353        // Set an impossibly low tick interval and tight budget ratio
354        // so that p95 forces an upward adjustment.
355        let cfg = EccCalibrationConfig {
356            calibration_ticks: 10,
357            tick_interval_ms: 1, // very low
358            tick_budget_ratio: 0.01, // very tight budget
359            vector_dimensions: 64,
360        };
361
362        let cal = run_calibration(&hnsw, &causal, &cfg);
363
364        assert!(
365            cal.tick_interval_ms >= cfg.tick_interval_ms,
366            "tick_interval_ms ({}) must be >= configured value ({})",
367            cal.tick_interval_ms,
368            cfg.tick_interval_ms,
369        );
370    }
371
372    // Bonus: single-tick calibration edge case (no causal edges)
373    #[test]
374    fn calibration_single_tick() {
375        let hnsw = make_hnsw();
376        let causal = make_causal();
377        let cfg = EccCalibrationConfig {
378            calibration_ticks: 1,
379            tick_interval_ms: 50,
380            tick_budget_ratio: 0.3,
381            vector_dimensions: 8,
382        };
383
384        let cal = run_calibration(&hnsw, &causal, &cfg);
385
386        assert_eq!(cal.hnsw_vector_count, 1);
387        assert_eq!(cal.causal_edge_count, 0, "no edges with a single tick");
388        assert!(hnsw.is_empty());
389        assert_eq!(causal.node_count(), 0);
390    }
391
392    // Bonus: headroom_ratio is within sane bounds
393    #[test]
394    fn calibration_headroom_sane() {
395        let hnsw = make_hnsw();
396        let causal = make_causal();
397        let cfg = small_config();
398
399        let cal = run_calibration(&hnsw, &causal, &cfg);
400
401        assert!(
402            cal.headroom_ratio >= 0.0 && cal.headroom_ratio <= 1.0,
403            "headroom_ratio ({}) should be in [0, 1] for a well-budgeted run",
404            cal.headroom_ratio,
405        );
406    }
407}