use std::collections::BTreeMap;
use sysinfo::System;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum GpuBackend {
Cuda,
Metal,
Rocm,
Vulkan, Sycl, CpuArm,
CpuX86,
Ascend,
}
impl GpuBackend {
pub fn label(&self) -> &'static str {
match self {
GpuBackend::Cuda => "CUDA",
GpuBackend::Metal => "Metal",
GpuBackend::Rocm => "ROCm",
GpuBackend::Vulkan => "Vulkan",
GpuBackend::Sycl => "SYCL",
GpuBackend::CpuArm => "CPU (ARM)",
GpuBackend::CpuX86 => "CPU (x86)",
GpuBackend::Ascend => "NPU (Ascend)",
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct GpuInfo {
pub name: String,
pub vram_gb: Option<f64>,
pub backend: GpuBackend,
pub count: u32, pub unified_memory: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SystemSpecs {
pub total_ram_gb: f64,
pub available_ram_gb: f64,
pub total_cpu_cores: usize,
pub cpu_name: String,
pub has_gpu: bool,
pub gpu_vram_gb: Option<f64>,
pub total_gpu_vram_gb: Option<f64>,
pub gpu_name: Option<String>,
pub gpu_count: u32,
pub unified_memory: bool,
pub backend: GpuBackend,
pub gpus: Vec<GpuInfo>,
pub cluster_mode: bool,
pub cluster_node_count: u32,
}
impl SystemSpecs {
pub fn detect() -> Self {
let mut sys = System::new_all();
sys.refresh_all();
let total_ram_bytes = sys.total_memory();
let available_ram_bytes = sys.available_memory();
let total_ram_gb = total_ram_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let available_ram_gb = if available_ram_bytes == 0 && total_ram_bytes > 0 {
Self::available_ram_fallback(&sys, total_ram_bytes, total_ram_gb)
} else {
available_ram_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
};
let total_cpu_cores = sys.cpus().len();
let cpu_name = Self::detect_cpu_name(&sys);
let gpus = Self::detect_all_gpus(total_ram_gb, &cpu_name);
let primary = gpus.first();
let has_gpu = !gpus.is_empty();
let gpu_vram_gb = primary.and_then(|g| g.vram_gb);
let total_gpu_vram_gb = primary.and_then(|g| g.vram_gb.map(|vram| vram * g.count as f64));
let gpu_name = primary.map(|g| g.name.clone());
let gpu_count = primary.map(|g| g.count).unwrap_or(0);
let unified_memory = primary.map(|g| g.unified_memory).unwrap_or(false);
let cpu_backend =
if cfg!(target_arch = "aarch64") || cpu_name.to_lowercase().contains("apple") {
GpuBackend::CpuArm
} else {
GpuBackend::CpuX86
};
let backend = primary.map(|g| g.backend).unwrap_or(cpu_backend);
SystemSpecs {
total_ram_gb,
available_ram_gb,
total_cpu_cores,
cpu_name,
has_gpu,
gpu_vram_gb,
total_gpu_vram_gb,
gpu_name,
gpu_count,
unified_memory,
backend,
gpus,
cluster_mode: false,
cluster_node_count: 0,
}
}
fn detect_all_gpus(total_ram_gb: f64, cpu_name: &str) -> Vec<GpuInfo> {
let mut gpus = Vec::new();
let nvidia = Self::detect_nvidia_gpus();
if nvidia.is_empty() {
if let Some(nvidia_sysfs) = Self::detect_nvidia_gpu_sysfs_info() {
gpus.push(nvidia_sysfs);
}
} else {
gpus.extend(nvidia);
}
if let Some(amd) = Self::detect_amd_gpu_rocm_info() {
gpus.push(amd);
} else if let Some(amd) = Self::detect_amd_gpu_sysfs_info() {
gpus.push(amd);
}
for wmi_gpu in Self::detect_gpu_windows_info() {
let dominated = gpus.iter().any(|existing| {
let existing_lower = existing.name.to_lowercase();
let wmi_lower = wmi_gpu.name.to_lowercase();
existing_lower.contains(&wmi_lower) || wmi_lower.contains(&existing_lower)
});
if !dominated {
gpus.push(wmi_gpu);
}
}
if is_amd_unified_memory_apu(cpu_name) {
let amd_idx = gpus.iter().position(|g| {
let lower = g.name.to_lowercase();
lower.contains("amd") || lower.contains("radeon")
});
if let Some(idx) = amd_idx {
gpus[idx].unified_memory = true;
gpus[idx].vram_gb = Some(total_ram_gb);
} else {
gpus.push(GpuInfo {
name: format!("{} (integrated)", cpu_name),
vram_gb: Some(total_ram_gb),
backend: GpuBackend::Vulkan,
count: 1,
unified_memory: true,
});
}
}
let is_nvidia_unified = gpus.iter().any(|g| is_nvidia_unified_memory_gpu(&g.name));
if is_nvidia_unified {
for gpu in &mut gpus {
if is_nvidia_unified_memory_gpu(&gpu.name) {
gpu.unified_memory = true;
gpu.vram_gb = Some(total_ram_gb);
}
}
}
if let Some(vram) = Self::detect_intel_gpu() {
let already_found = gpus.iter().any(|g| g.name.to_lowercase().contains("intel"));
if !already_found {
gpus.push(GpuInfo {
name: "Intel Arc".to_string(),
vram_gb: Some(vram),
backend: GpuBackend::Sycl,
count: 1,
unified_memory: false,
});
}
}
if let Some(vram) = Self::detect_apple_gpu(total_ram_gb) {
let name = if cpu_name.to_lowercase().contains("apple") {
cpu_name.to_string()
} else {
"Apple Silicon".to_string()
};
gpus.push(GpuInfo {
name,
vram_gb: Some(vram),
backend: GpuBackend::Metal,
count: 1,
unified_memory: true,
});
}
let ascend = Self::detect_ascend_npus();
if !ascend.is_empty() {
gpus.extend(ascend);
}
for vulkan_gpu in Self::detect_vulkan_gpu_info() {
let dominated = gpus
.iter()
.any(|existing| Self::is_same_gpu_name(&existing.name, &vulkan_gpu.name));
if !dominated {
gpus.push(vulkan_gpu);
}
}
gpus.sort_by(|a, b| {
let va = a.vram_gb.unwrap_or(0.0);
let vb = b.vram_gb.unwrap_or(0.0);
vb.partial_cmp(&va).unwrap_or(std::cmp::Ordering::Equal)
});
gpus
}
fn detect_nvidia_gpus() -> Vec<GpuInfo> {
if let Some(gpus) = Self::try_nvidia_smi_with_addressing_mode() {
return gpus;
}
let output = match std::process::Command::new("nvidia-smi")
.arg("--query-gpu=memory.total,name")
.arg("--format=csv,noheader,nounits")
.output()
{
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
let text = match String::from_utf8(output.stdout) {
Ok(t) => t,
Err(_) => return Vec::new(),
};
Self::parse_nvidia_smi_list(&text)
}
fn try_nvidia_smi_with_addressing_mode() -> Option<Vec<GpuInfo>> {
let output = std::process::Command::new("nvidia-smi")
.arg("--query-gpu=addressing_mode,memory.total,name")
.arg("--format=csv,noheader,nounits")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8(output.stdout).ok()?;
Some(Self::parse_nvidia_smi_extended(&text))
}
fn parse_nvidia_smi_extended(text: &str) -> Vec<GpuInfo> {
let mut grouped: BTreeMap<String, (u32, f64, bool)> = BTreeMap::new();
let total_ram_gb = read_proc_meminfo_total_gb();
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.splitn(3, ',').collect();
if parts.len() < 3 {
continue;
}
let addr_mode = parts[0].trim();
let is_unified = addr_mode.eq_ignore_ascii_case("ATS");
let name = parts[2].trim().to_string();
let name = if name.is_empty() {
"NVIDIA GPU".to_string()
} else {
name
};
let parsed_vram_mb = parts[1].trim().parse::<f64>().unwrap_or(0.0);
let vram_mb = if parsed_vram_mb > 0.0 {
parsed_vram_mb
} else if is_unified {
total_ram_gb.unwrap_or(0.0) * 1024.0
} else {
estimate_vram_from_name(&name) * 1024.0
};
let entry = grouped.entry(name).or_insert((0, 0.0, false));
entry.0 += 1;
if vram_mb > entry.1 {
entry.1 = vram_mb;
}
if is_unified {
entry.2 = true;
}
}
if grouped.is_empty() {
return Vec::new();
}
grouped
.into_iter()
.map(|(name, (count, per_card_vram_mb, is_unified))| GpuInfo {
name,
vram_gb: if per_card_vram_mb > 0.0 {
Some(per_card_vram_mb / 1024.0)
} else {
None
},
backend: GpuBackend::Cuda,
count,
unified_memory: is_unified,
})
.collect()
}
fn parse_nvidia_smi_list(text: &str) -> Vec<GpuInfo> {
let mut grouped: BTreeMap<String, (u32, f64)> = BTreeMap::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.splitn(2, ',').collect();
let name = parts
.get(1)
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.unwrap_or("NVIDIA GPU")
.to_string();
let parsed_vram_mb = parts
.first()
.and_then(|s| s.trim().parse::<f64>().ok())
.unwrap_or(0.0);
let vram_mb = if parsed_vram_mb > 0.0 {
parsed_vram_mb
} else {
estimate_vram_from_name(&name) * 1024.0
};
let entry = grouped.entry(name).or_insert((0, 0.0));
entry.0 += 1;
if vram_mb > entry.1 {
entry.1 = vram_mb;
}
}
if grouped.is_empty() {
return Vec::new();
}
grouped
.into_iter()
.map(|(name, (count, per_card_vram_mb))| GpuInfo {
name,
vram_gb: if per_card_vram_mb > 0.0 {
Some(per_card_vram_mb / 1024.0)
} else {
None
},
backend: GpuBackend::Cuda,
count,
unified_memory: false,
})
.collect()
}
fn detect_nvidia_gpu_sysfs_info() -> Option<GpuInfo> {
if !cfg!(target_os = "linux") {
return None;
}
let entries = std::fs::read_dir("/sys/class/drm").ok()?;
let mut gpu_count: u32 = 0;
let mut total_vram_bytes: u64 = 0;
let mut slot_hints: Vec<String> = Vec::new();
let mut backend = GpuBackend::Vulkan;
for entry in entries.flatten() {
let card_path = entry.path();
let fname = card_path.file_name()?.to_str()?.to_string();
if !fname.starts_with("card") || fname.contains('-') {
continue;
}
let device_path = card_path.join("device");
let vendor_path = device_path.join("vendor");
let Ok(vendor) = std::fs::read_to_string(&vendor_path) else {
continue;
};
if vendor.trim() != "0x10de" {
continue;
}
gpu_count += 1;
if let Ok(vram_str) = std::fs::read_to_string(device_path.join("mem_info_vram_total"))
&& let Ok(vram_bytes) = vram_str.trim().parse::<u64>()
&& vram_bytes > 0
{
total_vram_bytes = total_vram_bytes.max(vram_bytes);
}
if let Ok(uevent) = std::fs::read_to_string(device_path.join("uevent")) {
for line in uevent.lines() {
if let Some(slot) = line.strip_prefix("PCI_SLOT_NAME=") {
slot_hints.push(slot.to_string());
} else if let Some(driver) = line.strip_prefix("DRIVER=")
&& driver.eq_ignore_ascii_case("nvidia")
{
backend = GpuBackend::Cuda;
}
}
}
}
if gpu_count == 0 {
return None;
}
let name = Self::get_nvidia_gpu_name_lspci(&slot_hints)
.unwrap_or_else(|| "NVIDIA GPU".to_string());
let mut vram_gb = if total_vram_bytes > 0 {
Some(total_vram_bytes as f64 / (1024.0 * 1024.0 * 1024.0))
} else {
None
};
if vram_gb.is_none() {
let est = estimate_vram_from_name(&name);
if est > 0.0 {
vram_gb = Some(est);
}
}
let unified_memory = is_nvidia_unified_memory_gpu(&name);
Some(GpuInfo {
name,
vram_gb,
backend,
count: gpu_count,
unified_memory,
})
}
fn detect_amd_gpu_rocm_info() -> Option<GpuInfo> {
let vram_output = std::process::Command::new("rocm-smi")
.arg("--showmeminfo")
.arg("vram")
.output()
.ok()?;
if !vram_output.status.success() {
return None;
}
let vram_text = String::from_utf8(vram_output.stdout).ok()?;
let mut per_gpu_vram_bytes: Vec<u64> = Vec::new();
for line in vram_text.lines() {
let lower = line.to_lowercase();
if lower.contains("total") && !lower.contains("used") {
if let Some(val) = line
.split_whitespace()
.filter_map(|w| w.parse::<u64>().ok())
.next_back()
&& val > 0
{
per_gpu_vram_bytes.push(val);
}
}
}
const IGPU_VRAM_THRESHOLD: u64 = 2 * 1024 * 1024 * 1024; let discrete_vram: Vec<u64> = per_gpu_vram_bytes
.iter()
.copied()
.filter(|&v| v >= IGPU_VRAM_THRESHOLD)
.collect();
let (effective_vram, gpu_count) = if discrete_vram.is_empty() {
(per_gpu_vram_bytes, 1u32)
} else {
let count = discrete_vram.len() as u32;
(discrete_vram, count)
};
let gpu_name = std::process::Command::new("rocm-smi")
.arg("--showproductname")
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout).ok()
} else {
None
}
})
.and_then(|text| {
for line in text.lines() {
let lower = line.to_lowercase();
if (lower.contains("card series") || lower.contains("card model"))
&& let Some(val) = line.split(':').nth(1)
{
let name = val.trim().to_string();
if !name.is_empty() {
return Some(name);
}
}
}
None
});
let name = gpu_name.unwrap_or_else(|| "AMD GPU".to_string());
let max_per_gpu_bytes = effective_vram.into_iter().max().unwrap_or(0);
let vram_gb = if max_per_gpu_bytes > 0 {
Some(max_per_gpu_bytes as f64 / (1024.0 * 1024.0 * 1024.0))
} else {
let est = estimate_vram_from_name(&name);
if est > 0.0 { Some(est) } else { None }
};
Some(GpuInfo {
name,
vram_gb,
backend: GpuBackend::Rocm,
count: gpu_count,
unified_memory: false,
})
}
fn detect_amd_gpu_sysfs_info() -> Option<GpuInfo> {
if !cfg!(target_os = "linux") {
return None;
}
let mut slot_hints: Vec<String> = Vec::new();
let entries = std::fs::read_dir("/sys/class/drm").ok()?;
for entry in entries.flatten() {
let card_path = entry.path();
let fname = card_path.file_name()?.to_str()?.to_string();
if !fname.starts_with("card") || fname.contains('-') {
continue;
}
let device_path = card_path.join("device");
let vendor_path = device_path.join("vendor");
if let Ok(vendor) = std::fs::read_to_string(&vendor_path) {
if vendor.trim() != "0x1002" {
continue;
}
} else {
continue;
}
let mut vram_gb: Option<f64> = None;
let vram_path = device_path.join("mem_info_vram_total");
if let Ok(vram_str) = std::fs::read_to_string(&vram_path)
&& let Ok(vram_bytes) = vram_str.trim().parse::<u64>()
&& vram_bytes > 0
{
vram_gb = Some(vram_bytes as f64 / (1024.0 * 1024.0 * 1024.0));
}
if let Ok(uevent) = std::fs::read_to_string(device_path.join("uevent")) {
for line in uevent.lines() {
if let Some(slot) = line.strip_prefix("PCI_SLOT_NAME=") {
slot_hints.push(slot.to_string());
}
}
}
let gpu_name = Self::get_amd_gpu_name_lspci(&slot_hints);
let name = gpu_name.unwrap_or_else(|| "AMD GPU".to_string());
if vram_gb.is_none() {
let estimated = estimate_vram_from_name(&name);
if estimated > 0.0 {
vram_gb = Some(estimated);
}
}
return Some(GpuInfo {
name,
vram_gb,
backend: GpuBackend::Vulkan,
count: 1,
unified_memory: false,
});
}
None
}
fn get_amd_gpu_name_lspci(slot_hints: &[String]) -> Option<String> {
let text = Self::lspci_output()?;
for slot in slot_hints {
for line in text.lines() {
let lower = line.to_lowercase();
if line.starts_with(slot)
&& (lower.contains("vga") || lower.contains("3d") || lower.contains("display"))
&& (lower.contains("amd") || lower.contains("ati"))
&& let Some(model) = Self::extract_model_from_lspci_line(line)
{
return Some(model);
}
}
}
for line in text.lines() {
let lower = line.to_lowercase();
if (lower.contains("vga") || lower.contains("3d"))
&& (lower.contains("amd") || lower.contains("ati"))
&& let Some(model) = Self::extract_model_from_lspci_line(line)
{
return Some(model);
}
}
None
}
fn get_nvidia_gpu_name_lspci(slot_hints: &[String]) -> Option<String> {
let text = Self::lspci_output()?;
for slot in slot_hints {
for line in text.lines() {
let lower = line.to_lowercase();
if line.starts_with(slot)
&& (lower.contains("vga") || lower.contains("3d") || lower.contains("display"))
&& lower.contains("nvidia")
&& let Some(model) = Self::extract_model_from_lspci_line(line)
{
return Some(model);
}
}
}
for line in text.lines() {
let lower = line.to_lowercase();
if (lower.contains("vga") || lower.contains("3d") || lower.contains("display"))
&& lower.contains("nvidia")
&& let Some(model) = Self::extract_model_from_lspci_line(line)
{
return Some(model);
}
}
None
}
fn lspci_output() -> Option<String> {
let local = std::process::Command::new("lspci")
.arg("-nnD")
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok());
if local.is_some() {
return local;
}
std::process::Command::new("flatpak-spawn")
.args(["--host", "lspci", "-nnD"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
}
fn extract_model_from_lspci_line(line: &str) -> Option<String> {
let mut best: Option<String> = None;
let mut rest = line;
while let Some(start) = rest.find('[') {
let after = &rest[start + 1..];
let Some(end) = after.find(']') else { break };
let token = after[..end].trim();
let usable = !token.is_empty()
&& !token.contains(':')
&& !token.chars().all(|c| c.is_ascii_digit());
if usable
&& best
.as_ref()
.map(|current| token.len() > current.len())
.unwrap_or(true)
{
best = Some(token.to_string());
}
rest = &after[end + 1..];
}
if best.is_some() {
return best;
}
line.split_once(": ")
.map(|(_, right)| right.trim().to_string())
.filter(|s| !s.is_empty())
}
fn detect_gpu_windows_info() -> Vec<GpuInfo> {
if !cfg!(target_os = "windows") {
return Vec::new();
}
if let Ok(output) = std::process::Command::new("powershell")
.arg("-NoProfile")
.arg("-Command")
.arg("Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM | ForEach-Object { $_.Name + '|' + $_.AdapterRAM }")
.output()
&& output.status.success()
&& let Ok(text) = String::from_utf8(output.stdout) {
let gpus = Self::parse_windows_gpu_list(&text);
if !gpus.is_empty() {
return Self::prefer_discrete_gpus(gpus);
}
}
let gpus = Self::detect_gpu_windows_wmic_list();
Self::prefer_discrete_gpus(gpus)
}
fn detect_gpu_windows_wmic_list() -> Vec<GpuInfo> {
let output = match std::process::Command::new("wmic")
.arg("path")
.arg("win32_VideoController")
.arg("get")
.arg("Name,AdapterRAM")
.arg("/format:csv")
.output()
{
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
let text = match String::from_utf8(output.stdout) {
Ok(t) => t,
Err(_) => return Vec::new(),
};
let mut gpus = Vec::new();
for line in text.lines().skip(1) {
let line = line.trim();
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.split(',').collect();
if parts.len() >= 3 {
let raw_vram: u64 = parts[1].trim().parse().unwrap_or(0);
let name = parts[2..].join(",").trim().to_string();
let lower = name.to_lowercase();
if lower.contains("microsoft")
|| lower.contains("basic")
|| lower.contains("virtual")
{
continue;
}
let backend = Self::infer_gpu_backend(&name);
let vram_gb = Self::resolve_wmi_vram(raw_vram, &name);
gpus.push(GpuInfo {
name,
vram_gb,
backend,
count: 1,
unified_memory: false,
});
}
}
gpus
}
fn parse_windows_gpu_list(text: &str) -> Vec<GpuInfo> {
let mut gpus = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.splitn(2, '|').collect();
let name = parts[0].trim().to_string();
let raw_vram: u64 = parts
.get(1)
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0);
let lower = name.to_lowercase();
if lower.contains("microsoft")
|| lower.contains("basic")
|| lower.contains("virtual")
|| lower.is_empty()
{
continue;
}
let backend = Self::infer_gpu_backend(&name);
let vram_gb = Self::resolve_wmi_vram(raw_vram, &name);
gpus.push(GpuInfo {
name,
vram_gb,
backend,
count: 1,
unified_memory: false,
});
}
gpus
}
fn prefer_discrete_gpus(gpus: Vec<GpuInfo>) -> Vec<GpuInfo> {
let discrete: Vec<GpuInfo> = gpus
.iter()
.filter(|g| !Self::is_integrated_gpu_name(&g.name))
.cloned()
.collect();
if discrete.is_empty() {
gpus
} else {
discrete
}
}
fn is_integrated_gpu_name(name: &str) -> bool {
let lower = name.to_lowercase();
if lower.contains("intel") {
return lower.contains("uhd")
|| lower.contains("hd graphics")
|| (lower.contains("iris") && !lower.contains("arc"));
}
if lower.contains("radeon") && lower.contains("graphics") {
let has_discrete_tag = lower.contains("rx ")
|| lower.contains("pro ")
|| lower.contains("vega")
|| lower.contains(" vii")
|| lower.contains(" w");
return !has_discrete_tag;
}
false
}
fn resolve_wmi_vram(raw_bytes: u64, name: &str) -> Option<f64> {
let mut vram_gb = raw_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
if vram_gb < 0.1 || (vram_gb <= 4.1 && estimate_vram_from_name(name) > 4.1) {
let estimated = estimate_vram_from_name(name);
if estimated > 0.0 {
vram_gb = estimated;
}
}
if vram_gb > 0.0 { Some(vram_gb) } else { None }
}
fn infer_gpu_backend(name: &str) -> GpuBackend {
let lower = name.to_lowercase();
if lower.contains("nvidia")
|| lower.contains("geforce")
|| lower.contains("quadro")
|| lower.contains("tesla")
|| lower.contains("rtx")
{
GpuBackend::Cuda
} else if lower.contains("amd") || lower.contains("radeon") || lower.contains("ati") {
GpuBackend::Vulkan
} else if lower.contains("intel") || lower.contains("arc") {
GpuBackend::Sycl
} else {
GpuBackend::Vulkan
}
}
fn detect_intel_gpu() -> Option<f64> {
if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
for entry in entries.flatten() {
let card_path = entry.path();
let device_path = card_path.join("device");
let vendor_path = device_path.join("vendor");
if let Ok(vendor) = std::fs::read_to_string(&vendor_path)
&& vendor.trim() != "0x8086"
{
continue;
}
let vram_path = card_path.join("device/mem_info_vram_total");
if let Ok(vram_str) = std::fs::read_to_string(&vram_path)
&& let Ok(vram_bytes) = vram_str.trim().parse::<u64>()
&& vram_bytes > 0
{
let vram_gb = vram_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
return Some(vram_gb);
}
if let Some(text) = Self::lspci_output() {
for line in text.lines() {
let lower = line.to_lowercase();
if lower.contains("intel") && lower.contains("arc") {
return Some(0.0);
}
}
}
}
}
if let Some(text) = Self::lspci_output() {
for line in text.lines() {
let lower = line.to_lowercase();
if lower.contains("intel") && lower.contains("arc") {
return Some(0.0);
}
}
}
None
}
fn detect_apple_gpu(total_ram_gb: f64) -> Option<f64> {
let output = std::process::Command::new("system_profiler")
.arg("SPDisplaysDataType")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8(output.stdout).ok()?;
let is_apple_gpu = text.lines().any(|line| {
let lower = line.to_lowercase();
lower.contains("apple m") || lower.contains("apple gpu")
});
if is_apple_gpu {
Some(total_ram_gb)
} else {
None
}
}
fn has_command(command: &str) -> bool {
let Some(path_var) = std::env::var_os("PATH") else {
return false;
};
for path in std::env::split_paths(&path_var) {
let candidate = path.join(command);
if candidate.is_file() {
return true;
}
#[cfg(target_os = "windows")]
for ext in [".exe", ".cmd", ".bat", ".com"] {
let candidate = path.join(format!("{command}{ext}"));
if candidate.is_file() {
return true;
}
}
}
false
}
fn detect_vulkan_gpu_info() -> Vec<GpuInfo> {
if !Self::has_command("vulkaninfo") {
return Vec::new();
}
let output = match std::process::Command::new("vulkaninfo")
.arg("--summary")
.output()
{
Ok(o) if o.status.success() => o,
_ => match std::process::Command::new("vulkaninfo").output() {
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
},
};
let text = String::from_utf8_lossy(&output.stdout);
let mut grouped: BTreeMap<String, u32> = BTreeMap::new();
for name in Self::parse_vulkan_device_names(&text) {
if Self::is_software_vulkan_device(&name) {
continue;
}
*grouped.entry(name).or_insert(0) += 1;
}
grouped
.into_iter()
.map(|(name, count)| GpuInfo {
backend: GpuBackend::Vulkan,
count,
name,
unified_memory: false,
vram_gb: None,
})
.collect()
}
fn is_same_gpu_name(existing_name: &str, candidate_name: &str) -> bool {
Self::normalize_gpu_name_for_dedupe(existing_name)
== Self::normalize_gpu_name_for_dedupe(candidate_name)
}
fn normalize_gpu_name_for_dedupe(name: &str) -> String {
let mut normalized = String::with_capacity(name.len());
let mut last_was_separator = true;
for ch in name.chars().flat_map(char::to_lowercase) {
if ch.is_alphanumeric() {
normalized.push(ch);
last_was_separator = false;
} else if !last_was_separator {
normalized.push(' ');
last_was_separator = true;
}
}
normalized.trim().to_string()
}
fn parse_vulkan_device_names(text: &str) -> Vec<String> {
let mut names = Vec::new();
for line in text.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some((key, value)) = trimmed.split_once('=')
&& key.trim().eq_ignore_ascii_case("deviceName")
{
let name = value.trim();
if !name.is_empty() {
names.push(name.to_string());
}
continue;
}
if let Some(rest) = trimmed.strip_prefix("GPU id")
&& let Some(start) = rest.find('(')
&& let Some(end) = rest.rfind(')')
&& end > start + 1
{
let name = rest[start + 1..end].trim();
if !name.is_empty() {
names.push(name.to_string());
}
}
}
names
}
fn is_software_vulkan_device(name: &str) -> bool {
let lower = name.to_lowercase();
lower.contains("llvmpipe")
|| lower.contains("lavapipe")
|| lower.contains("swiftshader")
|| lower.contains("software rasterizer")
}
fn detect_ascend_npus() -> Vec<GpuInfo> {
let list_output = match std::process::Command::new("npu-smi")
.args(["info", "-l"])
.output()
{
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
let list_stdout = String::from_utf8_lossy(&list_output.stdout);
let ids: Vec<String> = list_stdout
.lines()
.filter(|line| line.contains("NPU ID"))
.filter_map(|line| line.split(':').next_back())
.map(|s| s.trim().to_string())
.collect();
if ids.is_empty() {
return Vec::new();
}
let mut npu_infos: Vec<GpuInfo> = Vec::new();
let npu_name = "Ascend NPU";
for id in &ids {
let mem_output = std::process::Command::new("npu-smi")
.args(["info", "-t", "memory", "-i", id])
.output();
if let Ok(o) = mem_output {
let s = String::from_utf8_lossy(&o.stdout);
let mem = s
.lines()
.find(|l| l.contains("HBM Capacity"))
.and_then(|l| l.split(':').next_back())
.and_then(|v| v.split_whitespace().next())
.and_then(|num| num.parse::<u64>().ok())
.unwrap_or(0);
let npu_info = GpuInfo {
name: npu_name.to_string(),
vram_gb: Some((mem as f64) / 1024.0),
backend: GpuBackend::Ascend,
count: 1,
unified_memory: false,
};
npu_infos.push(npu_info);
}
}
npu_infos
}
fn available_ram_fallback(sys: &System, total_bytes: u64, total_gb: f64) -> f64 {
let used = sys.used_memory();
if used > 0 && used < total_bytes {
return (total_bytes - used) as f64 / (1024.0 * 1024.0 * 1024.0);
}
if let Some(avail) = Self::available_ram_from_vm_stat() {
return avail;
}
total_gb * 0.8
}
fn available_ram_from_vm_stat() -> Option<f64> {
let output = std::process::Command::new("vm_stat").output().ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8(output.stdout).ok()?;
let page_size: u64 = text
.lines()
.next()
.and_then(|line| {
line.split("page size of ")
.nth(1)?
.split(' ')
.next()?
.parse()
.ok()
})
.unwrap_or(16384);
let mut free: u64 = 0;
let mut inactive: u64 = 0;
let mut purgeable: u64 = 0;
for line in text.lines() {
if let Some(val) = Self::parse_vm_stat_line(line, "Pages free") {
free = val;
} else if let Some(val) = Self::parse_vm_stat_line(line, "Pages inactive") {
inactive = val;
} else if let Some(val) = Self::parse_vm_stat_line(line, "Pages purgeable") {
purgeable = val;
}
}
let available_bytes = (free + inactive + purgeable) * page_size;
if available_bytes > 0 {
Some(available_bytes as f64 / (1024.0 * 1024.0 * 1024.0))
} else {
None
}
}
fn parse_vm_stat_line(line: &str, key: &str) -> Option<u64> {
if !line.starts_with(key) {
return None;
}
line.split(':')
.nth(1)?
.trim()
.trim_end_matches('.')
.parse()
.ok()
}
fn detect_cpu_name(sys: &System) -> String {
if let Some(cpu_name) = sys
.cpus()
.iter()
.map(|cpu| cpu.brand().trim())
.find(|brand| !brand.is_empty() && !brand.eq_ignore_ascii_case("unknown"))
{
return cpu_name.to_string();
}
if let Some(cpu_name) = Self::read_cpu_name_from_proc_cpuinfo() {
return cpu_name;
}
if let Some(cpu_name) = Self::read_android_soc_name() {
return cpu_name;
}
"Unknown CPU".to_string()
}
fn read_cpu_name_from_proc_cpuinfo() -> Option<String> {
#[cfg(target_os = "linux")]
{
let text = std::fs::read_to_string("/proc/cpuinfo").ok()?;
return Self::parse_cpu_name_from_cpuinfo(&text);
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
fn parse_cpu_name_from_cpuinfo(text: &str) -> Option<String> {
for key in ["model name", "hardware", "processor", "cpu model", "model"] {
for line in text.lines() {
let Some((lhs, rhs)) = line.split_once(':') else {
continue;
};
if lhs.trim().eq_ignore_ascii_case(key) {
let candidate = rhs.trim();
if !candidate.is_empty() && !candidate.eq_ignore_ascii_case("unknown") {
return Some(candidate.to_string());
}
}
}
}
None
}
fn read_android_soc_name() -> Option<String> {
#[cfg(target_os = "linux")]
{
let output = std::process::Command::new("getprop")
.arg("ro.soc.model")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let model = String::from_utf8(output.stdout).ok()?;
let model = model.trim();
if model.is_empty() {
return None;
}
return Some(model.to_string());
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
pub fn with_gpu_memory_override(mut self, vram_gb: f64) -> Self {
if self.gpus.is_empty() {
let backend = if cfg!(target_arch = "aarch64")
|| self.cpu_name.to_lowercase().contains("apple")
{
GpuBackend::Metal
} else {
GpuBackend::Cuda
};
self.gpus.push(GpuInfo {
name: "User-specified GPU".to_string(),
vram_gb: Some(vram_gb),
backend,
count: 1,
unified_memory: false,
});
self.has_gpu = true;
self.gpu_vram_gb = Some(vram_gb);
self.total_gpu_vram_gb = Some(vram_gb);
self.gpu_name = Some("User-specified GPU".to_string());
self.gpu_count = 1;
self.backend = backend;
} else {
self.gpus[0].vram_gb = Some(vram_gb);
self.gpu_vram_gb = Some(vram_gb);
let count = self.gpus[0].count;
self.total_gpu_vram_gb = Some(vram_gb * count as f64);
self.has_gpu = true;
}
self
}
pub fn with_ram_override(mut self, ram_gb: f64) -> Self {
self.total_ram_gb = ram_gb;
self.available_ram_gb = ram_gb * 0.9;
if self.unified_memory {
self.gpu_vram_gb = Some(ram_gb);
self.total_gpu_vram_gb = Some(ram_gb);
for gpu in &mut self.gpus {
if gpu.unified_memory {
gpu.vram_gb = Some(ram_gb);
}
}
}
self
}
pub fn with_cpu_core_override(mut self, cores: usize) -> Self {
self.total_cpu_cores = cores;
self
}
pub fn display(&self) {
println!("\n=== System Specifications ===");
println!("CPU: {} ({} cores)", self.cpu_name, self.total_cpu_cores);
println!("Total RAM: {:.2} GB", self.total_ram_gb);
println!("Available RAM: {:.2} GB", self.available_ram_gb);
println!("Backend: {}", self.backend.label());
if self.gpus.is_empty() {
println!("GPU: Not detected");
} else {
for (i, gpu) in self.gpus.iter().enumerate() {
let prefix = if self.gpus.len() > 1 {
format!("GPU {}: ", i + 1)
} else {
"GPU: ".to_string()
};
if gpu.unified_memory {
println!(
"{}{} (unified memory, {:.2} GB shared, {})",
prefix,
gpu.name,
gpu.vram_gb.unwrap_or(0.0),
gpu.backend.label(),
);
} else {
match gpu.vram_gb {
Some(vram) if vram > 0.0 => {
if gpu.count > 1 {
let total_vram = vram * gpu.count as f64;
println!(
"{}{} x{} ({:.2} GB VRAM each = {:.0} GB total, {})",
prefix,
gpu.name,
gpu.count,
vram,
total_vram,
gpu.backend.label()
);
} else {
println!(
"{}{} ({:.2} GB VRAM, {})",
prefix,
gpu.name,
vram,
gpu.backend.label()
);
}
}
Some(_) => println!(
"{}{} (shared system memory, {})",
prefix,
gpu.name,
gpu.backend.label()
),
None => println!(
"{}{} (VRAM unknown, {})",
prefix,
gpu.name,
gpu.backend.label()
),
}
}
}
}
println!();
}
}
pub fn parse_memory_size(s: &str) -> Option<f64> {
let s = s.trim();
if s.is_empty() {
return None;
}
let num_end = s
.find(|c: char| !c.is_ascii_digit() && c != '.')
.unwrap_or(s.len());
let (num_str, suffix) = s.split_at(num_end);
let value: f64 = num_str.parse().ok()?;
if value < 0.0 {
return None;
}
let suffix = suffix.trim().to_lowercase();
match suffix.as_str() {
"g" | "gb" | "gib" | "" => Some(value), "m" | "mb" | "mib" => Some(value / 1024.0), "t" | "tb" | "tib" => Some(value * 1024.0), _ => None,
}
}
pub fn is_running_in_wsl() -> bool {
static IS_WSL: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*IS_WSL.get_or_init(detect_running_in_wsl)
}
fn detect_running_in_wsl() -> bool {
if !cfg!(target_os = "linux") {
return false;
}
if std::env::var_os("WSL_INTEROP").is_some() || std::env::var_os("WSL_DISTRO_NAME").is_some() {
return true;
}
["/proc/sys/kernel/osrelease", "/proc/version"]
.iter()
.any(|path| {
std::fs::read_to_string(path)
.map(|text| text.to_ascii_lowercase().contains("microsoft"))
.unwrap_or(false)
})
}
fn is_amd_unified_memory_apu(cpu_name: &str) -> bool {
let lower = cpu_name.to_lowercase();
if lower.contains("ryzen ai") {
return true;
}
false
}
fn read_proc_meminfo_total_gb() -> Option<f64> {
let text = std::fs::read_to_string("/proc/meminfo").ok()?;
for line in text.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
return Some(kb as f64 / (1024.0 * 1024.0));
}
}
None
}
pub fn gpu_memory_bandwidth_gbps(name: &str) -> Option<f64> {
let lower = name.to_lowercase();
if lower.contains("5090") {
return Some(1792.0);
}
if lower.contains("5080") {
return Some(960.0);
}
if lower.contains("5070 ti") {
return Some(896.0);
}
if lower.contains("5070") {
return Some(672.0);
}
if lower.contains("5060 ti") {
return Some(448.0);
}
if lower.contains("5060") {
return Some(256.0);
}
if lower.contains("4090") {
return Some(1008.0);
}
if lower.contains("4080 super") {
return Some(736.0);
}
if lower.contains("4080") {
return Some(717.0);
}
if lower.contains("4070 ti super") {
return Some(672.0);
}
if lower.contains("4070 ti") {
return Some(504.0);
}
if lower.contains("4070 super") {
return Some(504.0);
}
if lower.contains("4070") {
return Some(504.0);
}
if lower.contains("4060 ti") {
return Some(288.0);
}
if lower.contains("4060") {
return Some(272.0);
}
if lower.contains("3090 ti") {
return Some(1008.0);
}
if lower.contains("3090") {
return Some(936.0);
}
if lower.contains("3080 ti") {
return Some(912.0);
}
if lower.contains("3080") {
return Some(760.0);
}
if lower.contains("3070 ti") {
return Some(608.0);
}
if lower.contains("3070") {
return Some(448.0);
}
if lower.contains("3060 ti") {
return Some(448.0);
}
if lower.contains("3060") {
return Some(360.0);
}
if lower.contains("2080 ti") {
return Some(616.0);
}
if lower.contains("2080 super") {
return Some(496.0);
}
if lower.contains("2080") {
return Some(448.0);
}
if lower.contains("2070 super") {
return Some(448.0);
}
if lower.contains("2070") {
return Some(448.0);
}
if lower.contains("2060 super") {
return Some(448.0);
}
if lower.contains("2060") {
return Some(336.0);
}
if lower.contains("1660 ti") {
return Some(288.0);
}
if lower.contains("1660 super") {
return Some(336.0);
}
if lower.contains("1660") {
return Some(192.0);
}
if lower.contains("1650 super") {
return Some(192.0);
}
if lower.contains("1650") {
return Some(128.0);
}
if lower.contains("h100 sxm") {
return Some(3350.0);
}
if lower.contains("h100") {
return Some(2039.0);
} if lower.contains("h200") {
return Some(4800.0);
}
if lower.contains("a100 sxm") {
return Some(2039.0);
}
if lower.contains("a100") {
return Some(1555.0);
} if lower.contains("l40s") {
return Some(864.0);
}
if lower.contains("l40") {
return Some(864.0);
}
if lower.contains("l4") {
return Some(300.0);
}
if lower.contains("a10g") {
return Some(600.0);
}
if lower.contains("a10") {
return Some(600.0);
}
if lower.contains("t4") {
return Some(320.0);
}
if lower.contains("v100 sxm") {
return Some(900.0);
}
if lower.contains("v100") {
return Some(897.0);
}
if lower.contains("a6000") {
return Some(768.0);
}
if lower.contains("a5000") {
return Some(768.0);
}
if lower.contains("a4000") {
return Some(448.0);
}
if lower.contains("9070 xt") {
return Some(624.0);
}
if lower.contains("9070") {
return Some(488.0);
}
if lower.contains("7900 xtx") {
return Some(960.0);
}
if lower.contains("7900 xt") {
return Some(800.0);
}
if lower.contains("7900 gre") {
return Some(576.0);
}
if lower.contains("7800 xt") {
return Some(624.0);
}
if lower.contains("7700 xt") {
return Some(432.0);
}
if lower.contains("7600") {
return Some(288.0);
}
if lower.contains("6950 xt") {
return Some(576.0);
}
if lower.contains("6900 xt") {
return Some(512.0);
}
if lower.contains("6800 xt") {
return Some(512.0);
}
if lower.contains("6800") {
return Some(512.0);
}
if lower.contains("6700 xt") {
return Some(384.0);
}
if lower.contains("6600 xt") {
return Some(256.0);
}
if lower.contains("6600") {
return Some(224.0);
}
if lower.contains("mi300x") {
return Some(5300.0);
}
if lower.contains("mi300") {
return Some(5300.0);
}
if lower.contains("mi250x") {
return Some(3277.0);
}
if lower.contains("mi250") {
return Some(3277.0);
}
if lower.contains("mi210") {
return Some(1638.0);
}
if lower.contains("mi100") {
return Some(1229.0);
}
if lower.contains("m4 ultra") {
return Some(819.0);
}
if lower.contains("m4 max") {
return Some(546.0);
}
if lower.contains("m4 pro") {
return Some(273.0);
}
if lower.contains("m4") {
return Some(120.0);
}
if lower.contains("m3 ultra") {
return Some(800.0);
}
if lower.contains("m3 max") {
return Some(400.0);
}
if lower.contains("m3 pro") {
return Some(150.0);
}
if lower.contains("m3") {
return Some(100.0);
}
if lower.contains("m2 ultra") {
return Some(800.0);
}
if lower.contains("m2 max") {
return Some(400.0);
}
if lower.contains("m2 pro") {
return Some(200.0);
}
if lower.contains("m2") {
return Some(100.0);
}
if lower.contains("m1 ultra") {
return Some(800.0);
}
if lower.contains("m1 max") {
return Some(400.0);
}
if lower.contains("m1 pro") {
return Some(200.0);
}
if lower.contains("m1") {
return Some(68.0);
}
None
}
pub fn gpu_compute_capability(name: &str) -> Option<(u8, u8)> {
let lower = name.to_lowercase();
if lower.contains("5090")
|| lower.contains("5080")
|| lower.contains("5070")
|| lower.contains("5060")
|| lower.contains("b200")
|| lower.contains("b100")
|| lower.contains("gb200")
|| lower.contains("gb100")
{
return Some((10, 0));
}
if lower.contains("h100") || lower.contains("h200") {
return Some((9, 0));
}
if lower.contains("4090")
|| lower.contains("4080")
|| lower.contains("4070")
|| lower.contains("4060")
|| lower.contains("l40")
|| lower.contains("l4")
{
return Some((8, 9));
}
if lower.contains("a100") {
return Some((8, 0));
}
if lower.contains("3090")
|| lower.contains("3080")
|| lower.contains("3070")
|| lower.contains("3060")
|| lower.contains("a10")
|| lower.contains("a6000")
|| lower.contains("a5000")
|| lower.contains("a4000")
|| lower.contains("a2000")
|| lower.contains("a16")
{
return Some((8, 6));
}
if lower.contains("2080")
|| lower.contains("2070")
|| lower.contains("2060")
|| lower.contains("1660")
|| lower.contains("1650")
|| lower.contains("t4")
{
return Some((7, 5));
}
if lower.contains("v100") || lower.contains("titan v") {
return Some((7, 0));
}
if lower.contains("p100")
|| lower.contains("1080")
|| lower.contains("1070")
|| lower.contains("1060")
|| lower.contains("1050")
|| lower.contains("p40")
|| lower.contains("p4")
{
return Some((6, 1));
}
None
}
pub fn quant_min_compute_capability(quantization: &str) -> Option<(u8, u8)> {
match quantization {
"AWQ-4bit" | "AWQ-8bit" => Some((7, 5)),
"GPTQ-Int4" | "GPTQ-Int8" => Some((7, 5)),
_ => None,
}
}
fn is_nvidia_unified_memory_gpu(name: &str) -> bool {
let lower = name.to_lowercase();
if lower.contains("gb10") || lower.contains("gb20") {
return true;
}
if lower.contains("2e12") {
return true;
}
false
}
fn estimate_vram_from_name(name: &str) -> f64 {
let lower = name.to_lowercase();
if lower.contains("5090") {
return 32.0;
}
if lower.contains("5080") {
return 16.0;
}
if lower.contains("5070 ti") {
return 16.0;
}
if lower.contains("5070") {
return 12.0;
}
if lower.contains("5060 ti") {
return 16.0;
}
if lower.contains("5060") {
return 8.0;
}
if lower.contains("4090") {
return 24.0;
}
if lower.contains("4080") {
return 16.0;
}
if lower.contains("4070 ti") {
return 12.0;
}
if lower.contains("4070") {
return 12.0;
}
if lower.contains("4060 ti") {
return 16.0;
}
if lower.contains("4060") {
return 8.0;
}
if lower.contains("3090") {
return 24.0;
}
if lower.contains("3080 ti") {
return 12.0;
}
if lower.contains("3080") {
return 10.0;
}
if lower.contains("3070") {
return 8.0;
}
if lower.contains("3060 ti") {
return 8.0;
}
if lower.contains("3060") {
return 12.0;
}
if lower.contains("h100") {
return 80.0;
}
if lower.contains("a100") {
return 80.0;
}
if lower.contains("l40") {
return 48.0;
}
if lower.contains("a6000") {
return 48.0;
}
if lower.contains("a5500") {
return 24.0;
}
if lower.contains("a5000") {
return 24.0;
}
if lower.contains("a4500") {
return 20.0;
}
if lower.contains("a4000") {
return 16.0;
}
if lower.contains("a2000") {
return 12.0;
}
if lower.contains("a10") {
return 24.0;
}
if lower.contains("t4") {
return 16.0;
}
if lower.contains("gb10") || lower.contains("2e12") {
return 128.0;
}
if lower.contains("gb20") {
return 128.0;
}
if lower.contains("9070 xt") {
return 16.0;
}
if lower.contains("9070") {
return 12.0;
}
if lower.contains("9060 xt") {
return 16.0;
}
if lower.contains("9060") {
return 8.0;
}
if lower.contains("7900 xtx") {
return 24.0;
}
if lower.contains("7900") {
return 20.0;
}
if lower.contains("7800") {
return 16.0;
}
if lower.contains("7700") {
return 12.0;
}
if lower.contains("7600") {
return 8.0;
}
if lower.contains("6950") {
return 16.0;
}
if lower.contains("6900") {
return 16.0;
}
if lower.contains("6800") {
return 16.0;
}
if lower.contains("6750") {
return 12.0;
}
if lower.contains("6700") {
return 12.0;
}
if lower.contains("6650") {
return 8.0;
}
if lower.contains("6600") {
return 8.0;
}
if lower.contains("6500") {
return 4.0;
}
if lower.contains("5700 xt") {
return 8.0;
}
if lower.contains("5700") {
return 8.0;
}
if lower.contains("5600") {
return 6.0;
}
if lower.contains("5500") {
return 4.0;
}
if lower.contains("8060s") {
return 32.0;
}
if lower.contains("8050s") {
return 24.0;
}
if lower.contains("8060") && !lower.contains("8060s") {
return 16.0;
}
if lower.contains("8050") && !lower.contains("8050s") {
return 12.0;
}
if lower.contains("890m") {
return 16.0;
}
if lower.contains("880m") {
return 12.0;
}
if lower.contains("870m") {
return 8.0;
}
if lower.contains("860m") {
return 8.0;
}
if (lower.contains("radeon") || lower.contains("amd"))
&& !lower.contains("rx ")
&& !lower.contains("hd ")
&& !lower.contains(" r5 ")
&& !lower.contains(" r7 ")
&& !lower.contains(" r9 ")
&& !lower.contains("8060")
&& !lower.contains("8050")
&& (lower.contains("graphics") || lower.contains("igpu"))
{
return 0.5;
}
if lower.contains("rtx") {
return 8.0;
}
if lower.contains("gtx") {
return 4.0;
}
if lower.contains("rx ") || lower.contains("radeon") {
return 8.0;
}
0.0
}
#[cfg(test)]
mod tests {
use super::SystemSpecs;
#[test]
fn test_parse_nvidia_smi_does_not_sum_multi_gpu_vram() {
let text = "24564, NVIDIA GeForce RTX 4090\n24564, NVIDIA GeForce RTX 4090\n";
let gpus = SystemSpecs::parse_nvidia_smi_list(text);
assert_eq!(gpus.len(), 1);
assert_eq!(gpus[0].count, 2);
let vram = gpus[0]
.vram_gb
.expect("VRAM should be parsed for RTX 4090 entries");
assert!(vram > 23.0 && vram < 25.0, "unexpected VRAM value: {vram}");
}
#[test]
fn test_parse_nvidia_smi_keeps_distinct_models() {
let text = "24564, NVIDIA GeForce RTX 4090\n16376, NVIDIA GeForce RTX 4080\n";
let gpus = SystemSpecs::parse_nvidia_smi_list(text);
assert_eq!(gpus.len(), 2);
assert!(gpus.iter().any(|g| g.name.contains("4090") && g.count == 1));
assert!(gpus.iter().any(|g| g.name.contains("4080") && g.count == 1));
}
#[test]
fn test_parse_nvidia_smi_gb10_gets_vram_estimate() {
let text = "0, NVIDIA GB10\n";
let gpus = SystemSpecs::parse_nvidia_smi_list(text);
assert_eq!(gpus.len(), 1);
assert!(gpus[0].name.contains("GB10"));
let vram = gpus[0].vram_gb.expect("GB10 should have estimated VRAM");
assert!(vram > 100.0, "GB10 VRAM should be ~128GB, got {vram}");
}
#[test]
fn test_estimate_vram_gb10() {
assert_eq!(super::estimate_vram_from_name("NVIDIA GB10"), 128.0);
assert_eq!(super::estimate_vram_from_name("NVIDIA GB20"), 128.0);
}
#[test]
fn test_estimate_vram_rtx_professional() {
assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A6000"), 48.0);
assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A5500"), 24.0);
assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A5000"), 24.0);
assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A4500"), 20.0);
assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A4000"), 16.0);
assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A2000"), 12.0);
}
#[test]
fn test_parse_extended_discrete_gpu_not_unified() {
let text = "None, 24564, NVIDIA GeForce RTX 4090\n";
let gpus = SystemSpecs::parse_nvidia_smi_extended(text);
assert_eq!(gpus.len(), 1);
assert_eq!(gpus[0].name, "NVIDIA GeForce RTX 4090");
assert!(
!gpus[0].unified_memory,
"discrete GPU should not be unified"
);
let vram = gpus[0].vram_gb.expect("VRAM should be present");
assert!(vram > 23.0 && vram < 25.0, "unexpected VRAM: {vram}");
}
#[test]
fn test_parse_extended_tegra_unified_memory() {
let text = "ATS, [N/A], NVIDIA Thor\n";
let gpus = SystemSpecs::parse_nvidia_smi_extended(text);
assert_eq!(gpus.len(), 1);
assert_eq!(gpus[0].name, "NVIDIA Thor");
assert!(gpus[0].unified_memory, "ATS should set unified_memory=true");
}
#[test]
fn test_parse_extended_multi_gpu_discrete() {
let text = "None, 24564, NVIDIA GeForce RTX 4090\nNone, 24564, NVIDIA GeForce RTX 4090\n";
let gpus = SystemSpecs::parse_nvidia_smi_extended(text);
assert_eq!(gpus.len(), 1);
assert_eq!(gpus[0].count, 2);
assert!(!gpus[0].unified_memory);
}
#[test]
fn test_gpu_bandwidth_known_gpus() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 4090"),
Some(1008.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 3060"),
Some(360.0)
);
assert_eq!(super::gpu_memory_bandwidth_gbps("Tesla T4"), Some(320.0));
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA H100 SXM"),
Some(3350.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA A100"),
Some(1555.0)
);
}
#[test]
fn test_gpu_bandwidth_apple_silicon() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("Apple M1 Max"),
Some(400.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("Apple M4 Pro"),
Some(273.0)
);
}
#[test]
fn test_gpu_bandwidth_unknown_returns_none() {
assert_eq!(super::gpu_memory_bandwidth_gbps("Some Random GPU"), None);
assert_eq!(super::gpu_memory_bandwidth_gbps(""), None);
}
#[test]
fn test_gpu_bandwidth_amd() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("AMD Radeon RX 7900 XTX"),
Some(960.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("AMD Instinct MI300X"),
Some(5300.0)
);
}
#[test]
fn test_parse_cpu_name_from_cpuinfo_prefers_model_name() {
let cpuinfo = "\
processor : 0
model name : Qualcomm Kryo 680
Hardware : Qualcomm Technologies, Inc SM8350
";
assert_eq!(
SystemSpecs::parse_cpu_name_from_cpuinfo(cpuinfo),
Some("Qualcomm Kryo 680".to_string())
);
}
#[test]
fn test_parse_cpu_name_from_cpuinfo_uses_hardware_fallback() {
let cpuinfo = "\
processor : 0
Hardware : Qualcomm Technologies, Inc SM8650
";
assert_eq!(
SystemSpecs::parse_cpu_name_from_cpuinfo(cpuinfo),
Some("Qualcomm Technologies, Inc SM8650".to_string())
);
}
#[test]
fn test_parse_vulkan_device_names_from_summary_output() {
let text = "\
GPU0:
deviceName = Adreno (TM) 740
GPU1:
deviceName = llvmpipe (LLVM 17.0.0, 256 bits)
";
let names = SystemSpecs::parse_vulkan_device_names(text);
assert_eq!(
names,
vec![
"Adreno (TM) 740".to_string(),
"llvmpipe (LLVM 17.0.0, 256 bits)".to_string()
]
);
}
#[test]
fn test_parse_vulkan_device_names_from_gpu_id_lines() {
let text = "\
GPU id = 0 (Adreno (TM) 740)
GPU id = 1 (NVIDIA GeForce RTX 4090)
";
let names = SystemSpecs::parse_vulkan_device_names(text);
assert_eq!(
names,
vec![
"Adreno (TM) 740".to_string(),
"NVIDIA GeForce RTX 4090".to_string()
]
);
}
#[test]
fn test_is_software_vulkan_device() {
assert!(SystemSpecs::is_software_vulkan_device(
"llvmpipe (LLVM 17.0.0, 256 bits)"
));
assert!(SystemSpecs::is_software_vulkan_device("SwiftShader Device"));
assert!(!SystemSpecs::is_software_vulkan_device("Adreno (TM) 740"));
}
#[test]
fn test_is_same_gpu_name_uses_normalized_exact_match() {
assert!(SystemSpecs::is_same_gpu_name(
"NVIDIA-GeForce RTX 4090",
"nvidia geforce rtx 4090"
));
assert!(!SystemSpecs::is_same_gpu_name("RTX", "RTX 4090"));
}
#[test]
fn test_normalize_gpu_name_for_dedupe() {
assert_eq!(
SystemSpecs::normalize_gpu_name_for_dedupe(" Adreno (TM) 740 "),
"adreno tm 740"
);
}
#[test]
fn test_gpu_backend_labels() {
assert_eq!(super::GpuBackend::Cuda.label(), "CUDA");
assert_eq!(super::GpuBackend::Metal.label(), "Metal");
assert_eq!(super::GpuBackend::Rocm.label(), "ROCm");
assert_eq!(super::GpuBackend::Vulkan.label(), "Vulkan");
assert_eq!(super::GpuBackend::Sycl.label(), "SYCL");
assert_eq!(super::GpuBackend::CpuArm.label(), "CPU (ARM)");
assert_eq!(super::GpuBackend::CpuX86.label(), "CPU (x86)");
assert_eq!(super::GpuBackend::Ascend.label(), "NPU (Ascend)");
}
#[test]
fn test_parse_memory_size_gb() {
assert_eq!(super::parse_memory_size("32G"), Some(32.0));
assert_eq!(super::parse_memory_size("32GB"), Some(32.0));
assert_eq!(super::parse_memory_size("32GiB"), Some(32.0));
assert_eq!(super::parse_memory_size("24g"), Some(24.0));
assert_eq!(super::parse_memory_size("24gb"), Some(24.0));
}
#[test]
fn test_parse_memory_size_mb() {
let result = super::parse_memory_size("16384M").unwrap();
assert!((result - 16.0).abs() < 0.01);
let result = super::parse_memory_size("8192MB").unwrap();
assert!((result - 8.0).abs() < 0.01);
}
#[test]
fn test_parse_memory_size_tb() {
let result = super::parse_memory_size("1T").unwrap();
assert!((result - 1024.0).abs() < 0.01);
let result = super::parse_memory_size("2TB").unwrap();
assert!((result - 2048.0).abs() < 0.01);
}
#[test]
fn test_parse_memory_size_bare_number() {
assert_eq!(super::parse_memory_size("16"), Some(16.0));
}
#[test]
fn test_parse_memory_size_whitespace() {
assert_eq!(super::parse_memory_size(" 32G "), Some(32.0));
}
#[test]
fn test_parse_memory_size_empty() {
assert_eq!(super::parse_memory_size(""), None);
assert_eq!(super::parse_memory_size(" "), None);
}
#[test]
fn test_parse_memory_size_invalid_suffix() {
assert_eq!(super::parse_memory_size("32X"), None);
assert_eq!(super::parse_memory_size("32KB"), None);
}
#[test]
fn test_parse_memory_size_fractional() {
assert_eq!(super::parse_memory_size("16.5G"), Some(16.5));
}
fn make_specs_no_gpu() -> SystemSpecs {
SystemSpecs {
total_ram_gb: 32.0,
available_ram_gb: 24.0,
total_cpu_cores: 8,
cpu_name: "Test CPU".to_string(),
has_gpu: false,
gpu_vram_gb: None,
total_gpu_vram_gb: None,
gpu_name: None,
gpu_count: 0,
unified_memory: false,
backend: super::GpuBackend::CpuX86,
gpus: vec![],
cluster_mode: false,
cluster_node_count: 0,
}
}
fn make_specs_with_gpu() -> SystemSpecs {
SystemSpecs {
total_ram_gb: 32.0,
available_ram_gb: 24.0,
total_cpu_cores: 8,
cpu_name: "Test CPU".to_string(),
has_gpu: true,
gpu_vram_gb: Some(8.0),
total_gpu_vram_gb: Some(8.0),
gpu_name: Some("NVIDIA RTX 3070".to_string()),
gpu_count: 1,
unified_memory: false,
backend: super::GpuBackend::Cuda,
gpus: vec![super::GpuInfo {
name: "NVIDIA RTX 3070".to_string(),
vram_gb: Some(8.0),
backend: super::GpuBackend::Cuda,
count: 1,
unified_memory: false,
}],
cluster_mode: false,
cluster_node_count: 0,
}
}
#[test]
fn test_gpu_override_creates_synthetic_gpu_when_none() {
let specs = make_specs_no_gpu().with_gpu_memory_override(24.0);
assert!(specs.has_gpu);
assert_eq!(specs.gpu_vram_gb, Some(24.0));
assert_eq!(specs.total_gpu_vram_gb, Some(24.0));
assert_eq!(specs.gpu_count, 1);
assert_eq!(specs.gpus.len(), 1);
assert_eq!(specs.gpus[0].name, "User-specified GPU");
}
#[test]
fn test_gpu_override_updates_existing_gpu() {
let specs = make_specs_with_gpu().with_gpu_memory_override(24.0);
assert_eq!(specs.gpu_vram_gb, Some(24.0));
assert_eq!(specs.total_gpu_vram_gb, Some(24.0));
assert_eq!(specs.gpus[0].vram_gb, Some(24.0));
assert_eq!(specs.gpus[0].name, "NVIDIA RTX 3070");
}
#[test]
fn test_gpu_override_multi_gpu_scales_total() {
let mut specs = make_specs_with_gpu();
specs.gpus[0].count = 2;
let specs = specs.with_gpu_memory_override(24.0);
assert_eq!(specs.gpu_vram_gb, Some(24.0));
assert_eq!(specs.total_gpu_vram_gb, Some(48.0));
}
#[test]
fn test_amd_unified_memory_apu_detection() {
assert!(super::is_amd_unified_memory_apu(
"AMD Ryzen AI MAX+ 395 w/ Radeon 8060S"
));
assert!(super::is_amd_unified_memory_apu(
"AMD Ryzen AI 9 HX 370 w/ Radeon 890M"
));
assert!(super::is_amd_unified_memory_apu("AMD Ryzen AI 7 350"));
assert!(!super::is_amd_unified_memory_apu("AMD Ryzen 9 7950X"));
assert!(!super::is_amd_unified_memory_apu("Intel Core i9-14900K"));
}
#[test]
fn test_bandwidth_rtx_20_series() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 2080 Ti"),
Some(616.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 2060"),
Some(336.0)
);
}
#[test]
fn test_bandwidth_gtx_16_series() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce GTX 1660 Ti"),
Some(288.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce GTX 1650"),
Some(128.0)
);
}
#[test]
fn test_bandwidth_rtx_50_series() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5090"),
Some(1792.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5080"),
Some(960.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5070 Ti"),
Some(896.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5070"),
Some(672.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5060 Ti"),
Some(448.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5060"),
Some(256.0)
);
}
#[test]
fn test_bandwidth_amd_rx_6000() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("AMD Radeon RX 6950 XT"),
Some(576.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("AMD Radeon RX 6700 XT"),
Some(384.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("AMD Radeon RX 6600"),
Some(224.0)
);
}
#[test]
fn test_bandwidth_nvidia_professional() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA RTX A6000"),
Some(768.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("NVIDIA RTX A4000"),
Some(448.0)
);
assert_eq!(super::gpu_memory_bandwidth_gbps("NVIDIA L40S"), Some(864.0));
assert_eq!(super::gpu_memory_bandwidth_gbps("NVIDIA L4"), Some(300.0));
}
#[test]
fn test_bandwidth_apple_silicon_all() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("Apple M4 Ultra"),
Some(819.0)
);
assert_eq!(super::gpu_memory_bandwidth_gbps("Apple M4"), Some(120.0));
assert_eq!(
super::gpu_memory_bandwidth_gbps("Apple M3 Ultra"),
Some(800.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("Apple M3 Max"),
Some(400.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("Apple M3 Pro"),
Some(150.0)
);
assert_eq!(super::gpu_memory_bandwidth_gbps("Apple M3"), Some(100.0));
assert_eq!(
super::gpu_memory_bandwidth_gbps("Apple M1 Pro"),
Some(200.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("Apple M1 Ultra"),
Some(800.0)
);
}
#[test]
fn test_bandwidth_amd_cdna() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("AMD Instinct MI250X"),
Some(3277.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("AMD Instinct MI210"),
Some(1638.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("AMD Instinct MI100"),
Some(1229.0)
);
}
#[test]
fn test_bandwidth_amd_rdna4() {
assert_eq!(
super::gpu_memory_bandwidth_gbps("AMD Radeon RX 9070 XT"),
Some(624.0)
);
assert_eq!(
super::gpu_memory_bandwidth_gbps("AMD Radeon RX 9070"),
Some(488.0)
);
}
#[test]
fn test_compute_capability_nvidia_generations() {
assert_eq!(super::gpu_compute_capability("Tesla P100"), Some((6, 1)));
assert_eq!(
super::gpu_compute_capability("Tesla V100-PCIE-16GB"),
Some((7, 0))
);
assert_eq!(super::gpu_compute_capability("Tesla T4"), Some((7, 5)));
assert_eq!(
super::gpu_compute_capability("NVIDIA GeForce RTX 2080 Ti"),
Some((7, 5))
);
assert_eq!(
super::gpu_compute_capability("NVIDIA GeForce GTX 1660 Ti"),
Some((7, 5))
);
assert_eq!(super::gpu_compute_capability("NVIDIA A100"), Some((8, 0)));
assert_eq!(
super::gpu_compute_capability("NVIDIA GeForce RTX 3090"),
Some((8, 6))
);
assert_eq!(
super::gpu_compute_capability("NVIDIA GeForce RTX 4090"),
Some((8, 9))
);
assert_eq!(super::gpu_compute_capability("NVIDIA L40S"), Some((8, 9)));
assert_eq!(
super::gpu_compute_capability("NVIDIA H100 SXM"),
Some((9, 0))
);
assert_eq!(
super::gpu_compute_capability("NVIDIA GeForce RTX 5090"),
Some((10, 0))
);
}
#[test]
fn test_compute_capability_unknown_returns_none() {
assert_eq!(super::gpu_compute_capability("Some Random GPU"), None);
assert_eq!(super::gpu_compute_capability("Apple M4 Max"), None);
assert_eq!(
super::gpu_compute_capability("AMD Radeon RX 7900 XTX"),
None
);
}
#[test]
fn test_is_integrated_gpu_name() {
assert!(SystemSpecs::is_integrated_gpu_name(
"Intel(R) UHD Graphics 770"
));
assert!(SystemSpecs::is_integrated_gpu_name(
"Intel(R) HD Graphics 630"
));
assert!(SystemSpecs::is_integrated_gpu_name(
"Intel(R) Iris(R) Xe Graphics"
));
assert!(SystemSpecs::is_integrated_gpu_name(
"Intel(R) Iris(R) Plus Graphics"
));
assert!(!SystemSpecs::is_integrated_gpu_name(
"Intel(R) Arc(TM) A770"
));
assert!(!SystemSpecs::is_integrated_gpu_name(
"Intel(R) Arc(TM) B580"
));
}
#[test]
fn test_is_integrated_gpu_name_amd() {
assert!(SystemSpecs::is_integrated_gpu_name(
"AMD Radeon(TM) Graphics"
));
assert!(SystemSpecs::is_integrated_gpu_name("AMD Radeon Graphics"));
assert!(!SystemSpecs::is_integrated_gpu_name(
"AMD Radeon RX 7900 XTX"
));
assert!(!SystemSpecs::is_integrated_gpu_name("AMD Radeon Pro W7900"));
}
#[test]
fn test_is_integrated_gpu_name_nvidia() {
assert!(!SystemSpecs::is_integrated_gpu_name(
"NVIDIA GeForce RTX 4090"
));
assert!(!SystemSpecs::is_integrated_gpu_name(
"NVIDIA GeForce GTX 1650"
));
}
#[test]
fn test_prefer_discrete_gpus_filters_integrated() {
use super::GpuBackend;
let gpus = vec![
super::GpuInfo {
name: "Intel(R) UHD Graphics 770".to_string(),
vram_gb: Some(8.0),
backend: GpuBackend::Vulkan,
count: 1,
unified_memory: false,
},
super::GpuInfo {
name: "NVIDIA GeForce RTX 4090".to_string(),
vram_gb: Some(4.0), backend: GpuBackend::Cuda,
count: 1,
unified_memory: false,
},
];
let result = SystemSpecs::prefer_discrete_gpus(gpus);
assert_eq!(result.len(), 1);
assert!(result[0].name.contains("RTX 4090"));
}
#[test]
fn test_prefer_discrete_gpus_keeps_igpu_only() {
use super::GpuBackend;
let gpus = vec![super::GpuInfo {
name: "Intel(R) UHD Graphics 770".to_string(),
vram_gb: Some(2.0),
backend: GpuBackend::Vulkan,
count: 1,
unified_memory: false,
}];
let result = SystemSpecs::prefer_discrete_gpus(gpus);
assert_eq!(result.len(), 1);
assert!(result[0].name.contains("UHD"));
}
#[test]
fn test_quant_min_compute_capability() {
assert_eq!(
super::quant_min_compute_capability("AWQ-4bit"),
Some((7, 5))
);
assert_eq!(
super::quant_min_compute_capability("AWQ-8bit"),
Some((7, 5))
);
assert_eq!(
super::quant_min_compute_capability("GPTQ-Int4"),
Some((7, 5))
);
assert_eq!(
super::quant_min_compute_capability("GPTQ-Int8"),
Some((7, 5))
);
assert_eq!(super::quant_min_compute_capability("Q4_K_M"), None);
assert_eq!(super::quant_min_compute_capability("Q8_0"), None);
}
#[test]
fn test_ram_override_updates_ram_values() {
let specs = SystemSpecs {
total_ram_gb: 32.0,
available_ram_gb: 24.0,
total_cpu_cores: 8,
cpu_name: "Test CPU".to_string(),
has_gpu: true,
gpu_vram_gb: Some(16.0),
total_gpu_vram_gb: Some(16.0),
gpu_name: Some("Test GPU".to_string()),
gpu_count: 1,
unified_memory: false,
backend: super::GpuBackend::Cuda,
gpus: vec![super::GpuInfo {
name: "Test GPU".to_string(),
vram_gb: Some(16.0),
backend: super::GpuBackend::Cuda,
count: 1,
unified_memory: false,
}],
cluster_mode: false,
cluster_node_count: 0,
};
let overridden = specs.with_ram_override(128.0);
assert_eq!(overridden.total_ram_gb, 128.0);
assert!((overridden.available_ram_gb - 115.2).abs() < 0.01);
assert_eq!(overridden.gpu_vram_gb, Some(16.0));
assert_eq!(overridden.total_gpu_vram_gb, Some(16.0));
}
#[test]
fn test_ram_override_unified_memory_updates_gpu() {
let specs = SystemSpecs {
total_ram_gb: 36.0,
available_ram_gb: 30.0,
total_cpu_cores: 10,
cpu_name: "Apple M2 Max".to_string(),
has_gpu: true,
gpu_vram_gb: Some(36.0),
total_gpu_vram_gb: Some(36.0),
gpu_name: Some("Apple M2 Max".to_string()),
gpu_count: 1,
unified_memory: true,
backend: super::GpuBackend::Metal,
gpus: vec![super::GpuInfo {
name: "Apple M2 Max".to_string(),
vram_gb: Some(36.0),
backend: super::GpuBackend::Metal,
count: 1,
unified_memory: true,
}],
cluster_mode: false,
cluster_node_count: 0,
};
let overridden = specs.with_ram_override(96.0);
assert_eq!(overridden.total_ram_gb, 96.0);
assert_eq!(overridden.gpu_vram_gb, Some(96.0));
assert_eq!(overridden.total_gpu_vram_gb, Some(96.0));
assert_eq!(overridden.gpus[0].vram_gb, Some(96.0));
}
#[test]
fn test_cpu_core_override() {
let specs = SystemSpecs {
total_ram_gb: 32.0,
available_ram_gb: 24.0,
total_cpu_cores: 8,
cpu_name: "Test CPU".to_string(),
has_gpu: false,
gpu_vram_gb: None,
total_gpu_vram_gb: None,
gpu_name: None,
gpu_count: 0,
unified_memory: false,
backend: super::GpuBackend::CpuX86,
gpus: vec![],
cluster_mode: false,
cluster_node_count: 0,
};
let overridden = specs.with_cpu_core_override(64);
assert_eq!(overridden.total_cpu_cores, 64);
assert_eq!(overridden.total_ram_gb, 32.0);
assert_eq!(overridden.available_ram_gb, 24.0);
assert!(!overridden.has_gpu);
}
}