1use crate::backend_kind::BackendKind;
18use crate::capabilities::Capabilities;
19use crate::error::{BackendError, BackendResult};
20
21#[derive(Debug, Clone)]
27pub struct BackendEntry {
28 pub kind: BackendKind,
30 pub available: bool,
32 pub priority: u32,
35 pub capabilities: Capabilities,
38}
39
40impl BackendEntry {
41 #[must_use]
45 pub fn new(kind: BackendKind, available: bool) -> Self {
46 Self {
47 kind,
48 available,
49 priority: kind.default_priority(),
50 capabilities: if kind == BackendKind::Cpu {
51 Capabilities::cpu()
52 } else {
53 Capabilities::default()
54 },
55 }
56 }
57
58 #[must_use]
60 pub fn with_priority(mut self, priority: u32) -> Self {
61 self.priority = priority;
62 self
63 }
64
65 #[must_use]
67 pub fn with_capabilities(mut self, capabilities: Capabilities) -> Self {
68 self.capabilities = capabilities;
69 self
70 }
71}
72
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
78pub struct SelectionRequest {
79 pub require_gpu: bool,
81 pub require_fp16: bool,
83 pub require_bf16: bool,
85 pub require_fp8: bool,
87 pub require_tensor_cores: bool,
89 pub require_unified_memory: bool,
91 pub require_peer_access: bool,
93 pub pin: Option<BackendKind>,
95}
96
97impl SelectionRequest {
98 #[must_use]
100 pub const fn any() -> Self {
101 Self {
102 require_gpu: false,
103 require_fp16: false,
104 require_bf16: false,
105 require_fp8: false,
106 require_tensor_cores: false,
107 require_unified_memory: false,
108 require_peer_access: false,
109 pin: None,
110 }
111 }
112
113 #[must_use]
116 pub const fn require_gpu() -> Self {
117 let mut r = Self::any();
118 r.require_gpu = true;
119 r
120 }
121
122 #[must_use]
124 pub const fn pinned(kind: BackendKind) -> Self {
125 let mut r = Self::any();
126 r.pin = Some(kind);
127 r
128 }
129
130 #[must_use]
160 pub const fn for_workload(mut self, workload_bytes: usize, gpu_threshold_bytes: usize) -> Self {
161 if !self.require_gpu && self.pin.is_none() && workload_bytes < gpu_threshold_bytes {
162 self.pin = Some(BackendKind::Cpu);
163 }
164 self
165 }
166
167 #[must_use]
169 pub fn is_satisfied_by(&self, entry: &BackendEntry) -> bool {
170 if !entry.available {
171 return false;
172 }
173 if let Some(pin) = self.pin {
174 if entry.kind != pin {
175 return false;
176 }
177 }
178 let caps = &entry.capabilities;
179 if self.require_gpu && !entry.kind.is_gpu() {
180 return false;
181 }
182 if self.require_fp16 && !caps.supports_fp16 {
183 return false;
184 }
185 if self.require_bf16 && !caps.supports_bf16 {
186 return false;
187 }
188 if self.require_fp8 && !caps.supports_fp8 {
189 return false;
190 }
191 if self.require_tensor_cores && !caps.tensor_cores {
192 return false;
193 }
194 if self.require_unified_memory && !caps.unified_memory {
195 return false;
196 }
197 if self.require_peer_access && !caps.peer_access {
198 return false;
199 }
200 true
201 }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
211pub enum OpClass {
212 MatMul,
214 Convolution,
216 Attention,
218 Reduction,
220 Elementwise,
222 Memory,
224}
225
226impl OpClass {
227 pub const ALL: [OpClass; 6] = [
229 OpClass::MatMul,
230 OpClass::Convolution,
231 OpClass::Attention,
232 OpClass::Reduction,
233 OpClass::Elementwise,
234 OpClass::Memory,
235 ];
236
237 #[must_use]
240 pub const fn prefers_tensor_cores(self) -> bool {
241 matches!(self, Self::MatMul | Self::Convolution | Self::Attention)
242 }
243}
244
245#[derive(Debug, Clone, Default)]
251pub struct BackendRegistry {
252 entries: Vec<BackendEntry>,
253}
254
255impl BackendRegistry {
256 #[must_use]
258 pub fn new() -> Self {
259 Self {
260 entries: Vec::new(),
261 }
262 }
263
264 #[must_use]
273 pub fn with_defaults() -> Self {
274 let mut reg = Self::new();
275 for kind in BackendKind::ALL {
276 reg.register(BackendEntry::new(kind, kind == BackendKind::Cpu));
277 }
278 reg
279 }
280
281 pub fn register(&mut self, entry: BackendEntry) {
285 if let Some(slot) = self.entries.iter_mut().find(|e| e.kind == entry.kind) {
286 *slot = entry;
287 } else {
288 self.entries.push(entry);
289 }
290 }
291
292 #[must_use]
294 pub fn len(&self) -> usize {
295 self.entries.len()
296 }
297
298 #[must_use]
300 pub fn is_empty(&self) -> bool {
301 self.entries.is_empty()
302 }
303
304 #[must_use]
306 pub fn get(&self, kind: BackendKind) -> Option<&BackendEntry> {
307 self.entries.iter().find(|e| e.kind == kind)
308 }
309
310 pub fn set_available(&mut self, kind: BackendKind, available: bool) -> bool {
312 if let Some(e) = self.entries.iter_mut().find(|e| e.kind == kind) {
313 e.available = available;
314 true
315 } else {
316 false
317 }
318 }
319
320 pub fn set_capabilities(&mut self, kind: BackendKind, caps: Capabilities) -> bool {
322 if let Some(e) = self.entries.iter_mut().find(|e| e.kind == kind) {
323 e.capabilities = caps;
324 true
325 } else {
326 false
327 }
328 }
329
330 #[must_use]
332 pub fn available_kinds(&self) -> Vec<BackendKind> {
333 self.entries
334 .iter()
335 .filter(|e| e.available)
336 .map(|e| e.kind)
337 .collect()
338 }
339
340 pub fn select(&self, req: &SelectionRequest) -> BackendResult<BackendKind> {
347 self.entries
348 .iter()
349 .filter(|e| req.is_satisfied_by(e))
350 .max_by(|a, b| {
351 a.priority
352 .cmp(&b.priority)
353 .then(a.kind.default_priority().cmp(&b.kind.default_priority()))
354 })
355 .map(|e| e.kind)
356 .ok_or_else(|| {
357 BackendError::Unsupported(format!(
358 "no registered backend satisfies the request {req:?}"
359 ))
360 })
361 }
362
363 pub fn select_best(&self) -> BackendResult<BackendKind> {
366 self.select(&SelectionRequest::any())
367 }
368
369 pub fn select_for_workload(
378 &self,
379 req: &SelectionRequest,
380 workload_bytes: usize,
381 gpu_threshold_bytes: usize,
382 ) -> BackendResult<BackendKind> {
383 let narrowed = req.for_workload(workload_bytes, gpu_threshold_bytes);
384 match self.select(&narrowed) {
385 Ok(kind) => Ok(kind),
386 Err(_) if narrowed != *req => self.select(req),
387 Err(e) => Err(e),
388 }
389 }
390
391 #[must_use]
399 pub fn fallback_chain(&self, req: &SelectionRequest) -> Vec<BackendKind> {
400 let mut ranked: Vec<(u32, BackendKind)> = self
404 .entries
405 .iter()
406 .filter(|e| req.is_satisfied_by(e))
407 .map(|e| (e.priority, e.kind))
408 .collect();
409 ranked.sort_by(|(pa, ka), (pb, kb)| {
410 pb.cmp(pa)
411 .then(kb.default_priority().cmp(&ka.default_priority()))
412 });
413 let mut chain: Vec<BackendKind> = ranked.into_iter().map(|(_, k)| k).collect();
414 if let Some(pos) = chain.iter().position(|&k| k == BackendKind::Cpu) {
417 let cpu = chain.remove(pos);
418 chain.push(cpu);
419 }
420 chain
421 }
422
423 pub fn route(&self, op: OpClass) -> BackendResult<BackendKind> {
430 if op.prefers_tensor_cores() {
431 let tc_req = SelectionRequest {
432 require_tensor_cores: true,
433 ..SelectionRequest::any()
434 };
435 if let Ok(kind) = self.select(&tc_req) {
436 return Ok(kind);
437 }
438 }
439 self.select_best()
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 fn three_backend_registry() -> BackendRegistry {
450 let mut reg = BackendRegistry::new();
451 reg.register(BackendEntry::new(BackendKind::Cuda, true));
452 let rocm_caps = Capabilities {
453 tensor_cores: true,
454 supports_fp16: true,
455 ..Capabilities::default()
456 };
457 reg.register(BackendEntry::new(BackendKind::Rocm, true).with_capabilities(rocm_caps));
458 reg.register(BackendEntry::new(BackendKind::Cpu, true));
459 reg
460 }
461
462 #[test]
463 fn defaults_have_cpu_available_and_gpus_absent() {
464 let reg = BackendRegistry::with_defaults();
465 assert_eq!(reg.len(), 7);
466 assert!(reg.get(BackendKind::Cpu).unwrap().available);
467 assert!(!reg.get(BackendKind::Cuda).unwrap().available);
468 assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
470 }
471
472 #[test]
473 fn select_picks_highest_priority_available() {
474 let reg = three_backend_registry();
475 assert_eq!(reg.select_best().unwrap(), BackendKind::Cuda);
477 }
478
479 #[test]
480 fn select_skips_unavailable_highest_priority() {
481 let mut reg = three_backend_registry();
482 reg.set_available(BackendKind::Cuda, false);
483 assert_eq!(reg.select_best().unwrap(), BackendKind::Rocm);
485 reg.set_available(BackendKind::Rocm, false);
486 assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
488 }
489
490 #[test]
491 fn select_honors_capability_requirements() {
492 let reg = three_backend_registry();
493 let req = SelectionRequest {
495 require_tensor_cores: true,
496 ..SelectionRequest::any()
497 };
498 assert_eq!(reg.select(&req).unwrap(), BackendKind::Rocm);
499 }
500
501 #[test]
502 fn select_require_gpu_excludes_cpu() {
503 let mut reg = BackendRegistry::new();
504 reg.register(BackendEntry::new(BackendKind::Cpu, true));
505 assert!(reg.select(&SelectionRequest::require_gpu()).is_err());
507 assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
509 }
510
511 #[test]
512 fn select_pinned_backend() {
513 let reg = three_backend_registry();
514 let req = SelectionRequest::pinned(BackendKind::Cpu);
515 assert_eq!(reg.select(&req).unwrap(), BackendKind::Cpu);
516 let mut reg2 = reg.clone();
518 reg2.set_available(BackendKind::Rocm, false);
519 assert!(
520 reg2.select(&SelectionRequest::pinned(BackendKind::Rocm))
521 .is_err()
522 );
523 }
524
525 #[test]
526 fn higher_explicit_priority_overrides_default() {
527 let mut reg = BackendRegistry::new();
528 reg.register(BackendEntry::new(BackendKind::Cuda, true));
530 reg.register(BackendEntry::new(BackendKind::Cpu, true).with_priority(1000));
531 assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
532 }
533
534 #[test]
535 fn fallback_chain_orders_by_priority_with_cpu_last() {
536 let reg = three_backend_registry();
537 let chain = reg.fallback_chain(&SelectionRequest::any());
538 assert_eq!(
539 chain,
540 vec![BackendKind::Cuda, BackendKind::Rocm, BackendKind::Cpu]
541 );
542 assert_eq!(*chain.last().unwrap(), BackendKind::Cpu);
544 }
545
546 #[test]
547 fn fallback_chain_respects_constraints() {
548 let reg = three_backend_registry();
549 let req = SelectionRequest {
550 require_tensor_cores: true,
551 ..SelectionRequest::any()
552 };
553 assert_eq!(reg.fallback_chain(&req), vec![BackendKind::Rocm]);
555 }
556
557 #[test]
558 fn fallback_chain_empty_when_nothing_qualifies() {
559 let mut reg = three_backend_registry();
560 reg.set_available(BackendKind::Cuda, false);
561 reg.set_available(BackendKind::Rocm, false);
562 reg.set_available(BackendKind::Cpu, false);
563 assert!(reg.fallback_chain(&SelectionRequest::any()).is_empty());
564 assert!(reg.select_best().is_err());
565 }
566
567 #[test]
568 fn route_matmul_prefers_tensor_core_backend() {
569 let reg = three_backend_registry();
570 assert_eq!(reg.route(OpClass::MatMul).unwrap(), BackendKind::Rocm);
573 assert_eq!(reg.route(OpClass::Elementwise).unwrap(), BackendKind::Cuda);
575 }
576
577 #[test]
578 fn route_falls_back_when_no_tensor_cores_anywhere() {
579 let mut reg = BackendRegistry::new();
580 reg.register(BackendEntry::new(BackendKind::Cuda, true)); reg.register(BackendEntry::new(BackendKind::Cpu, true));
582 assert_eq!(reg.route(OpClass::MatMul).unwrap(), BackendKind::Cuda);
584 }
585
586 #[test]
587 fn register_replaces_same_kind() {
588 let mut reg = BackendRegistry::new();
589 reg.register(BackendEntry::new(BackendKind::Cuda, false));
590 assert!(!reg.get(BackendKind::Cuda).unwrap().available);
591 reg.register(BackendEntry::new(BackendKind::Cuda, true));
592 assert_eq!(reg.len(), 1);
593 assert!(reg.get(BackendKind::Cuda).unwrap().available);
594 }
595
596 #[test]
597 fn available_kinds_lists_only_available() {
598 let mut reg = three_backend_registry();
599 reg.set_available(BackendKind::Rocm, false);
600 let avail = reg.available_kinds();
601 assert!(avail.contains(&BackendKind::Cuda));
602 assert!(avail.contains(&BackendKind::Cpu));
603 assert!(!avail.contains(&BackendKind::Rocm));
604 }
605
606 #[test]
607 fn set_methods_report_presence() {
608 let mut reg = BackendRegistry::new();
609 assert!(!reg.set_available(BackendKind::Cuda, true));
610 reg.register(BackendEntry::new(BackendKind::Cuda, false));
611 assert!(reg.set_available(BackendKind::Cuda, true));
612 assert!(reg.set_capabilities(BackendKind::Cuda, Capabilities::default()));
613 assert!(!reg.set_capabilities(BackendKind::Metal, Capabilities::default()));
614 }
615
616 #[test]
617 fn op_class_tensor_core_preference() {
618 assert!(OpClass::MatMul.prefers_tensor_cores());
619 assert!(OpClass::Attention.prefers_tensor_cores());
620 assert!(!OpClass::Elementwise.prefers_tensor_cores());
621 assert!(!OpClass::Memory.prefers_tensor_cores());
622 assert_eq!(OpClass::ALL.len(), 6);
623 }
624
625 const THRESHOLD: usize = 64 * 1024;
629
630 #[test]
631 fn for_workload_pins_cpu_below_threshold_only() {
632 let below = SelectionRequest::any().for_workload(THRESHOLD - 1, THRESHOLD);
633 assert_eq!(below.pin, Some(BackendKind::Cpu));
634 let at = SelectionRequest::any().for_workload(THRESHOLD, THRESHOLD);
636 assert_eq!(at, SelectionRequest::any());
637 let above = SelectionRequest::any().for_workload(THRESHOLD + 1, THRESHOLD);
638 assert_eq!(above, SelectionRequest::any());
639 }
640
641 #[test]
642 fn for_workload_respects_explicit_preferences() {
643 let gpu = SelectionRequest::require_gpu().for_workload(16, THRESHOLD);
645 assert_eq!(gpu.pin, None);
646 assert!(gpu.require_gpu);
647 let pinned = SelectionRequest::pinned(BackendKind::Metal).for_workload(16, THRESHOLD);
649 assert_eq!(pinned.pin, Some(BackendKind::Metal));
650 }
651
652 #[test]
653 fn select_for_workload_routes_small_to_cpu_and_large_to_gpu() {
654 let reg = three_backend_registry();
655 let any = SelectionRequest::any();
656 assert_eq!(
657 reg.select_for_workload(&any, 1024, THRESHOLD)
658 .expect("small workload must select a backend"),
659 BackendKind::Cpu,
660 "a 1 KiB workload must stay on the host"
661 );
662 assert_eq!(
663 reg.select_for_workload(&any, 1024 * 1024, THRESHOLD)
664 .expect("large workload must select a backend"),
665 BackendKind::Cuda,
666 "a 1 MiB workload must reach the highest-priority GPU"
667 );
668 }
669
670 #[test]
671 fn select_for_workload_falls_back_when_no_cpu_entry() {
672 let mut reg = BackendRegistry::new();
673 reg.register(BackendEntry::new(BackendKind::Cuda, true));
674 assert_eq!(
677 reg.select_for_workload(&SelectionRequest::any(), 16, THRESHOLD)
678 .expect("must fall back to the unnarrowed request"),
679 BackendKind::Cuda
680 );
681 }
682
683 #[test]
684 fn select_for_workload_still_fails_when_nothing_qualifies() {
685 let mut reg = three_backend_registry();
686 for kind in BackendKind::ALL {
687 reg.set_available(kind, false);
688 }
689 assert!(
690 reg.select_for_workload(&SelectionRequest::any(), 16, THRESHOLD)
691 .is_err()
692 );
693 }
694
695 #[test]
696 fn selection_request_constructors() {
697 assert!(SelectionRequest::require_gpu().require_gpu);
698 assert_eq!(
699 SelectionRequest::pinned(BackendKind::Metal).pin,
700 Some(BackendKind::Metal)
701 );
702 assert_eq!(SelectionRequest::any(), SelectionRequest::default());
703 }
704}