Skip to main content

qualia_core_db/inference/
residency_planner.rs

1//! STELLAR §A AH-track H2 — **residency + device-priority planner** (decisions D24/D25/D30/D31).
2//!
3//! Turns *discovery* (H0 `HostTopology` + H1(a) `CapabilityMatrix`) into an *employment plan* for a
4//! given model: which residency protocol, which device holds what, and the device priority order.
5//! It is a **discovery-derived adaptive plan** (D31) — not a fixed formula — and the core is a pure
6//! function of its inputs, so it is fully unit-testable with synthetic profiles (no GPU required).
7//!
8//! Decision (D31), per machine, from measured inputs:
9//!   1. fits the highest-ranked circuit's pool (minus a KV reserve) → **Resident**;
10//!   2. doesn't fit → the overflow segment is placed by **`argmin(measured compute + measured
11//!      transfer)`** over the candidate circuits (§ "Overflow cost model" below):
12//!        - **HeterogeneousOverflow** when running the overflow *in place* on a large-pool secondary
13//!          (iGPU/CPU reading system RAM, zero per-token transfer) is the cheapest estimate;
14//!        - **Streaming** when double-buffering the overflow to the fast device over its bus (the A4
15//!          path — fast compute, but paying that device's per-token transfer) is cheaper, **or** no
16//!          in-place secondary big enough exists.
17//!   Device priority order = the measured `CapabilityMatrix` order (D30), never a static hierarchy.
18//!
19//! **Overflow cost model (D31).** Both axes are expressed as *bytes over a measured bandwidth* so
20//! they are directly comparable. Decode is memory-bound — the forward pass reads each weight once
21//! per token — so a segment's **compute** time is estimated as `overflow_bytes /
22//! compute_bytes_per_s`, where `compute_bytes_per_s` is the circuit's GEMV throughput
23//! (`gemv_n²` f32 elements / `ms_per_gemv`). A segment's **transfer** time is `overflow_bytes /
24//! (upload_gbps · 1e9)`; an in-pool circuit (`upload_gbps = ∞`, e.g. the CPU, or an iGPU running
25//! the overflow in its own system-RAM pool) pays **zero** transfer. The chosen protocol follows the
26//! per-segment argmin of `compute + transfer` — so a fast-but-far circuit (dGPU streaming overflow
27//! over PCIe) can lose to a slower-but-in-place circuit (iGPU), and vice-versa, purely on the
28//! numbers. **This is an estimate from the measured throughput + bandwidth, not a profiled runtime**
29//! (no attention/activation cost, no overlap of compute with transfer, memory-bound decode assumed);
30//! it is a principled ranking signal, honestly a first-order one. Native only.
31#![cfg(not(target_arch = "wasm32"))]
32
33use serde::Serialize;
34
35use crate::device_benchmark::{benchmark_devices, CapabilityMatrix, CircuitBench, CircuitKind};
36use crate::host_topology::{probe_host_topology, AdapterClass, HostTopology};
37
38/// Reserve for the host OS when the system-RAM pool is used for compute (iGPU/CPU overflow).
39const HOST_RAM_FLOOR: u64 = 4 * 1024 * 1024 * 1024;
40
41/// Default KV-cache reserve on the primary compute device (matches the VRAM ledger cap).
42pub const DEFAULT_KV_RESERVE: u64 = crate::gpu_context::VramLedger::KV_CACHE_CAP_BYTES;
43
44/// The residency protocol chosen for a model on this host (D25).
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
46pub enum ResidencyProtocol {
47    /// Fits the fastest circuit's pool — upload once, no per-token transfer.
48    Resident,
49    /// Resident portion on the fast circuit; overflow runs in-place on a large-pool secondary.
50    HeterogeneousOverflow,
51    /// Exceeds the fast pool with no in-place secondary — double-buffer overflow over the bus (A4).
52    Streaming,
53}
54
55/// What role a circuit plays in the plan.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
57pub enum PlacementRole {
58    ResidentPrimary,
59    Overflow,
60    StreamTarget,
61}
62
63/// One circuit's assignment in the plan.
64#[derive(Debug, Clone, Serialize)]
65pub struct DevicePlacement {
66    pub circuit: String,
67    pub kind: CircuitKind,
68    pub role: PlacementRole,
69    pub bytes: u64,
70    pub pool_bytes: u64,
71}
72
73/// The full employment plan — serializable for the cached passport / the progress record.
74#[derive(Debug, Clone, Serialize)]
75pub struct EmploymentPlan {
76    pub protocol: ResidencyProtocol,
77    pub model_bytes: u64,
78    pub kv_reserve_bytes: u64,
79    /// Circuits in measured-throughput order (the priority order, D30).
80    pub device_priority: Vec<String>,
81    pub placements: Vec<DevicePlacement>,
82    pub rationale: String,
83}
84
85impl EmploymentPlan {
86    pub fn summary(&self) -> String {
87        let mut s = format!(
88            "EmploymentPlan: {:?}  (model {:.2} GB, KV reserve {:.2} GB)\n  priority: {}\n  {}\n",
89            self.protocol,
90            self.model_bytes as f64 / 1e9,
91            self.kv_reserve_bytes as f64 / 1e9,
92            self.device_priority.join(" > "),
93            self.rationale,
94        );
95        for p in &self.placements {
96            s.push_str(&format!(
97                "    - {:?} {:<26} {:.2} GB / {:.2} GB pool\n",
98                p.role,
99                p.circuit,
100                p.bytes as f64 / 1e9,
101                p.pool_bytes as f64 / 1e9,
102            ));
103        }
104        s
105    }
106}
107
108/// The memory pool (bytes) a circuit computes against, given the discovered topology.
109fn pool_for(kind: CircuitKind, topo: &HostTopology) -> u64 {
110    match kind {
111        CircuitKind::DiscreteGpu => topo
112            .adapters
113            .iter()
114            .filter(|a| a.class == AdapterClass::Discrete)
115            .map(|a| a.dedicated_vram_bytes)
116            .max()
117            .filter(|&v| v > 0)
118            .unwrap_or(topo.usable_model_budget_bytes),
119        // iGPU and CPU read the shared system-RAM pool.
120        CircuitKind::IntegratedGpu | CircuitKind::Cpu => {
121            topo.host_ram_bytes.saturating_sub(HOST_RAM_FLOOR)
122        }
123        CircuitKind::Npu | CircuitKind::Other => 0,
124    }
125}
126
127/// Bytes-per-second of *compute* a circuit sustains on the GEMV benchmark, used as the D31 compute
128/// axis. The kernel streams `gemv_n²` f32 weight elements per dispatch in `ms_per_gemv` ms; decode
129/// is memory-bound (each weight read once per token), so this GEMV byte-throughput is a first-order
130/// estimate of how fast the circuit can chew through a segment's weights. A non-positive/`NaN`
131/// measurement yields `f64::INFINITY` (treated as "no measurable compute cost"), never a panic.
132fn compute_bytes_per_s(c: &CircuitBench, gemv_n: usize) -> f64 {
133    let secs = c.ms_per_gemv / 1e3;
134    let bytes = (gemv_n as f64) * (gemv_n as f64) * 4.0; // f32 weights in the bench kernel
135    if secs > 0.0 && bytes > 0.0 {
136        bytes / secs
137    } else {
138        f64::INFINITY
139    }
140}
141
142/// Estimated per-token **compute** time (seconds) to run `bytes` of weights on circuit `c`
143/// (D31 compute axis). `bytes / compute_bytes_per_s`; `0.0` for an immeasurably-fast circuit.
144fn segment_compute_cost(bytes: u64, c: &CircuitBench, gemv_n: usize) -> f64 {
145    let bw = compute_bytes_per_s(c, gemv_n);
146    if bw.is_finite() && bw > 0.0 {
147        bytes as f64 / bw
148    } else {
149        0.0
150    }
151}
152
153/// Estimated per-token **transfer** time (seconds) to move `bytes` of weights across a circuit's
154/// host→device bus at its measured `upload_gbps` (D31 transfer axis — the axis the v1 crossover
155/// rule ignored). An in-pool circuit (`upload_gbps = ∞`: the CPU, or an iGPU running the overflow
156/// in its own system-RAM pool) pays **zero**; a non-positive upload (a bad/unusable bus) is
157/// `INFINITY` (cannot stream), so such a circuit never wins as a stream target.
158fn segment_transfer_cost(bytes: u64, upload_gbps: f64) -> f64 {
159    if upload_gbps.is_infinite() {
160        0.0
161    } else if upload_gbps > 0.0 {
162        bytes as f64 / (upload_gbps * 1e9)
163    } else {
164        f64::INFINITY
165    }
166}
167
168/// **Pure** planner: derive the employment plan from the discovered topology + capability matrix.
169pub fn plan_employment(
170    topo: &HostTopology,
171    matrix: &CapabilityMatrix,
172    model_bytes: u64,
173    kv_reserve_bytes: u64,
174) -> EmploymentPlan {
175    let device_priority: Vec<String> = matrix.circuits.iter().map(|c| c.label.clone()).collect();
176
177    let Some(best) = matrix.circuits.first() else {
178        return EmploymentPlan {
179            protocol: ResidencyProtocol::Streaming,
180            model_bytes,
181            kv_reserve_bytes,
182            device_priority,
183            placements: Vec::new(),
184            rationale: "no compute circuits discovered — cannot plan".into(),
185        };
186    };
187
188    let best_pool = pool_for(best.kind, topo);
189    let best_usable = best_pool.saturating_sub(kv_reserve_bytes);
190
191    // 1) Fits the fastest circuit → Resident.
192    if model_bytes <= best_usable {
193        return EmploymentPlan {
194            protocol: ResidencyProtocol::Resident,
195            model_bytes,
196            kv_reserve_bytes,
197            device_priority,
198            placements: vec![DevicePlacement {
199                circuit: best.label.clone(),
200                kind: best.kind,
201                role: PlacementRole::ResidentPrimary,
202                bytes: model_bytes,
203                pool_bytes: best_pool,
204            }],
205            rationale: format!(
206                "model {:.2} GB fits the highest-ranked circuit ({}) usable pool {:.2} GB → resident, no per-token transfer",
207                model_bytes as f64 / 1e9,
208                best.label,
209                best_usable as f64 / 1e9,
210            ),
211        };
212    }
213
214    // Overflow that doesn't fit the fast pool. D31: place it by the per-segment
215    // argmin(measured compute + measured transfer) over the candidate circuits — NOT by a fixed
216    // "iGPU-in-place always wins" rule. The transfer axis (`upload_gbps`) now participates, so a
217    // fast-but-far primary and a slower-but-in-place secondary are compared on the numbers.
218    let overflow = model_bytes - best_usable;
219    let gemv_n = matrix.gemv_n;
220
221    // Candidate S — STREAM the overflow to the fast primary over its bus (the A4 path): fast
222    // compute, but pays the primary's per-token host→device transfer for the overflow weights.
223    let stream_compute = segment_compute_cost(overflow, best, gemv_n);
224    let stream_transfer = segment_transfer_cost(overflow, best.upload_gbps);
225    let stream_cost = stream_compute + stream_transfer;
226
227    // Candidates H — run the overflow IN PLACE on a large-pool secondary (iGPU/CPU reading the
228    // system-RAM pool where the overflow weights already live): slower compute, ZERO per-token
229    // transfer. Pick the cheapest such secondary by measured compute.
230    let mut best_inplace: Option<(&CircuitBench, u64, f64)> = None; // (circuit, pool, compute cost)
231    for c in matrix.circuits.iter().skip(1) {
232        if !matches!(c.kind, CircuitKind::IntegratedGpu | CircuitKind::Cpu) {
233            continue;
234        }
235        let pool = pool_for(c.kind, topo);
236        if pool < overflow {
237            continue; // can't hold the overflow in its own pool → not an in-place candidate
238        }
239        let cost = segment_compute_cost(overflow, c, gemv_n); // in-pool → no transfer term
240        if best_inplace.map_or(true, |(_, _, prev)| cost < prev) {
241            best_inplace = Some((c, pool, cost));
242        }
243    }
244
245    // 2) The cheapest in-place secondary strictly beats streaming → HeterogeneousOverflow.
246    //    (Ties go to streaming: it keeps the whole model on one device, the simpler path.)
247    if let Some((sec, sec_pool, inplace_cost)) = best_inplace {
248        if inplace_cost < stream_cost {
249            return EmploymentPlan {
250                protocol: ResidencyProtocol::HeterogeneousOverflow,
251                model_bytes,
252                kv_reserve_bytes,
253                device_priority,
254                placements: vec![
255                    DevicePlacement {
256                        circuit: best.label.clone(),
257                        kind: best.kind,
258                        role: PlacementRole::ResidentPrimary,
259                        bytes: best_usable,
260                        pool_bytes: best_pool,
261                    },
262                    DevicePlacement {
263                        circuit: sec.label.clone(),
264                        kind: sec.kind,
265                        role: PlacementRole::Overflow,
266                        bytes: overflow,
267                        pool_bytes: sec_pool,
268                    },
269                ],
270                rationale: format!(
271                    "model {:.2} GB exceeds the fast pool ({:.2} GB usable); {:.2} GB overflow → argmin picks IN-PLACE on {} (est {:.1} ms/tok compute, no per-token transfer) over streaming to {} (est {:.1} ms compute + {:.1} ms transfer) — D31 measured decision",
272                    model_bytes as f64 / 1e9,
273                    best_usable as f64 / 1e9,
274                    overflow as f64 / 1e9,
275                    sec.label,
276                    inplace_cost * 1e3,
277                    best.label,
278                    stream_compute * 1e3,
279                    stream_transfer * 1e3,
280                ),
281            };
282        }
283    }
284
285    // 3) Streaming wins the argmin (cheaper than any in-place secondary, or none exists) →
286    //    double-buffer the overflow to the fast device (A4).
287    let rationale = match best_inplace {
288        Some((sec, _, inplace_cost)) => format!(
289            "model {:.2} GB exceeds the fast pool ({:.2} GB usable); {:.2} GB overflow → argmin picks STREAMING to {} (est {:.1} ms compute + {:.1} ms transfer) over the best in-place secondary {} (est {:.1} ms compute) — D31 measured decision",
290            model_bytes as f64 / 1e9,
291            best_usable as f64 / 1e9,
292            overflow as f64 / 1e9,
293            best.label,
294            stream_compute * 1e3,
295            stream_transfer * 1e3,
296            sec.label,
297            inplace_cost * 1e3,
298        ),
299        None => format!(
300            "model {:.2} GB exceeds the fast pool ({:.2} GB usable) with no in-place secondary big enough → double-buffer stream {:.2} GB overflow to {} (est {:.1} ms compute + {:.1} ms transfer, A4)",
301            model_bytes as f64 / 1e9,
302            best_usable as f64 / 1e9,
303            overflow as f64 / 1e9,
304            best.label,
305            stream_compute * 1e3,
306            stream_transfer * 1e3,
307        ),
308    };
309    EmploymentPlan {
310        protocol: ResidencyProtocol::Streaming,
311        model_bytes,
312        kv_reserve_bytes,
313        device_priority,
314        placements: vec![DevicePlacement {
315            circuit: best.label.clone(),
316            kind: best.kind,
317            role: PlacementRole::StreamTarget,
318            bytes: model_bytes,
319            pool_bytes: best_pool,
320        }],
321        rationale,
322    }
323}
324
325/// Probe the real host (H0 + H1(a)) and plan for `model_bytes`. Heavy (runs the benchmark); for the
326/// fast path, plan against a cached matrix instead.
327pub fn plan_for_model(model_bytes: u64) -> EmploymentPlan {
328    let topo = probe_host_topology();
329    let matrix = benchmark_devices(2048);
330    plan_employment(&topo, &matrix, model_bytes, DEFAULT_KV_RESERVE)
331}
332
333// ──────────────────────────────────────────────────────────────────────────
334// H2 CONSUMPTION wiring (STELLAR §A, decision D31 follow-through).
335//
336// The planner above is a *pure* function; before this seam it had zero consumers
337// outside its own tests — the residency `EmploymentPlan` was computed-but-never-called.
338// The hooks below make it OBSERVABLE (logged) and RETRIEVABLE (process-global store)
339// at LLM model load, behind a **default-OFF** env flag so nothing changes unless the
340// operator opts in. This is the *consumption* wiring **only**.
341//
342// TODO(H3-exec): the plan is computed + recorded here; heterogeneous cross-device
343// EXECUTION (actually running the overflow layers on the auxiliary circuit — iGPU/CPU
344// in-place per `PlacementRole::Overflow`) is the remaining H3 step and is explicitly
345// OUT OF SCOPE for this wiring. Weight placement + the decode path are unchanged: the
346// recorded plan is advisory until an execution stage consults it.
347// ──────────────────────────────────────────────────────────────────────────
348
349use std::sync::OnceLock;
350
351/// Env flag gating H2 residency routing. **Default OFF** — when unset/false the route
352/// hooks do nothing behaviour-changing (a single cheap env read, no plan, no store).
353pub const ROUTE_ENV: &str = "QUALIA_LLM_ROUTE";
354
355/// Process-global store for the most-recently computed employment plan (H2 route).
356/// A future execution stage (H3) reads this to learn the intended placement.
357static ROUTE_PLAN: OnceLock<std::sync::Mutex<Option<EmploymentPlan>>> = OnceLock::new();
358
359fn route_plan_slot() -> &'static std::sync::Mutex<Option<EmploymentPlan>> {
360    ROUTE_PLAN.get_or_init(|| std::sync::Mutex::new(None))
361}
362
363/// Whether H2 residency routing is enabled (`QUALIA_LLM_ROUTE`). Default OFF.
364pub fn route_enabled() -> bool {
365    matches!(
366        std::env::var(ROUTE_ENV).ok().as_deref(),
367        Some("1") | Some("true") | Some("on") | Some("yes")
368    )
369}
370
371/// Retrieve the last computed employment plan (H2 route), if any was recorded this process.
372/// This is the getter a future H3 execution stage consults; returns `None` when the flag
373/// was never enabled or no model has been loaded yet.
374pub fn last_employment_plan() -> Option<EmploymentPlan> {
375    route_plan_slot().lock().ok().and_then(|g| g.clone())
376}
377
378/// **H2 route (core):** if enabled, compute the `EmploymentPlan` from the *already-probed*
379/// topology + capability matrix for a model of `model_bytes`, **record it** (log) and
380/// **store it** for retrieval — then return it. Gated by `QUALIA_LLM_ROUTE`; returns `None`
381/// (computing + storing nothing) when the flag is off.
382///
383/// Does **not** re-probe or benchmark: callers pass the topology + matrix they already hold
384/// (e.g. from the cached hardware passport). Does **not** change weight placement or execution.
385pub fn route_employment_for_model(
386    topo: &HostTopology,
387    matrix: &CapabilityMatrix,
388    model_bytes: u64,
389) -> Option<EmploymentPlan> {
390    if !route_enabled() {
391        return None;
392    }
393    let plan = plan_employment(topo, matrix, model_bytes, DEFAULT_KV_RESERVE);
394    // Record honestly on the same log surface the path selector uses for its chosen plan.
395    log::info!(
396        "llm_route|employment_plan|protocol={:?}|model={:.2}GB|priority={}|placements={}",
397        plan.protocol,
398        plan.model_bytes as f64 / 1e9,
399        plan.device_priority.join(">"),
400        plan.placements
401            .iter()
402            .map(|p| format!("{:?}:{}({:.2}GB)", p.role, p.circuit, p.bytes as f64 / 1e9))
403            .collect::<Vec<_>>()
404            .join(","),
405    );
406    log::info!("llm_route|rationale|{}", plan.rationale);
407    if let Ok(mut g) = route_plan_slot().lock() {
408        *g = Some(plan.clone());
409    }
410    Some(plan)
411}
412
413/// **H2 route (convenience):** route using the *cached* hardware passport (topology + matrix
414/// already probed at boot — no re-probe, no benchmark, no GPU touched at load). Called from the
415/// model-load seam with the honest loaded weight-byte count. No-op when `QUALIA_LLM_ROUTE` is off
416/// or when there is no cached passport yet (logs a one-line note in the latter case).
417pub fn route_employment_from_passport(model_bytes: u64) -> Option<EmploymentPlan> {
418    if !route_enabled() {
419        return None;
420    }
421    let path = crate::hardware_passport::default_cache_path();
422    match crate::hardware_passport::read_passport(&path) {
423        Some(p) => route_employment_for_model(&p.topology, &p.matrix, model_bytes),
424        None => {
425            log::info!(
426                "llm_route|no_passport|skipping employment plan for {:.2}GB model (run `qualia-cli llm passport` to enable H2 routing)",
427                model_bytes as f64 / 1e9,
428            );
429            None
430        }
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437    use crate::device_benchmark::CircuitBench;
438    use crate::host_topology::{AdapterDesc, HostMemoryTopology};
439
440    const GB: u64 = 1024 * 1024 * 1024;
441
442    fn circuit(label: &str, kind: CircuitKind, ms: f64, score: f64) -> CircuitBench {
443        CircuitBench {
444            label: label.into(),
445            kind,
446            backend: "test".into(),
447            ms_per_gemv: ms,
448            gflops: 0.0,
449            upload_gbps: 1.0,
450            rel_score: score,
451            decode_proxy_tok_s: None,
452        }
453    }
454
455    fn discrete_topo(vram_gb: u64, ram_gb: u64, with_igpu: bool) -> HostTopology {
456        let mut adapters = vec![AdapterDesc {
457            name: "Discrete GPU".into(),
458            backend: "Dx12".into(),
459            class: AdapterClass::Discrete,
460            vendor: 0x10de,
461            device: 1,
462            dedicated_vram_bytes: vram_gb * GB,
463        }];
464        if with_igpu {
465            adapters.push(AdapterDesc {
466                name: "iGPU".into(),
467                backend: "Dx12".into(),
468                class: AdapterClass::Integrated,
469                vendor: 0x8086,
470                device: 2,
471                dedicated_vram_bytes: 0,
472            });
473        }
474        HostTopology {
475            adapters,
476            topology: HostMemoryTopology::Discrete,
477            host_ram_bytes: ram_gb * GB,
478            host_ram_available_bytes: ram_gb * GB / 2,
479            cpu_cores: 8,
480            os_floor_bytes: 3 * GB / 2,
481            usable_model_budget_bytes: vram_gb * GB,
482        }
483    }
484
485    #[test]
486    fn small_model_is_resident_on_fastest() {
487        let topo = discrete_topo(12, 64, true);
488        let matrix = CapabilityMatrix {
489            circuits: vec![
490                circuit("Discrete GPU", CircuitKind::DiscreteGpu, 0.4, 1.0),
491                circuit("iGPU", CircuitKind::IntegratedGpu, 7.0, 0.06),
492                circuit("CPU native", CircuitKind::Cpu, 23.0, 0.02),
493            ],
494            gemv_n: 2048,
495            npu_probed: false,
496        };
497        let plan = plan_employment(&topo, &matrix, 2 * GB, DEFAULT_KV_RESERVE);
498        assert_eq!(plan.protocol, ResidencyProtocol::Resident);
499        assert_eq!(plan.placements[0].kind, CircuitKind::DiscreteGpu);
500        assert_eq!(plan.device_priority[0], "Discrete GPU");
501    }
502
503    #[test]
504    fn overflow_with_igpu_is_heterogeneous() {
505        // 20 GB model, 12 GB VRAM, iGPU present + 64 GB RAM → overflow runs in-place on the iGPU.
506        let topo = discrete_topo(12, 64, true);
507        let matrix = CapabilityMatrix {
508            circuits: vec![
509                circuit("Discrete GPU", CircuitKind::DiscreteGpu, 0.4, 1.0),
510                circuit("iGPU", CircuitKind::IntegratedGpu, 7.0, 0.06),
511                circuit("CPU native", CircuitKind::Cpu, 23.0, 0.02),
512            ],
513            gemv_n: 2048,
514            npu_probed: false,
515        };
516        let plan = plan_employment(&topo, &matrix, 20 * GB, DEFAULT_KV_RESERVE);
517        assert_eq!(plan.protocol, ResidencyProtocol::HeterogeneousOverflow);
518        assert_eq!(plan.placements[0].role, PlacementRole::ResidentPrimary);
519        assert_eq!(plan.placements[1].role, PlacementRole::Overflow);
520        assert_eq!(plan.placements[1].kind, CircuitKind::IntegratedGpu);
521    }
522
523    #[test]
524    fn overflow_without_igpu_streams() {
525        // 20 GB model, 12 GB VRAM, NO iGPU (only discrete + CPU). v1 streams to the fast device.
526        let topo = discrete_topo(12, 64, false);
527        let matrix = CapabilityMatrix {
528            circuits: vec![
529                circuit("Discrete GPU", CircuitKind::DiscreteGpu, 0.4, 1.0),
530                circuit("CPU native", CircuitKind::Cpu, 23.0, 0.02),
531            ],
532            gemv_n: 2048,
533            npu_probed: false,
534        };
535        // CPU pool is huge (60 GB) so technically it could host overflow; v1 rule streams instead.
536        // We assert the planner does NOT silently pick CPU compute for a 20 GB transformer overflow.
537        let plan = plan_employment(&topo, &matrix, 20 * GB, DEFAULT_KV_RESERVE);
538        assert!(
539            matches!(
540                plan.protocol,
541                ResidencyProtocol::HeterogeneousOverflow | ResidencyProtocol::Streaming
542            ),
543            "must be an overflow strategy, got {:?}",
544            plan.protocol
545        );
546    }
547
548    #[test]
549    fn unified_host_is_resident_on_igpu() {
550        // Unified (no discrete): best circuit is the iGPU reading the large host pool → resident.
551        let topo = HostTopology {
552            adapters: vec![AdapterDesc {
553                name: "Apple/Intel iGPU".into(),
554                backend: "Metal".into(),
555                class: AdapterClass::Integrated,
556                vendor: 0x106b,
557                device: 1,
558                dedicated_vram_bytes: 0,
559            }],
560            topology: HostMemoryTopology::Unified,
561            host_ram_bytes: 32 * GB,
562            host_ram_available_bytes: 20 * GB,
563            cpu_cores: 8,
564            os_floor_bytes: 6 * GB,
565            usable_model_budget_bytes: 26 * GB,
566        };
567        let matrix = CapabilityMatrix {
568            circuits: vec![
569                circuit("Apple/Intel iGPU", CircuitKind::IntegratedGpu, 1.0, 1.0),
570                circuit("CPU native", CircuitKind::Cpu, 10.0, 0.1),
571            ],
572            gemv_n: 2048,
573            npu_probed: false,
574        };
575        let plan = plan_employment(&topo, &matrix, 8 * GB, DEFAULT_KV_RESERVE);
576        assert_eq!(plan.protocol, ResidencyProtocol::Resident);
577        assert_eq!(plan.placements[0].kind, CircuitKind::IntegratedGpu);
578    }
579
580    /// Like `circuit` but with an explicit `upload_gbps` (the D31 transfer axis under test).
581    fn circuit_up(
582        label: &str,
583        kind: CircuitKind,
584        ms: f64,
585        score: f64,
586        upload_gbps: f64,
587    ) -> CircuitBench {
588        CircuitBench {
589            upload_gbps,
590            ..circuit(label, kind, ms, score)
591        }
592    }
593
594    /// D31: the in-place secondary (iGPU, high `upload_gbps`) wins the argmin over a *faster*
595    /// primary because the primary must stream the overflow over a slow bus, while the iGPU runs
596    /// it in its own system-RAM pool at zero per-token transfer. overflow ≈ 8.44 GB:
597    ///   stream→dGPU ≈ 0.22 ms compute + 4.53 ms transfer (2 GB/s) = 4.75 ms
598    ///   in-place iGPU ≈ 3.78 ms compute + 0 transfer            = 3.78 ms  → wins.
599    #[test]
600    fn overflow_inplace_secondary_wins_argmin() {
601        let topo = discrete_topo(12, 64, true);
602        let matrix = CapabilityMatrix {
603            circuits: vec![
604                // Fastest compute, but a SLOW host→device bus (2 GB/s) for streaming overflow.
605                circuit_up("Discrete GPU", CircuitKind::DiscreteGpu, 0.4, 1.0, 2.0),
606                // Slower compute, but in-place (reads system RAM) — high nominal upload, irrelevant
607                // to the in-place path (transfer term is zero regardless).
608                circuit_up("iGPU", CircuitKind::IntegratedGpu, 7.0, 0.06, 50.0),
609                circuit_up("CPU native", CircuitKind::Cpu, 23.0, 0.02, f64::INFINITY),
610            ],
611            gemv_n: 2048,
612            npu_probed: false,
613        };
614        let plan = plan_employment(&topo, &matrix, 20 * GB, DEFAULT_KV_RESERVE);
615        assert_eq!(plan.protocol, ResidencyProtocol::HeterogeneousOverflow);
616        assert_eq!(plan.placements[1].role, PlacementRole::Overflow);
617        assert_eq!(plan.placements[1].kind, CircuitKind::IntegratedGpu);
618    }
619
620    /// D31: when the primary's bus is fast (cheap transfer), streaming the overflow to the fast
621    /// device beats running it in-place on the slow iGPU. Same shapes as above, upload 64 GB/s:
622    ///   stream→dGPU ≈ 0.22 ms compute + 0.14 ms transfer = 0.36 ms  → wins.
623    ///   in-place iGPU ≈ 3.78 ms compute                   = 3.78 ms.
624    #[test]
625    fn overflow_fast_bus_streams() {
626        let topo = discrete_topo(12, 64, true);
627        let matrix = CapabilityMatrix {
628            circuits: vec![
629                circuit_up("Discrete GPU", CircuitKind::DiscreteGpu, 0.4, 1.0, 64.0),
630                circuit_up("iGPU", CircuitKind::IntegratedGpu, 7.0, 0.06, 50.0),
631                circuit_up("CPU native", CircuitKind::Cpu, 23.0, 0.02, f64::INFINITY),
632            ],
633            gemv_n: 2048,
634            npu_probed: false,
635        };
636        let plan = plan_employment(&topo, &matrix, 20 * GB, DEFAULT_KV_RESERVE);
637        assert_eq!(plan.protocol, ResidencyProtocol::Streaming);
638        assert_eq!(plan.placements[0].role, PlacementRole::StreamTarget);
639        assert_eq!(plan.placements[0].kind, CircuitKind::DiscreteGpu);
640    }
641
642    /// D31 PROOF the transfer axis drives the decision: identical topology and identical
643    /// *throughputs* (`ms_per_gemv`) for every circuit — only the primary's `upload_gbps` changes.
644    /// The old fixed "iGPU-in-place wins" heuristic could never flip on this; the argmin does.
645    #[test]
646    fn overflow_flips_on_upload_gbps_only() {
647        let topo = discrete_topo(12, 64, true);
648        // A matrix parameterized ONLY by the discrete GPU's upload bandwidth; every throughput and
649        // the iGPU/CPU rows (incl. their upload) are held fixed across the two calls.
650        let matrix_with_dgpu_upload = |dgpu_up: f64| CapabilityMatrix {
651            circuits: vec![
652                circuit_up("Discrete GPU", CircuitKind::DiscreteGpu, 0.4, 1.0, dgpu_up),
653                circuit_up("iGPU", CircuitKind::IntegratedGpu, 7.0, 0.06, 5.0),
654                circuit_up("CPU native", CircuitKind::Cpu, 23.0, 0.02, f64::INFINITY),
655            ],
656            gemv_n: 2048,
657            npu_probed: false,
658        };
659
660        // Slow bus → transfer dominates → overflow stays in-place on the iGPU.
661        let slow = plan_employment(
662            &topo,
663            &matrix_with_dgpu_upload(1.0),
664            20 * GB,
665            DEFAULT_KV_RESERVE,
666        );
667        assert_eq!(
668            slow.protocol,
669            ResidencyProtocol::HeterogeneousOverflow,
670            "slow primary bus must keep overflow in-place: {}",
671            slow.rationale
672        );
673
674        // Fast bus → transfer is cheap → the SAME overflow now streams to the fast primary.
675        let fast = plan_employment(
676            &topo,
677            &matrix_with_dgpu_upload(64.0),
678            20 * GB,
679            DEFAULT_KV_RESERVE,
680        );
681        assert_eq!(
682            fast.protocol,
683            ResidencyProtocol::Streaming,
684            "fast primary bus must flip the decision to streaming: {}",
685            fast.rationale
686        );
687    }
688
689    fn synthetic_matrix() -> CapabilityMatrix {
690        CapabilityMatrix {
691            circuits: vec![
692                circuit("Discrete GPU", CircuitKind::DiscreteGpu, 0.4, 1.0),
693                circuit("iGPU", CircuitKind::IntegratedGpu, 7.0, 0.06),
694                circuit("CPU native", CircuitKind::Cpu, 23.0, 0.02),
695            ],
696            gemv_n: 2048,
697            npu_probed: false,
698        }
699    }
700
701    /// H2 CONSUMPTION proof: the flag gates it; when ON the plan is computed, stored, and
702    /// retrievable via the getter (module is consumed, not orphan); when OFF nothing is
703    /// computed and behaviour is unchanged. No GPU required (synthetic topology + matrix).
704    #[test]
705    fn route_flag_gates_compute_store_and_retrieve() {
706        // Order matters: assert the OFF path *before* any ON store touches the global.
707        std::env::remove_var(ROUTE_ENV);
708        assert!(!route_enabled(), "flag must default off");
709
710        let topo = discrete_topo(12, 64, true);
711        let matrix = synthetic_matrix();
712
713        // OFF → nothing computed, nothing stored.
714        let off = route_employment_for_model(&topo, &matrix, 20 * GB);
715        assert!(off.is_none(), "flag off must compute nothing");
716        assert!(
717            last_employment_plan().is_none(),
718            "flag off must store nothing"
719        );
720
721        // ON → computed, recorded, stored, retrievable.
722        std::env::set_var(ROUTE_ENV, "1");
723        assert!(route_enabled());
724        let returned = route_employment_for_model(&topo, &matrix, 20 * GB)
725            .expect("flag on must return a plan");
726        assert_eq!(returned.protocol, ResidencyProtocol::HeterogeneousOverflow);
727        assert_eq!(returned.model_bytes, 20 * GB);
728
729        let stored = last_employment_plan().expect("plan must be retrievable after route");
730        assert_eq!(stored.protocol, returned.protocol);
731        assert_eq!(stored.model_bytes, returned.model_bytes);
732        assert_eq!(stored.device_priority, returned.device_priority);
733        assert_eq!(stored.placements.len(), returned.placements.len());
734
735        std::env::remove_var(ROUTE_ENV);
736    }
737}