1use serde::{Deserialize, Serialize};
11use std::path::{Path, PathBuf};
12
13pub const MACHINE_GPU_PROFILE_VERSION: u32 = 1;
14pub const MACHINE_GPU_PROFILE_NAME: &str = "machine-gpu-profile.json";
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
17pub struct ToolchainAvailability {
18 pub wgpu: bool,
19 pub cuda_toolkit: bool,
20 pub dxc_cli: bool,
22 pub metal_xcrun: bool,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
26pub struct AdapterFeatures {
27 pub name: String,
28 pub backend: String,
29 pub discrete: bool,
30 pub subgroups: bool,
31 pub coopmat: bool,
32 pub shader_f16: bool,
33 pub timestamp_query: bool,
34 pub topology_hash: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct MeasuredDecodePath {
40 pub wgpu_backend: String,
41 pub inference_mode: String,
42 pub p64_path: String,
43 pub tok_s: f64,
44 pub coherence_ok: bool,
45 pub tokens: u32,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
50pub struct RecommendedPath {
51 pub wgpu_backend: String,
53 pub inference_mode: String,
55 pub rationale: String,
57 pub env: Vec<(String, String)>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62pub struct MachineGpuProfile {
63 pub version: u32,
64 pub written_unix_ms: u64,
65 pub host: String,
66 pub toolchain: ToolchainAvailability,
67 pub adapter: AdapterFeatures,
68 pub native_tiers: Vec<String>,
70 pub measured_paths: Vec<MeasuredDecodePath>,
71 pub recommended: RecommendedPath,
72 pub notes: Vec<String>,
73}
74
75impl MachineGpuProfile {
76 pub fn now_ms() -> u64 {
77 std::time::SystemTime::now()
78 .duration_since(std::time::UNIX_EPOCH)
79 .map(|d| d.as_millis() as u64)
80 .unwrap_or(0)
81 }
82
83 pub fn default_path(out_dir: &Path) -> PathBuf {
84 out_dir.join(MACHINE_GPU_PROFILE_NAME)
85 }
86
87 pub fn write_json(&self, path: &Path) -> Result<(), String> {
88 let json = serde_json::to_string_pretty(self).map_err(|e| format!("serialize: {e}"))?;
89 if let Some(parent) = path.parent() {
90 std::fs::create_dir_all(parent).map_err(|e| format!("mkdir: {e}"))?;
91 }
92 std::fs::write(path, json).map_err(|e| format!("write {}: {e}", path.display()))
93 }
94
95 pub fn load_json(path: &Path) -> Result<Option<Self>, String> {
96 if !path.is_file() {
97 return Ok(None);
98 }
99 let bytes = std::fs::read(path).map_err(|e| format!("read: {e}"))?;
100 let p: Self = serde_json::from_slice(&bytes).map_err(|e| format!("parse: {e}"))?;
101 Ok(Some(p))
102 }
103
104 pub fn recompute_recommended(&mut self) {
106 let best = self
107 .measured_paths
108 .iter()
109 .filter(|p| p.coherence_ok)
110 .max_by(|a, b| {
111 a.tok_s
112 .partial_cmp(&b.tok_s)
113 .unwrap_or(std::cmp::Ordering::Equal)
114 });
115 if let Some(b) = best {
116 self.recommended = RecommendedPath {
117 wgpu_backend: b.wgpu_backend.clone(),
118 inference_mode: b.inference_mode.clone(),
119 rationale: format!(
120 "highest coherent decode-proxy {:.2} tok/s on {} + {} (package {})",
121 b.tok_s, b.wgpu_backend, b.inference_mode, b.p64_path
122 ),
123 env: vec![
124 ("QUALIA_WGPU_BACKEND".into(), b.wgpu_backend.clone()),
125 ("QUALIA_INFERENCE_MODE".into(), b.inference_mode.clone()),
126 ],
127 };
128 }
129 }
130
131 pub fn apply_env_script_ps1(&self) -> String {
132 let mut s = String::from(
134 "# Machine GPU capability profile: prefer native over WGSL-only defaults\n",
135 );
136 for (k, v) in &self.recommended.env {
137 s.push_str(&format!("$env:{k}='{v}'\n"));
138 }
139 s.push_str(
140 "# Forge: CUDA densify decode GEMV stays lab-only unless you know the package\n",
141 );
142 s.push_str(
143 "# $env:QUALIA_LLM_CUDA_TC_DECODE='1' # only after oracle green for that layout\n",
144 );
145 s
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn recompute_picks_fastest_coherent() {
155 let mut p = MachineGpuProfile {
156 version: 1,
157 written_unix_ms: 0,
158 host: "test".into(),
159 toolchain: Default::default(),
160 adapter: Default::default(),
161 native_tiers: vec!["cuda".into(), "vulkan".into()],
162 measured_paths: vec![
163 MeasuredDecodePath {
164 wgpu_backend: "dx12".into(),
165 inference_mode: "portable".into(),
166 p64_path: "a.p64".into(),
167 tok_s: 40.0,
168 coherence_ok: true,
169 tokens: 16,
170 },
171 MeasuredDecodePath {
172 wgpu_backend: "vulkan".into(),
173 inference_mode: "fast-verify".into(),
174 p64_path: "a.p64".into(),
175 tok_s: 100.0,
176 coherence_ok: true,
177 tokens: 16,
178 },
179 MeasuredDecodePath {
180 wgpu_backend: "dx12".into(),
181 inference_mode: "cuda".into(),
182 p64_path: "a.p64".into(),
183 tok_s: 200.0,
184 coherence_ok: false,
185 tokens: 16,
186 },
187 ],
188 recommended: Default::default(),
189 notes: vec![],
190 };
191 p.recompute_recommended();
192 assert_eq!(p.recommended.wgpu_backend, "vulkan");
193 assert_eq!(p.recommended.inference_mode, "fast-verify");
194
195 let ps1 = p.apply_env_script_ps1();
197 assert!(ps1.is_ascii(), "apply script must be ASCII: {ps1}");
198 assert!(ps1.contains("$env:QUALIA_WGPU_BACKEND='vulkan'"));
199 assert!(ps1.contains("$env:QUALIA_INFERENCE_MODE='fast-verify'"));
200 }
201}