1use crate::installers::common::utils::{
18 command_exists, print_error, print_step, print_success, print_warning,
19};
20use std::fmt;
21
22#[derive(Debug, Clone)]
28pub struct GpuArchInfo {
29 pub gpu_arch: String,
31 pub hsa_override_gfx_version: String,
33}
34
35impl Default for GpuArchInfo {
36 fn default() -> Self {
37 Self {
38 gpu_arch: "gfx1100".to_string(),
39 hsa_override_gfx_version: "11.0.0".to_string(),
40 }
41 }
42}
43
44impl GpuArchInfo {
45 pub fn from_arch(arch: &str) -> Self {
49 let hsa_version = match arch {
50 "gfx1100" => "11.0.0",
51 "gfx1101" => "11.0.1",
52 "gfx1102" => "11.0.2",
53 "gfx1200" => "12.0.0",
54 "gfx1201" => "12.0.1",
55 "gfx1030" => "10.3.0",
56 "gfx1031" => "10.3.1",
57 "gfx1032" => "10.3.2",
58 _ => "11.0.0", };
60 Self {
61 gpu_arch: arch.to_string(),
62 hsa_override_gfx_version: hsa_version.to_string(),
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct GpuFilterResult {
70 pub discrete_gpu_indices: Vec<u32>,
72 pub hip_visible_devices: String,
74 pub gpu_count: usize,
76}
77
78#[derive(Debug, Clone)]
80pub struct SystemDependency {
81 pub package: &'static str,
83 pub description: &'static str,
85}
86
87impl SystemDependency {
88 pub fn required_packages() -> Vec<&'static str> {
92 vec![
93 "build-essential",
94 "cmake",
95 "git",
96 "python3-dev",
97 "python3-pip",
98 "libnuma-dev",
99 "pciutils",
100 "mesa-utils",
101 "clinfo",
102 ]
103 }
104}
105
106impl fmt::Display for SystemDependency {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 write!(f, "{} ({})", self.package, self.description)
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct DependencyCheckResult {
115 pub package: String,
117 pub installed: bool,
119 pub mapped_name: String,
121}
122
123#[derive(Debug, Clone)]
125pub struct EnvFileResult {
126 pub success: bool,
128 pub path: Option<String>,
130 pub errors: Vec<String>,
132}
133
134impl EnvFileResult {
135 pub fn success(path: String) -> Self {
137 Self {
138 success: true,
139 path: Some(path),
140 errors: Vec::new(),
141 }
142 }
143
144 pub fn failure(error: String) -> Self {
146 Self {
147 success: false,
148 path: None,
149 errors: vec![error],
150 }
151 }
152}
153
154pub fn detect_discrete_gpus() -> GpuFilterResult {
168 let mut discrete_indices: Vec<u32> = Vec::new();
169
170 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
172 if command_exists(&rocminfo_path) || command_exists("rocminfo") {
173 discrete_indices = detect_gpus_from_rocminfo();
174 }
175
176 if discrete_indices.is_empty() {
178 discrete_indices = detect_gpus_from_lspci();
179 }
180
181 if discrete_indices.is_empty() {
183 discrete_indices = detect_gpus_from_render_nodes();
184 }
185
186 if discrete_indices.is_empty() {
188 discrete_indices.push(0);
189 }
190
191 let gpu_count = discrete_indices.len();
192 let hip_visible_devices = discrete_indices
193 .iter()
194 .map(|i| i.to_string())
195 .collect::<Vec<_>>()
196 .join(",");
197
198 GpuFilterResult {
199 discrete_gpu_indices: discrete_indices,
200 hip_visible_devices,
201 gpu_count,
202 }
203}
204
205fn detect_gpus_from_rocminfo() -> Vec<u32> {
207 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
208 let output = match std::process::Command::new(&rocminfo_path).output() {
209 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
210 _ => return Vec::new(),
211 };
212
213 let mut discrete_indices: Vec<u32> = Vec::new();
214 let mut current_gpu_id: Option<u32> = None;
215 let mut is_igpu = false;
216 let mut gpu_agent_counter = 0u32; for line in output.lines() {
219 let trimmed = line.trim();
220
221 if trimmed.starts_with("Agent") && trimmed.contains(char::is_numeric) {
223 }
225
226 if trimmed.starts_with("GPU ID:") {
227 if let Some(id) = current_gpu_id {
229 if !is_igpu && !discrete_indices.contains(&id) {
230 discrete_indices.push(id);
231 }
232 }
233
234 current_gpu_id = trimmed
236 .strip_prefix("GPU ID:")
237 .and_then(|s| s.trim().parse::<u32>().ok());
238 is_igpu = false;
239 } else if trimmed.starts_with("Marketing Name:") {
240 let marketing_name = trimmed
241 .strip_prefix("Marketing Name:")
242 .map(|s| s.trim())
243 .unwrap_or("");
244
245 if is_integrated_gpu_name(marketing_name) {
247 is_igpu = true;
248 }
249
250 if current_gpu_id.is_none() {
252 }
255 } else if trimmed.starts_with("Device Type:") {
256 let device_type = trimmed
257 .strip_prefix("Device Type:")
258 .map(|s| s.trim())
259 .unwrap_or("");
260
261 if device_type.contains("CPU") {
263 is_igpu = true;
264 }
265
266 if let Some(id) = current_gpu_id {
268 if !is_igpu && !discrete_indices.contains(&id) {
269 discrete_indices.push(id);
270 }
271 } else if device_type.contains("GPU") {
272 if !is_igpu && !discrete_indices.contains(&gpu_agent_counter) {
274 discrete_indices.push(gpu_agent_counter);
275 }
276 gpu_agent_counter += 1;
277 }
278 }
279 }
280
281 if let Some(id) = current_gpu_id {
283 if !is_igpu && !discrete_indices.contains(&id) {
284 discrete_indices.push(id);
285 }
286 }
287
288 discrete_indices.sort();
289 discrete_indices.dedup();
290 discrete_indices
291}
292
293fn detect_gpus_from_lspci() -> Vec<u32> {
295 if !command_exists("lspci") {
296 return Vec::new();
297 }
298
299 let output = match std::process::Command::new("lspci").output() {
300 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
301 _ => return Vec::new(),
302 };
303
304 let mut discrete_count = 0u32;
305 for line in output.lines() {
306 let lower = line.to_lowercase();
307 if !((lower.contains("amd")
308 || lower.contains("radeon")
309 || lower.contains("advanced micro devices"))
310 && (lower.contains("vga") || lower.contains("3d") || lower.contains("display")))
311 {
312 continue;
313 }
314 if is_integrated_gpu_name(line) {
316 continue;
317 }
318 discrete_count += 1;
319 }
320
321 if discrete_count > 0 {
322 (0..discrete_count).collect()
323 } else {
324 Vec::new()
325 }
326}
327
328fn detect_gpus_from_render_nodes() -> Vec<u32> {
331 let render_entries: Vec<_> = std::fs::read_dir("/dev/dri")
332 .map(|entries| {
333 entries
334 .filter_map(|e| e.ok())
335 .filter(|e| e.file_name().to_string_lossy().starts_with("render"))
336 .collect()
337 })
338 .unwrap_or_default();
339
340 if render_entries.is_empty() {
341 return Vec::new();
342 }
343
344 let discrete_count = render_entries
346 .iter()
347 .filter(|entry| {
348 let name = entry.file_name().to_string_lossy().to_string();
349 let node_num: String = name
351 .trim_start_matches("render")
352 .chars()
353 .take_while(|c| c.is_ascii_digit())
354 .collect();
355 if node_num.is_empty() {
356 return true; }
358
359 let sysfs_path = format!("/sys/class/drm/card{}-render/device/uevent", node_num);
361 if let Ok(uevent) = std::fs::read_to_string(&sysfs_path) {
362 for line in uevent.lines() {
364 if line.starts_with("PCI_ID=") {
365 return true;
367 }
368 }
369 }
370
371 let card_sysfs = format!("/sys/class/drm/card{}/device/device", node_num);
373 if let Ok(pci_id) = std::fs::read_to_string(&card_sysfs) {
374 let pci_id = pci_id.trim();
375 if pci_id == "0x164e" {
377 return false; }
379 }
380
381 true })
383 .count();
384
385 if discrete_count > 0 {
386 (0..discrete_count as u32).collect()
387 } else if !render_entries.is_empty() {
388 (0..render_entries.len() as u32).collect()
390 } else {
391 Vec::new()
392 }
393}
394
395pub fn is_integrated_gpu_name(name: &str) -> bool {
400 crate::gpu::is_integrated_gpu_name(name)
401}
402
403pub fn detect_correct_gpu_arch(marketing_name: Option<&str>) -> GpuArchInfo {
416 let name = match marketing_name {
417 Some(n) => n,
418 None => return GpuArchInfo::default(),
419 };
420
421 let lower = name.to_lowercase();
422
423 if is_integrated_gpu_name(name) {
425 return GpuArchInfo::default();
426 }
427
428 let arch = if lower.contains("7900 xtx")
431 || lower.contains("7900xtx")
432 || lower.contains("7900 xt")
433 || lower.contains("7900xt")
434 || lower.contains("7900 gre")
435 || lower.contains("7900gre")
436 {
437 "gfx1100"
438 } else if lower.contains("7800 xt")
439 || lower.contains("7800xt")
440 || lower.contains("7800 gre")
441 || lower.contains("7800gre")
442 || lower.contains("7700 xt")
443 || lower.contains("7700xt")
444 {
445 "gfx1101"
446 } else if lower.contains("7600 xt") || lower.contains("7600xt") || lower.contains("7600") {
447 "gfx1102"
448 } else if lower.contains("9070 xt") || lower.contains("9070xt") {
449 "gfx1200"
450 } else {
451 "gfx1100" };
453
454 GpuArchInfo::from_arch(arch)
455}
456
457pub fn check_system_dependency(package: &str) -> DependencyCheckResult {
470 use crate::installers::common::distro::DistroFacade;
471 use crate::installers::common::package_mappings::map_package_name;
472
473 let distro = DistroFacade::detect();
474 let pkg_mgr = distro.package_manager();
475 let mapped = map_package_name(package, pkg_mgr);
476 let installed = check_package_installed(&mapped);
477
478 DependencyCheckResult {
479 package: package.to_string(),
480 installed,
481 mapped_name: mapped,
482 }
483}
484
485pub fn check_all_dependencies() -> Vec<DependencyCheckResult> {
489 SystemDependency::required_packages()
490 .into_iter()
491 .map(check_system_dependency)
492 .collect()
493}
494
495fn check_package_installed(package: &str) -> bool {
497 if command_exists("dpkg-query") {
499 let output = std::process::Command::new("dpkg-query")
500 .args(["-W", "-f=${Status}", package])
501 .output();
502 if let Ok(o) = output {
503 let stdout = String::from_utf8_lossy(&o.stdout);
504 if stdout.contains("install ok installed") {
505 return true;
506 }
507 }
508 }
509
510 if command_exists("rpm") {
512 let output = std::process::Command::new("rpm")
513 .args(["-q", package])
514 .output();
515 if let Ok(o) = output {
516 if o.status.success() {
517 return true;
518 }
519 }
520 }
521
522 if command_exists("pacman") {
524 let output = std::process::Command::new("pacman")
525 .args(["-Q", package])
526 .output();
527 if let Ok(o) = output {
528 if o.status.success() {
529 return true;
530 }
531 }
532 }
533
534 false
535}
536
537pub fn create_env_file(
550 hip_visible_devices: &str,
551 rocm_path: &str,
552 rocm_version: &str,
553 rocm_channel: &str,
554 gpu_arch: &str,
555 hsa_override_gfx_version: &str,
556 python_bin: &str,
557) -> EnvFileResult {
558 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
559 let env_path = format!("{home}/.mlstack_env");
560 let fish_env_path = format!("{home}/.config/fish/conf.d/mlstack_env.fish");
561
562 let content = generate_env_file_content(
563 hip_visible_devices,
564 rocm_path,
565 rocm_version,
566 rocm_channel,
567 gpu_arch,
568 hsa_override_gfx_version,
569 python_bin,
570 );
571
572 if let Err(e) = std::fs::write(&env_path, &content) {
574 let msg = format!("Failed to write environment file: {e}");
575 print_error(&msg);
576 return EnvFileResult::failure(msg);
577 }
578
579 let fish_content = generate_fish_env_file_content(
581 hip_visible_devices,
582 rocm_path,
583 rocm_version,
584 rocm_channel,
585 gpu_arch,
586 hsa_override_gfx_version,
587 python_bin,
588 );
589 if let Some(parent) = std::path::Path::new(&fish_env_path).parent() {
591 let _ = std::fs::create_dir_all(parent);
592 }
593 if let Err(e) = std::fs::write(&fish_env_path, &fish_content) {
594 print_warning(&format!("Failed to write fish env file: {e}"));
596 } else {
597 print_success(&format!("Fish env file created: {fish_env_path}"));
598 }
599
600 print_success(&format!("Environment file created: {env_path}"));
601 EnvFileResult::success(env_path)
602}
603
604pub fn generate_env_file_content(
615 hip_visible_devices: &str,
616 rocm_path: &str,
617 rocm_version: &str,
618 rocm_channel: &str,
619 gpu_arch: &str,
620 hsa_override_gfx_version: &str,
621 python_bin: &str,
622) -> String {
623 let home = std::env::var("HOME").unwrap_or_else(|_| "$HOME".to_string());
624
625 format!(
626 r#"# ML Stack Environment File
627# Created by Enhanced ML Stack Environment Setup (Rust native)
628# Date: {date}
629
630# GPU Selection
631# Only set if not already set
632if [ -z "${{HIP_VISIBLE_DEVICES:-}}" ]; then export HIP_VISIBLE_DEVICES={hip_visible_devices}; fi
633if [ -z "${{CUDA_VISIBLE_DEVICES:-}}" ]; then export CUDA_VISIBLE_DEVICES={hip_visible_devices}; fi
634if [ -z "${{PYTORCH_ROCM_DEVICE:-}}" ]; then export PYTORCH_ROCM_DEVICE={hip_visible_devices}; fi
635
636# ROCm Settings
637# Only set if not already set
638if [ -z "${{ROCM_HOME:-}}" ]; then export ROCM_HOME={rocm_path}; fi
639if [ -z "${{CUDA_HOME:-}}" ]; then export CUDA_HOME={rocm_path}; fi
640if [ -z "${{ROCM_VERSION:-}}" ]; then export ROCM_VERSION={rocm_version}; fi
641if [ -z "${{ROCM_CHANNEL:-}}" ]; then export ROCM_CHANNEL={rocm_channel}; fi
642# GPU_ARCH is set based on detected hardware (corrected for rocminfo bugs)
643export GPU_ARCH={gpu_arch}
644
645# Path Settings - Hardcoded safe paths to prevent "command not found" errors
646export PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:{rocm_path}/bin:{rocm_path}/hip/bin:$PATH"
647export LD_LIBRARY_PATH="$HOME/.mlstack/libmpi-compat:$HOME/.mlstack/libmpi-compat-user-$(id -u):{rocm_path}/lib:{rocm_path}/hip/lib:{rocm_path}/opencl/lib:${{LD_LIBRARY_PATH:-}}"
648
649# Performance Settings
650# HSA_OVERRIDE_GFX_VERSION is set based on detected GPU_ARCH
651export HSA_OVERRIDE_GFX_VERSION={hsa_override_gfx_version}
652export HSA_ENABLE_SDMA=0
653export GPU_MAX_HEAP_SIZE=100
654export GPU_MAX_ALLOC_PERCENT=100
655export HSA_TOOLS_LIB=/opt/rocm/lib/rocprofiler-sdk/librocprofiler-sdk-tool.so
656
657# MIOpen Settings
658# Only set if not already set
659if [ -z "${{MIOPEN_DEBUG_CONV_IMPLICIT_GEMM:-}}" ]; then export MIOPEN_DEBUG_CONV_IMPLICIT_GEMM=1; fi
660if [ -z "${{MIOPEN_FIND_MODE:-}}" ]; then export MIOPEN_FIND_MODE=3; fi
661if [ -z "${{MIOPEN_FIND_ENFORCE:-}}" ]; then export MIOPEN_FIND_ENFORCE=3; fi
662
663# PyTorch Settings
664# Only set if not already set
665# (Stage 2: the CUDA build-arch env var is intentionally NOT exported on a
666# ROCm stack — ROCm uses PYTORCH_ROCM_ARCH. No CUDA env leakage.)
667if [ -z "${{PYTORCH_ALLOC_CONF:-}}" ]; then export PYTORCH_ALLOC_CONF="max_split_size_mb:512"; fi
668if [ -z "${{PYTORCH_HIP_ALLOC_CONF:-}}" ]; then export PYTORCH_HIP_ALLOC_CONF="max_split_size_mb:512"; fi
669if [ -z "${{VLLM_WORKER_MULTIPROC_METHOD:-}}" ]; then export VLLM_WORKER_MULTIPROC_METHOD=spawn; fi
670if [ -z "${{VLLM_ROCM_USE_AITER:-}}" ]; then export VLLM_ROCM_USE_AITER=0; fi
671if [ -z "${{MLSTACK_TRITON_HOME:-}}" ]; then export MLSTACK_TRITON_HOME="$HOME/.cache/mlstack/triton"; fi
672if [ -z "${{TRITON_HOME:-}}" ]; then export TRITON_HOME="$MLSTACK_TRITON_HOME"; fi
673if [ -z "${{TRITON_CACHE_DIR:-}}" ]; then export TRITON_CACHE_DIR="$TRITON_HOME/cache"; fi
674if [ -z "${{TRITON_DUMP_DIR:-}}" ]; then export TRITON_DUMP_DIR="$TRITON_HOME/dump"; fi
675if [ -z "${{TRITON_OVERRIDE_DIR:-}}" ]; then export TRITON_OVERRIDE_DIR="$TRITON_HOME/override"; fi
676mkdir -p "$TRITON_CACHE_DIR" "$TRITON_DUMP_DIR" "$TRITON_OVERRIDE_DIR" 2>/dev/null || true
677
678# MPI Settings
679# Only set if not already set
680# (Stage 2: the OMPI CUDA-opal support vars are intentionally NOT set — that is
681# the CUDA-built OpenMPI path; a ROCm stack uses the ROCm accelerator path.)
682if [ -z "${{OMPI_MCA_btl_openib_allow_ib:-}}" ]; then export OMPI_MCA_btl_openib_allow_ib=true; fi
683if [ -z "${{OMPI_MCA_btl_openib_warn_no_device_params_found:-}}" ]; then export OMPI_MCA_btl_openib_warn_no_device_params_found=0; fi
684if [ -z "${{OMPI_MCA_coll_hcoll_enable:-}}" ]; then export OMPI_MCA_coll_hcoll_enable=0; fi
685if [ -z "${{OMPI_MCA_pml:-}}" ]; then export OMPI_MCA_pml=ucx; fi
686if [ -z "${{OMPI_MCA_osc:-}}" ]; then export OMPI_MCA_osc=ucx; fi
687if [ -z "${{OMPI_MCA_btl:-}}" ]; then export OMPI_MCA_btl=^openib,uct; fi
688# Prevent UCX ROCm memory registration segfault on multi-GPU systems
689export UCX_MEMTYPE_CACHE=no
690export UCX_LOG_LEVEL=error
691
692# Global flags for seamless pip installs (PEP 668 override)
693export PIP_BREAK_SYSTEM_PACKAGES=1
694export UV_PIP_BREAK_SYSTEM_PACKAGES=1
695export UV_SYSTEM_PYTHON=1
696
697# Python Interpreter
698# Canonical Python binary — all ML components install to this interpreter.
699# Ensures a single unified environment regardless of how many Pythons exist.
700export MLSTACK_PYTHON_BIN={python_bin}
701export UV_PYTHON={python_bin}
702
703# Flash Attention AMD
704export FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE
705
706# ONNX Runtime Settings
707# Only add if not already in PYTHONPATH
708if ! echo "${{PYTHONPATH:-}}" | grep -q "{home}/onnxruntime_build/onnxruntime/build/Linux/Release"; then
709 export PYTHONPATH={home}/onnxruntime_build/onnxruntime/build/Linux/Release:${{PYTHONPATH:-}}
710fi
711"#,
712 date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
713 hip_visible_devices = hip_visible_devices,
714 rocm_path = rocm_path,
715 rocm_version = rocm_version,
716 rocm_channel = rocm_channel,
717 gpu_arch = gpu_arch,
718 hsa_override_gfx_version = hsa_override_gfx_version,
719 home = home,
720 python_bin = python_bin,
721 )
722}
723
724pub fn generate_fish_env_file_content(
731 hip_visible_devices: &str,
732 rocm_path: &str,
733 rocm_version: &str,
734 rocm_channel: &str,
735 gpu_arch: &str,
736 hsa_override_gfx_version: &str,
737 python_bin: &str,
738) -> String {
739 let home = std::env::var("HOME").unwrap_or_else(|_| "$HOME".to_string());
740
741 let onnx_path = format!("{home}/onnxruntime_build/onnxruntime/build/Linux/Release");
743
744 format!(
745 r##"# ML Stack Environment File — Fish Shell Native
746# Generated by Enhanced ML Stack Environment Setup (Rust native)
747# Date: {date}
748#
749# This file is auto-generated. Edit via rusty-stack's env setup, not by hand.
750
751# --- GPU Selection ---
752set -q HIP_VISIBLE_DEVICES; or set -gx HIP_VISIBLE_DEVICES {hip_visible_devices}
753set -q CUDA_VISIBLE_DEVICES; or set -gx CUDA_VISIBLE_DEVICES {hip_visible_devices}
754set -q PYTORCH_ROCM_DEVICE; or set -gx PYTORCH_ROCM_DEVICE {hip_visible_devices}
755
756# --- ROCm Settings ---
757set -q ROCM_HOME; or set -gx ROCM_HOME {rocm_path}
758set -q CUDA_HOME; or set -gx CUDA_HOME {rocm_path}
759set -q ROCM_VERSION; or set -gx ROCM_VERSION {rocm_version}
760set -q ROCM_CHANNEL; or set -gx ROCM_CHANNEL {rocm_channel}
761# GPU_ARCH is set based on detected hardware (corrected for rocminfo bugs)
762set -gx GPU_ARCH {gpu_arch}
763
764# --- Path Settings ---
765set -gx PATH /usr/local/bin /usr/bin /bin /usr/local/games /usr/games {rocm_path}/bin {rocm_path}/hip/bin $PATH
766set -gx LD_LIBRARY_PATH $HOME/.mlstack/libmpi-compat $HOME/.mlstack/libmpi-compat-user-(id -u) {rocm_path}/lib {rocm_path}/hip/lib {rocm_path}/opencl/lib $LD_LIBRARY_PATH
767
768# --- Performance Settings ---
769set -gx HSA_OVERRIDE_GFX_VERSION {hsa_override_gfx_version}
770set -gx HSA_ENABLE_SDMA 0
771set -gx GPU_MAX_HEAP_SIZE 100
772set -gx GPU_MAX_ALLOC_PERCENT 100
773set -gx HSA_TOOLS_LIB /opt/rocm/lib/rocprofiler-sdk/librocprofiler-sdk-tool.so
774
775# --- MIOpen Settings ---
776set -q MIOPEN_DEBUG_CONV_IMPLICIT_GEMM; or set -gx MIOPEN_DEBUG_CONV_IMPLICIT_GEMM 1
777set -q MIOPEN_FIND_MODE; or set -gx MIOPEN_FIND_MODE 3
778set -q MIOPEN_FIND_ENFORCE; or set -gx MIOPEN_FIND_ENFORCE 3
779
780# --- PyTorch Settings ---
781# (Stage 2: the CUDA build-arch env var is intentionally NOT set on a ROCm
782# stack — ROCm uses PYTORCH_ROCM_ARCH. No CUDA env leakage.)
783set -q PYTORCH_ALLOC_CONF; or set -gx PYTORCH_ALLOC_CONF "max_split_size_mb:512"
784set -q PYTORCH_HIP_ALLOC_CONF; or set -gx PYTORCH_HIP_ALLOC_CONF "max_split_size_mb:512"
785set -q VLLM_WORKER_MULTIPROC_METHOD; or set -gx VLLM_WORKER_MULTIPROC_METHOD spawn
786set -q VLLM_ROCM_USE_AITER; or set -gx VLLM_ROCM_USE_AITER 0
787set -q MLSTACK_TRITON_HOME; or set -gx MLSTACK_TRITON_HOME "$HOME/.cache/mlstack/triton"
788set -q TRITON_HOME; or set -gx TRITON_HOME $MLSTACK_TRITON_HOME
789set -q TRITON_CACHE_DIR; or set -gx TRITON_CACHE_DIR "$TRITON_HOME/cache"
790set -q TRITON_DUMP_DIR; or set -gx TRITON_DUMP_DIR "$TRITON_HOME/dump"
791set -q TRITON_OVERRIDE_DIR; or set -gx TRITON_OVERRIDE_DIR "$TRITON_HOME/override"
792mkdir -p $TRITON_CACHE_DIR $TRITON_DUMP_DIR $TRITON_OVERRIDE_DIR 2>/dev/null; or true
793
794# --- Global flags for seamless pip installs (PEP 668 override) ---
795set -q PIP_BREAK_SYSTEM_PACKAGES; or set -gx PIP_BREAK_SYSTEM_PACKAGES 1
796set -q UV_PIP_BREAK_SYSTEM_PACKAGES; or set -gx UV_PIP_BREAK_SYSTEM_PACKAGES 1
797set -gx UV_SYSTEM_PYTHON 1
798
799# --- Python Interpreter ---
800# Canonical Python binary — all ML components install to this interpreter.
801# Ensures a single unified environment regardless of how many Pythons exist.
802set -gx MLSTACK_PYTHON_BIN {python_bin}
803set -gx UV_PYTHON {python_bin}
804
805# --- Flash Attention AMD ---
806set -gx FLASH_ATTENTION_TRITON_AMD_ENABLE TRUE
807
808# --- MPI/UCX Settings ---
809# (Stage 2: the OMPI CUDA-opal support vars are intentionally NOT set —
810# CUDA-built OpenMPI path; a ROCm stack uses the ROCm accelerator path.)
811set -q OMPI_MCA_btl_openib_allow_ib; or set -gx OMPI_MCA_btl_openib_allow_ib true
812set -q OMPI_MCA_btl_openib_warn_no_device_params_found; or set -gx OMPI_MCA_btl_openib_warn_no_device_params_found 0
813set -q OMPI_MCA_coll_hcoll_enable; or set -gx OMPI_MCA_coll_hcoll_enable 0
814set -q OMPI_MCA_pml; or set -gx OMPI_MCA_pml ucx
815set -q OMPI_MCA_osc; or set -gx OMPI_MCA_osc ucx
816set -q OMPI_MCA_btl; or set -gx OMPI_MCA_btl '^openib,uct'
817set -gx UCX_MEMTYPE_CACHE no
818set -gx UCX_LOG_LEVEL error
819
820# --- ONNX Runtime Settings ---
821if not string match -q '*{onnx_path}*' "$PYTHONPATH"
822 set -gx PYTHONPATH {onnx_path} $PYTHONPATH
823end
824"##,
825 date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
826 hip_visible_devices = hip_visible_devices,
827 rocm_path = rocm_path,
828 rocm_version = rocm_version,
829 rocm_channel = rocm_channel,
830 gpu_arch = gpu_arch,
831 hsa_override_gfx_version = hsa_override_gfx_version,
832 python_bin = python_bin,
833 onnx_path = onnx_path,
834 )
835}
836
837pub fn setup_environment() -> (EnvFileResult, GpuFilterResult, GpuArchInfo) {
855 print_step("Enhanced ML Stack Environment Setup");
856
857 print_step("Detecting AMD GPUs...");
859 let gpu_filter = detect_discrete_gpus();
860 if gpu_filter.gpu_count > 0 {
861 print_success(&format!(
862 "Detected {} discrete GPU(s): {}",
863 gpu_filter.gpu_count, gpu_filter.hip_visible_devices
864 ));
865 } else {
866 print_warning("No discrete GPUs detected, using defaults");
867 }
868
869 let rocm_env = crate::installers::common::rocm_env::RocmEnv::detect();
871 let rocm_path = rocm_env
872 .path()
873 .map(|p| p.to_string_lossy().to_string())
874 .unwrap_or_else(|| "/opt/rocm".to_string());
875 let rocm_version = if rocm_env.version().is_empty() {
876 "7.2.0".to_string()
877 } else {
878 rocm_env.version().to_string()
879 };
880 let rocm_channel = std::env::var("ROCM_CHANNEL").unwrap_or_else(|_| "latest".to_string());
881
882 if rocm_env.is_detected() {
883 print_success(&format!("ROCm {rocm_version} detected at {rocm_path}"));
884 } else {
885 print_warning("ROCm not detected, using default paths");
886 }
887
888 let gpu_arch_info = detect_gpu_arch_from_system();
890
891 print_step("Creating environment file...");
893 let python_bin = crate::platform::environment::resolve_canonical_python_bin();
894 let env_result = create_env_file(
895 &gpu_filter.hip_visible_devices,
896 &rocm_path,
897 &rocm_version,
898 &rocm_channel,
899 &gpu_arch_info.gpu_arch,
900 &gpu_arch_info.hsa_override_gfx_version,
901 &python_bin,
902 );
903
904 if env_result.success {
905 print_success("Environment setup complete");
906 }
907
908 (env_result, gpu_filter, gpu_arch_info)
909}
910
911fn detect_gpu_arch_from_system() -> GpuArchInfo {
913 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
914 if !command_exists(&rocminfo_path) && !command_exists("rocminfo") {
915 return GpuArchInfo::default();
916 }
917
918 let output = match std::process::Command::new(&rocminfo_path).output() {
919 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
920 _ => return GpuArchInfo::default(),
921 };
922
923 let mut marketing_name = String::new();
924
925 for line in output.lines() {
926 let trimmed = line.trim();
927 if trimmed.starts_with("Marketing Name:") {
928 let name = trimmed
929 .strip_prefix("Marketing Name:")
930 .map(|s| s.trim())
931 .unwrap_or("");
932
933 if is_integrated_gpu_name(name) {
935 continue;
936 }
937
938 if !name.is_empty() {
939 marketing_name = name.to_string();
940 break;
941 }
942 }
943 }
944
945 detect_correct_gpu_arch(if marketing_name.is_empty() {
946 None
947 } else {
948 Some(&marketing_name)
949 })
950}
951
952#[cfg(test)]
957mod tests {
958 use super::*;
959
960 #[test]
963 fn test_gpu_arch_info_default() {
964 let info = GpuArchInfo::default();
965 assert_eq!(info.gpu_arch, "gfx1100");
966 assert_eq!(info.hsa_override_gfx_version, "11.0.0");
967 }
968
969 #[test]
970 fn test_gpu_arch_info_from_arch_all_variants() {
971 let cases = vec![
972 ("gfx1100", "11.0.0"),
973 ("gfx1101", "11.0.1"),
974 ("gfx1102", "11.0.2"),
975 ("gfx1200", "12.0.0"),
976 ("gfx1201", "12.0.1"),
977 ("gfx1030", "10.3.0"),
978 ("gfx1031", "10.3.1"),
979 ("gfx1032", "10.3.2"),
980 ("gfx9999", "11.0.0"), ];
982
983 for (arch, expected_hsa) in cases {
984 let info = GpuArchInfo::from_arch(arch);
985 assert_eq!(info.gpu_arch, arch, "GPU arch mismatch for {arch}");
986 assert_eq!(
987 info.hsa_override_gfx_version, expected_hsa,
988 "HSA version mismatch for {arch}"
989 );
990 }
991 }
992
993 #[test]
996 fn test_is_integrated_gpu_name_positive() {
997 assert!(is_integrated_gpu_name("Raphael"));
998 assert!(is_integrated_gpu_name("AMD Ryzen Graphics"));
999 assert!(is_integrated_gpu_name("Integrated Graphics"));
1000 assert!(is_integrated_gpu_name("iGPU Device"));
1001 assert!(is_integrated_gpu_name("AMD Radeon Graphics"));
1002 }
1003
1004 #[test]
1005 fn test_is_integrated_gpu_name_negative() {
1006 assert!(!is_integrated_gpu_name("Radeon RX 7900 XTX"));
1007 assert!(!is_integrated_gpu_name("Radeon RX 7800 XT"));
1008 assert!(!is_integrated_gpu_name("Radeon RX 9070 XT"));
1009 assert!(!is_integrated_gpu_name("Unknown GPU"));
1010 }
1011
1012 #[test]
1013 fn test_is_integrated_gpu_name_case_insensitive() {
1014 assert!(is_integrated_gpu_name("raphael"));
1015 assert!(is_integrated_gpu_name("INTEGRATED"));
1016 assert!(is_integrated_gpu_name("AMD RADEON GRAPHICS"));
1017 }
1018
1019 #[test]
1022 fn test_detect_discrete_gpus_returns_result() {
1023 let result = detect_discrete_gpus();
1024 assert!(!result.hip_visible_devices.is_empty());
1026 assert!(result.gpu_count >= 1);
1027 }
1028
1029 #[test]
1030 fn test_detect_gpus_from_lspci_returns_vec() {
1031 let result = detect_gpus_from_lspci();
1033 let _ = result;
1035 }
1036
1037 #[test]
1038 fn test_detect_gpus_from_render_nodes_returns_vec() {
1039 let result = detect_gpus_from_render_nodes();
1040 let _ = result;
1041 }
1042
1043 #[test]
1046 fn test_detect_correct_gpu_arch_7900_xtx() {
1047 let info = detect_correct_gpu_arch(Some("Radeon RX 7900 XTX"));
1048 assert_eq!(info.gpu_arch, "gfx1100");
1049 assert_eq!(info.hsa_override_gfx_version, "11.0.0");
1050 }
1051
1052 #[test]
1053 fn test_detect_correct_gpu_arch_7800_xt() {
1054 let info = detect_correct_gpu_arch(Some("Radeon RX 7800 XT"));
1055 assert_eq!(info.gpu_arch, "gfx1101");
1056 }
1057
1058 #[test]
1059 fn test_detect_correct_gpu_arch_7600() {
1060 let info = detect_correct_gpu_arch(Some("Radeon RX 7600"));
1061 assert_eq!(info.gpu_arch, "gfx1102");
1062 }
1063
1064 #[test]
1065 fn test_detect_correct_gpu_arch_9070_xt() {
1066 let info = detect_correct_gpu_arch(Some("Radeon RX 9070 XT"));
1067 assert_eq!(info.gpu_arch, "gfx1200");
1068 }
1069
1070 #[test]
1071 fn test_detect_correct_gpu_arch_none() {
1072 let info = detect_correct_gpu_arch(None);
1073 assert_eq!(info.gpu_arch, "gfx1100"); }
1075
1076 #[test]
1077 fn test_detect_correct_gpu_arch_unknown() {
1078 let info = detect_correct_gpu_arch(Some("Unknown GPU Model"));
1079 assert_eq!(info.gpu_arch, "gfx1100"); }
1081
1082 #[test]
1083 fn test_detect_correct_gpu_arch_integrated_skipped() {
1084 let info = detect_correct_gpu_arch(Some("AMD Ryzen Graphics"));
1085 assert_eq!(info.gpu_arch, "gfx1100"); }
1087
1088 #[test]
1089 fn test_detect_correct_gpu_arch_case_insensitive() {
1090 let info = detect_correct_gpu_arch(Some("radeon rx 7900 xtx"));
1091 assert_eq!(info.gpu_arch, "gfx1100");
1092 }
1093
1094 #[test]
1097 fn test_system_dependency_required_packages() {
1098 let packages = SystemDependency::required_packages();
1099 assert!(!packages.is_empty());
1100 assert!(packages.contains(&"git"));
1101 assert!(packages.contains(&"cmake"));
1102 assert!(packages.contains(&"build-essential"));
1103 }
1104
1105 #[test]
1106 fn test_check_system_dependency_git() {
1107 let result = check_system_dependency("git");
1108 assert_eq!(result.package, "git");
1109 assert!(result.installed, "git should be installed");
1111 }
1112
1113 #[test]
1114 fn test_check_system_dependency_nonexistent() {
1115 let result = check_system_dependency("nonexistent-pkg-xyz-12345");
1116 assert!(!result.installed);
1117 }
1118
1119 #[test]
1120 fn test_check_all_dependencies_returns_results() {
1121 let results = check_all_dependencies();
1122 assert!(!results.is_empty(), "Should check at least one dependency");
1123 assert_eq!(results.len(), SystemDependency::required_packages().len());
1124 }
1125
1126 #[test]
1129 fn test_generate_env_file_has_header() {
1130 let content = generate_env_file_content(
1131 "0",
1132 "/opt/rocm",
1133 "7.2.0",
1134 "latest",
1135 "gfx1100",
1136 "11.0.0",
1137 "python3",
1138 );
1139 assert!(content.starts_with("# ML Stack Environment File"));
1140 }
1141
1142 #[test]
1143 fn test_generate_env_file_has_gpu_selection() {
1144 let content = generate_env_file_content(
1145 "0,1",
1146 "/opt/rocm",
1147 "7.2.0",
1148 "latest",
1149 "gfx1100",
1150 "11.0.0",
1151 "python3",
1152 );
1153 assert!(content.contains("HIP_VISIBLE_DEVICES"));
1154 assert!(content.contains("CUDA_VISIBLE_DEVICES"));
1155 assert!(content.contains("PYTORCH_ROCM_DEVICE"));
1156 assert!(content.contains("0,1"));
1157 }
1158
1159 #[test]
1160 fn test_generate_env_file_has_rocm_settings() {
1161 let content = generate_env_file_content(
1162 "0",
1163 "/opt/rocm",
1164 "7.2.0",
1165 "latest",
1166 "gfx1100",
1167 "11.0.0",
1168 "python3",
1169 );
1170 assert!(content.contains("ROCM_HOME"));
1171 assert!(content.contains("CUDA_HOME"));
1172 assert!(content.contains("ROCM_VERSION"));
1173 assert!(content.contains("ROCM_CHANNEL"));
1174 assert!(content.contains("GPU_ARCH"));
1175 assert!(content.contains("gfx1100"));
1176 }
1177
1178 #[test]
1179 fn test_generate_env_file_has_path_settings() {
1180 let content = generate_env_file_content(
1181 "0",
1182 "/opt/rocm",
1183 "7.2.0",
1184 "latest",
1185 "gfx1100",
1186 "11.0.0",
1187 "python3",
1188 );
1189 assert!(content.contains("PATH="));
1190 assert!(content.contains("LD_LIBRARY_PATH"));
1191 assert!(content.contains("/opt/rocm/bin"));
1192 }
1193
1194 #[test]
1195 fn test_generate_env_file_has_performance_settings() {
1196 let content = generate_env_file_content(
1197 "0",
1198 "/opt/rocm",
1199 "7.2.0",
1200 "latest",
1201 "gfx1100",
1202 "11.0.0",
1203 "python3",
1204 );
1205 assert!(content.contains("HSA_OVERRIDE_GFX_VERSION"));
1206 assert!(content.contains("HSA_ENABLE_SDMA"));
1207 assert!(content.contains("GPU_MAX_HEAP_SIZE"));
1208 assert!(content.contains("GPU_MAX_ALLOC_PERCENT"));
1209 assert!(content.contains("HSA_TOOLS_LIB"));
1210 }
1211
1212 #[test]
1213 fn test_generate_env_file_has_miopen_settings() {
1214 let content = generate_env_file_content(
1215 "0",
1216 "/opt/rocm",
1217 "7.2.0",
1218 "latest",
1219 "gfx1100",
1220 "11.0.0",
1221 "python3",
1222 );
1223 assert!(content.contains("MIOPEN_DEBUG_CONV_IMPLICIT_GEMM"));
1224 assert!(content.contains("MIOPEN_FIND_MODE"));
1225 assert!(content.contains("MIOPEN_FIND_ENFORCE"));
1226 }
1227
1228 #[test]
1229 fn test_generate_env_file_has_pytorch_settings() {
1230 let content = generate_env_file_content(
1231 "0",
1232 "/opt/rocm",
1233 "7.2.0",
1234 "latest",
1235 "gfx1100",
1236 "11.0.0",
1237 "python3",
1238 );
1239 assert!(!content.contains("TORCH_CUDA_ARCH_LIST"));
1240 assert!(content.contains("PYTORCH_ALLOC_CONF"));
1241 assert!(content.contains("PYTORCH_HIP_ALLOC_CONF"));
1242 assert!(content.contains("VLLM_WORKER_MULTIPROC_METHOD"));
1243 }
1244
1245 #[test]
1246 fn test_generate_env_file_has_triton_settings() {
1247 let content = generate_env_file_content(
1248 "0",
1249 "/opt/rocm",
1250 "7.2.0",
1251 "latest",
1252 "gfx1100",
1253 "11.0.0",
1254 "python3",
1255 );
1256 assert!(content.contains("MLSTACK_TRITON_HOME"));
1257 assert!(content.contains("TRITON_HOME"));
1258 assert!(content.contains("TRITON_CACHE_DIR"));
1259 }
1260
1261 #[test]
1262 fn test_generate_env_file_has_mpi_settings() {
1263 let content = generate_env_file_content(
1264 "0",
1265 "/opt/rocm",
1266 "7.2.0",
1267 "latest",
1268 "gfx1100",
1269 "11.0.0",
1270 "python3",
1271 );
1272 assert!(
1273 !content.contains("opal_cuda_support"),
1274 "must NOT set any OMPI CUDA-opal MCA var (Stage 2: ROCm uses the ROCm accelerator path)"
1275 );
1276 assert!(content.contains("OMPI_MCA_pml"));
1277 assert!(content.contains("OMPI_MCA_osc"));
1278 assert!(content.contains("OMPI_MCA_btl"));
1279 }
1280
1281 #[test]
1282 fn test_generate_env_file_has_onnx_runtime() {
1283 let content = generate_env_file_content(
1284 "0",
1285 "/opt/rocm",
1286 "7.2.0",
1287 "latest",
1288 "gfx1100",
1289 "11.0.0",
1290 "python3",
1291 );
1292 assert!(content.contains("PYTHONPATH"));
1293 assert!(content.contains("onnxruntime"));
1294 }
1295
1296 #[test]
1297 fn test_generate_env_file_uses_guard_syntax() {
1298 let content = generate_env_file_content(
1299 "0",
1300 "/opt/rocm",
1301 "7.2.0",
1302 "latest",
1303 "gfx1100",
1304 "11.0.0",
1305 "python3",
1306 );
1307 assert!(content.contains("if [ -z \"${HIP_VISIBLE_DEVICES:-}\" ]"));
1309 assert!(content.contains("if [ -z \"${ROCM_HOME:-}\" ]"));
1310 }
1311
1312 #[test]
1313 fn test_generate_env_file_gpu_arch_not_guarded() {
1314 let content = generate_env_file_content(
1316 "0",
1317 "/opt/rocm",
1318 "7.2.0",
1319 "latest",
1320 "gfx1100",
1321 "11.0.0",
1322 "python3",
1323 );
1324 assert!(content.contains("export GPU_ARCH=gfx1100"));
1325 }
1326
1327 #[test]
1328 fn test_generate_env_file_custom_values() {
1329 let content = generate_env_file_content(
1330 "0,1",
1331 "/opt/rocm-7.0",
1332 "7.0.0",
1333 "legacy",
1334 "gfx1030",
1335 "10.3.0",
1336 "python3",
1337 );
1338 assert!(content.contains("0,1"));
1339 assert!(content.contains("/opt/rocm-7.0"));
1340 assert!(content.contains("7.0.0"));
1341 assert!(content.contains("legacy"));
1342 assert!(content.contains("gfx1030"));
1343 assert!(content.contains("10.3.0"));
1344 }
1345
1346 #[test]
1349 fn test_create_env_file_creates_file_and_fish() {
1350 let tmp_dir = tempfile::tempdir().expect("Failed to create temp dir");
1351 let _env_path = tmp_dir.path().join(".mlstack_env");
1352 let _fish_env_path = tmp_dir.path().join(".config/fish/conf.d/mlstack_env.fish");
1353
1354 let original_home = std::env::var("HOME").ok();
1356 std::env::set_var("HOME", tmp_dir.path());
1357
1358 let result = create_env_file(
1359 "0",
1360 "/opt/rocm",
1361 "7.2.0",
1362 "latest",
1363 "gfx1100",
1364 "11.0.0",
1365 "python3",
1366 );
1367
1368 if let Some(home) = original_home {
1370 std::env::set_var("HOME", home);
1371 } else {
1372 std::env::remove_var("HOME");
1373 }
1374
1375 assert!(result.success, "create_env_file should succeed");
1376 assert!(result.path.is_some());
1377
1378 let path = result.path.unwrap();
1380 let content = std::fs::read_to_string(&path).expect("Should be able to read env file");
1381 assert!(content.contains("HIP_VISIBLE_DEVICES"));
1382 assert!(content.contains("ROCM_HOME"));
1383 assert!(content.contains("PIP_BREAK_SYSTEM_PACKAGES=1"));
1384 assert!(content.contains("UV_SYSTEM_PYTHON=1"));
1385 }
1386
1387 #[test]
1390 fn test_generate_fish_env_file_has_fish_syntax() {
1391 let content = generate_fish_env_file_content(
1392 "0",
1393 "/opt/rocm",
1394 "7.2.0",
1395 "latest",
1396 "gfx1100",
1397 "11.0.0",
1398 "python3",
1399 );
1400 assert!(!content.contains("if [ -z"));
1402 assert!(!content.contains("; fi"));
1403 assert!(!content.contains("export "));
1404 assert!(!content.contains("${"));
1405 assert!(content.contains("set -gx"));
1407 assert!(content.contains("set -q"));
1408 assert!(content.contains("; or set -gx"));
1409 }
1410
1411 #[test]
1412 fn test_generate_fish_env_file_has_required_vars() {
1413 let content = generate_fish_env_file_content(
1414 "0,1",
1415 "/opt/rocm",
1416 "7.2.0",
1417 "latest",
1418 "gfx1100",
1419 "11.0.0",
1420 "python3",
1421 );
1422 assert!(content.contains("HIP_VISIBLE_DEVICES"));
1423 assert!(content.contains("ROCM_HOME"));
1424 assert!(content.contains("ROCM_VERSION"));
1425 assert!(content.contains("GPU_ARCH"));
1426 assert!(content.contains("HSA_OVERRIDE_GFX_VERSION"));
1427 assert!(content.contains("PIP_BREAK_SYSTEM_PACKAGES"));
1428 assert!(content.contains("UV_PIP_BREAK_SYSTEM_PACKAGES"));
1429 assert!(content.contains("UV_SYSTEM_PYTHON"));
1430 assert!(content.contains("FLASH_ATTENTION_TRITON_AMD_ENABLE"));
1431 assert!(content.contains("0,1"));
1432 }
1433
1434 #[test]
1435 fn test_generate_fish_env_file_has_no_bash_patterns() {
1436 let content = generate_fish_env_file_content(
1437 "0",
1438 "/opt/rocm",
1439 "7.2.0",
1440 "latest",
1441 "gfx1100",
1442 "11.0.0",
1443 "python3",
1444 );
1445 assert!(!content.contains("if ! "));
1447 assert!(!content.contains("fi\n"));
1448 assert!(!content.contains("; then"));
1449 }
1450
1451 #[test]
1452 fn test_generate_bash_env_file_has_pip_break_system_packages() {
1453 let content = generate_env_file_content(
1454 "0",
1455 "/opt/rocm",
1456 "7.2.0",
1457 "latest",
1458 "gfx1100",
1459 "11.0.0",
1460 "python3",
1461 );
1462 assert!(content.contains("export PIP_BREAK_SYSTEM_PACKAGES=1"));
1463 assert!(content.contains("export UV_PIP_BREAK_SYSTEM_PACKAGES=1"));
1464 assert!(content.contains("export UV_SYSTEM_PYTHON=1"));
1465 assert!(content.contains("export FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE"));
1466 }
1467
1468 #[test]
1471 fn test_setup_environment_returns_results() {
1472 let (env_result, gpu_result, gpu_arch) = setup_environment();
1473 let _ = env_result.success;
1475 assert!(!gpu_result.hip_visible_devices.is_empty());
1476 assert!(!gpu_arch.gpu_arch.is_empty());
1477 }
1478}