1#![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
38const HOST_RAM_FLOOR: u64 = 4 * 1024 * 1024 * 1024;
40
41pub const DEFAULT_KV_RESERVE: u64 = crate::gpu_context::VramLedger::KV_CACHE_CAP_BYTES;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
46pub enum ResidencyProtocol {
47 Resident,
49 HeterogeneousOverflow,
51 Streaming,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
57pub enum PlacementRole {
58 ResidentPrimary,
59 Overflow,
60 StreamTarget,
61}
62
63#[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#[derive(Debug, Clone, Serialize)]
75pub struct EmploymentPlan {
76 pub protocol: ResidencyProtocol,
77 pub model_bytes: u64,
78 pub kv_reserve_bytes: u64,
79 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
108fn 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 CircuitKind::IntegratedGpu | CircuitKind::Cpu => {
121 topo.host_ram_bytes.saturating_sub(HOST_RAM_FLOOR)
122 }
123 CircuitKind::Npu | CircuitKind::Other => 0,
124 }
125}
126
127fn 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; if secs > 0.0 && bytes > 0.0 {
136 bytes / secs
137 } else {
138 f64::INFINITY
139 }
140}
141
142fn 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
153fn 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
168pub 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 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 let overflow = model_bytes - best_usable;
219 let gemv_n = matrix.gemv_n;
220
221 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 let mut best_inplace: Option<(&CircuitBench, u64, f64)> = None; 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; }
239 let cost = segment_compute_cost(overflow, c, gemv_n); if best_inplace.map_or(true, |(_, _, prev)| cost < prev) {
241 best_inplace = Some((c, pool, cost));
242 }
243 }
244
245 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 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
325pub 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
333use std::sync::OnceLock;
350
351pub const ROUTE_ENV: &str = "QUALIA_LLM_ROUTE";
354
355static 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
363pub 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
371pub fn last_employment_plan() -> Option<EmploymentPlan> {
375 route_plan_slot().lock().ok().and_then(|g| g.clone())
376}
377
378pub 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 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
413pub 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 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 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 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 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 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 #[test]
600 fn overflow_inplace_secondary_wins_argmin() {
601 let topo = discrete_topo(12, 64, true);
602 let matrix = CapabilityMatrix {
603 circuits: vec![
604 circuit_up("Discrete GPU", CircuitKind::DiscreteGpu, 0.4, 1.0, 2.0),
606 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 #[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 #[test]
646 fn overflow_flips_on_upload_gbps_only() {
647 let topo = discrete_topo(12, 64, true);
648 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 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 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 #[test]
705 fn route_flag_gates_compute_store_and_retrieve() {
706 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 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 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}