use crate::backend_kind::BackendKind;
use crate::capabilities::Capabilities;
use crate::error::{BackendError, BackendResult};
#[derive(Debug, Clone)]
pub struct BackendEntry {
pub kind: BackendKind,
pub available: bool,
pub priority: u32,
pub capabilities: Capabilities,
}
impl BackendEntry {
#[must_use]
pub fn new(kind: BackendKind, available: bool) -> Self {
Self {
kind,
available,
priority: kind.default_priority(),
capabilities: if kind == BackendKind::Cpu {
Capabilities::cpu()
} else {
Capabilities::default()
},
}
}
#[must_use]
pub fn with_priority(mut self, priority: u32) -> Self {
self.priority = priority;
self
}
#[must_use]
pub fn with_capabilities(mut self, capabilities: Capabilities) -> Self {
self.capabilities = capabilities;
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SelectionRequest {
pub require_gpu: bool,
pub require_fp16: bool,
pub require_bf16: bool,
pub require_fp8: bool,
pub require_tensor_cores: bool,
pub require_unified_memory: bool,
pub require_peer_access: bool,
pub pin: Option<BackendKind>,
}
impl SelectionRequest {
#[must_use]
pub const fn any() -> Self {
Self {
require_gpu: false,
require_fp16: false,
require_bf16: false,
require_fp8: false,
require_tensor_cores: false,
require_unified_memory: false,
require_peer_access: false,
pin: None,
}
}
#[must_use]
pub const fn require_gpu() -> Self {
let mut r = Self::any();
r.require_gpu = true;
r
}
#[must_use]
pub const fn pinned(kind: BackendKind) -> Self {
let mut r = Self::any();
r.pin = Some(kind);
r
}
#[must_use]
pub fn is_satisfied_by(&self, entry: &BackendEntry) -> bool {
if !entry.available {
return false;
}
if let Some(pin) = self.pin {
if entry.kind != pin {
return false;
}
}
let caps = &entry.capabilities;
if self.require_gpu && !entry.kind.is_gpu() {
return false;
}
if self.require_fp16 && !caps.supports_fp16 {
return false;
}
if self.require_bf16 && !caps.supports_bf16 {
return false;
}
if self.require_fp8 && !caps.supports_fp8 {
return false;
}
if self.require_tensor_cores && !caps.tensor_cores {
return false;
}
if self.require_unified_memory && !caps.unified_memory {
return false;
}
if self.require_peer_access && !caps.peer_access {
return false;
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OpClass {
MatMul,
Convolution,
Attention,
Reduction,
Elementwise,
Memory,
}
impl OpClass {
pub const ALL: [OpClass; 6] = [
OpClass::MatMul,
OpClass::Convolution,
OpClass::Attention,
OpClass::Reduction,
OpClass::Elementwise,
OpClass::Memory,
];
#[must_use]
pub const fn prefers_tensor_cores(self) -> bool {
matches!(self, Self::MatMul | Self::Convolution | Self::Attention)
}
}
#[derive(Debug, Clone, Default)]
pub struct BackendRegistry {
entries: Vec<BackendEntry>,
}
impl BackendRegistry {
#[must_use]
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
#[must_use]
pub fn with_defaults() -> Self {
let mut reg = Self::new();
for kind in BackendKind::ALL {
reg.register(BackendEntry::new(kind, kind == BackendKind::Cpu));
}
reg
}
pub fn register(&mut self, entry: BackendEntry) {
if let Some(slot) = self.entries.iter_mut().find(|e| e.kind == entry.kind) {
*slot = entry;
} else {
self.entries.push(entry);
}
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn get(&self, kind: BackendKind) -> Option<&BackendEntry> {
self.entries.iter().find(|e| e.kind == kind)
}
pub fn set_available(&mut self, kind: BackendKind, available: bool) -> bool {
if let Some(e) = self.entries.iter_mut().find(|e| e.kind == kind) {
e.available = available;
true
} else {
false
}
}
pub fn set_capabilities(&mut self, kind: BackendKind, caps: Capabilities) -> bool {
if let Some(e) = self.entries.iter_mut().find(|e| e.kind == kind) {
e.capabilities = caps;
true
} else {
false
}
}
#[must_use]
pub fn available_kinds(&self) -> Vec<BackendKind> {
self.entries
.iter()
.filter(|e| e.available)
.map(|e| e.kind)
.collect()
}
pub fn select(&self, req: &SelectionRequest) -> BackendResult<BackendKind> {
self.entries
.iter()
.filter(|e| req.is_satisfied_by(e))
.max_by(|a, b| {
a.priority
.cmp(&b.priority)
.then(a.kind.default_priority().cmp(&b.kind.default_priority()))
})
.map(|e| e.kind)
.ok_or_else(|| {
BackendError::Unsupported(format!(
"no registered backend satisfies the request {req:?}"
))
})
}
pub fn select_best(&self) -> BackendResult<BackendKind> {
self.select(&SelectionRequest::any())
}
#[must_use]
pub fn fallback_chain(&self, req: &SelectionRequest) -> Vec<BackendKind> {
let mut ranked: Vec<(u32, BackendKind)> = self
.entries
.iter()
.filter(|e| req.is_satisfied_by(e))
.map(|e| (e.priority, e.kind))
.collect();
ranked.sort_by(|(pa, ka), (pb, kb)| {
pb.cmp(pa)
.then(kb.default_priority().cmp(&ka.default_priority()))
});
let mut chain: Vec<BackendKind> = ranked.into_iter().map(|(_, k)| k).collect();
if let Some(pos) = chain.iter().position(|&k| k == BackendKind::Cpu) {
let cpu = chain.remove(pos);
chain.push(cpu);
}
chain
}
pub fn route(&self, op: OpClass) -> BackendResult<BackendKind> {
if op.prefers_tensor_cores() {
let tc_req = SelectionRequest {
require_tensor_cores: true,
..SelectionRequest::any()
};
if let Ok(kind) = self.select(&tc_req) {
return Ok(kind);
}
}
self.select_best()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn three_backend_registry() -> BackendRegistry {
let mut reg = BackendRegistry::new();
reg.register(BackendEntry::new(BackendKind::Cuda, true));
let rocm_caps = Capabilities {
tensor_cores: true,
supports_fp16: true,
..Capabilities::default()
};
reg.register(BackendEntry::new(BackendKind::Rocm, true).with_capabilities(rocm_caps));
reg.register(BackendEntry::new(BackendKind::Cpu, true));
reg
}
#[test]
fn defaults_have_cpu_available_and_gpus_absent() {
let reg = BackendRegistry::with_defaults();
assert_eq!(reg.len(), 7);
assert!(reg.get(BackendKind::Cpu).unwrap().available);
assert!(!reg.get(BackendKind::Cuda).unwrap().available);
assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
}
#[test]
fn select_picks_highest_priority_available() {
let reg = three_backend_registry();
assert_eq!(reg.select_best().unwrap(), BackendKind::Cuda);
}
#[test]
fn select_skips_unavailable_highest_priority() {
let mut reg = three_backend_registry();
reg.set_available(BackendKind::Cuda, false);
assert_eq!(reg.select_best().unwrap(), BackendKind::Rocm);
reg.set_available(BackendKind::Rocm, false);
assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
}
#[test]
fn select_honors_capability_requirements() {
let reg = three_backend_registry();
let req = SelectionRequest {
require_tensor_cores: true,
..SelectionRequest::any()
};
assert_eq!(reg.select(&req).unwrap(), BackendKind::Rocm);
}
#[test]
fn select_require_gpu_excludes_cpu() {
let mut reg = BackendRegistry::new();
reg.register(BackendEntry::new(BackendKind::Cpu, true));
assert!(reg.select(&SelectionRequest::require_gpu()).is_err());
assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
}
#[test]
fn select_pinned_backend() {
let reg = three_backend_registry();
let req = SelectionRequest::pinned(BackendKind::Cpu);
assert_eq!(reg.select(&req).unwrap(), BackendKind::Cpu);
let mut reg2 = reg.clone();
reg2.set_available(BackendKind::Rocm, false);
assert!(
reg2.select(&SelectionRequest::pinned(BackendKind::Rocm))
.is_err()
);
}
#[test]
fn higher_explicit_priority_overrides_default() {
let mut reg = BackendRegistry::new();
reg.register(BackendEntry::new(BackendKind::Cuda, true));
reg.register(BackendEntry::new(BackendKind::Cpu, true).with_priority(1000));
assert_eq!(reg.select_best().unwrap(), BackendKind::Cpu);
}
#[test]
fn fallback_chain_orders_by_priority_with_cpu_last() {
let reg = three_backend_registry();
let chain = reg.fallback_chain(&SelectionRequest::any());
assert_eq!(
chain,
vec![BackendKind::Cuda, BackendKind::Rocm, BackendKind::Cpu]
);
assert_eq!(*chain.last().unwrap(), BackendKind::Cpu);
}
#[test]
fn fallback_chain_respects_constraints() {
let reg = three_backend_registry();
let req = SelectionRequest {
require_tensor_cores: true,
..SelectionRequest::any()
};
assert_eq!(reg.fallback_chain(&req), vec![BackendKind::Rocm]);
}
#[test]
fn fallback_chain_empty_when_nothing_qualifies() {
let mut reg = three_backend_registry();
reg.set_available(BackendKind::Cuda, false);
reg.set_available(BackendKind::Rocm, false);
reg.set_available(BackendKind::Cpu, false);
assert!(reg.fallback_chain(&SelectionRequest::any()).is_empty());
assert!(reg.select_best().is_err());
}
#[test]
fn route_matmul_prefers_tensor_core_backend() {
let reg = three_backend_registry();
assert_eq!(reg.route(OpClass::MatMul).unwrap(), BackendKind::Rocm);
assert_eq!(reg.route(OpClass::Elementwise).unwrap(), BackendKind::Cuda);
}
#[test]
fn route_falls_back_when_no_tensor_cores_anywhere() {
let mut reg = BackendRegistry::new();
reg.register(BackendEntry::new(BackendKind::Cuda, true)); reg.register(BackendEntry::new(BackendKind::Cpu, true));
assert_eq!(reg.route(OpClass::MatMul).unwrap(), BackendKind::Cuda);
}
#[test]
fn register_replaces_same_kind() {
let mut reg = BackendRegistry::new();
reg.register(BackendEntry::new(BackendKind::Cuda, false));
assert!(!reg.get(BackendKind::Cuda).unwrap().available);
reg.register(BackendEntry::new(BackendKind::Cuda, true));
assert_eq!(reg.len(), 1);
assert!(reg.get(BackendKind::Cuda).unwrap().available);
}
#[test]
fn available_kinds_lists_only_available() {
let mut reg = three_backend_registry();
reg.set_available(BackendKind::Rocm, false);
let avail = reg.available_kinds();
assert!(avail.contains(&BackendKind::Cuda));
assert!(avail.contains(&BackendKind::Cpu));
assert!(!avail.contains(&BackendKind::Rocm));
}
#[test]
fn set_methods_report_presence() {
let mut reg = BackendRegistry::new();
assert!(!reg.set_available(BackendKind::Cuda, true));
reg.register(BackendEntry::new(BackendKind::Cuda, false));
assert!(reg.set_available(BackendKind::Cuda, true));
assert!(reg.set_capabilities(BackendKind::Cuda, Capabilities::default()));
assert!(!reg.set_capabilities(BackendKind::Metal, Capabilities::default()));
}
#[test]
fn op_class_tensor_core_preference() {
assert!(OpClass::MatMul.prefers_tensor_cores());
assert!(OpClass::Attention.prefers_tensor_cores());
assert!(!OpClass::Elementwise.prefers_tensor_cores());
assert!(!OpClass::Memory.prefers_tensor_cores());
assert_eq!(OpClass::ALL.len(), 6);
}
#[test]
fn selection_request_constructors() {
assert!(SelectionRequest::require_gpu().require_gpu);
assert_eq!(
SelectionRequest::pinned(BackendKind::Metal).pin,
Some(BackendKind::Metal)
);
assert_eq!(SelectionRequest::any(), SelectionRequest::default());
}
}