1#![cfg(not(target_arch = "wasm32"))]
13
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum AdapterClass {
19 Discrete,
20 Integrated,
21 Cpu,
22 Virtual,
23 Other,
24}
25
26impl AdapterClass {
27 fn from_wgpu(t: wgpu::DeviceType) -> Self {
28 match t {
29 wgpu::DeviceType::DiscreteGpu => Self::Discrete,
30 wgpu::DeviceType::IntegratedGpu => Self::Integrated,
31 wgpu::DeviceType::Cpu => Self::Cpu,
32 wgpu::DeviceType::VirtualGpu => Self::Virtual,
33 wgpu::DeviceType::Other => Self::Other,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum HostMemoryTopology {
41 Discrete,
43 Unified,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct AdapterDesc {
50 pub name: String,
51 pub backend: String,
52 pub class: AdapterClass,
53 pub vendor: u32,
54 pub device: u32,
55 pub dedicated_vram_bytes: u64,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct HostTopology {
62 pub adapters: Vec<AdapterDesc>,
63 pub topology: HostMemoryTopology,
64 pub host_ram_bytes: u64,
65 pub host_ram_available_bytes: u64,
66 pub cpu_cores: usize,
67 pub os_floor_bytes: u64,
69 pub usable_model_budget_bytes: u64,
71}
72
73const DISCRETE_VRAM_FLOOR: u64 = 1_500 * 1024 * 1024; const UNIFIED_HOST_FLOOR_MAX: u64 = 6 * 1024 * 1024 * 1024; fn backend_rank(b: wgpu::Backend) -> u8 {
78 match b {
79 wgpu::Backend::Metal => 0,
81 wgpu::Backend::Dx12 => 1,
82 wgpu::Backend::Vulkan => 2,
83 wgpu::Backend::Gl => 3,
84 _ => 4,
85 }
86}
87
88pub fn probe_host_topology() -> HostTopology {
90 use sysinfo::System;
91 let sys = System::new_all();
92 let host_ram_bytes = sys.total_memory(); let host_ram_available_bytes = sys.available_memory();
94 let cpu_cores = num_cpus::get();
95
96 let instance = wgpu::Instance::default();
98 let raw = pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all()));
99 let mut best: std::collections::HashMap<(u32, u32), (u8, AdapterDesc)> =
100 std::collections::HashMap::new();
101 for adapter in raw {
102 let info = adapter.get_info();
103 let rank = backend_rank(info.backend);
104 let desc = AdapterDesc {
105 name: info.name.clone(),
106 backend: format!("{:?}", info.backend),
107 class: AdapterClass::from_wgpu(info.device_type),
108 vendor: info.vendor,
109 device: info.device,
110 dedicated_vram_bytes: 0,
111 };
112 best.entry((info.vendor, info.device))
113 .and_modify(|(r, d)| {
114 if rank < *r {
115 *r = rank;
116 *d = desc.clone();
117 }
118 })
119 .or_insert((rank, desc));
120 }
121 let mut adapters: Vec<AdapterDesc> = best.into_values().map(|(_, d)| d).collect();
122 let vendors_with_real_dev: std::collections::HashSet<u32> = adapters
127 .iter()
128 .filter(|a| a.device != 0)
129 .map(|a| a.vendor)
130 .collect();
131 adapters.retain(|a| a.device != 0 || !vendors_with_real_dev.contains(&a.vendor));
132 adapters.sort_by_key(|a| (a.class as u8, a.vendor, a.device));
134
135 let has_discrete = adapters.iter().any(|a| a.class == AdapterClass::Discrete);
136 let topology = if has_discrete {
137 HostMemoryTopology::Discrete
138 } else {
139 HostMemoryTopology::Unified
140 };
141
142 #[cfg(target_os = "windows")]
144 if has_discrete {
145 if let Ok(mem) = crate::directml_bridge::probe_best_adapter_memory() {
146 let vram = mem.dedicated_vram_bytes.max(mem.local_budget_bytes);
147 if let Some(disc) = adapters
148 .iter_mut()
149 .find(|a| a.class == AdapterClass::Discrete)
150 {
151 disc.dedicated_vram_bytes = vram;
152 }
153 }
154 }
155
156 let (os_floor_bytes, usable_model_budget_bytes) = match topology {
157 HostMemoryTopology::Discrete => {
158 let vram = adapters
159 .iter()
160 .filter(|a| a.class == AdapterClass::Discrete)
161 .map(|a| a.dedicated_vram_bytes)
162 .max()
163 .unwrap_or(0);
164 let floor = DISCRETE_VRAM_FLOOR.min(vram / 4); (floor, vram.saturating_sub(floor))
166 }
167 HostMemoryTopology::Unified => {
168 let floor = UNIFIED_HOST_FLOOR_MAX.min(host_ram_bytes / 4);
170 (floor, host_ram_bytes.saturating_sub(floor))
171 }
172 };
173
174 HostTopology {
175 adapters,
176 topology,
177 host_ram_bytes,
178 host_ram_available_bytes,
179 cpu_cores,
180 os_floor_bytes,
181 usable_model_budget_bytes,
182 }
183}
184
185impl HostTopology {
186 pub fn has_heterogeneous_overflow(&self) -> bool {
189 self.topology == HostMemoryTopology::Discrete
190 && self
191 .adapters
192 .iter()
193 .any(|a| a.class == AdapterClass::Integrated)
194 }
195
196 pub fn summary(&self) -> String {
198 let mut s = format!(
199 "HostTopology: {:?} | host RAM {:.1} GB ({:.1} GB free) | {} cores | OS floor {:.1} GB | model budget {:.1} GB | heterogeneous={}\n",
200 self.topology,
201 self.host_ram_bytes as f64 / 1e9,
202 self.host_ram_available_bytes as f64 / 1e9,
203 self.cpu_cores,
204 self.os_floor_bytes as f64 / 1e9,
205 self.usable_model_budget_bytes as f64 / 1e9,
206 self.has_heterogeneous_overflow(),
207 );
208 for a in &self.adapters {
209 s.push_str(&format!(
210 " - {:?} [{}] {} (vendor 0x{:04x} dev 0x{:04x}){}\n",
211 a.class,
212 a.backend,
213 a.name,
214 a.vendor,
215 a.device,
216 if a.dedicated_vram_bytes > 0 {
217 format!(" VRAM {:.1} GB", a.dedicated_vram_bytes as f64 / 1e9)
218 } else {
219 String::new()
220 },
221 ));
222 }
223 s
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn h0_probes_host_topology() {
233 let topo = probe_host_topology();
234 eprintln!("{}", topo.summary());
235
236 assert!(topo.host_ram_bytes > 0, "host RAM must be sensed");
238 assert!(topo.cpu_cores > 0, "cpu cores must be sensed");
239
240 if topo.adapters.is_empty() {
241 eprintln!("[h0] no wgpu adapters (headless CI) — RAM/CPU still sensed");
242 return;
243 }
244
245 let any_discrete = topo
247 .adapters
248 .iter()
249 .any(|a| a.class == AdapterClass::Discrete);
250 assert_eq!(
251 any_discrete,
252 topo.topology == HostMemoryTopology::Discrete,
253 "topology must match presence of a discrete adapter"
254 );
255 assert!(
257 topo.usable_model_budget_bytes
258 <= topo
259 .host_ram_bytes
260 .max(
261 topo.adapters
262 .iter()
263 .map(|a| a.dedicated_vram_bytes)
264 .max()
265 .unwrap_or(0)
266 )
267 .max(1)
268 );
269 }
270}