use std::fmt;
use crate::GpuInfo;
use crate::vendor::GpuVendor;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoteKind {
HardwareUnusable,
ToolFailed,
Unparsable,
MaskApplied,
VendorMismatch,
}
impl NoteKind {
pub const ALL_NAMES: &'static str =
"hardware_unusable, tool_failed, unparsable, mask_applied, vendor_mismatch";
pub fn as_str(self) -> &'static str {
match self {
NoteKind::HardwareUnusable => "hardware_unusable",
NoteKind::ToolFailed => "tool_failed",
NoteKind::Unparsable => "unparsable",
NoteKind::MaskApplied => "mask_applied",
NoteKind::VendorMismatch => "vendor_mismatch",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"hardware_unusable" => Some(NoteKind::HardwareUnusable),
"tool_failed" => Some(NoteKind::ToolFailed),
"unparsable" => Some(NoteKind::Unparsable),
"mask_applied" => Some(NoteKind::MaskApplied),
"vendor_mismatch" => Some(NoteKind::VendorMismatch),
_ => None,
}
}
pub fn explains_absence(self) -> bool {
matches!(
self,
NoteKind::HardwareUnusable
| NoteKind::ToolFailed
| NoteKind::Unparsable
| NoteKind::VendorMismatch
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SurveyNote {
pub vendor: GpuVendor,
pub kind: NoteKind,
pub message: String,
}
impl fmt::Display for SurveyNote {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.vendor, self.message)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GpuSurvey {
pub devices: Vec<GpuInfo>,
pub notes: Vec<SurveyNote>,
}
impl GpuSurvey {
pub(crate) fn note(&mut self, vendor: GpuVendor, kind: NoteKind, message: String) {
self.notes.push(SurveyNote {
vendor,
kind,
message,
});
}
pub fn require_devices(&self) -> Result<&[GpuInfo], String> {
if !self.devices.is_empty() {
return Ok(&self.devices);
}
let reasons: Vec<&str> = self
.notes
.iter()
.filter(|n| n.kind.explains_absence())
.map(|n| n.message.as_str())
.collect();
if reasons.is_empty() {
return Err(
"no GPUs detected on this host (no NVIDIA or AMD device found). \
Install a GPU driver, or select devices explicitly."
.to_string(),
);
}
Err(format!("no usable GPUs detected: {}", reasons.join("; ")))
}
pub fn has_vendor(&self, vendor: GpuVendor) -> bool {
self.devices.iter().any(|g| g.vendor == vendor)
}
pub fn vendors(&self) -> Vec<GpuVendor> {
let mut out: Vec<GpuVendor> = Vec::new();
for g in &self.devices {
if !out.contains(&g.vendor) {
out.push(g.vendor);
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vendor::GpuArch;
fn dev(index: u8, vendor: GpuVendor, arch: GpuArch) -> GpuInfo {
GpuInfo {
index,
vendor,
name: format!("Test {index}"),
arch,
total_memory_mb: 8192,
}
}
fn sm(major: u32, minor: u32) -> GpuArch {
GpuArch::Sm { major, minor }
}
#[test]
fn require_devices_passes_through_a_populated_sweep() {
let s = GpuSurvey {
devices: vec![dev(0, GpuVendor::Nvidia, sm(8, 6))],
notes: vec![],
};
assert_eq!(s.require_devices().unwrap().len(), 1);
}
#[test]
fn empty_sweep_says_no_gpus_rather_than_inventing_a_cause() {
let err = GpuSurvey::default().require_devices().unwrap_err();
assert!(err.contains("no GPUs detected"), "got: {err}");
}
#[test]
fn absence_quotes_the_note_that_explains_it() {
let mut s = GpuSurvey::default();
s.note(
GpuVendor::Amd,
NoteKind::HardwareUnusable,
"an AMD GPU is present but ROCm is not installed".into(),
);
let err = s.require_devices().unwrap_err();
assert!(err.contains("ROCm is not installed"), "got: {err}");
}
#[test]
fn a_mask_note_alone_does_not_masquerade_as_a_hardware_fault() {
let mut s = GpuSurvey::default();
s.note(
GpuVendor::Nvidia,
NoteKind::MaskApplied,
"hidden by mask".into(),
);
let err = s.require_devices().unwrap_err();
assert!(err.contains("no GPUs detected"), "got: {err}");
assert!(!err.contains("hidden by mask"), "got: {err}");
}
#[test]
fn reports_vendors_present_without_deduping_away_a_mixed_box() {
let s = GpuSurvey {
devices: vec![
dev(0, GpuVendor::Nvidia, sm(12, 0)),
dev(1, GpuVendor::Amd, GpuArch::Gfx("gfx1030".into())),
dev(2, GpuVendor::Nvidia, sm(6, 1)),
],
notes: vec![],
};
assert_eq!(s.vendors(), vec![GpuVendor::Nvidia, GpuVendor::Amd]);
assert!(s.has_vendor(GpuVendor::Amd));
}
#[test]
fn has_vendor_is_false_when_only_a_note_mentions_it() {
let mut s = GpuSurvey {
devices: vec![dev(0, GpuVendor::Nvidia, sm(8, 6))],
notes: vec![],
};
s.note(GpuVendor::Amd, NoteKind::HardwareUnusable, "no ROCm".into());
assert!(!s.has_vendor(GpuVendor::Amd));
assert_eq!(s.vendors(), vec![GpuVendor::Nvidia]);
}
}