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 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 Some(value) => unsafe { std::env::set_var(self.key, value) },
635 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}