Skip to main content

qualia_core_db/platform/
hardware_passport.rs

1//! STELLAR §A AH-track H1(a) cache — the **hardware passport** (decision D26).
2//!
3//! Probing the host (H0 topology + H1(a) cross-circuit benchmark) costs real time; doing it every
4//! boot burns TTFT (A7). The passport caches the discovered topology + capability matrix to a small
5//! **CBOR** blob keyed by the host's **adapter identifiers** (vendor:device handles — identifiers,
6//! not an "identity"). On later boots, if the current adapter set matches the cached key, the probe
7//! is skipped; a topology change invalidates the cache and forces a re-probe (D26/D28).
8//!
9//! CBOR (via `ciborium`, serde-compatible) is used rather than JSON: it round-trips IEEE-754 floats
10//! natively — including the `f64::INFINITY` that marks an in-pool (no-transfer) circuit — and a
11//! compact binary blob fits the project's `.q42` binary-first ethos. **Cache only (no signing):** the
12//! human-key *signing* of the passport (H1(b)) is gated on the identity remediation and lives
13//! elsewhere; this module never claims trust, only fast-boot.
14//!
15//! Native only.
16#![cfg(not(target_arch = "wasm32"))]
17
18use serde::{Deserialize, Serialize};
19use std::path::PathBuf;
20
21use crate::device_benchmark::{benchmark_devices, CapabilityMatrix, CircuitKind};
22use crate::host_topology::{probe_host_topology, HostTopology};
23use std::path::Path;
24
25/// Bump when the passport layout changes (older blobs are then ignored → re-probe).
26/// v2: optional `decode_proxy_tok_s` per circuit + ranking by real decode when measured.
27pub const PASSPORT_VERSION: u32 = 2;
28
29/// Default representative GEMV side length for the cached benchmark.
30pub const PASSPORT_GEMV_N: usize = 2048;
31
32/// Cached discovery: topology + measured capability matrix, keyed by adapter identifiers.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct HardwarePassport {
35    pub version: u32,
36    /// Stable key from the discovered adapter identifiers (sorted `vendor:device` handles).
37    pub key: String,
38    pub topology: HostTopology,
39    pub matrix: CapabilityMatrix,
40    /// Best GPU backend token for inference (`dx12` / `vulkan` / `metal` / `gl`), if any.
41    /// Derived from the measured matrix; used when `QUALIA_WGPU_BACKEND` is unset.
42    #[serde(default)]
43    pub preferred_inference_backend: Option<String>,
44    /// GEMV n used for the matrix (for operator honesty).
45    #[serde(default)]
46    pub probe_gemv_n: usize,
47    /// Model path used for decode-proxy ranking (if any).
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub decode_proxy_model: Option<String>,
50    /// Decode tokens used for the proxy (0 = none).
51    #[serde(default)]
52    pub decode_proxy_tokens: u32,
53}
54
55/// Stable key from the discovered adapter identifiers (sorted `vendor:device`). Identifiers (handles),
56/// never an identity — this is referential integrity for "is this the same hardware set", nothing more.
57pub fn topology_key(topo: &HostTopology) -> String {
58    let mut ids: Vec<String> = topo
59        .adapters
60        .iter()
61        .map(|a| format!("{:04x}:{:04x}", a.vendor, a.device))
62        .collect();
63    ids.sort();
64    ids.join(",")
65}
66
67/// Default cache location (OS temp dir). Callers may pass any path (e.g. a per-user data dir).
68pub fn default_cache_path() -> PathBuf {
69    std::env::temp_dir().join("qualia_hardware_passport.cbor")
70}
71
72/// Serialize a passport to a CBOR blob on disk.
73pub fn write_passport(passport: &HardwarePassport, path: &Path) -> Result<(), String> {
74    let mut buf = Vec::new();
75    ciborium::into_writer(passport, &mut buf).map_err(|e| format!("cbor encode: {e}"))?;
76    std::fs::write(path, &buf).map_err(|e| format!("write {}: {e}", path.display()))
77}
78
79/// Read + decode a passport (CBOR). Returns `None` on missing file, decode error, or version mismatch.
80pub fn read_passport(path: &Path) -> Option<HardwarePassport> {
81    let bytes = std::fs::read(path).ok()?;
82    let passport: HardwarePassport = ciborium::from_reader(&bytes[..]).ok()?;
83    if passport.version != PASSPORT_VERSION {
84        return None;
85    }
86    Some(passport)
87}
88
89/// Fast-boot entry: return the cached passport iff its adapter-identifier key matches the current
90/// host; otherwise probe (H0 + H1(a) benchmark), cache, and return. `(passport, was_cached)`.
91pub fn load_or_probe(path: &Path, gemv_n: usize) -> (HardwarePassport, bool) {
92    let topology = probe_host_topology();
93    let current_key = topology_key(&topology);
94
95    if let Some(cached) = read_passport(path) {
96        if cached.key == current_key {
97            return (cached, true); // fast-boot: skip the probe (TTFT)
98        }
99        // key mismatch ⇒ hardware changed (D26/D28) → re-probe below.
100    }
101
102    let matrix = benchmark_devices(gemv_n);
103    let preferred = matrix
104        .best()
105        .and_then(|c| backend_env_token(&c.backend))
106        .map(str::to_string);
107    let fresh = HardwarePassport {
108        version: PASSPORT_VERSION,
109        key: current_key,
110        topology,
111        matrix,
112        preferred_inference_backend: preferred,
113        probe_gemv_n: gemv_n,
114        decode_proxy_model: None,
115        decode_proxy_tokens: 0,
116    };
117    let _ = write_passport(&fresh, path);
118    (fresh, false)
119}
120
121/// Default small-model candidates for decode-proxy ranking (first existing wins).
122pub fn default_decode_proxy_model() -> Option<std::path::PathBuf> {
123    if let Ok(p) = std::env::var("QUALIA_LLM_PROFILE_MODEL") {
124        let pb = std::path::PathBuf::from(p);
125        if pb.is_file() {
126            return Some(pb);
127        }
128    }
129    const CANDIDATES: &[&str] = &[
130        r"C:\LLM_Models\P64\smollm2-360m-instruct-q8_0.f16.p64",
131        r"C:\LLM_Models\P64\smollm2-360m-instruct-q8_0.p64",
132        r"C:\LLM_Models\GGUF\smollm2-360m-instruct-q8_0.gguf",
133        r"C:\LLM_Models\GGUF\lmstudio-community\smollm2-360m-instruct-q8_0.gguf",
134    ];
135    CANDIDATES
136        .iter()
137        .map(std::path::PathBuf::from)
138        .find(|p| p.is_file())
139}
140
141/// Fixed factual probe used for coherence + throughput (same prompt as browser llmdemo gate).
142pub const DECODE_PROXY_PROBE_PROMPT: &str = "The capital of France is";
143
144/// Result of a decode-proxy measurement: speed **and** whether the completion is usable.
145#[derive(Debug, Clone)]
146pub struct DecodeProxyResult {
147    pub tok_s: f64,
148    pub text: String,
149    /// True when completion contains the expected factual anchor (e.g. "Paris").
150    pub coherence_ok: bool,
151    /// Backend that actually completed measured token forwards. This is derived
152    /// from production-path counters, never from the requested env label.
153    pub execution_path: String,
154    pub resident_hits: u64,
155    pub resident_fallbacks: u64,
156    pub cuda_mega_hits: u64,
157    pub cuda_mega_fallbacks: u64,
158}
159
160/// Greedy factual gate: completion must contain "paris" (case-insensitive).
161/// Garbage token streams fail closed — speed alone is not excellence.
162pub fn decode_proxy_coherence_ok(text: &str) -> bool {
163    text.to_ascii_lowercase().contains("paris")
164}
165
166/// Run a short resident decode on `model` under the **current** process backend.
167/// Returns tok/s + text + coherence, or `None` on hard failure.
168///
169/// Warm-up: one short decode builds pipelines / resident plan so the timed
170/// measurement is not dominated by first-token cold cost.
171///
172/// Safe from CLI (which already owns a Tokio multi-thread runtime): nested
173/// `block_on` panics, so we hop to a fresh OS thread for the measurement.
174pub fn measure_decode_proxy(model: &Path, n_tokens: u32) -> Option<DecodeProxyResult> {
175    crate::wgsl_forge::dispatch::ensure_cuda_runtime_path();
176    let n = n_tokens.max(8).min(128);
177    let path = model.to_path_buf();
178    // Greedy, bounded — comparable across backends.
179    crate::llm_bench::set_sampler_config(None);
180    let path_str = path.to_str()?.to_string();
181    let join = std::thread::Builder::new()
182        .name("decode-proxy".into())
183        .spawn(move || {
184            let prompt = DECODE_PROXY_PROBE_PROMPT;
185            // Warm: compile shaders + resident plan (discard rate).
186            let _ = crate::llm_bench::decode_with_metrics_blocking(&path_str, prompt, 4);
187            crate::llm_bench::reset_resident_path_counts();
188            crate::llm_bench::reset_cuda_mega_path_counts();
189            let measured = crate::llm_bench::decode_with_metrics_blocking(&path_str, prompt, n);
190            let resident = crate::llm_bench::resident_path_counts();
191            let cuda = crate::llm_bench::cuda_mega_path_counts();
192            (measured, resident, cuda)
193        })
194        .ok()?
195        .join();
196    match join {
197        Ok((Ok((text, tok_s)), resident, cuda)) if tok_s > 0.0 => {
198            let coherence_ok = decode_proxy_coherence_ok(&text);
199            let execution_path = match (resident.0 > 0, cuda.0 > 0) {
200                (true, false) => "wgpu-resident",
201                (false, true) => "cuda-c",
202                (true, true) => "mixed",
203                (false, false) => "legacy-or-unattributed",
204            }
205            .to_string();
206            Some(DecodeProxyResult {
207                tok_s,
208                text,
209                coherence_ok,
210                execution_path,
211                resident_hits: resident.0,
212                resident_fallbacks: resident.1,
213                cuda_mega_hits: cuda.0,
214                cuda_mega_fallbacks: cuda.1,
215            })
216        }
217        Ok((Ok(_), _, _)) => None,
218        Ok((Err(e), _, _)) => {
219            log::warn!("decode_proxy|fail|{e}");
220            None
221        }
222        Err(_) => {
223            log::warn!("decode_proxy|thread_panic");
224            None
225        }
226    }
227}
228
229/// Back-compat: tok/s only (callers that ignore quality still work).
230pub fn measure_decode_proxy_tok_s(model: &Path, n_tokens: u32) -> Option<f64> {
231    measure_decode_proxy(model, n_tokens).map(|r| r.tok_s)
232}
233
234/// Attach decode-proxy tok/s to GPU circuits by spawning a child process per backend
235/// (shared_gpu is process-wide OnceLock — cannot switch backends in-process).
236///
237/// `self_exe` should be the current CLI binary (`std::env::current_exe()`).
238/// Child runs: `llm decode-proxy <model> --tokens N` with `QUALIA_WGPU_BACKEND` set.
239pub fn attach_decode_proxy_via_subprocess(
240    matrix: &mut CapabilityMatrix,
241    model: &Path,
242    n_tokens: u32,
243    self_exe: &Path,
244) {
245    use std::process::Command;
246    let n = n_tokens.max(8).min(64);
247    // Measure only **discrete** GPU rows (iGPU would inherit a false tok/s if we
248    // keyed only by backend token — wgpu picks the discrete card under QUALIA_WGPU_BACKEND).
249    let mut seen_tokens = std::collections::HashSet::<String>::new();
250    for c in matrix.circuits.iter_mut() {
251        if c.kind != CircuitKind::DiscreteGpu {
252            continue;
253        }
254        let Some(token) = backend_env_token(&c.backend) else {
255            continue;
256        };
257        if !seen_tokens.insert(token.to_string()) {
258            continue;
259        }
260        let output = Command::new(self_exe)
261            .args([
262                "llm",
263                "decode-proxy",
264                &model.display().to_string(),
265                "--tokens",
266                &n.to_string(),
267            ])
268            .env("QUALIA_WGPU_BACKEND", token)
269            .env("QUALIA_P64_INTEGRITY", "metadata")
270            .env("RUST_LOG", "error")
271            .output();
272        let tok_s = match output {
273            Ok(o) if o.status.success() => {
274                let stdout = String::from_utf8_lossy(&o.stdout);
275                parse_decode_proxy_line(&stdout)
276            }
277            Ok(o) => {
278                log::warn!(
279                    "decode_proxy|child_fail|backend={token}|{}",
280                    String::from_utf8_lossy(&o.stderr)
281                );
282                None
283            }
284            Err(e) => {
285                log::warn!("decode_proxy|spawn_fail|backend={token}|{e}");
286                None
287            }
288        };
289        c.decode_proxy_tok_s = tok_s;
290        if let Some(t) = tok_s {
291            log::info!("decode_proxy|ok|backend={token}|{t:.2} tok/s");
292        }
293    }
294    // Copy to other discrete rows that share the same backend token only.
295    let mut by_token: std::collections::HashMap<String, f64> = std::collections::HashMap::new();
296    for c in &matrix.circuits {
297        if c.kind != CircuitKind::DiscreteGpu {
298            continue;
299        }
300        if let (Some(tok), Some(t)) = (backend_env_token(&c.backend), c.decode_proxy_tok_s) {
301            by_token.insert(tok.to_string(), t);
302        }
303    }
304    for c in matrix.circuits.iter_mut() {
305        if c.kind != CircuitKind::DiscreteGpu || c.decode_proxy_tok_s.is_some() {
306            continue;
307        }
308        if let Some(tok) = backend_env_token(&c.backend) {
309            if let Some(&t) = by_token.get(tok) {
310                c.decode_proxy_tok_s = Some(t);
311            }
312        }
313    }
314    matrix.apply_decode_proxy_ranking();
315}
316
317/// Parse `DECODE_PROXY tok_s=12.34 backend=dx12 coherence=1` from child stdout.
318pub fn parse_decode_proxy_line(stdout: &str) -> Option<f64> {
319    parse_decode_proxy_record(stdout).map(|r| r.tok_s)
320}
321
322/// Full parse of the machine line from `llm decode-proxy`.
323#[derive(Debug, Clone)]
324pub struct DecodeProxyLine {
325    pub tok_s: f64,
326    pub coherence_ok: Option<bool>,
327    pub execution_path: Option<String>,
328}
329
330pub fn parse_decode_proxy_record(stdout: &str) -> Option<DecodeProxyLine> {
331    for line in stdout.lines() {
332        let line = line.trim();
333        if let Some(rest) = line.strip_prefix("DECODE_PROXY ") {
334            let mut tok_s: Option<f64> = None;
335            let mut coherence_ok: Option<bool> = None;
336            let mut execution_path: Option<String> = None;
337            for part in rest.split_whitespace() {
338                if let Some(v) = part.strip_prefix("tok_s=") {
339                    tok_s = v.parse().ok();
340                } else if let Some(v) = part.strip_prefix("coherence=") {
341                    coherence_ok = Some(v == "1" || v.eq_ignore_ascii_case("true"));
342                } else if let Some(v) = part.strip_prefix("path=") {
343                    execution_path = Some(v.to_string());
344                }
345            }
346            if let Some(tok_s) = tok_s {
347                return Some(DecodeProxyLine {
348                    tok_s,
349                    coherence_ok,
350                    execution_path,
351                });
352            }
353        }
354    }
355    None
356}
357
358/// Convenience: fast-boot against the default cache path + GEMV size.
359pub fn load_or_probe_default() -> (HardwarePassport, bool) {
360    load_or_probe(&default_cache_path(), PASSPORT_GEMV_N)
361}
362
363/// Best measured GPU backend name from a cached passport, if any (`"Dx12"`, `"Vulkan"`, `"Metal"`…).
364/// Does **not** re-probe. Used by `gpu_context` when `QUALIA_WGPU_BACKEND` is unset so the
365/// machine's measured ranking (not a static Windows→DX12 rule alone) selects the path.
366pub fn cached_preferred_wgpu_backend() -> Option<String> {
367    let path = default_cache_path();
368    let passport = read_passport(&path)?;
369    // Prefer the stored token when present (stable across schema evolution).
370    if let Some(ref t) = passport.preferred_inference_backend {
371        return Some(t.clone());
372    }
373    let best = passport.matrix.best()?;
374    // CPU-only win → do not pin a GPU backend.
375    if matches!(best.kind, crate::device_benchmark::CircuitKind::Cpu) {
376        return None;
377    }
378    Some(best.backend.clone())
379}
380
381/// Map a passport `backend` string (`"Dx12"`, `"Vulkan"`, …) to a `QUALIA_WGPU_BACKEND` env value.
382pub fn backend_env_token(backend: &str) -> Option<&'static str> {
383    let s = backend.to_ascii_lowercase();
384    if s.contains("dx12") || s.contains("d3d12") {
385        Some("dx12")
386    } else if s.contains("vulkan") {
387        Some("vulkan")
388    } else if s.contains("metal") {
389        Some("metal")
390    } else if s.contains("gl") {
391        Some("gl")
392    } else {
393        None
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400    use crate::device_benchmark::{CapabilityMatrix, CircuitBench, CircuitKind};
401    use crate::host_topology::{AdapterClass, AdapterDesc, HostMemoryTopology, HostTopology};
402
403    const GB: u64 = 1024 * 1024 * 1024;
404
405    fn synthetic_passport() -> HardwarePassport {
406        let topology = HostTopology {
407            adapters: vec![
408                AdapterDesc {
409                    name: "Discrete GPU".into(),
410                    backend: "Dx12".into(),
411                    class: AdapterClass::Discrete,
412                    vendor: 0x10de,
413                    device: 0x2571,
414                    dedicated_vram_bytes: 12 * GB,
415                },
416                AdapterDesc {
417                    name: "iGPU".into(),
418                    backend: "Dx12".into(),
419                    class: AdapterClass::Integrated,
420                    vendor: 0x8086,
421                    device: 0x1912,
422                    dedicated_vram_bytes: 0,
423                },
424            ],
425            topology: HostMemoryTopology::Discrete,
426            host_ram_bytes: 64 * GB,
427            host_ram_available_bytes: 32 * GB,
428            cpu_cores: 8,
429            os_floor_bytes: 3 * GB / 2,
430            usable_model_budget_bytes: 12 * GB,
431        };
432        let matrix = CapabilityMatrix {
433            circuits: vec![
434                CircuitBench {
435                    label: "Discrete GPU".into(),
436                    kind: CircuitKind::DiscreteGpu,
437                    backend: "Dx12".into(),
438                    ms_per_gemv: 0.43,
439                    gflops: 19.5,
440                    upload_gbps: 3.3,
441                    rel_score: 1.0,
442                    decode_proxy_tok_s: Some(18.0),
443                },
444                CircuitBench {
445                    label: "CPU native".into(),
446                    kind: CircuitKind::Cpu,
447                    backend: "native".into(),
448                    ms_per_gemv: 23.0,
449                    gflops: 0.4,
450                    upload_gbps: f64::INFINITY, // in-pool — must survive the round-trip
451                    rel_score: 0.02,
452                    decode_proxy_tok_s: None,
453                },
454            ],
455            gemv_n: 2048,
456            npu_probed: false,
457        };
458        HardwarePassport {
459            version: PASSPORT_VERSION,
460            key: topology_key(&topology),
461            topology,
462            matrix,
463            preferred_inference_backend: Some("dx12".into()),
464            probe_gemv_n: 2048,
465            decode_proxy_model: None,
466            decode_proxy_tokens: 0,
467        }
468    }
469
470    #[test]
471    fn cbor_round_trip_preserves_infinity_and_key() {
472        let p = synthetic_passport();
473        let dir = tempfile::tempdir().unwrap();
474        let path = dir.path().join("passport.cbor");
475
476        write_passport(&p, &path).expect("write");
477        let back = read_passport(&path).expect("read");
478
479        assert_eq!(back.version, PASSPORT_VERSION);
480        assert_eq!(back.key, p.key);
481        assert_eq!(back.key, "10de:2571,8086:1912");
482        assert_eq!(back.topology.adapters.len(), 2);
483        // The in-pool sentinel (infinite bandwidth) must survive CBOR — the reason for CBOR over JSON.
484        let cpu = back
485            .matrix
486            .circuits
487            .iter()
488            .find(|c| c.kind == CircuitKind::Cpu)
489            .unwrap();
490        assert!(
491            cpu.upload_gbps.is_infinite(),
492            "f64::INFINITY must round-trip through CBOR"
493        );
494        assert!((back.matrix.circuits[0].ms_per_gemv - 0.43).abs() < 1e-9);
495    }
496
497    #[test]
498    fn version_mismatch_is_ignored() {
499        let mut p = synthetic_passport();
500        p.version = 999;
501        let dir = tempfile::tempdir().unwrap();
502        let path = dir.path().join("passport.cbor");
503        write_passport(&p, &path).unwrap();
504        assert!(
505            read_passport(&path).is_none(),
506            "stale version must be rejected → re-probe"
507        );
508    }
509
510    #[test]
511    fn parse_decode_proxy_line_extracts_tok_s() {
512        let s = "noise\nDECODE_PROXY tok_s=12.50 backend=dx12 coherence=1\nmore\n";
513        assert!((parse_decode_proxy_line(s).unwrap() - 12.5).abs() < 1e-9);
514        let r = parse_decode_proxy_record(s).unwrap();
515        assert_eq!(r.coherence_ok, Some(true));
516        assert_eq!(r.execution_path, None);
517
518        let attributed =
519            "DECODE_PROXY tok_s=44.25 backend=cuda path=cuda-c coherence=1 cuda_hits=16\n";
520        let attributed = parse_decode_proxy_record(attributed).unwrap();
521        assert_eq!(attributed.execution_path.as_deref(), Some("cuda-c"));
522        assert!(parse_decode_proxy_line("nope").is_none());
523        assert!(decode_proxy_coherence_ok("… Paris is lovely"));
524        assert!(!decode_proxy_coherence_ok("asdkfjhasdf"));
525    }
526
527    #[test]
528    fn decode_proxy_ranking_prefers_higher_tok_s() {
529        let mut matrix = CapabilityMatrix {
530            circuits: vec![
531                CircuitBench {
532                    label: "fast gemv slow decode".into(),
533                    kind: CircuitKind::DiscreteGpu,
534                    backend: "Vulkan".into(),
535                    ms_per_gemv: 0.1,
536                    gflops: 50.0,
537                    upload_gbps: 4.0,
538                    rel_score: 1.0,
539                    decode_proxy_tok_s: Some(5.0),
540                },
541                CircuitBench {
542                    label: "slower gemv fast decode".into(),
543                    kind: CircuitKind::DiscreteGpu,
544                    backend: "Dx12".into(),
545                    ms_per_gemv: 0.2,
546                    gflops: 25.0,
547                    upload_gbps: 4.0,
548                    rel_score: 0.5,
549                    decode_proxy_tok_s: Some(18.0),
550                },
551            ],
552            gemv_n: 512,
553            npu_probed: false,
554        };
555        matrix.apply_decode_proxy_ranking();
556        assert_eq!(matrix.best().unwrap().backend, "Dx12");
557        assert!((matrix.best().unwrap().rel_score - 1.0).abs() < 1e-9);
558    }
559
560    /// Real fast-boot path: first call probes + caches (was_cached=false); second loads (true).
561    /// Runs the benchmark once (CPU always available); GPU rows appear if an adapter is present.
562    #[test]
563    fn load_or_probe_caches_then_hits() {
564        let dir = tempfile::tempdir().unwrap();
565        let path = dir.path().join("passport.cbor");
566
567        let (first, cached1) = load_or_probe(&path, 512);
568        assert!(!cached1, "first call must probe");
569        assert!(path.exists(), "probe must write the cache");
570        assert!(!first.matrix.circuits.is_empty());
571
572        let (second, cached2) = load_or_probe(&path, 512);
573        assert!(cached2, "second call must hit the cache (fast-boot)");
574        assert_eq!(second.key, first.key);
575    }
576}