Skip to main content

bv_core/
hardware.rs

1use std::fmt;
2use std::process::Command;
3
4use sysinfo::{Disks, System};
5
6use crate::manifest::CudaVersion;
7
8// Detected hardware
9
10#[derive(Debug, Clone)]
11pub struct GpuInfo {
12    pub name: String,
13    pub vram_mb: u64,
14    pub driver_version: Option<String>,
15    pub cuda_version: Option<CudaVersion>,
16}
17
18#[derive(Debug, Clone)]
19pub struct DetectedHardware {
20    pub cpu_cores: u32,
21    pub ram_mb: u64,
22    pub disk_free_mb: u64,
23    pub gpus: Vec<GpuInfo>,
24}
25
26impl DetectedHardware {
27    pub fn detect() -> Self {
28        let mut sys = System::new();
29        sys.refresh_cpu_all();
30        sys.refresh_memory();
31
32        let cpu_cores = sys.cpus().len() as u32;
33        let ram_mb = sys.total_memory() / (1024 * 1024);
34
35        let disk_free_mb = {
36            let disks = Disks::new_with_refreshed_list();
37            // Use the disk with the most free space as a conservative proxy.
38            disks
39                .iter()
40                .map(|d| d.available_space() / (1024 * 1024))
41                .max()
42                .unwrap_or(0)
43        };
44
45        let gpus = detect_gpus();
46
47        Self {
48            cpu_cores,
49            ram_mb,
50            disk_free_mb,
51            gpus,
52        }
53    }
54
55    pub fn ram_gb(&self) -> f64 {
56        self.ram_mb as f64 / 1024.0
57    }
58
59    pub fn disk_free_gb(&self) -> f64 {
60        self.disk_free_mb as f64 / 1024.0
61    }
62}
63
64// Hardware mismatch
65
66#[derive(Debug, Clone)]
67pub enum HardwareMismatch {
68    NoGpu,
69    InsufficientVram {
70        required_gb: u32,
71        available_gb: u32,
72    },
73    CudaTooOld {
74        required: CudaVersion,
75        available: CudaVersion,
76    },
77    NoCuda {
78        required: CudaVersion,
79    },
80    InsufficientRam {
81        required_gb: f64,
82        available_gb: f64,
83    },
84    InsufficientDisk {
85        required_gb: f64,
86        available_gb: f64,
87    },
88}
89
90impl fmt::Display for HardwareMismatch {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            HardwareMismatch::NoGpu => {
94                write!(f, "NVIDIA GPU required but none detected")
95            }
96            HardwareMismatch::InsufficientVram {
97                required_gb,
98                available_gb,
99            } => {
100                write!(
101                    f,
102                    "GPU requires ≥{required_gb} GB VRAM, but best available is {available_gb} GB"
103                )
104            }
105            HardwareMismatch::CudaTooOld {
106                required,
107                available,
108            } => {
109                write!(f, "CUDA ≥{required} required, driver supports {available}")
110            }
111            HardwareMismatch::NoCuda { required } => {
112                write!(f, "CUDA ≥{required} required but no CUDA driver detected")
113            }
114            HardwareMismatch::InsufficientRam {
115                required_gb,
116                available_gb,
117            } => {
118                write!(
119                    f,
120                    "{required_gb:.0} GB RAM required, only {available_gb:.1} GB available"
121                )
122            }
123            HardwareMismatch::InsufficientDisk {
124                required_gb,
125                available_gb,
126            } => {
127                write!(
128                    f,
129                    "{required_gb:.0} GB free disk required, only {available_gb:.1} GB available"
130                )
131            }
132        }
133    }
134}
135
136// GPU detection
137
138fn detect_gpus() -> Vec<GpuInfo> {
139    let output = match Command::new("nvidia-smi")
140        .args([
141            "--query-gpu=name,memory.total,driver_version",
142            "--format=csv,noheader,nounits",
143        ])
144        .output()
145    {
146        Ok(o) if o.status.success() => o,
147        _ => return vec![],
148    };
149
150    let cuda_ver = detect_cuda_version();
151    let stdout = String::from_utf8_lossy(&output.stdout);
152
153    stdout
154        .lines()
155        .filter_map(|line| parse_gpu_csv(line, cuda_ver.clone()))
156        .collect()
157}
158
159fn parse_gpu_csv(line: &str, cuda_version: Option<CudaVersion>) -> Option<GpuInfo> {
160    let parts: Vec<&str> = line.splitn(3, ',').map(str::trim).collect();
161    if parts.len() < 2 {
162        return None;
163    }
164    let name = parts[0].to_string();
165    let vram_mb = parts[1].parse::<u64>().ok()?;
166    let driver_version = parts.get(2).map(|s| s.to_string());
167
168    Some(GpuInfo {
169        name,
170        vram_mb,
171        driver_version,
172        cuda_version,
173    })
174}
175
176/// Parse "CUDA Version: X.Y" from the `nvidia-smi` plain-text header.
177fn detect_cuda_version() -> Option<CudaVersion> {
178    let output = Command::new("nvidia-smi").output().ok()?;
179    if !output.status.success() {
180        return None;
181    }
182    let stdout = String::from_utf8_lossy(&output.stdout);
183    for line in stdout.lines() {
184        if let Some(rest) = line.find("CUDA Version:").map(|i| &line[i + 13..])
185            && let Some(ver_str) = rest.split_whitespace().next()
186        {
187            return ver_str.parse().ok();
188        }
189    }
190    None
191}