Skip to main content

mesh_llm_hardware_profile/
lib.rs

1use mesh_llm_native_runtime::host::HostGpuProbe;
2use mesh_llm_native_runtime::{
3    HostCudaProfile, HostGpuProfile, HostRocmProfile, HostRuntimeProfile, HostVulkanProfile,
4    NativeRuntimeBackendKind,
5};
6use std::collections::{BTreeMap, BTreeSet};
7use std::process::Command;
8
9mod rocm;
10
11pub fn host_runtime_profile() -> HostRuntimeProfile {
12    let mut gpus = detect_gpus();
13    apply_gpu_arch_overrides(&mut gpus);
14    let cuda = detect_cuda_profile(&gpus);
15    let rocm = detect_rocm_profile(&gpus);
16    let vulkan = detect_vulkan_profile();
17    HostRuntimeProfile {
18        os: std::env::consts::OS.to_string(),
19        arch: std::env::consts::ARCH.to_string(),
20        target_triple: option_env!("TARGET").map(str::to_string),
21        available_flavors: detected_native_runtime_flavors(
22            &gpus,
23            cuda.as_ref(),
24            rocm.as_ref(),
25            vulkan.as_ref(),
26        ),
27        gpus,
28        cuda,
29        rocm,
30        vulkan,
31    }
32}
33
34pub fn detected_native_runtime_flavors(
35    gpus: &[HostGpuProfile],
36    cuda: Option<&HostCudaProfile>,
37    rocm: Option<&HostRocmProfile>,
38    vulkan: Option<&HostVulkanProfile>,
39) -> BTreeSet<NativeRuntimeBackendKind> {
40    let mut flavors = BTreeSet::from([NativeRuntimeBackendKind::Cpu]);
41    if cfg!(target_os = "macos") {
42        flavors.insert(NativeRuntimeBackendKind::Metal);
43    }
44    if cuda.is_some() {
45        flavors.insert(NativeRuntimeBackendKind::Cuda);
46    }
47    if rocm.is_some() {
48        flavors.insert(NativeRuntimeBackendKind::Rocm);
49    }
50    if vulkan.is_some() {
51        flavors.insert(NativeRuntimeBackendKind::Vulkan);
52    }
53    for gpu in gpus {
54        insert_label_flavors(&mut flavors, &gpu.display_name);
55        if let Some(device) = &gpu.backend_device {
56            insert_label_flavors(&mut flavors, device);
57        }
58    }
59    flavors
60}
61
62fn detect_gpus() -> Vec<HostGpuProfile> {
63    merge_nvidia_and_fallback_gpus(detect_nvidia_gpu_profiles(), fallback_gpu_profiles())
64}
65
66fn merge_nvidia_and_fallback_gpus(
67    mut nvidia_gpus: Vec<HostGpuProfile>,
68    mut fallback_gpus: Vec<HostGpuProfile>,
69) -> Vec<HostGpuProfile> {
70    if nvidia_gpus.is_empty() {
71        return fallback_gpus;
72    }
73
74    fallback_gpus.retain(|gpu| !looks_like_nvidia_gpu_label(&gpu.display_name));
75    nvidia_gpus.extend(fallback_gpus);
76    nvidia_gpus
77}
78
79fn fallback_gpu_profiles() -> Vec<HostGpuProfile> {
80    gpu_labels()
81        .into_iter()
82        .map(fallback_gpu_profile_from_label)
83        .collect()
84}
85
86fn fallback_gpu_profile_from_label(label: String) -> HostGpuProfile {
87    HostGpuProfile {
88        display_name: label,
89        backend_device: None,
90        stable_id: None,
91        vram_bytes: None,
92        unified_memory: cfg!(target_os = "macos"),
93        probe: None,
94        cuda_sm: None,
95        rocm_gfx: None,
96    }
97}
98
99fn looks_like_nvidia_gpu_label(label: &str) -> bool {
100    let label = label.to_ascii_lowercase();
101    label.contains("nvidia") || label.contains("cuda")
102}
103
104fn detect_nvidia_gpu_profiles() -> Vec<HostGpuProfile> {
105    let Some(nvidia_smi) = command_output("nvidia-smi", &["-L"]) else {
106        return Vec::new();
107    };
108    let compute_caps = command_output(
109        "nvidia-smi",
110        &[
111            "--query-gpu=index,compute_cap",
112            "--format=csv,noheader,nounits",
113        ],
114    )
115    .map(|output| nvidia_compute_caps_by_index(&output))
116    .unwrap_or_default();
117    let lspci = command_output("lspci", &[]).unwrap_or_default();
118    let proc_entries = linux_nvidia_proc_information_entries();
119    let borrowed_entries: Vec<(&str, &str)> = proc_entries
120        .iter()
121        .map(|entry| (entry.path.as_str(), entry.info.as_str()))
122        .collect();
123    nvidia_gpu_profiles_from_probe_outputs(&nvidia_smi, &compute_caps, &lspci, &borrowed_entries)
124}
125
126#[derive(Clone, Debug, Eq, PartialEq)]
127struct NvidiaSmiGpu {
128    index: usize,
129    name: String,
130    vendor_uuid: Option<String>,
131}
132
133#[derive(Clone, Debug, Eq, PartialEq)]
134struct NvidiaProcInformationEntry {
135    path: String,
136    info: String,
137}
138
139#[derive(Clone, Debug, Eq, PartialEq)]
140struct NvidiaProcProbe {
141    pci_bdf: Option<String>,
142    vendor_uuid: Option<String>,
143    probe: HostGpuProbe,
144}
145
146fn nvidia_gpu_profiles_from_probe_outputs(
147    nvidia_smi_output: &str,
148    compute_caps: &BTreeMap<usize, String>,
149    lspci_output: &str,
150    proc_entries: &[(&str, &str)],
151) -> Vec<HostGpuProfile> {
152    let mut proc_probes = proc_entries
153        .iter()
154        .map(|(path, info)| nvidia_proc_probe(path, info))
155        .collect::<Vec<_>>();
156
157    parse_nvidia_smi_list(nvidia_smi_output)
158        .into_iter()
159        .map(|gpu| {
160            let pci_bdf = nvidia_lspci_bdf_for_name(lspci_output, &gpu.name);
161            let probe = take_matching_nvidia_probe(
162                &mut proc_probes,
163                gpu.vendor_uuid.as_deref(),
164                pci_bdf.as_deref(),
165            );
166            HostGpuProfile {
167                display_name: gpu.name,
168                backend_device: Some(format!("CUDA{}", gpu.index)),
169                stable_id: gpu
170                    .vendor_uuid
171                    .as_ref()
172                    .map(|uuid| format!("uuid:{uuid}"))
173                    .or_else(|| pci_bdf.as_ref().map(|bdf| format!("pci:{bdf}"))),
174                vram_bytes: None,
175                unified_memory: false,
176                probe,
177                cuda_sm: compute_caps.get(&gpu.index).cloned(),
178                rocm_gfx: None,
179            }
180        })
181        .collect()
182}
183
184fn nvidia_compute_caps_by_index(output: &str) -> BTreeMap<usize, String> {
185    output
186        .lines()
187        .filter_map(|line| {
188            let (index, compute_cap) = line.split_once(',')?;
189            let index = index.trim().parse::<usize>().ok()?;
190            let cuda_sm = cuda_sm_from_compute_cap(compute_cap.trim())?;
191            Some((index, cuda_sm))
192        })
193        .collect()
194}
195
196fn cuda_sm_from_compute_cap(value: &str) -> Option<String> {
197    let (major, minor) = value.split_once('.')?;
198    let major = major.trim();
199    let minor = minor.trim();
200    if major.is_empty()
201        || minor.is_empty()
202        || !major.chars().all(|ch| ch.is_ascii_digit())
203        || !minor.chars().all(|ch| ch.is_ascii_digit())
204    {
205        return None;
206    }
207    Some(format!("{major}{minor}"))
208}
209
210fn parse_nvidia_smi_list(output: &str) -> Vec<NvidiaSmiGpu> {
211    output
212        .lines()
213        .filter_map(|line| {
214            let line = line.trim();
215            let body = line.strip_prefix("GPU ")?;
216            let (index, rest) = body.split_once(':')?;
217            let index = index.trim().parse::<usize>().ok()?;
218            let rest = rest.trim();
219            let (name, vendor_uuid) = match rest.rsplit_once(" (UUID: ") {
220                Some((name, uuid)) => (name.trim(), uuid.strip_suffix(')').map(str::trim)),
221                None => (rest, None),
222            };
223            (!name.is_empty()).then(|| NvidiaSmiGpu {
224                index,
225                name: name.to_string(),
226                vendor_uuid: vendor_uuid.map(ToOwned::to_owned),
227            })
228        })
229        .collect()
230}
231
232fn nvidia_lspci_bdf_for_name(output: &str, name: &str) -> Option<String> {
233    let name = name.to_ascii_lowercase();
234    output.lines().find_map(|line| {
235        let line = line.trim();
236        if !looks_like_display_controller(line) {
237            return None;
238        }
239        let lower = line.to_ascii_lowercase();
240        if !name
241            .split_whitespace()
242            .filter(|token| *token != "nvidia" && *token != "geforce")
243            .all(|token| lower.contains(token))
244        {
245            return None;
246        }
247        line.split_whitespace().next().map(normalize_pci_bdf)
248    })
249}
250
251fn normalize_pci_bdf(bdf: &str) -> String {
252    if bdf.matches(':').count() == 1 {
253        format!("0000:{bdf}")
254    } else {
255        bdf.to_ascii_lowercase()
256    }
257}
258
259fn nvidia_proc_probe(path: &str, info: &str) -> NvidiaProcProbe {
260    let fields = nvidia_proc_fields(info);
261    NvidiaProcProbe {
262        pci_bdf: fields
263            .get("Bus Location")
264            .map(String::as_str)
265            .map(normalize_pci_bdf),
266        vendor_uuid: fields.get("GPU UUID").cloned(),
267        probe: HostGpuProbe {
268            source: "linux_nvidia_proc".to_string(),
269            path: Some(path.to_string()),
270            fields,
271            raw_lines: info.lines().map(str::to_string).collect(),
272        },
273    }
274}
275
276fn nvidia_proc_fields(info: &str) -> BTreeMap<String, String> {
277    info.lines()
278        .filter_map(|line| {
279            let (key, value) = line.split_once(':')?;
280            let key = key.trim();
281            if key.is_empty() {
282                return None;
283            }
284            Some((key.to_string(), value.trim().to_string()))
285        })
286        .collect()
287}
288
289fn take_matching_nvidia_probe(
290    probes: &mut Vec<NvidiaProcProbe>,
291    vendor_uuid: Option<&str>,
292    pci_bdf: Option<&str>,
293) -> Option<HostGpuProbe> {
294    let index = probes.iter().position(|probe| {
295        vendor_uuid.is_some_and(|uuid| probe.vendor_uuid.as_deref() == Some(uuid))
296            || pci_bdf.is_some_and(|bdf| probe.pci_bdf.as_deref() == Some(bdf))
297    })?;
298    Some(probes.remove(index).probe)
299}
300
301fn detect_cuda_profile(gpus: &[HostGpuProfile]) -> Option<HostCudaProfile> {
302    let mut toolkit_majors = env_u32_set("MESH_LLM_CUDA_TOOLKIT_MAJORS");
303    if let Some(major) = env_u32("MESH_LLM_CUDA_TOOLKIT_MAJOR") {
304        toolkit_majors.insert(major);
305    }
306    if toolkit_majors.is_empty() {
307        toolkit_majors.extend(cuda_majors_from_nvidia_smi());
308    }
309    let mut gpu_arches = env_string_set("MESH_LLM_CUDA_GPU_ARCHES");
310    gpu_arches.extend(gpus.iter().filter_map(|gpu| gpu.cuda_sm.clone()));
311    let has_cuda_label = gpus.iter().any(|gpu| {
312        let label = gpu.display_name.to_ascii_lowercase();
313        label.contains("nvidia") || label.contains("cuda")
314    });
315    if toolkit_majors.is_empty() && gpu_arches.is_empty() && !has_cuda_label {
316        return None;
317    }
318    Some(HostCudaProfile {
319        toolkit_majors,
320        driver_version: std::env::var("MESH_LLM_CUDA_DRIVER_VERSION").ok(),
321        gpu_arches,
322    })
323}
324
325fn detect_rocm_profile(gpus: &[HostGpuProfile]) -> Option<HostRocmProfile> {
326    let mut gpu_arches = env_string_set("MESH_LLM_ROCM_GPU_ARCHES");
327    gpu_arches.extend(rocm::gpu_arches());
328    detect_rocm_profile_with_arches(
329        gpus,
330        gpu_arches,
331        std::env::var("MESH_LLM_ROCM_VERSION").ok(),
332    )
333}
334
335fn detect_rocm_profile_with_arches(
336    gpus: &[HostGpuProfile],
337    mut gpu_arches: BTreeSet<String>,
338    version: Option<String>,
339) -> Option<HostRocmProfile> {
340    gpu_arches.extend(gpus.iter().filter_map(|gpu| gpu.rocm_gfx.clone()));
341    let has_rocm_label = gpus.iter().any(|gpu| {
342        let label = gpu.display_name.to_ascii_lowercase();
343        label.contains("amd") || label.contains("radeon") || label.contains("rocm")
344    });
345    if gpu_arches.is_empty() && version.is_none() && !has_rocm_label {
346        return None;
347    }
348    Some(HostRocmProfile {
349        version,
350        gpu_arches,
351    })
352}
353
354fn detect_vulkan_profile() -> Option<HostVulkanProfile> {
355    let api_version = std::env::var("MESH_LLM_VULKAN_API_VERSION").ok();
356    let enabled = std::env::var("MESH_LLM_VULKAN_AVAILABLE")
357        .ok()
358        .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"));
359    if enabled || api_version.is_some() || command_output("vulkaninfo", &["--summary"]).is_some() {
360        return Some(HostVulkanProfile { api_version });
361    }
362    None
363}
364
365fn apply_gpu_arch_overrides(gpus: &mut [HostGpuProfile]) {
366    let cuda_arches = env_string_vec("MESH_LLM_CUDA_GPU_ARCHES");
367    let rocm_arches = env_string_vec("MESH_LLM_ROCM_GPU_ARCHES");
368    for (index, gpu) in gpus.iter_mut().enumerate() {
369        if let Some(cuda_sm) = cuda_arches.get(index) {
370            gpu.cuda_sm = Some(cuda_sm.clone());
371        }
372        if let Some(rocm_gfx) = rocm_arches.get(index) {
373            gpu.rocm_gfx = Some(rocm_gfx.clone());
374        }
375    }
376}
377
378fn cuda_majors_from_nvidia_smi() -> BTreeSet<u32> {
379    let Some(output) = command_output("nvidia-smi", &[]) else {
380        return BTreeSet::new();
381    };
382    cuda_majors_from_nvidia_smi_output(&output)
383}
384
385fn cuda_majors_from_nvidia_smi_output(output: &str) -> BTreeSet<u32> {
386    let mut majors = BTreeSet::new();
387    for token in output.split_whitespace() {
388        if let Some(major) = cuda_major_from_token(token) {
389            majors.insert(major);
390        }
391    }
392    for line in output.lines() {
393        for marker in ["CUDA Version:", "CUDA UMD Version:"] {
394            if let Some((_, version)) = line.split_once(marker)
395                && let Some(major) = leading_major_version(version)
396            {
397                majors.insert(major);
398            }
399        }
400    }
401    majors
402}
403
404fn cuda_major_from_token(token: &str) -> Option<u32> {
405    token
406        .strip_prefix("CUDA")?
407        .trim_start_matches("Version:")
408        .trim_matches(|ch: char| !ch.is_ascii_digit())
409        .split('.')
410        .next()
411        .and_then(|value| value.parse::<u32>().ok())
412}
413
414fn leading_major_version(value: &str) -> Option<u32> {
415    value
416        .trim()
417        .trim_start_matches(|ch: char| !ch.is_ascii_digit())
418        .split('.')
419        .next()
420        .and_then(|value| value.parse::<u32>().ok())
421}
422
423fn gpu_labels() -> Vec<String> {
424    let mut labels = Vec::new();
425    append_command_lines(&mut labels, "vulkaninfo", &["--summary"]);
426    append_platform_gpu_labels(&mut labels);
427    labels.sort();
428    labels.dedup();
429    labels
430}
431
432#[cfg(target_os = "linux")]
433fn append_platform_gpu_labels(labels: &mut Vec<String>) {
434    append_command_lines(labels, "lspci", &[]);
435}
436
437#[cfg(target_os = "linux")]
438fn linux_nvidia_proc_information_entries() -> Vec<NvidiaProcInformationEntry> {
439    let Ok(entries) = std::fs::read_dir("/proc/driver/nvidia/gpus") else {
440        return Vec::new();
441    };
442    entries
443        .flatten()
444        .filter_map(|entry| {
445            let path = entry.path().join("information");
446            let info = std::fs::read_to_string(&path).ok()?;
447            Some(NvidiaProcInformationEntry {
448                path: path.display().to_string(),
449                info,
450            })
451        })
452        .collect()
453}
454
455#[cfg(not(target_os = "linux"))]
456fn linux_nvidia_proc_information_entries() -> Vec<NvidiaProcInformationEntry> {
457    Vec::new()
458}
459
460#[cfg(target_os = "windows")]
461fn append_platform_gpu_labels(labels: &mut Vec<String>) {
462    append_command_lines(
463        labels,
464        "powershell",
465        &[
466            "-NoProfile",
467            "-Command",
468            "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name",
469        ],
470    );
471}
472
473#[cfg(target_os = "macos")]
474fn append_platform_gpu_labels(labels: &mut Vec<String>) {
475    append_command_lines(labels, "system_profiler", &["SPDisplaysDataType"]);
476}
477
478#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
479fn append_platform_gpu_labels(_labels: &mut Vec<String>) {}
480
481fn append_command_lines(labels: &mut Vec<String>, program: &str, args: &[&str]) {
482    let Some(output) = command_output(program, args) else {
483        return;
484    };
485    labels.extend(gpu_labels_from_command_output(program, args, &output));
486}
487
488fn gpu_labels_from_command_output(program: &str, args: &[&str], output: &str) -> Vec<String> {
489    match (program, args) {
490        ("nvidia-smi", ["-L"]) => output
491            .lines()
492            .map(str::trim)
493            .filter(|line| line.starts_with("GPU ") && line.contains(':'))
494            .map(str::to_string)
495            .collect(),
496        ("vulkaninfo", ["--summary"]) => vulkaninfo_device_names(output),
497        ("lspci", []) => output
498            .lines()
499            .map(str::trim)
500            .filter(|line| looks_like_display_controller(line))
501            .map(str::to_string)
502            .collect(),
503        _ => output
504            .lines()
505            .map(str::trim)
506            .filter(|line| looks_like_gpu_label(line))
507            .map(str::to_string)
508            .collect(),
509    }
510}
511
512fn vulkaninfo_device_names(output: &str) -> Vec<String> {
513    output
514        .lines()
515        .map(str::trim)
516        .filter_map(|line| line.strip_prefix("deviceName"))
517        .filter_map(|line| line.split_once('=').map(|(_, value)| value.trim()))
518        .filter(|value| !value.is_empty())
519        .filter(|value| !looks_like_software_vulkan_adapter(value))
520        .map(str::to_string)
521        .collect()
522}
523
524fn looks_like_software_vulkan_adapter(value: &str) -> bool {
525    let label = value.to_ascii_lowercase();
526    [
527        "llvmpipe",
528        "swiftshader",
529        "lavapipe",
530        "softpipe",
531        "software rasterizer",
532    ]
533    .iter()
534    .any(|marker| label.contains(marker))
535}
536
537fn looks_like_display_controller(line: &str) -> bool {
538    let label = line.to_ascii_lowercase();
539    (label.contains("vga compatible controller")
540        || label.contains("3d controller")
541        || label.contains("display controller"))
542        && looks_like_gpu_label(line)
543}
544
545fn command_output(program: &str, args: &[&str]) -> Option<String> {
546    let output = Command::new(program).args(args).output().ok()?;
547    output
548        .status
549        .success()
550        .then(|| String::from_utf8(output.stdout).ok())
551        .flatten()
552}
553
554fn looks_like_gpu_label(line: &str) -> bool {
555    let label = line.to_ascii_lowercase();
556    label.contains("gpu")
557        || label.contains("nvidia")
558        || label.contains("cuda")
559        || label.contains("amd")
560        || label.contains("radeon")
561        || label.contains("rocm")
562        || label.contains("vulkan")
563        || label.contains("metal")
564}
565
566fn insert_label_flavors(flavors: &mut BTreeSet<NativeRuntimeBackendKind>, label: &str) {
567    let label = label.to_ascii_lowercase();
568    if label.contains("cuda") || label.contains("nvidia") {
569        flavors.insert(NativeRuntimeBackendKind::Cuda);
570    }
571    if label.contains("rocm")
572        || label.contains("hip")
573        || label.contains("amd")
574        || label.contains("radeon")
575    {
576        flavors.insert(NativeRuntimeBackendKind::Rocm);
577    }
578    if label.contains("vulkan") {
579        flavors.insert(NativeRuntimeBackendKind::Vulkan);
580    }
581}
582
583fn env_u32(name: &str) -> Option<u32> {
584    std::env::var(name).ok()?.parse().ok()
585}
586
587fn env_u32_set(name: &str) -> BTreeSet<u32> {
588    env_string_vec(name)
589        .into_iter()
590        .filter_map(|value| value.parse().ok())
591        .collect()
592}
593
594fn env_string_set(name: &str) -> BTreeSet<String> {
595    env_string_vec(name).into_iter().collect()
596}
597
598fn env_string_vec(name: &str) -> Vec<String> {
599    std::env::var(name)
600        .ok()
601        .map(|value| {
602            value
603                .split(',')
604                .map(str::trim)
605                .filter(|value| !value.is_empty())
606                .map(ToOwned::to_owned)
607                .collect()
608        })
609        .unwrap_or_default()
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615
616    struct EnvVarGuard {
617        key: &'static str,
618        previous: Option<String>,
619    }
620
621    impl EnvVarGuard {
622        fn clear(key: &'static str) -> Self {
623            let previous = std::env::var(key).ok();
624            // SAFETY: this test module only mutates these override vars inside scoped guards.
625            unsafe { std::env::remove_var(key) };
626            Self { key, previous }
627        }
628    }
629
630    impl Drop for EnvVarGuard {
631        fn drop(&mut self) {
632            match &self.previous {
633                // SAFETY: restore the scoped test mutation before the guard leaves scope.
634                Some(value) => unsafe { std::env::set_var(self.key, value) },
635                // SAFETY: restore the scoped test mutation before the guard leaves scope.
636                None => unsafe { std::env::remove_var(self.key) },
637            }
638        }
639    }
640
641    fn profile(label: &str) -> HostGpuProfile {
642        HostGpuProfile {
643            display_name: label.to_string(),
644            backend_device: None,
645            stable_id: None,
646            vram_bytes: None,
647            unified_memory: false,
648            probe: None,
649            cuda_sm: None,
650            rocm_gfx: None,
651        }
652    }
653
654    struct ExpectedNvidiaProcGpu<'a> {
655        display_name: &'a str,
656        backend_device: &'a str,
657        cuda_sm: &'a str,
658        stable_id: &'a str,
659        probe_path: &'a str,
660        irq: &'a str,
661        dma_mask: &'a str,
662    }
663
664    fn assert_nvidia_proc_gpu(gpu: &HostGpuProfile, expected: ExpectedNvidiaProcGpu<'_>) {
665        assert_eq!(gpu.display_name, expected.display_name);
666        assert_eq!(gpu.backend_device.as_deref(), Some(expected.backend_device));
667        assert_eq!(gpu.cuda_sm.as_deref(), Some(expected.cuda_sm));
668        assert_eq!(gpu.stable_id.as_deref(), Some(expected.stable_id));
669        let probe = gpu
670            .probe
671            .as_ref()
672            .unwrap_or_else(|| panic!("{} probe details", expected.display_name));
673        assert_eq!(probe.source, "linux_nvidia_proc");
674        assert_eq!(probe.path.as_deref(), Some(expected.probe_path));
675        assert_eq!(
676            probe.fields.get("IRQ").map(String::as_str),
677            Some(expected.irq)
678        );
679        assert_eq!(
680            probe.fields.get("DMA Mask").map(String::as_str),
681            Some(expected.dma_mask)
682        );
683    }
684
685    #[test]
686    fn nvidia_labels_enable_cuda() {
687        let flavors = detected_native_runtime_flavors(
688            &[profile("NVIDIA GeForce RTX 4090")],
689            None,
690            None,
691            None,
692        );
693
694        assert!(flavors.contains(&NativeRuntimeBackendKind::Cpu));
695        assert!(flavors.contains(&NativeRuntimeBackendKind::Cuda));
696    }
697
698    #[test]
699    fn amd_labels_enable_rocm() {
700        let flavors =
701            detected_native_runtime_flavors(&[profile("AMD Radeon PRO W7900")], None, None, None);
702
703        assert!(flavors.contains(&NativeRuntimeBackendKind::Rocm));
704    }
705
706    #[test]
707    fn kfd_architecture_evidence_enables_rocm_without_inventory_synthesis() {
708        let profile =
709            detect_rocm_profile_with_arches(&[], BTreeSet::from(["gfx942".to_string()]), None)
710                .expect("KFD architecture should enable a ROCm runtime profile");
711
712        assert_eq!(profile.gpu_arches, BTreeSet::from(["gfx942".to_string()]));
713    }
714
715    #[test]
716    fn mi300x_kfd_evidence_selects_rocm_over_cpu_runtime() {
717        use mesh_llm_native_runtime::{
718            NativeRuntimeArtifact, NativeRuntimeBackend, NativeRuntimePlatform, RuntimeSelection,
719            select_native_runtime_from_artifacts,
720        };
721
722        let rocm =
723            detect_rocm_profile_with_arches(&[], BTreeSet::from(["gfx942".to_string()]), None)
724                .expect("MI300X KFD evidence should produce a ROCm profile");
725        let runtime_profile = HostRuntimeProfile {
726            os: "linux".to_string(),
727            arch: "x86_64".to_string(),
728            target_triple: None,
729            available_flavors: detected_native_runtime_flavors(&[], None, Some(&rocm), None),
730            gpus: Vec::new(),
731            cuda: None,
732            rocm: Some(rocm),
733            vulkan: None,
734        };
735        let artifact = |id: &str, backend: NativeRuntimeBackend| NativeRuntimeArtifact {
736            id: id.to_string(),
737            mesh_version: Some("test".to_string()),
738            skippy_abi: "test-abi".to_string(),
739            platform: NativeRuntimePlatform {
740                os: "linux".to_string(),
741                arch: "x86_64".to_string(),
742                target: None,
743            },
744            backend,
745            rank: 0,
746            libraries: vec!["lib/libmeshllm_ffi.so".to_string()],
747            files: Default::default(),
748            tools: Default::default(),
749            url: None,
750            sha256: None,
751            signature: None,
752        };
753        let artifacts = vec![
754            artifact("runtime-cpu", NativeRuntimeBackend::cpu()),
755            artifact(
756                "runtime-rocm",
757                NativeRuntimeBackend::rocm(vec!["gfx942".to_string()]),
758            ),
759        ];
760
761        let selected = select_native_runtime_from_artifacts(
762            &artifacts,
763            &runtime_profile,
764            "test",
765            Some("test-abi"),
766            &RuntimeSelection::Recommended,
767        )
768        .expect("MI300X should select the compatible ROCm runtime");
769
770        assert_eq!(
771            selected.artifact.backend.kind,
772            NativeRuntimeBackendKind::Rocm
773        );
774    }
775
776    #[test]
777    fn fallback_profiles_do_not_synthesize_backend_ordinals() {
778        let gpu = fallback_gpu_profile_from_label("AMD Radeon PRO W7900".to_string());
779
780        assert_eq!(gpu.display_name, "AMD Radeon PRO W7900");
781        assert_eq!(gpu.backend_device, None);
782        assert_eq!(gpu.stable_id, None);
783        assert!(
784            detected_native_runtime_flavors(&[gpu], None, None, None)
785                .contains(&NativeRuntimeBackendKind::Rocm)
786        );
787    }
788
789    #[test]
790    fn parses_cuda_version_label_from_nvidia_smi_banner() {
791        let output = "| NVIDIA-SMI 595.78 Driver Version: 595.78 CUDA Version: 13.2 |\n";
792
793        assert_eq!(
794            cuda_majors_from_nvidia_smi_output(output),
795            BTreeSet::from([13])
796        );
797    }
798
799    #[test]
800    fn parses_cuda_umd_version_label_from_nvidia_smi_banner() {
801        let output = "| NVIDIA-SMI 610.43.02 KMD Version: 610.43.02 CUDA UMD Version: 13.3 |\n";
802
803        assert_eq!(
804            cuda_majors_from_nvidia_smi_output(output),
805            BTreeSet::from([13])
806        );
807    }
808
809    #[test]
810    fn parses_nvidia_compute_caps_as_cuda_arches() {
811        let output = "\
8120, 12.0
8131, 8.6
814";
815
816        assert_eq!(
817            nvidia_compute_caps_by_index(output),
818            BTreeMap::from([(0, "120".to_string()), (1, "86".to_string())])
819        );
820    }
821
822    #[test]
823    fn empty_gpu_arch_overrides_preserve_detected_arches() {
824        let _cuda_arches = EnvVarGuard::clear("MESH_LLM_CUDA_GPU_ARCHES");
825        let _rocm_arches = EnvVarGuard::clear("MESH_LLM_ROCM_GPU_ARCHES");
826        let mut gpus = vec![HostGpuProfile {
827            cuda_sm: Some("120".to_string()),
828            rocm_gfx: Some("gfx1200".to_string()),
829            ..profile("NVIDIA GeForce RTX 5090")
830        }];
831
832        apply_gpu_arch_overrides(&mut gpus);
833
834        assert_eq!(gpus[0].cuda_sm.as_deref(), Some("120"));
835        assert_eq!(gpus[0].rocm_gfx.as_deref(), Some("gfx1200"));
836    }
837
838    #[test]
839    fn vulkaninfo_labels_keep_only_device_names() {
840        let output = "\
841VULKANINFO
842Vulkan Instance Version: 1.4.321
843GPU0:
844deviceName         = NVIDIA Tegra Orin (nvgpu)
845deviceType         = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU
846driverName         = NVIDIA
847";
848
849        assert_eq!(
850            gpu_labels_from_command_output("vulkaninfo", &["--summary"], output),
851            vec!["NVIDIA Tegra Orin (nvgpu)".to_string()]
852        );
853    }
854
855    #[test]
856    fn vulkaninfo_labels_ignore_software_adapters() {
857        let output = "\
858GPU0:
859deviceName         = llvmpipe (LLVM 18.1.8, 256 bits)
860GPU1:
861deviceName         = SwiftShader Device (Subzero)
862GPU2:
863deviceName         = AMD Radeon PRO W7900
864";
865
866        assert_eq!(
867            gpu_labels_from_command_output("vulkaninfo", &["--summary"], output),
868            vec!["AMD Radeon PRO W7900".to_string()]
869        );
870    }
871
872    #[test]
873    fn lspci_labels_ignore_nvidia_pci_bridges() {
874        let output = "\
8750004:00:00.0 PCI bridge: NVIDIA Corporation Device 229c (rev a1)
8760008:01:00.0 3D controller: NVIDIA Corporation GA102GL [RTX A6000] (rev a1)
877";
878
879        assert_eq!(
880            gpu_labels_from_command_output("lspci", &[], output),
881            vec![
882                "0008:01:00.0 3D controller: NVIDIA Corporation GA102GL [RTX A6000] (rev a1)"
883                    .to_string()
884            ]
885        );
886    }
887
888    #[test]
889    fn nvidia_probe_results_merge_with_fallback_labels() {
890        let nvidia_smi = "\
891GPU 0: NVIDIA GeForce RTX 5090 (UUID: GPU-80ded6bd-1a89-2628-3d94-902187dbab1d)
892";
893        let lspci = "\
89401:00.0 VGA compatible controller: NVIDIA Corporation GB202 [GeForce RTX 5090] (rev a1)
895";
896        let compute_caps = BTreeMap::from([(0, "120".to_string())]);
897        let nvidia_gpus =
898            nvidia_gpu_profiles_from_probe_outputs(nvidia_smi, &compute_caps, lspci, &[]);
899        let fallback_gpus = vec![
900            profile("NVIDIA Corporation GB202 [GeForce RTX 5090]"),
901            profile("AMD Radeon PRO W7900"),
902        ];
903        let merged = merge_nvidia_and_fallback_gpus(nvidia_gpus, fallback_gpus);
904
905        let names = merged
906            .iter()
907            .map(|gpu| gpu.display_name.as_str())
908            .collect::<Vec<_>>();
909        assert_eq!(names, ["NVIDIA GeForce RTX 5090", "AMD Radeon PRO W7900"]);
910        assert_eq!(merged[0].cuda_sm.as_deref(), Some("120"));
911    }
912
913    #[test]
914    fn nvidia_proc_details_are_nested_under_matching_gpus() {
915        let nvidia_smi = "\
916GPU 0: NVIDIA GeForce RTX 5090 (UUID: GPU-80ded6bd-1a89-2628-3d94-902187dbab1d)
917GPU 1: NVIDIA GeForce RTX 3080 (UUID: GPU-6b7fe24c-5f15-4ac5-88d6-c8934135a4ea)
918";
919        let lspci = "\
92001:00.0 VGA compatible controller: NVIDIA Corporation GB202 [GeForce RTX 5090] (rev a1)
92106:00.0 VGA compatible controller: NVIDIA Corporation GA102 [GeForce RTX 3080] (rev a1)
922";
923        let proc_entries = vec![
924            (
925                "/proc/driver/nvidia/gpus/0000:01:00.0/information",
926                "\
927Model: \t\t NVIDIA GeForce RTX 5090
928IRQ:   \t\t 16
929GPU UUID: \t GPU-80ded6bd-1a89-2628-3d94-902187dbab1d
930Video BIOS: \t 98.02.2e.40.7f
931Bus Type: \t PCIe
932DMA Size: \t 52 bits
933DMA Mask: \t 0xfffffffffffff
934Bus Location: \t 0000:01:00.0
935Device Minor: \t 0
936GPU Firmware: \t 610.43.02
937GPU Excluded:\t No
938",
939            ),
940            (
941                "/proc/driver/nvidia/gpus/0000:06:00.0/information",
942                "\
943Model: \t\t NVIDIA GeForce RTX 3080
944IRQ:   \t\t 184
945GPU UUID: \t GPU-6b7fe24c-5f15-4ac5-88d6-c8934135a4ea
946Video BIOS: \t 94.02.42.80.31
947Bus Type: \t PCIe
948DMA Size: \t 47 bits
949DMA Mask: \t 0x7fffffffffff
950Bus Location: \t 0000:06:00.0
951Device Minor: \t 1
952GPU Firmware: \t 610.43.02
953GPU Excluded:\t No
954",
955            ),
956        ];
957
958        let compute_caps = BTreeMap::from([(0, "120".to_string()), (1, "86".to_string())]);
959        let gpus =
960            nvidia_gpu_profiles_from_probe_outputs(nvidia_smi, &compute_caps, lspci, &proc_entries);
961
962        assert_eq!(gpus.len(), 2);
963        assert_nvidia_proc_gpu(
964            &gpus[0],
965            ExpectedNvidiaProcGpu {
966                display_name: "NVIDIA GeForce RTX 5090",
967                backend_device: "CUDA0",
968                cuda_sm: "120",
969                stable_id: "uuid:GPU-80ded6bd-1a89-2628-3d94-902187dbab1d",
970                probe_path: "/proc/driver/nvidia/gpus/0000:01:00.0/information",
971                irq: "16",
972                dma_mask: "0xfffffffffffff",
973            },
974        );
975        assert_nvidia_proc_gpu(
976            &gpus[1],
977            ExpectedNvidiaProcGpu {
978                display_name: "NVIDIA GeForce RTX 3080",
979                backend_device: "CUDA1",
980                cuda_sm: "86",
981                stable_id: "uuid:GPU-6b7fe24c-5f15-4ac5-88d6-c8934135a4ea",
982                probe_path: "/proc/driver/nvidia/gpus/0000:06:00.0/information",
983                irq: "184",
984                dma_mask: "0x7fffffffffff",
985            },
986        );
987
988        let names: Vec<&str> = gpus.iter().map(|gpu| gpu.display_name.as_str()).collect();
989        assert!(!names.iter().any(|name| name.contains("DMA Mask")));
990        assert!(!names.iter().any(|name| name.contains("IRQ")));
991        assert!(!names.iter().any(|name| name.contains("Bus Location")));
992    }
993}