diff --git a/crates/mesh-llm-system/src/hardware/mod.rs b/crates/mesh-llm-system/src/hardware/mod.rs index a72254bb6..c454d15e2 100644 --- a/crates/mesh-llm-system/src/hardware/mod.rs +++ b/crates/mesh-llm-system/src/hardware/mod.rs @@ -235,6 +235,17 @@ fn read_windows_total_ram_bytes() -> Option { parse_windows_total_physical_memory(&output) } +/// Per-GPU VRAM in adapter order. +/// +/// Adapters that report no VRAM (virtual and headless displays commonly report +/// none) keep a zero entry so this stays index-aligned with the adapter name +/// list that `hydrate_gpu_facts_with_identities` indexes into. Dropping those +/// entries shifts every later adapter's VRAM onto the wrong device. +#[cfg(any(target_os = "windows", test))] +fn windows_per_gpu_vram(controllers: &[(String, u64)]) -> Vec { + controllers.iter().map(|(_, ram)| *ram).collect() +} + #[cfg(target_os = "windows")] fn read_windows_video_controllers() -> Vec<(String, u64)> { let Some(output) = powershell_output( @@ -677,14 +688,10 @@ impl Collector for DefaultCollector { survey.vram_bytes = total + (ram_offload as f64 * 0.90) as u64; } } else { - let per_gpu: Vec = windows_gpus - .iter() - .map(|(_, ram)| *ram) - .filter(|ram| *ram > 0) - .collect(); + let per_gpu: Vec = windows_per_gpu_vram(&windows_gpus); let total: u64 = per_gpu.iter().sum(); + survey.gpu_vram = per_gpu; if total > 0 { - survey.gpu_vram = per_gpu; let ram_offload = system_ram.saturating_sub(total); survey.vram_bytes = total + (ram_offload as f64 * 0.90) as u64; } else if system_ram > 0 { diff --git a/crates/mesh-llm-system/src/hardware/tests.rs b/crates/mesh-llm-system/src/hardware/tests.rs index 2fde61f5d..becbc2d73 100644 --- a/crates/mesh-llm-system/src/hardware/tests.rs +++ b/crates/mesh-llm-system/src/hardware/tests.rs @@ -833,6 +833,52 @@ fn test_parse_windows_video_controller_json_single_object() { ); } +#[test] +fn test_windows_per_gpu_vram_keeps_zero_vram_adapters_aligned() { + let controllers = vec![ + ("Virtual Display Adapter".to_string(), 0), + ("AMD Radeon RX 9070 XT".to_string(), 4_293_918_720), + ]; + + assert_eq!( + windows_per_gpu_vram(&controllers), + vec![0, 4_293_918_720], + "adapters reporting no VRAM must keep a slot so later adapters stay aligned" + ); +} + +#[test] +fn test_windows_per_gpu_vram_keeps_all_zero_adapters_aligned() { + let controllers = vec![ + ("Virtual Display Adapter".to_string(), 0), + ("Headless Display Adapter".to_string(), 0), + ]; + + assert_eq!(windows_per_gpu_vram(&controllers), vec![0, 0]); +} + +#[test] +fn test_windows_zero_vram_adapter_does_not_shift_vram_onto_wrong_gpu() { + let controllers = vec![ + ("Virtual Display Adapter".to_string(), 0), + ("AMD Radeon RX 9070 XT".to_string(), 4_293_918_720), + ]; + let names: Vec = controllers.iter().map(|(name, _)| name.clone()).collect(); + let mut survey = HardwareSurvey { + gpu_count: 2, + gpu_vram: windows_per_gpu_vram(&controllers), + ..Default::default() + }; + + hydrate_gpu_facts_with_identities(&mut survey, &[Metric::GpuFacts], &[], names, 2, false); + + assert_eq!(survey.gpus.len(), 2); + assert_eq!(survey.gpus[0].display_name, "Virtual Display Adapter"); + assert_eq!(survey.gpus[0].vram_bytes, 0); + assert_eq!(survey.gpus[1].display_name, "AMD Radeon RX 9070 XT"); + assert_eq!(survey.gpus[1].vram_bytes, 4_293_918_720); +} + #[test] fn test_parse_windows_total_physical_memory() { assert_eq!(