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]
132 pub fn is_satisfied_by(&self, entry: &BackendEntry) -> bool {
133 if !entry.available {
134 return false;
135 }
136 if let Some(pin) = self.pin {
137 if entry.kind != pin {
138 return false;
139 }
140 }
141 let caps = &entry.capabilities;
142 if self.require_gpu && !entry.kind.is_gpu() {
143 return false;
144 }
145 if self.require_fp16 && !caps.supports_fp16 {
146 return false;
147 }
148 if self.require_bf16 && !caps.supports_bf16 {
149 return false;
150 }
151 if self.require_fp8 && !caps.supports_fp8 {
152 return false;
153 }
154 if self.require_tensor_cores && !caps.tensor_cores {
155 return false;
156 }
157 if self.require_unified_memory && !caps.unified_memory {
158 return false;
159 }
160 if self.require_peer_access && !caps.peer_access {
161 return false;
162 }
163 true
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
174pub enum OpClass {
175 MatMul,
177 Convolution,
179 Attention,
181 Reduction,
183 Elementwise,
185 Memory,
187}
188
189impl OpClass {
190 pub const ALL: [OpClass; 6] = [
192 OpClass::MatMul,
193 OpClass::Convolution,
194 OpClass::Attention,
195 OpClass::Reduction,
196 OpClass::Elementwise,
197 OpClass::Memory,
198 ];
199
200 #[must_use]
203 pub const fn prefers_tensor_cores(self) -> bool {
204 matches!(self, Self::MatMul | Self::Convolution | Self::Attention)
205 }
206}
207
208#[derive(Debug, Clone, Default)]
214pub struct BackendRegistry {
215 entries: Vec<BackendEntry>,
216}
217
218impl BackendRegistry {
219 #[must_use]
221 pub fn new() -> Self {
222 Self {
223 entries: Vec::new(),
224 }
225 }
226
227 #[must_use]
236 pub fn with_defaults() -> Self {
237 let mut reg = Self::new();
238 for kind in BackendKind::ALL {
239 reg.register(BackendEntry::new(kind, kind == BackendKind::Cpu));
240 }
241 reg
242 }
243
244 pub fn register(&mut self, entry: BackendEntry) {
248 if let Some(slot) = self.entries.iter_mut().find(|e| e.kind == entry.kind) {
249 *slot = entry;
250 } else {
251 self.entries.push(entry);
252 }
253 }
254
255 #[must_use]
257 pub fn len(&self) -> usize {
258 self.entries.len()
259 }
260
261 #[must_use]
263 pub fn is_empty(&self) -> bool {
264 self.entries.is_empty()
265 }
266
267 #[must_use]
269 pub fn get(&self, kind: BackendKind) -> Option<&BackendEntry> {
270 self.entries.iter().find(|e| e.kind == kind)
271 }
272
273 pub fn set_available(&mut self, kind: BackendKind, available: bool) -> bool {
275 if let Some(e) = self.entries.iter_mut().find(|e| e.kind == kind) {
276 e.available = available;
277 true
278 } else {
279 false
280 }
281 }
282
283 pub fn set_capabilities(&mut self, kind: BackendKind, caps: Capabilities) -> bool {
285 if let Some(e) = self.entries.iter_mut().find(|e| e.kind == kind) {
286 e.capabilities = caps;
287 true
288 } else {
289 false
290 }
291 }
292
293 #[must_use]
295 pub fn available_kinds(&self) -> Vec<BackendKind> {
296 self.entries
297 .iter()
298 .filter(|e| e.available)
299 .map(|e| e.kind)
300 .collect()
301 }
302
303 pub fn select(&self, req: &SelectionRequest) -> BackendResult<BackendKind> {
310 self.entries
311 .iter()
312 .filter(|e| req.is_satisfied_by(e))
313 .max_by(|a, b| {
314 a.priority
315 .cmp(&b.priority)
316 .then(a.kind.default_priority().cmp(&b.kind.default_priority()))
317 })
318 .map(|e| e.kind)
319 .ok_or_else(|| {
320 BackendError::Unsupported(format!(
321 "no registered backend satisfies the request {req:?}"
322 ))
323 })
324 }
325
326 pub fn select_best(&self) -> BackendResult<BackendKind> {
329 self.select(&SelectionRequest::any())
330 }
331
332 #[must_use]
340 pub fn fallback_chain(&self, req: &SelectionRequest) -> Vec<BackendKind> {
341 let mut ranked: Vec<(u32, BackendKind)> = self
345 .entries
346 .iter()
347 .filter(|e| req.is_satisfied_by(e))
348 .map(|e| (e.priority, e.kind))
349 .collect();
350 ranked.sort_by(|(pa, ka), (pb, kb)| {
351 pb.cmp(pa)
352 .then(kb.default_priority().cmp(&ka.default_priority()))
353 });
354 let mut chain: Vec<BackendKind> = ranked.into_iter().map(|(_, k)| k).collect();
355 if let Some(pos) = chain.iter().position(|&k| k == BackendKind::Cpu) {
358 let cpu = chain.remove(pos);
359 chain.push(cpu);
360 }
361 chain
362 }
363
364 pub fn route(&self, op: OpClass) -> BackendResult<BackendKind> {
371 if op.prefers_tensor_cores() {
372 let tc_req = SelectionRequest {
373 require_tensor_cores: true,
374 ..SelectionRequest::any()
375 };
376 if let Ok(kind) = self.select(&tc_req) {
377 return Ok(kind);
378 }
379 }
380 self.select_best()
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 fn three_backend_registry() -> BackendRegistry {
391 let mut reg = BackendRegistry::new();
392 reg.register(BackendEntry::new(BackendKind::Cuda, true));
393 let rocm_caps = Capabilities {
394 tensor_cores: true,
395 supports_fp16: true,
396 ..Capabilities::default()
397 };
398 reg.register(BackendEntry::new(BackendKind::Rocm, true).with_capabilities(rocm_caps));
399 reg.register(BackendEntry::new(BackendKind::Cpu, true));
400 reg
401 }
402
403 #[test]
404 fn defaults_have_cpu_available_and_gpus_absent() {
405 let reg = BackendRegistry::with_defaults();
406 assert_eq!(reg.len(), 7);
407 assert!(reg.get(BackendKind::Cpu).unwrap().available);
408 assert!(!reg.get(BackendKind::Cuda).unwrap().available);
409 assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
411 }
412
413 #[test]
414 fn select_picks_highest_priority_available() {
415 let reg = three_backend_registry();
416 assert_eq!(reg.select_best().unwrap(), BackendKind::Cuda);
418 }
419
420 #[test]
421 fn select_skips_unavailable_highest_priority() {
422 let mut reg = three_backend_registry();
423 reg.set_available(BackendKind::Cuda, false);
424 assert_eq!(reg.select_best().unwrap(), BackendKind::Rocm);
426 reg.set_available(BackendKind::Rocm, false);
427 assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
429 }
430
431 #[test]
432 fn select_honors_capability_requirements() {
433 let reg = three_backend_registry();
434 let req = SelectionRequest {
436 require_tensor_cores: true,
437 ..SelectionRequest::any()
438 };
439 assert_eq!(reg.select(&req).unwrap(), BackendKind::Rocm);
440 }
441
442 #[test]
443 fn select_require_gpu_excludes_cpu() {
444 let mut reg = BackendRegistry::new();
445 reg.register(BackendEntry::new(BackendKind::Cpu, true));
446 assert!(reg.select(&SelectionRequest::require_gpu()).is_err());
448 assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
450 }
451
452 #[test]
453 fn select_pinned_backend() {
454 let reg = three_backend_registry();
455 let req = SelectionRequest::pinned(BackendKind::Cpu);
456 assert_eq!(reg.select(&req).unwrap(), BackendKind::Cpu);
457 let mut reg2 = reg.clone();
459 reg2.set_available(BackendKind::Rocm, false);
460 assert!(
461 reg2.select(&SelectionRequest::pinned(BackendKind::Rocm))
462 .is_err()
463 );
464 }
465
466 #[test]
467 fn higher_explicit_priority_overrides_default() {
468 let mut reg = BackendRegistry::new();
469 reg.register(BackendEntry::new(BackendKind::Cuda, true));
471 reg.register(BackendEntry::new(BackendKind::Cpu, true).with_priority(1000));
472 assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
473 }
474
475 #[test]
476 fn fallback_chain_orders_by_priority_with_cpu_last() {
477 let reg = three_backend_registry();
478 let chain = reg.fallback_chain(&SelectionRequest::any());
479 assert_eq!(
480 chain,
481 vec![BackendKind::Cuda, BackendKind::Rocm, BackendKind::Cpu]
482 );
483 assert_eq!(*chain.last().unwrap(), BackendKind::Cpu);
485 }
486
487 #[test]
488 fn fallback_chain_respects_constraints() {
489 let reg = three_backend_registry();
490 let req = SelectionRequest {
491 require_tensor_cores: true,
492 ..SelectionRequest::any()
493 };
494 assert_eq!(reg.fallback_chain(&req), vec![BackendKind::Rocm]);
496 }
497
498 #[test]
499 fn fallback_chain_empty_when_nothing_qualifies() {
500 let mut reg = three_backend_registry();
501 reg.set_available(BackendKind::Cuda, false);
502 reg.set_available(BackendKind::Rocm, false);
503 reg.set_available(BackendKind::Cpu, false);
504 assert!(reg.fallback_chain(&SelectionRequest::any()).is_empty());
505 assert!(reg.select_best().is_err());
506 }
507
508 #[test]
509 fn route_matmul_prefers_tensor_core_backend() {
510 let reg = three_backend_registry();
511 assert_eq!(reg.route(OpClass::MatMul).unwrap(), BackendKind::Rocm);
514 assert_eq!(reg.route(OpClass::Elementwise).unwrap(), BackendKind::Cuda);
516 }
517
518 #[test]
519 fn route_falls_back_when_no_tensor_cores_anywhere() {
520 let mut reg = BackendRegistry::new();
521 reg.register(BackendEntry::new(BackendKind::Cuda, true)); reg.register(BackendEntry::new(BackendKind::Cpu, true));
523 assert_eq!(reg.route(OpClass::MatMul).unwrap(), BackendKind::Cuda);
525 }
526
527 #[test]
528 fn register_replaces_same_kind() {
529 let mut reg = BackendRegistry::new();
530 reg.register(BackendEntry::new(BackendKind::Cuda, false));
531 assert!(!reg.get(BackendKind::Cuda).unwrap().available);
532 reg.register(BackendEntry::new(BackendKind::Cuda, true));
533 assert_eq!(reg.len(), 1);
534 assert!(reg.get(BackendKind::Cuda).unwrap().available);
535 }
536
537 #[test]
538 fn available_kinds_lists_only_available() {
539 let mut reg = three_backend_registry();
540 reg.set_available(BackendKind::Rocm, false);
541 let avail = reg.available_kinds();
542 assert!(avail.contains(&BackendKind::Cuda));
543 assert!(avail.contains(&BackendKind::Cpu));
544 assert!(!avail.contains(&BackendKind::Rocm));
545 }
546
547 #[test]
548 fn set_methods_report_presence() {
549 let mut reg = BackendRegistry::new();
550 assert!(!reg.set_available(BackendKind::Cuda, true));
551 reg.register(BackendEntry::new(BackendKind::Cuda, false));
552 assert!(reg.set_available(BackendKind::Cuda, true));
553 assert!(reg.set_capabilities(BackendKind::Cuda, Capabilities::default()));
554 assert!(!reg.set_capabilities(BackendKind::Metal, Capabilities::default()));
555 }
556
557 #[test]
558 fn op_class_tensor_core_preference() {
559 assert!(OpClass::MatMul.prefers_tensor_cores());
560 assert!(OpClass::Attention.prefers_tensor_cores());
561 assert!(!OpClass::Elementwise.prefers_tensor_cores());
562 assert!(!OpClass::Memory.prefers_tensor_cores());
563 assert_eq!(OpClass::ALL.len(), 6);
564 }
565
566 #[test]
567 fn selection_request_constructors() {
568 assert!(SelectionRequest::require_gpu().require_gpu);
569 assert_eq!(
570 SelectionRequest::pinned(BackendKind::Metal).pin,
571 Some(BackendKind::Metal)
572 );
573 assert_eq!(SelectionRequest::any(), SelectionRequest::default());
574 }
575}