Skip to main content

qualia_core_db/platform/
hetero_dispatch.rs

1//! Vendor-neutral heterogeneous compute dispatch + storage / precision policy.
2//!
3//! This module **replaces the former CUDA/cuFile GPUDirect-Storage bridge**
4//! (`cuda_bridge.rs`, removed). That bridge was the engine's one vendor-locked
5//! appendage — NVIDIA-only, Linux-only, and unverifiable without specific hardware.
6//! Removing it dissolves the hardware boundary entirely. The four capabilities it
7//! was meant to provide are delivered here on the engine's GENERAL stack — portable
8//! `wgpu` ([`super::gpu`]) + memory-mapped I/O ([`super::host`]) — so they build,
9//! run, and verify on any backend (Vulkan / DX12 / Metal / WebGPU), no vendor SDK:
10//!
11//! 1. **Unified-memory zero-copy** ([`ZeroCopyStrategy`]) — choose mmap-direct on
12//!    integrated / unified-memory GPUs (Apple Silicon via Metal, which `wgpu` maps
13//!    transparently — CPU and GPU share physical RAM, so the mmap'd region is GPU
14//!    visible with no copy) vs a one-time staging upload on discrete GPUs.
15//! 2. **Hardware-agnostic fallback dispatcher** ([`HeterogeneousDispatcher`]) —
16//!    route a job to the GPU (`wgpu`) when it fits, else NPU, else CPU; and tile a
17//!    matmul across passes when VRAM is exhausted instead of hard-failing.
18//! 3. **Kernel stream fusion** ([`plan_fusion`]) — group consecutive same-shape
19//!    element-wise tensor ops into a single `wgpu` compute pass (the portable
20//!    analogue of CUDA stream fusion; the engine's fused shaders already do this at
21//!    the shader level). Fewer passes ⇒ fewer dispatch / PCIe round-trips.
22//! 4. **Mixed-precision policy** ([`select_precision`]) — pick f32/f16/q8/q4 from
23//!    the host's VRAM / power / thermal budget.
24//!
25//! ## The one capability deliberately NOT ported
26//! NVIDIA GPUDirect-Storage's *true* NVMe→VRAM DMA (bypassing system RAM) has **no
27//! portable `wgpu` equivalent**. The vendor-neutral substitute (and the engine's
28//! actual path) is `mmap` + OS page cache + a one-time staging upload — zero-heap,
29//! standard OS mechanics, identical across an A2000 / Apple M-series / generic Linux
30//! box. GDS-class throughput only matters when streaming a 70B model off an NVMe
31//! array into an 80 GB datacenter GPU — the deployment the affordability rail
32//! explicitly does not target, so nothing on the critical path is lost.
33//!
34//! ## Future, optional Vulkan zero-copy fast-path (documented, NOT built)
35//! If a specific deployment ever justifies skipping the staging copy, the
36//! vendor-neutral way is a `wgpu-hal` Vulkan fast-path that imports the mmap'd
37//! weights as device memory via `VK_EXT_external_memory_host` (broadly supported,
38//! cross-vendor) + Resizable-BAR — lit up ONLY on the Vulkan backend, behind a
39//! `vulkan_zero_copy_import` feature, additive over the portable path. It is
40//! deliberately unbuilt: it needs `unsafe` wgpu-hal and is backend-specific
41//! (DX12/Metal have their own external-memory mechanisms), which would re-introduce
42//! exactly the per-backend coupling that removing CUDA just eliminated. Build it
43//! only when a real deployment needs it; never as the core storage dependency.
44//!
45//! All routines here are pure-scalar policy / planning logic — **zero heap**, no
46//! recursion, run anywhere.
47
48// ── 1. Unified-memory zero-copy strategy ───────────────────────────────────────
49
50/// How weight data reaches the GPU for a given device.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ZeroCopyStrategy {
53    /// Integrated / unified-memory GPU (Apple Silicon via Metal; integrated
54    /// Intel/AMD): the `mmap`'d region is directly GPU-visible — no host→device copy.
55    MmapDirect,
56    /// Discrete GPU: upload the `mmap`'d region once via a staging buffer.
57    StagingUpload,
58}
59
60impl ZeroCopyStrategy {
61    /// Pick the strategy from whether the adapter has unified memory.
62    pub fn for_device(is_unified_memory: bool) -> Self {
63        if is_unified_memory {
64            Self::MmapDirect
65        } else {
66            Self::StagingUpload
67        }
68    }
69
70    /// Map a `wgpu` device type to the strategy: integrated GPUs share memory with
71    /// the host (unified) ⇒ mmap-direct; discrete GPUs ⇒ staging upload. CPU/other
72    /// adapters are treated as unified (the "device" buffer is host memory).
73    pub fn for_wgpu_device_type(device_type: wgpu::DeviceType) -> Self {
74        match device_type {
75            wgpu::DeviceType::DiscreteGpu | wgpu::DeviceType::VirtualGpu => Self::StagingUpload,
76            _ => Self::MmapDirect,
77        }
78    }
79
80    /// Whether a host→device copy is required (false on unified memory).
81    pub fn requires_host_copy(self) -> bool {
82        matches!(self, Self::StagingUpload)
83    }
84}
85
86// ── 2. Hardware-agnostic fallback dispatcher ────────────────────────────────────
87
88/// The compute backend a job is routed to.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum ComputeBackend {
91    /// Portable `wgpu` GPU path ([`super::gpu::WebGpuIntegrator`]).
92    Gpu,
93    /// A neural-processing unit, when present.
94    Npu,
95    /// CPU fallback — always available, never hard-fails.
96    Cpu,
97}
98
99/// What the host can offer the dispatcher.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct HostCapabilities {
102    pub gpu_available: bool,
103    /// Usable VRAM in bytes (e.g. `wgpu` `limits().max_buffer_size`).
104    pub vram_available: u64,
105    pub npu_available: bool,
106    pub cpu_threads: u32,
107}
108
109/// Routes compute jobs across GPU / NPU / CPU and degrades gracefully under VRAM
110/// pressure instead of hard-failing (the behaviour the CUDA bridge lacked).
111#[derive(Debug, Clone, Copy)]
112pub struct HeterogeneousDispatcher {
113    caps: HostCapabilities,
114}
115
116impl HeterogeneousDispatcher {
117    pub fn new(caps: HostCapabilities) -> Self {
118        Self { caps }
119    }
120
121    /// Choose a backend for a job needing `vram_required` bytes: GPU if present and
122    /// it fits → else NPU if present → else CPU (always works). When the GPU is
123    /// present but the job is larger than VRAM, the caller should GPU-tile (see
124    /// [`Self::gpu_tiles`]) rather than fall straight to CPU.
125    pub fn select_backend(&self, vram_required: u64) -> ComputeBackend {
126        if self.caps.gpu_available {
127            if vram_required <= self.caps.vram_available || self.caps.vram_available > 0 {
128                // GPU present: it fits, or it can be tiled to fit (see gpu_tiles).
129                return ComputeBackend::Gpu;
130            }
131        }
132        if self.caps.npu_available {
133            ComputeBackend::Npu
134        } else {
135            ComputeBackend::Cpu
136        }
137    }
138
139    /// How many sequential tiles a `total_bytes` GPU job must be split into so each
140    /// tile fits in available VRAM — graceful degradation instead of an OOM hard
141    /// fail. Returns 1 when it already fits (or when there's no GPU/VRAM to tile
142    /// into, in which case the job runs on the CPU as a single pass).
143    pub fn gpu_tiles(&self, total_bytes: u64) -> u32 {
144        if !self.caps.gpu_available || self.caps.vram_available == 0 {
145            return 1;
146        }
147        // ceil(total / vram), clamped to ≥ 1.
148        let tiles = total_bytes.div_ceil(self.caps.vram_available);
149        tiles.max(1).min(u32::MAX as u64) as u32
150    }
151
152    pub fn capabilities(&self) -> HostCapabilities {
153        self.caps
154    }
155}
156
157// ── 3. Kernel / stream fusion planning ──────────────────────────────────────────
158
159/// The fusability class of a tensor op in a dispatch sequence.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum TensorOpKind {
162    /// Element-wise (add, mul, activation…): fusable with adjacent same-shape
163    /// element-wise ops into one compute pass.
164    Elementwise,
165    /// A fusion barrier (reduction, matmul, reshape): forces a new pass.
166    Barrier,
167}
168
169/// One op in a planned dispatch sequence. `shape` is a shape token; element-wise ops
170/// only fuse with neighbours of the same shape.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct TensorOp {
173    pub kind: TensorOpKind,
174    pub shape: u64,
175}
176
177/// Plan stream fusion: the number of `wgpu` compute passes a sequence needs after
178/// fusing each maximal run of same-shape element-wise ops into a single pass.
179/// Barriers each take their own pass. Result is in `1..=ops.len()`; fewer passes ⇒
180/// fewer dispatch / PCIe round-trips. Zero-heap (single linear scan).
181pub fn plan_fusion(ops: &[TensorOp]) -> u32 {
182    let mut passes = 0u32;
183    let mut i = 0usize;
184    while i < ops.len() {
185        passes += 1;
186        if ops[i].kind == TensorOpKind::Barrier {
187            i += 1;
188            continue;
189        }
190        // Absorb the maximal run of same-shape element-wise ops into this pass.
191        let shape = ops[i].shape;
192        i += 1;
193        while i < ops.len() && ops[i].kind == TensorOpKind::Elementwise && ops[i].shape == shape {
194            i += 1;
195        }
196    }
197    passes
198}
199
200// ── 4. Mixed-precision policy ────────────────────────────────────────────────────
201
202/// Numeric precision for weights / activations.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum Precision {
205    F32,
206    F16,
207    Q8,
208    Q4,
209}
210
211impl Precision {
212    /// Bytes per weight element.
213    pub fn bytes_per_weight(self) -> f64 {
214        match self {
215            Self::F32 => 4.0,
216            Self::F16 => 2.0,
217            Self::Q8 => 1.0,
218            Self::Q4 => 0.5,
219        }
220    }
221}
222
223/// The host's power / thermal / memory envelope for the precision decision.
224#[derive(Debug, Clone, Copy, PartialEq)]
225pub struct PowerThermalBudget {
226    pub vram_budget_bytes: u64,
227    /// Available power headroom, milliwatts.
228    pub power_budget_mw: u32,
229    /// Degrees C below the thermal-throttle point.
230    pub thermal_headroom_c: f32,
231}
232
233/// Pick the precision tuned to the host's thermodynamic / power / memory constraints:
234/// choose the coarsest precision that still fits `param_count` weights in the VRAM
235/// budget, and — when power or thermal headroom is tight — never run the heavy
236/// precisions (f32/f16 draw more power and generate more heat). Returns `Q4` if even
237/// q4 overflows VRAM (the caller must then tile or offload).
238pub fn select_precision(param_count: u64, budget: &PowerThermalBudget) -> Precision {
239    let fits = |p: Precision| {
240        (param_count as f64 * p.bytes_per_weight()) as u64 <= budget.vram_budget_bytes
241    };
242    // Tight envelope ⇒ cap the *maximum* precision we'll consider (lower precision =
243    // fewer FLOPs = less heat/draw). Thresholds: < 5°C headroom or < 15 W budget.
244    let throttling = budget.thermal_headroom_c < 5.0 || budget.power_budget_mw < 15_000;
245    let candidates: &[Precision] = if throttling {
246        &[Precision::Q8, Precision::Q4]
247    } else {
248        &[Precision::F32, Precision::F16, Precision::Q8, Precision::Q4]
249    };
250    for &p in candidates {
251        if fits(p) {
252            return p;
253        }
254    }
255    Precision::Q4
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn zero_copy_strategy_follows_memory_architecture() {
264        // Unified memory (Apple Silicon / integrated) → no host copy.
265        assert_eq!(
266            ZeroCopyStrategy::for_device(true),
267            ZeroCopyStrategy::MmapDirect
268        );
269        assert!(!ZeroCopyStrategy::for_device(true).requires_host_copy());
270        // Discrete GPU → staging upload.
271        assert_eq!(
272            ZeroCopyStrategy::for_device(false),
273            ZeroCopyStrategy::StagingUpload
274        );
275        assert!(ZeroCopyStrategy::for_device(false).requires_host_copy());
276        // wgpu device-type mapping.
277        assert_eq!(
278            ZeroCopyStrategy::for_wgpu_device_type(wgpu::DeviceType::IntegratedGpu),
279            ZeroCopyStrategy::MmapDirect
280        );
281        assert_eq!(
282            ZeroCopyStrategy::for_wgpu_device_type(wgpu::DeviceType::DiscreteGpu),
283            ZeroCopyStrategy::StagingUpload
284        );
285    }
286
287    #[test]
288    fn dispatcher_routes_and_falls_back() {
289        // GPU present and job fits → GPU.
290        let d = HeterogeneousDispatcher::new(HostCapabilities {
291            gpu_available: true,
292            vram_available: 8 << 30,
293            npu_available: true,
294            cpu_threads: 16,
295        });
296        assert_eq!(d.select_backend(1 << 30), ComputeBackend::Gpu);
297
298        // No GPU, NPU present → NPU.
299        let d2 = HeterogeneousDispatcher::new(HostCapabilities {
300            gpu_available: false,
301            vram_available: 0,
302            npu_available: true,
303            cpu_threads: 8,
304        });
305        assert_eq!(d2.select_backend(1 << 30), ComputeBackend::Npu);
306
307        // No GPU, no NPU → CPU (always works, never hard-fails).
308        let d3 = HeterogeneousDispatcher::new(HostCapabilities {
309            gpu_available: false,
310            vram_available: 0,
311            npu_available: false,
312            cpu_threads: 4,
313        });
314        assert_eq!(d3.select_backend(1 << 30), ComputeBackend::Cpu);
315    }
316
317    #[test]
318    fn vram_exhaustion_tiles_instead_of_failing() {
319        let d = HeterogeneousDispatcher::new(HostCapabilities {
320            gpu_available: true,
321            vram_available: 2 << 30, // 2 GiB
322            npu_available: false,
323            cpu_threads: 8,
324        });
325        assert_eq!(d.gpu_tiles(1 << 30), 1, "fits in one tile");
326        assert_eq!(d.gpu_tiles(2 << 30), 1, "exactly fits");
327        assert_eq!(
328            d.gpu_tiles(5 << 30),
329            3,
330            "5 GiB / 2 GiB → 3 tiles, no OOM hard-fail"
331        );
332        // No GPU → single CPU pass.
333        let cpu = HeterogeneousDispatcher::new(HostCapabilities {
334            gpu_available: false,
335            vram_available: 0,
336            npu_available: false,
337            cpu_threads: 4,
338        });
339        assert_eq!(cpu.gpu_tiles(99 << 30), 1);
340    }
341
342    #[test]
343    fn fusion_collapses_elementwise_runs() {
344        let ew = |s| TensorOp {
345            kind: TensorOpKind::Elementwise,
346            shape: s,
347        };
348        let barrier = |s| TensorOp {
349            kind: TensorOpKind::Barrier,
350            shape: s,
351        };
352
353        // Three same-shape element-wise ops fuse into one pass.
354        assert_eq!(plan_fusion(&[ew(1), ew(1), ew(1)]), 1);
355        // A barrier (matmul/reduction) splits the run: EW | barrier | EW = 3 passes.
356        assert_eq!(plan_fusion(&[ew(1), barrier(1), ew(1)]), 3);
357        // Different shapes don't fuse.
358        assert_eq!(plan_fusion(&[ew(1), ew(2)]), 2);
359        // Mixed run: (EW EW) | barrier | (EW EW EW) = 3 passes.
360        assert_eq!(
361            plan_fusion(&[ew(1), ew(1), barrier(9), ew(2), ew(2), ew(2)]),
362            3
363        );
364        assert_eq!(plan_fusion(&[]), 0);
365    }
366
367    #[test]
368    fn precision_fits_budget_and_respects_thermals() {
369        let gib = 1u64 << 30;
370        // 1B params, roomy VRAM + headroom → f32.
371        let roomy = PowerThermalBudget {
372            vram_budget_bytes: 8 * gib,
373            power_budget_mw: 60_000,
374            thermal_headroom_c: 30.0,
375        };
376        assert_eq!(select_precision(1_000_000_000, &roomy), Precision::F32);
377
378        // 1B params, only ~1.5 GiB VRAM → must drop to q8 (1 GB) — f32/f16 overflow.
379        let tight_vram = PowerThermalBudget {
380            vram_budget_bytes: gib + gib / 2,
381            power_budget_mw: 60_000,
382            thermal_headroom_c: 30.0,
383        };
384        assert_eq!(select_precision(1_000_000_000, &tight_vram), Precision::Q8);
385
386        // Throttling (low thermal headroom) → never f32/f16 even with VRAM to spare.
387        let throttling = PowerThermalBudget {
388            vram_budget_bytes: 64 * gib,
389            power_budget_mw: 60_000,
390            thermal_headroom_c: 2.0,
391        };
392        assert_eq!(select_precision(1_000_000_000, &throttling), Precision::Q8);
393
394        // Model too big for any precision → Q4 (caller tiles/offloads).
395        let huge = PowerThermalBudget {
396            vram_budget_bytes: gib,
397            power_budget_mw: 60_000,
398            thermal_headroom_c: 30.0,
399        };
400        assert_eq!(select_precision(10_000_000_000, &huge), Precision::Q4);
401    }
402}