1#![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
25pub const PASSPORT_VERSION: u32 = 2;
28
29pub const PASSPORT_GEMV_N: usize = 2048;
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct HardwarePassport {
35 pub version: u32,
36 pub key: String,
38 pub topology: HostTopology,
39 pub matrix: CapabilityMatrix,
40 #[serde(default)]
43 pub preferred_inference_backend: Option<String>,
44 #[serde(default)]
46 pub probe_gemv_n: usize,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub decode_proxy_model: Option<String>,
50 #[serde(default)]
52 pub decode_proxy_tokens: u32,
53}
54
55pub 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
67pub fn default_cache_path() -> PathBuf {
69 std::env::temp_dir().join("qualia_hardware_passport.cbor")
70}
71
72pub 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
79pub 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
89pub 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); }
99 }
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
121pub 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
141pub const DECODE_PROXY_PROBE_PROMPT: &str = "The capital of France is";
143
144#[derive(Debug, Clone)]
146pub struct DecodeProxyResult {
147 pub tok_s: f64,
148 pub text: String,
149 pub coherence_ok: bool,
151 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
160pub fn decode_proxy_coherence_ok(text: &str) -> bool {
163 text.to_ascii_lowercase().contains("paris")
164}
165
166pub 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 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 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
229pub 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
234pub 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 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 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
317pub fn parse_decode_proxy_line(stdout: &str) -> Option<f64> {
319 parse_decode_proxy_record(stdout).map(|r| r.tok_s)
320}
321
322#[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
358pub fn load_or_probe_default() -> (HardwarePassport, bool) {
360 load_or_probe(&default_cache_path(), PASSPORT_GEMV_N)
361}
362
363pub fn cached_preferred_wgpu_backend() -> Option<String> {
367 let path = default_cache_path();
368 let passport = read_passport(&path)?;
369 if let Some(ref t) = passport.preferred_inference_backend {
371 return Some(t.clone());
372 }
373 let best = passport.matrix.best()?;
374 if matches!(best.kind, crate::device_benchmark::CircuitKind::Cpu) {
376 return None;
377 }
378 Some(best.backend.clone())
379}
380
381pub 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, 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 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 #[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}