mod drm;
mod metal;
mod nvidia;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Vendor {
Nvidia,
Amd,
Intel,
Apple,
Unknown,
}
impl std::fmt::Display for Vendor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Vendor::Nvidia => "NVIDIA",
Vendor::Amd => "AMD",
Vendor::Intel => "Intel",
Vendor::Apple => "Apple",
Vendor::Unknown => "Unknown",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct GpuInfo {
pub name: String,
pub vendor: Vendor,
pub total_bytes: u64,
pub free_bytes: Option<u64>,
pub used_bytes: Option<u64>,
}
impl std::fmt::Display for GpuInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} ({}): {:.1} GiB total",
self.name,
self.vendor,
gib(self.total_bytes)
)?;
if let Some(free) = self.free_bytes {
write!(f, ", {:.1} GiB free", gib(free))?;
}
Ok(())
}
}
#[allow(clippy::cast_precision_loss)] fn gib(bytes: u64) -> f64 {
bytes as f64 / (1024.0 * 1024.0 * 1024.0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ComputeCapability {
pub major: u32,
pub minor: u32,
}
impl ComputeCapability {
#[must_use]
pub const fn new(major: u32, minor: u32) -> Self {
Self { major, minor }
}
}
impl std::fmt::Display for ComputeCapability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CudaVersion {
pub major: u32,
pub minor: u32,
}
impl CudaVersion {
#[must_use]
pub const fn new(major: u32, minor: u32) -> Self {
Self { major, minor }
}
}
impl std::fmt::Display for CudaVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CudaHost {
pub compute_capability: ComputeCapability,
pub driver_version: CudaVersion,
}
#[must_use]
pub fn detect() -> Vec<GpuInfo> {
let mut gpus = Vec::new();
gpus.extend(nvidia::detect());
gpus.extend(drm::detect());
gpus.extend(metal::detect());
gpus
}
#[must_use]
pub fn cuda_host() -> Option<CudaHost> {
nvidia::cuda_host()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_never_panics() {
for gpu in detect() {
assert!(!gpu.name.is_empty());
let _ = gpu.to_string();
}
}
#[test]
fn display_includes_free_when_present() {
let gpu = GpuInfo {
name: "Test GPU".to_string(),
vendor: Vendor::Nvidia,
total_bytes: 24 * 1024 * 1024 * 1024,
free_bytes: Some(12 * 1024 * 1024 * 1024),
used_bytes: Some(12 * 1024 * 1024 * 1024),
};
let shown = gpu.to_string();
assert!(shown.contains("NVIDIA"));
assert!(shown.contains("24.0 GiB total"));
assert!(shown.contains("12.0 GiB free"));
}
#[test]
fn display_omits_free_when_absent() {
let gpu = GpuInfo {
name: "AMD GPU (card0)".to_string(),
vendor: Vendor::Amd,
total_bytes: 8 * 1024 * 1024 * 1024,
free_bytes: None,
used_bytes: None,
};
let shown = gpu.to_string();
assert!(shown.contains("8.0 GiB total"));
assert!(!shown.contains("free"));
}
#[test]
fn vendor_display_covers_every_variant() {
assert_eq!(Vendor::Nvidia.to_string(), "NVIDIA");
assert_eq!(Vendor::Amd.to_string(), "AMD");
assert_eq!(Vendor::Intel.to_string(), "Intel");
assert_eq!(Vendor::Apple.to_string(), "Apple");
assert_eq!(Vendor::Unknown.to_string(), "Unknown");
}
#[test]
fn gib_converts_using_binary_units() {
assert!((gib(0) - 0.0).abs() < f64::EPSILON);
assert!((gib(1024 * 1024 * 1024) - 1.0).abs() < f64::EPSILON);
assert!((gib(3 * 1024 * 1024 * 1024 / 2) - 1.5).abs() < f64::EPSILON);
}
#[test]
fn display_rounds_to_one_decimal_place() {
let gpu = GpuInfo {
name: "Rounding".to_string(),
vendor: Vendor::Nvidia,
total_bytes: 25 * 1024 * 1024 * 1024 + 256 * 1024 * 1024,
free_bytes: None,
used_bytes: None,
};
assert!(gpu.to_string().contains("25.2 GiB total"));
}
#[test]
fn detect_results_have_consistent_memory_fields() {
for gpu in detect() {
assert!(!gpu.name.is_empty());
if let Some(free) = gpu.free_bytes {
assert!(free <= gpu.total_bytes, "free must not exceed total");
}
if let (Some(free), Some(used)) = (gpu.free_bytes, gpu.used_bytes) {
assert!(
free.saturating_add(used) <= gpu.total_bytes.saturating_add(used),
"free/used must be coherent",
);
}
}
}
#[test]
fn versions_display_as_major_dot_minor() {
assert_eq!(ComputeCapability::new(8, 6).to_string(), "8.6");
assert_eq!(CudaVersion::new(12, 9).to_string(), "12.9");
assert_eq!(ComputeCapability::new(8, 10).to_string(), "8.10");
}
#[test]
fn versions_order_by_major_then_minor() {
assert!(ComputeCapability::new(8, 6) > ComputeCapability::new(8, 0));
assert!(ComputeCapability::new(9, 0) > ComputeCapability::new(8, 9));
assert_eq!(ComputeCapability::new(8, 6), ComputeCapability::new(8, 6));
assert!(CudaVersion::new(12, 9) > CudaVersion::new(12, 0));
assert!(CudaVersion::new(13, 0) > CudaVersion::new(12, 9));
assert!(ComputeCapability::new(9, 0) > ComputeCapability::new(8, 10));
}
#[test]
fn cuda_host_is_environment_dependent_but_coherent() {
if let Some(cuda) = cuda_host() {
assert!(
cuda.compute_capability.major > 0,
"a real device has a nonzero major capability",
);
assert!(cuda.driver_version.major > 0, "a real driver has a version");
assert_eq!(
cuda_host(),
Some(cuda),
"host/driver properties must be stable across calls",
);
}
}
#[test]
fn gpu_info_equality_compares_all_fields() {
let base = GpuInfo {
name: "G".to_string(),
vendor: Vendor::Intel,
total_bytes: 16 * 1024 * 1024 * 1024,
free_bytes: None,
used_bytes: None,
};
assert_eq!(base.clone(), base);
let mut other = base.clone();
other.vendor = Vendor::Amd;
assert_ne!(base, other);
}
}