use crate::DeviceLocation;
use crate::error::Result;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum BackendId {
Cpu,
Cuda,
Vulkan,
Metal,
}
impl BackendId {
pub fn as_str(self) -> &'static str {
match self {
BackendId::Cpu => "cpu",
BackendId::Cuda => "cuda",
BackendId::Vulkan => "vulkan",
BackendId::Metal => "metal",
}
}
}
impl std::fmt::Display for BackendId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DeviceDescriptor {
pub backend: BackendId,
pub device_index: u32,
pub hardware_sku: String,
pub vendor_id: u32,
pub device_id: u32,
pub compute_capability: Option<(u32, u32)>,
pub subgroup_width: Option<u32>,
pub driver_version: String,
pub total_memory_bytes: u64,
pub location: DeviceLocation,
}
impl DeviceDescriptor {
pub fn equivalence_key(&self) -> EquivalenceKey {
EquivalenceKey {
backend: self.backend,
vendor_id: self.vendor_id,
device_id: self.device_id,
compute_capability: self.compute_capability,
driver_version: self.driver_version.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EquivalenceKey {
pub backend: BackendId,
pub vendor_id: u32,
pub device_id: u32,
pub compute_capability: Option<(u32, u32)>,
pub driver_version: String,
}
pub trait BackendProbe {
fn enumerate_devices() -> Result<Vec<DeviceDescriptor>>;
}
impl DeviceLocation {
pub fn backend(self) -> BackendId {
match self {
DeviceLocation::Cpu => BackendId::Cpu,
DeviceLocation::Cuda { .. } => BackendId::Cuda,
DeviceLocation::Vulkan { .. } => BackendId::Vulkan,
DeviceLocation::Metal { .. } => BackendId::Metal,
}
}
}
impl BackendId {
pub fn default_location(self) -> DeviceLocation {
match self {
BackendId::Cpu => DeviceLocation::Cpu,
BackendId::Cuda => DeviceLocation::Cuda { gpu_id: 0 },
BackendId::Vulkan => DeviceLocation::Vulkan { gpu_id: 0 },
BackendId::Metal => DeviceLocation::Metal { gpu_id: 0 },
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PciAddress {
pub domain: u32,
pub bus: u32,
pub device: u32,
pub function: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DeviceIdentity {
pub uuid: Option<[u8; 16]>,
pub pci: Option<PciAddress>,
}
impl DeviceIdentity {
pub fn usable_uuid(&self) -> Option<[u8; 16]> {
self.uuid.filter(|u| u.iter().any(|b| *b != 0))
}
pub fn same_physical_device(&self, other: &DeviceIdentity) -> bool {
if let (Some(a), Some(b)) = (self.usable_uuid(), other.usable_uuid()) {
return a == b;
}
match (self.pci, other.pci) {
(Some(a), Some(b)) => {
a.domain == b.domain
&& a.bus == b.bus
&& a.device == b.device
&& match (a.function, b.function) {
(Some(x), Some(y)) => x == y,
_ => true,
}
}
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backend_id_as_str_is_stable() {
assert_eq!(BackendId::Cpu.as_str(), "cpu");
assert_eq!(BackendId::Cuda.as_str(), "cuda");
assert_eq!(BackendId::Vulkan.as_str(), "vulkan");
assert_eq!(BackendId::Metal.as_str(), "metal");
}
#[test]
fn equivalence_key_collapses_identical_cuda_devices() {
let make = |idx: u32| DeviceDescriptor {
backend: BackendId::Cuda,
device_index: idx,
hardware_sku: "NVIDIA GeForce RTX 4090".to_string(),
vendor_id: 0x10DE,
device_id: 0x2684,
compute_capability: Some((8, 9)),
subgroup_width: Some(32), driver_version: "550.54.14".to_string(),
total_memory_bytes: 25_769_803_776,
location: DeviceLocation::Cuda {
gpu_id: idx as usize,
},
};
let a = make(0);
let b = make(1);
assert_eq!(a.equivalence_key(), b.equivalence_key());
assert_ne!(a, b);
}
#[test]
fn equivalence_key_splits_same_silicon_across_backends() {
let cuda = DeviceDescriptor {
backend: BackendId::Cuda,
device_index: 0,
hardware_sku: "NVIDIA GeForce RTX 4090".to_string(),
vendor_id: 0x10DE,
device_id: 0x2684,
compute_capability: Some((8, 9)),
subgroup_width: Some(32),
driver_version: "550.54.14".to_string(),
total_memory_bytes: 25_769_803_776,
location: DeviceLocation::Cuda { gpu_id: 0 },
};
let vulkan = DeviceDescriptor {
backend: BackendId::Vulkan,
device_index: 0,
hardware_sku: "NVIDIA GeForce RTX 4090".to_string(),
vendor_id: 0x10DE,
device_id: 0x2684,
compute_capability: None, subgroup_width: Some(32),
driver_version: "550.54.14".to_string(),
total_memory_bytes: 25_769_803_776,
location: DeviceLocation::Cpu, };
assert_ne!(cuda.equivalence_key(), vulkan.equivalence_key());
}
#[test]
fn equivalence_key_splits_on_driver_version() {
let base = DeviceDescriptor {
backend: BackendId::Cuda,
device_index: 0,
hardware_sku: "X".to_string(),
vendor_id: 1,
device_id: 2,
compute_capability: Some((8, 9)),
subgroup_width: Some(32),
driver_version: "550".to_string(),
total_memory_bytes: 0,
location: DeviceLocation::Cuda { gpu_id: 0 },
};
let newer = DeviceDescriptor {
driver_version: "560".to_string(),
..base.clone()
};
assert_ne!(base.equivalence_key(), newer.equivalence_key());
}
#[test]
fn equivalence_key_ignores_subgroup_width() {
let base = DeviceDescriptor {
backend: BackendId::Vulkan,
device_index: 0,
hardware_sku: "X".to_string(),
vendor_id: 1,
device_id: 2,
compute_capability: None,
subgroup_width: None,
driver_version: "550".to_string(),
total_memory_bytes: 0,
location: DeviceLocation::Vulkan { gpu_id: 0 },
};
let with_width = DeviceDescriptor {
subgroup_width: Some(64),
..base.clone()
};
assert_ne!(base, with_width);
assert_eq!(base.equivalence_key(), with_width.equivalence_key());
}
#[test]
fn location_to_backend_ignores_the_ordinal() {
assert_eq!(DeviceLocation::Cpu.backend(), BackendId::Cpu);
for gpu_id in [0usize, 1, 7] {
assert_eq!(DeviceLocation::Cuda { gpu_id }.backend(), BackendId::Cuda);
assert_eq!(
DeviceLocation::Vulkan { gpu_id }.backend(),
BackendId::Vulkan
);
assert_eq!(DeviceLocation::Metal { gpu_id }.backend(), BackendId::Metal);
}
}
#[test]
fn default_location_round_trips_for_every_backend() {
for b in [
BackendId::Cpu,
BackendId::Cuda,
BackendId::Vulkan,
BackendId::Metal,
] {
assert_eq!(b.default_location().backend(), b, "{b:?}");
}
assert_eq!(
BackendId::Cuda.default_location(),
DeviceLocation::Cuda { gpu_id: 0 }
);
assert_eq!(BackendId::Cpu.default_location(), DeviceLocation::Cpu);
}
fn ident(uuid: Option<u8>, pci: Option<(u32, u32, u32, Option<u32>)>) -> DeviceIdentity {
DeviceIdentity {
uuid: uuid.map(|b| [b; 16]),
pci: pci.map(|(domain, bus, device, function)| PciAddress {
domain,
bus,
device,
function,
}),
}
}
#[test]
fn identity_uuid_decides_when_both_sides_have_one() {
let a = ident(Some(1), Some((0, 1, 0, None)));
assert!(a.same_physical_device(&ident(Some(1), Some((0, 9, 9, None)))));
assert!(!a.same_physical_device(&ident(Some(2), Some((0, 1, 0, None)))));
}
#[test]
fn identity_zero_uuid_is_absent_so_pci_decides() {
let zero = DeviceIdentity {
uuid: Some([0; 16]),
pci: Some(PciAddress {
domain: 0,
bus: 3,
device: 0,
function: Some(0),
}),
};
let other_zero_other_bus = DeviceIdentity {
uuid: Some([0; 16]),
pci: Some(PciAddress {
domain: 0,
bus: 4,
device: 0,
function: Some(0),
}),
};
assert!(!zero.same_physical_device(&other_zero_other_bus));
assert!(zero.same_physical_device(&zero));
}
#[test]
fn identity_pci_fallback_and_unknown() {
let cuda = ident(None, Some((0, 1, 0, None)));
assert!(cuda.same_physical_device(&ident(None, Some((0, 1, 0, Some(0))))));
assert!(!cuda.same_physical_device(&ident(None, Some((0, 2, 0, Some(0))))));
assert!(
!ident(Some(1), Some((0, 1, 0, Some(1))))
.same_physical_device(&ident(None, Some((0, 1, 0, Some(0)))))
);
assert!(!ident(None, None).same_physical_device(&ident(None, None)));
}
}