Skip to main content

qualia_core_db/gpu_context/
device_registry.rs

1//! Device-per-circuit registry — obtain a `wgpu::Device` for a SPECIFIC adapter/circuit
2//! (STELLAR §A H3 foundation for heterogeneous GPU routing).
3//!
4//! [`super::shared_gpu`] / [`super::try_shared_gpu`] give you the single process-wide *primary*
5//! device (a `PowerPreference::HighPerformance` pick — the discrete GPU on a discrete+integrated
6//! box). This module lets code obtain a device for a **specific** enumerated circuit — e.g. the
7//! integrated GPU — so audio/vision work can run off the LLM's silicon.
8//!
9//! **Role policy:** keep the primary circuit free for the LLM; audio/vision use the *auxiliary*
10//! circuit. [`try_auxiliary_gpu`] gives callers the fallback chain **auxiliary → primary → None**
11//! (the caller then degrades to CPU) so they always get *a* device or `None`, never a panic.
12//!
13//! Mirrors [`super::try_shared_gpu`]'s discipline: no `unwrap()` outside tests, and it NEVER panics
14//! on a missing or failed device — every failure path returns `None`. Native only.
15#![cfg(not(target_arch = "wasm32"))]
16
17use std::collections::hash_map::Entry;
18use std::sync::OnceLock;
19
20use super::{init_shared_gpu_for_adapter, try_shared_gpu, GpuAdapterCaps, SharedGpuContext};
21
22/// One enumerated compute circuit: the live `wgpu::Adapter`, its capability snapshot, and a stable
23/// identity (vendor / device / backend) matching `device_benchmark` / `host_topology`.
24pub struct CircuitAdapter {
25    /// The live adapter (cheap `Arc`-backed handle) this circuit routes to.
26    pub adapter: wgpu::Adapter,
27    /// Immutable capability snapshot (carries vendor / device / backend / device_type).
28    pub caps: GpuAdapterCaps,
29}
30
31impl CircuitAdapter {
32    /// wgpu device class for role heuristics.
33    #[inline]
34    pub fn device_type(&self) -> wgpu::DeviceType {
35        self.caps.device_type
36    }
37
38    /// Stable physical identity — matches `device_benchmark` / `host_topology` conventions.
39    #[inline]
40    pub fn identity(&self) -> (u32, u32, wgpu::Backend) {
41        (self.caps.vendor, self.caps.device, self.caps.backend)
42    }
43}
44
45/// The wgpu instance the registry enumerated adapters from. Cloned into each per-circuit device so
46/// the device and any surface it creates share one instance.
47static REGISTRY_INSTANCE: OnceLock<wgpu::Instance> = OnceLock::new();
48/// Enumerate-once cache of routable circuits (deduped to one entry per physical GPU).
49static ADAPTERS: OnceLock<Vec<CircuitAdapter>> = OnceLock::new();
50/// Per-circuit device cache — one `OnceLock<Option<..>>` slot per enumerated adapter (by index).
51static DEVICES: OnceLock<Vec<OnceLock<Option<SharedGpuContext>>>> = OnceLock::new();
52
53/// Prefer discrete GPUs, then integrated, then virtual/other, with software CPU adapters last.
54fn device_type_rank(t: wgpu::DeviceType) -> u8 {
55    match t {
56        wgpu::DeviceType::DiscreteGpu => 0,
57        wgpu::DeviceType::IntegratedGpu => 1,
58        wgpu::DeviceType::VirtualGpu => 2,
59        wgpu::DeviceType::Other => 3,
60        wgpu::DeviceType::Cpu => 4,
61    }
62}
63
64/// Backend preference when the same physical GPU is enumerated on several backends
65/// (mirrors `host_topology` / `device_benchmark`).
66fn backend_rank(b: wgpu::Backend) -> u8 {
67    match b {
68        wgpu::Backend::Metal => 0,
69        wgpu::Backend::Dx12 => 1,
70        wgpu::Backend::Vulkan => 2,
71        wgpu::Backend::Gl => 3,
72        _ => 4,
73    }
74}
75
76#[inline]
77fn registry_instance() -> &'static wgpu::Instance {
78    REGISTRY_INSTANCE.get_or_init(wgpu::Instance::default)
79}
80
81/// Enumerate every adapter once (cached), deduped to **one entry per physical circuit**
82/// (vendor, device) keeping the preferred backend, and sorted discrete → integrated → other → cpu
83/// for a deterministic, index-stable list. Returns an empty slice on a headless / GPU-less box.
84///
85/// The returned indices are the stable handles used by [`try_device_for_adapter`],
86/// [`primary_circuit_index`], and [`auxiliary_circuit_index`].
87pub fn enumerate_circuits() -> &'static [CircuitAdapter] {
88    ADAPTERS.get_or_init(|| {
89        let instance = registry_instance();
90        let raw = pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all()));
91        // Dedup by physical (vendor, device); keep the top-ranked backend for each.
92        let mut best: std::collections::HashMap<(u32, u32), (u8, wgpu::Adapter, GpuAdapterCaps)> =
93            std::collections::HashMap::new();
94        for adapter in raw {
95            let caps = GpuAdapterCaps::from_adapter(&adapter);
96            let rank = backend_rank(caps.backend);
97            match best.entry((caps.vendor, caps.device)) {
98                Entry::Occupied(mut e) => {
99                    if rank < e.get().0 {
100                        e.insert((rank, adapter, caps));
101                    }
102                }
103                Entry::Vacant(e) => {
104                    e.insert((rank, adapter, caps));
105                }
106            }
107        }
108        let mut circuits: Vec<CircuitAdapter> = best
109            .into_values()
110            .map(|(_, adapter, caps)| CircuitAdapter { adapter, caps })
111            .collect();
112        // Drop device==0 phantom rows for a vendor that also enumerated a real device id
113        // (some backends, notably GL, report device 0 for a card already seen with a real id).
114        let real_vendors: std::collections::HashSet<u32> = circuits
115            .iter()
116            .filter(|c| c.caps.device != 0)
117            .map(|c| c.caps.vendor)
118            .collect();
119        circuits.retain(|c| c.caps.device != 0 || !real_vendors.contains(&c.caps.vendor));
120        // Deterministic, stable ordering: discrete first, then integrated, then the rest.
121        circuits.sort_by_key(|c| {
122            (
123                device_type_rank(c.caps.device_type),
124                c.caps.vendor,
125                c.caps.device,
126            )
127        });
128        circuits
129    })
130}
131
132/// Per-circuit device slots, lazily sized to the number of enumerated circuits.
133fn device_slots() -> &'static Vec<OnceLock<Option<SharedGpuContext>>> {
134    DEVICES.get_or_init(|| {
135        let n = enumerate_circuits().len();
136        let mut v = Vec::with_capacity(n);
137        for _ in 0..n {
138            v.push(OnceLock::new());
139        }
140        v
141    })
142}
143
144/// Lazily build (and cache) a [`SharedGpuContext`] for the enumerated circuit at `index`.
145///
146/// Returns `None` for an out-of-range index, when no tokio runtime can be started, or on any
147/// device-creation failure — it **NEVER panics** (mirrors [`super::try_shared_gpu`]). Idempotent:
148/// repeated calls return the same cached device, or the same cached `None` (each circuit probes at
149/// most once).
150pub fn try_device_for_adapter(index: usize) -> Option<&'static SharedGpuContext> {
151    let circuits = enumerate_circuits();
152    if index >= circuits.len() {
153        return None;
154    }
155    device_slots()[index]
156        .get_or_init(|| {
157            let handle = match tokio::runtime::Handle::try_current() {
158                Ok(h) => h,
159                Err(_) => match tokio::runtime::Runtime::new() {
160                    Ok(rt) => Box::leak(Box::new(rt)).handle().clone(),
161                    Err(_) => return None,
162                },
163            };
164            let instance = registry_instance().clone();
165            let adapter = circuits[index].adapter.clone();
166            tokio::task::block_in_place(|| {
167                handle
168                    .block_on(init_shared_gpu_for_adapter(instance, adapter))
169                    .ok()
170            })
171        })
172        .as_ref()
173}
174
175/// Index of the **primary** circuit — the discrete / HighPerformance adapter that
176/// [`super::shared_gpu`] uses. If the process-wide shared device is already initialized, matches its
177/// exact identity (that is definitionally the primary). Otherwise picks the first discrete GPU
178/// (what `HighPerformance` selects), else the top-ranked adapter. `None` only when no circuit
179/// exists. Never forces the shared device to initialize.
180///
181/// Policy: keep the primary circuit free for the LLM; audio/vision use the auxiliary circuit.
182pub fn primary_circuit_index() -> Option<usize> {
183    let circuits = enumerate_circuits();
184    if circuits.is_empty() {
185        return None;
186    }
187    // If the process-wide shared (primary) device already exists, match it exactly — but do NOT
188    // force initialization here (peek only).
189    if let Some(Some(shared)) = super::SHARED_GPU.get() {
190        let want = (shared.adapter_caps.vendor, shared.adapter_caps.device);
191        if let Some(i) = circuits
192            .iter()
193            .position(|c| (c.caps.vendor, c.caps.device) == want)
194        {
195            return Some(i);
196        }
197    }
198    circuits
199        .iter()
200        .position(|c| c.caps.device_type == wgpu::DeviceType::DiscreteGpu)
201        .or(Some(0))
202}
203
204/// Index of the best **auxiliary** circuit — the best NON-primary circuit, so the primary stays
205/// free for the LLM. Prefers a (non-primary) `DeviceType::IntegratedGpu`; otherwise the next-best
206/// non-primary circuit (the list is already ranked discrete → integrated → …). Returns `None` when
207/// only one circuit exists.
208///
209/// A measured `device_benchmark::CapabilityMatrix` ranking would be more precise, but benchmarking
210/// spawns worker processes and is not "cheaply available", so this uses the DeviceType heuristic.
211///
212/// Policy: keep the primary circuit free for the LLM; audio/vision use the auxiliary circuit.
213pub fn auxiliary_circuit_index() -> Option<usize> {
214    let circuits = enumerate_circuits();
215    if circuits.len() < 2 {
216        return None;
217    }
218    let primary = primary_circuit_index();
219    let is_primary = |i: usize| Some(i) == primary;
220    // Prefer a non-primary integrated GPU.
221    if let Some(i) = (0..circuits.len()).find(|&i| {
222        !is_primary(i) && circuits[i].caps.device_type == wgpu::DeviceType::IntegratedGpu
223    }) {
224        return Some(i);
225    }
226    // Else the first non-primary circuit.
227    (0..circuits.len()).find(|&i| !is_primary(i))
228}
229
230/// A device for the **auxiliary** circuit if one exists, else the **primary** shared device, else
231/// `None`. Fallback chain: **auxiliary → primary → None** (the caller then degrades to CPU). Never
232/// panics — every failure resolves to `None`.
233///
234/// Policy: keep the primary circuit free for the LLM; audio/vision call this to use the auxiliary
235/// GPU, transparently falling back to the primary (or CPU) when there is only one circuit.
236pub fn try_auxiliary_gpu() -> Option<&'static SharedGpuContext> {
237    auxiliary_circuit_index()
238        .and_then(try_device_for_adapter)
239        .or_else(try_shared_gpu)
240}
241
242#[cfg(all(test, not(target_arch = "wasm32")))]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn enumerate_is_nonempty_or_headless() {
248        // Enumeration returns a cached list; its length + identities are stable across calls
249        // (may be 0 on a headless box — we assert idempotence, not a nonzero count).
250        let a = enumerate_circuits();
251        let b = enumerate_circuits();
252        assert_eq!(
253            a.len(),
254            b.len(),
255            "enumeration must be cached (stable length)"
256        );
257        for (x, y) in a.iter().zip(b.iter()) {
258            assert_eq!(
259                x.identity(),
260                y.identity(),
261                "cached identities must be stable"
262            );
263        }
264    }
265
266    #[test]
267    #[serial_test::serial(gpu)]
268    fn try_device_for_adapter_never_panics_and_caches() {
269        let n = enumerate_circuits().len();
270        if n == 0 {
271            // GPU-less / headless: index 0 must return None without panic.
272            assert!(try_device_for_adapter(0).is_none());
273            return;
274        }
275        let first = try_device_for_adapter(0).is_some();
276        let second = try_device_for_adapter(0).is_some();
277        assert_eq!(
278            first, second,
279            "try_device_for_adapter must be cached + consistent"
280        );
281        // Out-of-range never panics.
282        assert!(try_device_for_adapter(n + 1000).is_none());
283    }
284
285    #[test]
286    #[serial_test::serial(gpu)]
287    fn try_auxiliary_gpu_never_panics() {
288        // Consistent across calls, never panics.
289        let a = try_auxiliary_gpu().is_some();
290        let b = try_auxiliary_gpu().is_some();
291        assert_eq!(a, b, "try_auxiliary_gpu must be consistent across calls");
292        // When a working primary device exists, the fallback chain (aux → primary) must yield one.
293        if try_shared_gpu().is_some() {
294            assert!(a, "aux must fall back to the working primary device");
295        }
296    }
297
298    #[test]
299    #[serial_test::serial(gpu)]
300    fn primary_and_auxiliary_indices_differ_when_multi_adapter() {
301        let n = enumerate_circuits().len();
302        let p = primary_circuit_index();
303        let aux = auxiliary_circuit_index();
304        if n >= 2 {
305            assert!(p.is_some(), "multi-adapter → a primary exists");
306            assert!(aux.is_some(), "multi-adapter → an auxiliary exists");
307            assert_ne!(p, aux, "primary and auxiliary must differ");
308        } else if n == 1 {
309            assert_eq!(p, Some(0), "single adapter → primary is index 0");
310            assert_eq!(
311                aux, None,
312                "single adapter → no auxiliary (fallback covers callers)"
313            );
314        } else {
315            assert_eq!(p, None);
316            assert_eq!(aux, None);
317        }
318    }
319}