Skip to main content

qualia_core_db/platform/compute_bridge/
backend.rs

1//! The open backend registry (HARDWARE_BACKEND_AUTOSELECT_PLAN.md §2.2, §4).
2//!
3//! The bridge's expansion point: backends are **not** a closed enum. Each
4//! acceleration method (CPU, the wgpu backends, later CUDA / ROCm / oneAPI / NPU
5//! runtimes) is one `impl ProbeableBackend` registered into a [`BackendRegistry`].
6//! The benchmark loop, the ranking, the passport schema and `ComputePolicy::select`
7//! all iterate the registry — so **adding a backend is one `register()` call and
8//! never edits the decision tree** (the load-bearing requirement). A backend that
9//! is not `available()` on this machine simply contributes no rows.
10//!
11//! `BackendId` is a `Copy` `&'static str` so it is zero-heap to pass around and
12//! string-keyed in the passport (forward-compatible: a passport written by a core
13//! build is still readable by an expansion build — the new backend is just absent
14//! and gets probed on next boot).
15
16use super::kernel_class::KernelClass;
17use crate::device_benchmark::CircuitBench;
18
19/// Stable, string-keyed backend identifier. `Copy`, zero-heap. Built-ins:
20/// `"cpu"`, `"wgpu"` (which itself reports per-adapter circuits via wgpu's
21/// `Vulkan`/`Dx12`/`Metal`/`Gl`); expansion ids: `"cuda"`, `"rocm"`, `"oneapi"`,
22/// `"npu-directml"`, …
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct BackendId(pub &'static str);
25
26impl BackendId {
27    pub const CPU: BackendId = BackendId("cpu");
28    pub const WGPU: BackendId = BackendId("wgpu");
29
30    pub fn as_str(self) -> &'static str {
31        self.0
32    }
33}
34
35impl core::fmt::Display for BackendId {
36    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37        f.write_str(self.0)
38    }
39}
40
41/// Why a dispatch could not run on the requested backend. The dispatcher must
42/// degrade to CPU on any of these, never panic (plan §7: CPU never hard-fails).
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum DispatchError {
45    /// The backend exists but cannot run this kernel class.
46    UnsupportedClass(KernelClass),
47    /// The backend's runtime/SDK is not present on this host.
48    Unavailable(BackendId),
49    /// A backend-internal failure (driver, allocation, …) — caller falls back to CPU.
50    BackendFailure(String),
51}
52
53impl core::fmt::Display for DispatchError {
54    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55        match self {
56            DispatchError::UnsupportedClass(c) => {
57                write!(f, "backend cannot run kernel class {}", c.label())
58            }
59            DispatchError::Unavailable(b) => write!(f, "backend {b} is not available on this host"),
60            DispatchError::BackendFailure(m) => write!(f, "backend failure: {m}"),
61        }
62    }
63}
64impl std::error::Error for DispatchError {}
65
66/// Per-class problem sizes for the measurement panel. Kept small; cached in the
67/// passport so the heavy pass runs once per machine (plan §3 cost guard). `quick()`
68/// shrinks the sizes for low-tier devices / fast boot.
69#[derive(Debug, Clone, Copy)]
70pub struct KernelPanel {
71    /// GEMV/GEMM side length (DenseLinear).
72    pub dense_n: usize,
73    /// Vector length for ElementwiseMap / Reduction / Scan.
74    pub vector_len: usize,
75    /// Grid length for the 1-D Stencil pass.
76    pub grid_n: usize,
77    /// Particle count for the AllPairs (N-body) pass.
78    pub nbody_n: usize,
79    /// Transform length for the FFT (must be a power of two).
80    pub fft_n: usize,
81    /// Monte-Carlo steps for the Divergent pass.
82    pub mc_steps: usize,
83}
84
85impl Default for KernelPanel {
86    fn default() -> Self {
87        Self {
88            dense_n: 1024,
89            vector_len: 1 << 20,
90            grid_n: 1 << 20,
91            nbody_n: 2048,
92            fft_n: 1 << 16,
93            mc_steps: 1 << 20,
94        }
95    }
96}
97
98impl KernelPanel {
99    /// Smaller panel for fast boot / Tier-0 devices (plan §3 `--quick`).
100    pub fn quick() -> Self {
101        Self {
102            dense_n: 256,
103            vector_len: 1 << 16,
104            grid_n: 1 << 16,
105            nbody_n: 512,
106            fft_n: 1 << 12,
107            mc_steps: 1 << 16,
108        }
109    }
110}
111
112/// One acceleration method. Implementors register into a [`BackendRegistry`]; the
113/// rest of the bridge only ever sees them through this trait, which is why adding a
114/// backend never edits `select()`.
115pub trait ProbeableBackend: Send + Sync {
116    /// Stable identifier (`"cpu"`, `"wgpu"`, `"cuda"`, …).
117    fn id(&self) -> BackendId;
118
119    /// Is this backend's runtime/SDK present and usable on THIS machine? A `false`
120    /// backend contributes no rows and is never selected.
121    fn available(&self) -> bool;
122
123    /// Measure this backend on one kernel class, returning a row per physical
124    /// circuit it can drive (e.g. wgpu returns one row per adapter). Empty when the
125    /// backend cannot run the class or is unavailable — recorded honestly as "no
126    /// rows," never a fabricated number.
127    fn probe_class(&self, class: KernelClass, panel: &KernelPanel) -> Vec<CircuitBench>;
128}
129
130/// The registry of acceleration methods. Heap-using and boot-time only (the heavy
131/// probe runs once and is cached in the passport — never on a hot path).
132#[derive(Default)]
133pub struct BackendRegistry {
134    backends: Vec<Box<dyn ProbeableBackend>>,
135}
136
137impl BackendRegistry {
138    pub fn new() -> Self {
139        Self {
140            backends: Vec::new(),
141        }
142    }
143
144    /// Register a backend. Adding one here is the *only* change needed to bring a
145    /// new acceleration method into the benchmark, ranking and policy.
146    pub fn register(&mut self, backend: Box<dyn ProbeableBackend>) -> &mut Self {
147        self.backends.push(backend);
148        self
149    }
150
151    /// All registered backends (including currently-unavailable ones).
152    pub fn iter(&self) -> impl Iterator<Item = &dyn ProbeableBackend> {
153        self.backends.iter().map(|b| b.as_ref())
154    }
155
156    /// Backends actually usable on this host.
157    pub fn available(&self) -> impl Iterator<Item = &dyn ProbeableBackend> {
158        self.backends
159            .iter()
160            .map(|b| b.as_ref())
161            .filter(|b| b.available())
162    }
163
164    pub fn len(&self) -> usize {
165        self.backends.len()
166    }
167
168    pub fn is_empty(&self) -> bool {
169        self.backends.is_empty()
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::device_benchmark::CircuitKind;
177
178    /// A synthetic backend proving the registry is open: it registers and is iterated
179    /// without any change to the registry, panel, matrix, or policy code.
180    struct StubBackend {
181        id: BackendId,
182        up: bool,
183    }
184    impl ProbeableBackend for StubBackend {
185        fn id(&self) -> BackendId {
186            self.id
187        }
188        fn available(&self) -> bool {
189            self.up
190        }
191        fn probe_class(&self, _class: KernelClass, _panel: &KernelPanel) -> Vec<CircuitBench> {
192            if !self.up {
193                return Vec::new();
194            }
195            vec![CircuitBench {
196                label: self.id.as_str().to_string(),
197                kind: CircuitKind::Other,
198                backend: self.id.as_str().to_string(),
199                ms_per_gemv: 1.0,
200                gflops: 1.0,
201                upload_gbps: 1.0,
202                rel_score: 1.0,
203                decode_proxy_tok_s: None,
204            }]
205        }
206    }
207
208    #[test]
209    fn registry_is_open_and_iterates_members() {
210        let mut reg = BackendRegistry::new();
211        reg.register(Box::new(StubBackend {
212            id: BackendId("alpha"),
213            up: true,
214        }))
215        .register(Box::new(StubBackend {
216            id: BackendId("beta"),
217            up: false,
218        }));
219        assert_eq!(reg.len(), 2);
220        // Only the available backend is offered for work.
221        let avail: Vec<_> = reg.available().map(|b| b.id()).collect();
222        assert_eq!(avail, vec![BackendId("alpha")]);
223        // Unavailable backend contributes no rows (honest "not probed").
224        let rows: Vec<_> = reg
225            .iter()
226            .flat_map(|b| b.probe_class(KernelClass::DenseLinear, &KernelPanel::quick()))
227            .collect();
228        assert_eq!(rows.len(), 1, "only the available backend yields a row");
229    }
230
231    #[test]
232    fn backend_id_is_copy_and_string_keyed() {
233        let a = BackendId::CPU;
234        let b = a; // Copy
235        assert_eq!(a, b);
236        assert_eq!(BackendId::WGPU.as_str(), "wgpu");
237    }
238}