1use crate::installers::common::utils::{
18 command_exists, print_error, print_step, print_success, print_warning,
19};
20use std::fmt;
21
22pub fn rocm_version_is_therock(version: &str) -> bool {
33 let mut it = version.trim().split('.');
34 let major: u32 = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
35 let minor: u32 = it
36 .next()
37 .and_then(|v| {
38 v.chars()
39 .take_while(|c| c.is_ascii_digit())
40 .collect::<String>()
41 .parse()
42 .ok()
43 })
44 .unwrap_or(0);
45 major >= 10 || (major == 7 && minor >= 14)
46}
47
48#[derive(Debug, Clone)]
50pub struct GpuArchInfo {
51 pub gpu_arch: String,
53 pub hsa_override_gfx_version: String,
55}
56
57impl Default for GpuArchInfo {
58 fn default() -> Self {
59 Self {
60 gpu_arch: "gfx1100".to_string(),
61 hsa_override_gfx_version: "11.0.0".to_string(),
62 }
63 }
64}
65
66impl GpuArchInfo {
67 pub fn from_arch(arch: &str) -> Self {
71 let hsa_version = match arch {
72 "gfx1100" => "11.0.0",
73 "gfx1101" => "11.0.1",
74 "gfx1102" => "11.0.2",
75 "gfx1200" => "12.0.0",
76 "gfx1201" => "12.0.1",
77 "gfx1030" => "10.3.0",
78 "gfx1031" => "10.3.1",
79 "gfx1032" => "10.3.2",
80 _ => "11.0.0", };
82 Self {
83 gpu_arch: arch.to_string(),
84 hsa_override_gfx_version: hsa_version.to_string(),
85 }
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct GpuFilterResult {
92 pub discrete_gpu_indices: Vec<u32>,
94 pub hip_visible_devices: String,
96 pub gpu_count: usize,
98}
99
100#[derive(Debug, Clone)]
102pub struct SystemDependency {
103 pub package: &'static str,
105 pub description: &'static str,
107}
108
109impl SystemDependency {
110 pub fn required_packages() -> Vec<&'static str> {
114 vec![
115 "build-essential",
116 "cmake",
117 "git",
118 "python3-dev",
119 "python3-pip",
120 "libnuma-dev",
121 "pciutils",
122 "mesa-utils",
123 "clinfo",
124 ]
125 }
126}
127
128impl fmt::Display for SystemDependency {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 write!(f, "{} ({})", self.package, self.description)
131 }
132}
133
134#[derive(Debug, Clone)]
136pub struct DependencyCheckResult {
137 pub package: String,
139 pub installed: bool,
141 pub mapped_name: String,
143}
144
145#[derive(Debug, Clone)]
147pub struct EnvFileResult {
148 pub success: bool,
150 pub path: Option<String>,
152 pub errors: Vec<String>,
154}
155
156impl EnvFileResult {
157 pub fn success(path: String) -> Self {
159 Self {
160 success: true,
161 path: Some(path),
162 errors: Vec::new(),
163 }
164 }
165
166 pub fn failure(error: String) -> Self {
168 Self {
169 success: false,
170 path: None,
171 errors: vec![error],
172 }
173 }
174}
175
176pub fn detect_discrete_gpus() -> GpuFilterResult {
190 let mut discrete_indices: Vec<u32> = Vec::new();
191
192 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
194 if command_exists(&rocminfo_path) || command_exists("rocminfo") {
195 discrete_indices = detect_gpus_from_rocminfo();
196 }
197
198 if discrete_indices.is_empty() {
200 discrete_indices = detect_gpus_from_lspci();
201 }
202
203 if discrete_indices.is_empty() {
205 discrete_indices = detect_gpus_from_render_nodes();
206 }
207
208 if discrete_indices.is_empty() {
210 discrete_indices.push(0);
211 }
212
213 let gpu_count = discrete_indices.len();
214 let hip_visible_devices = discrete_indices
215 .iter()
216 .map(|i| i.to_string())
217 .collect::<Vec<_>>()
218 .join(",");
219
220 GpuFilterResult {
221 discrete_gpu_indices: discrete_indices,
222 hip_visible_devices,
223 gpu_count,
224 }
225}
226
227fn detect_gpus_from_rocminfo() -> Vec<u32> {
229 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
230 let output = match std::process::Command::new(&rocminfo_path).output() {
231 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
232 _ => return Vec::new(),
233 };
234
235 let mut discrete_indices: Vec<u32> = Vec::new();
236 let mut current_gpu_id: Option<u32> = None;
237 let mut is_igpu = false;
238 let mut gpu_agent_counter = 0u32; for line in output.lines() {
241 let trimmed = line.trim();
242
243 if trimmed.starts_with("Agent") && trimmed.contains(char::is_numeric) {
245 }
247
248 if trimmed.starts_with("GPU ID:") {
249 if let Some(id) = current_gpu_id {
251 if !is_igpu && !discrete_indices.contains(&id) {
252 discrete_indices.push(id);
253 }
254 }
255
256 current_gpu_id = trimmed
258 .strip_prefix("GPU ID:")
259 .and_then(|s| s.trim().parse::<u32>().ok());
260 is_igpu = false;
261 } else if trimmed.starts_with("Marketing Name:") {
262 let marketing_name = trimmed
263 .strip_prefix("Marketing Name:")
264 .map(|s| s.trim())
265 .unwrap_or("");
266
267 if is_integrated_gpu_name(marketing_name) {
269 is_igpu = true;
270 }
271
272 if current_gpu_id.is_none() {
274 }
277 } else if trimmed.starts_with("Device Type:") {
278 let device_type = trimmed
279 .strip_prefix("Device Type:")
280 .map(|s| s.trim())
281 .unwrap_or("");
282
283 if device_type.contains("CPU") {
285 is_igpu = true;
286 }
287
288 if let Some(id) = current_gpu_id {
290 if !is_igpu && !discrete_indices.contains(&id) {
291 discrete_indices.push(id);
292 }
293 } else if device_type.contains("GPU") {
294 if !is_igpu && !discrete_indices.contains(&gpu_agent_counter) {
296 discrete_indices.push(gpu_agent_counter);
297 }
298 gpu_agent_counter += 1;
299 }
300 }
301 }
302
303 if let Some(id) = current_gpu_id {
305 if !is_igpu && !discrete_indices.contains(&id) {
306 discrete_indices.push(id);
307 }
308 }
309
310 discrete_indices.sort();
311 discrete_indices.dedup();
312 discrete_indices
313}
314
315fn detect_gpus_from_lspci() -> Vec<u32> {
317 if !command_exists("lspci") {
318 return Vec::new();
319 }
320
321 let output = match std::process::Command::new("lspci").output() {
322 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
323 _ => return Vec::new(),
324 };
325
326 let mut discrete_count = 0u32;
327 for line in output.lines() {
328 let lower = line.to_lowercase();
329 if !((lower.contains("amd")
330 || lower.contains("radeon")
331 || lower.contains("advanced micro devices"))
332 && (lower.contains("vga") || lower.contains("3d") || lower.contains("display")))
333 {
334 continue;
335 }
336 if is_integrated_gpu_name(line) {
338 continue;
339 }
340 discrete_count += 1;
341 }
342
343 if discrete_count > 0 {
344 (0..discrete_count).collect()
345 } else {
346 Vec::new()
347 }
348}
349
350fn detect_gpus_from_render_nodes() -> Vec<u32> {
353 let render_entries: Vec<_> = std::fs::read_dir("/dev/dri")
354 .map(|entries| {
355 entries
356 .filter_map(|e| e.ok())
357 .filter(|e| e.file_name().to_string_lossy().starts_with("render"))
358 .collect()
359 })
360 .unwrap_or_default();
361
362 if render_entries.is_empty() {
363 return Vec::new();
364 }
365
366 let discrete_count = render_entries
368 .iter()
369 .filter(|entry| {
370 let name = entry.file_name().to_string_lossy().to_string();
371 let node_num: String = name
373 .trim_start_matches("render")
374 .chars()
375 .take_while(|c| c.is_ascii_digit())
376 .collect();
377 if node_num.is_empty() {
378 return true; }
380
381 let sysfs_path = format!("/sys/class/drm/card{}-render/device/uevent", node_num);
383 if let Ok(uevent) = std::fs::read_to_string(&sysfs_path) {
384 for line in uevent.lines() {
386 if line.starts_with("PCI_ID=") {
387 return true;
389 }
390 }
391 }
392
393 let card_sysfs = format!("/sys/class/drm/card{}/device/device", node_num);
395 if let Ok(pci_id) = std::fs::read_to_string(&card_sysfs) {
396 let pci_id = pci_id.trim();
397 if pci_id == "0x164e" {
399 return false; }
401 }
402
403 true })
405 .count();
406
407 if discrete_count > 0 {
408 (0..discrete_count as u32).collect()
409 } else if !render_entries.is_empty() {
410 (0..render_entries.len() as u32).collect()
412 } else {
413 Vec::new()
414 }
415}
416
417pub fn is_integrated_gpu_name(name: &str) -> bool {
422 crate::gpu::is_integrated_gpu_name(name)
423}
424
425pub fn detect_correct_gpu_arch(marketing_name: Option<&str>) -> GpuArchInfo {
438 let name = match marketing_name {
439 Some(n) => n,
440 None => return GpuArchInfo::default(),
441 };
442
443 if is_integrated_gpu_name(name) {
445 return GpuArchInfo::default();
446 }
447
448 let arch = crate::gpu::gfx_from_marketing_name(name).unwrap_or("gfx1100");
450
451 GpuArchInfo::from_arch(arch)
452}
453
454pub fn check_system_dependency(package: &str) -> DependencyCheckResult {
467 use crate::installers::common::distro::DistroFacade;
468 use crate::installers::common::package_mappings::map_package_name;
469
470 let distro = DistroFacade::detect();
471 let pkg_mgr = distro.package_manager();
472 let mapped = map_package_name(package, pkg_mgr);
473 let installed = check_package_installed(&mapped);
474
475 DependencyCheckResult {
476 package: package.to_string(),
477 installed,
478 mapped_name: mapped,
479 }
480}
481
482pub fn check_all_dependencies() -> Vec<DependencyCheckResult> {
486 SystemDependency::required_packages()
487 .into_iter()
488 .map(check_system_dependency)
489 .collect()
490}
491
492fn check_package_installed(package: &str) -> bool {
494 if command_exists("dpkg-query") {
496 let output = std::process::Command::new("dpkg-query")
497 .args(["-W", "-f=${Status}", package])
498 .output();
499 if let Ok(o) = output {
500 let stdout = String::from_utf8_lossy(&o.stdout);
501 if stdout.contains("install ok installed") {
502 return true;
503 }
504 }
505 }
506
507 if command_exists("rpm") {
509 let output = std::process::Command::new("rpm")
510 .args(["-q", package])
511 .output();
512 if let Ok(o) = output {
513 if o.status.success() {
514 return true;
515 }
516 }
517 }
518
519 if command_exists("pacman") {
521 let output = std::process::Command::new("pacman")
522 .args(["-Q", package])
523 .output();
524 if let Ok(o) = output {
525 if o.status.success() {
526 return true;
527 }
528 }
529 }
530
531 false
532}
533
534pub fn create_env_file(
547 hip_visible_devices: &str,
548 rocm_path: &str,
549 rocm_version: &str,
550 rocm_channel: &str,
551 gpu_arch: &str,
552 hsa_override_gfx_version: &str,
553 python_bin: &str,
554) -> EnvFileResult {
555 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
556 let env_path = format!("{home}/.mlstack_env");
557 let fish_env_path = format!("{home}/.config/fish/conf.d/mlstack_env.fish");
558
559 let content = generate_env_file_content(
560 hip_visible_devices,
561 rocm_path,
562 rocm_version,
563 rocm_channel,
564 gpu_arch,
565 hsa_override_gfx_version,
566 python_bin,
567 );
568
569 if let Err(e) = std::fs::write(&env_path, &content) {
571 let msg = format!("Failed to write environment file: {e}");
572 print_error(&msg);
573 return EnvFileResult::failure(msg);
574 }
575
576 let fish_content = generate_fish_env_file_content(
578 hip_visible_devices,
579 rocm_path,
580 rocm_version,
581 rocm_channel,
582 gpu_arch,
583 hsa_override_gfx_version,
584 python_bin,
585 );
586 if let Some(parent) = std::path::Path::new(&fish_env_path).parent() {
588 let _ = std::fs::create_dir_all(parent);
589 }
590 if let Err(e) = std::fs::write(&fish_env_path, &fish_content) {
591 print_warning(&format!("Failed to write fish env file: {e}"));
593 } else {
594 print_success(&format!("Fish env file created: {fish_env_path}"));
595 }
596
597 print_success(&format!("Environment file created: {env_path}"));
598 EnvFileResult::success(env_path)
599}
600
601pub fn generate_env_file_content(
612 hip_visible_devices: &str,
613 rocm_path: &str,
614 rocm_version: &str,
615 rocm_channel: &str,
616 gpu_arch: &str,
617 hsa_override_gfx_version: &str,
618 python_bin: &str,
619) -> String {
620 let hsa_override_line = if rocm_version_is_therock(rocm_version) {
621 "# TheRock runtime (7.14+/10.x): HSA_OVERRIDE_GFX_VERSION breaks HSA init; keep it unset.\nunset HSA_OVERRIDE_GFX_VERSION".to_string()
622 } else {
623 format!("export HSA_OVERRIDE_GFX_VERSION={hsa_override_gfx_version}")
624 };
625 let home = std::env::var("HOME").unwrap_or_else(|_| "$HOME".to_string());
626 let rccl_block = rccl_overlay_bash_block(&home);
629
630 format!(
631 r#"# ML Stack Environment File
632# Created by Enhanced ML Stack Environment Setup (Rust native)
633# Date: {date}
634
635# GPU Selection — device filter is MANAGED (unconditional). It must reflect the
636# detected discrete GPUs on every source so a stale override can never re-expose
637# an integrated GPU (the iGPU segfault). ROCR_VISIBLE_DEVICES is the authoritative
638# ROCr (HSA) runtime filter; HIP/CUDA alias it for the HIP + CUDA-compat layers.
639# Discrete GPUs only (iGPUs filtered): {hip_visible_devices}
640export ROCR_VISIBLE_DEVICES={hip_visible_devices}
641export HIP_VISIBLE_DEVICES={hip_visible_devices}
642export CUDA_VISIBLE_DEVICES={hip_visible_devices}
643export PYTORCH_ROCM_DEVICE={hip_visible_devices}
644
645# ROCm Settings
646# Only set if not already set
647if [ -z "${{ROCM_HOME:-}}" ]; then export ROCM_HOME={rocm_path}; fi
648if [ -z "${{ROCM_PATH:-}}" ]; then export ROCM_PATH={rocm_path}; fi
649if [ -z "${{HIP_PATH:-}}" ]; then export HIP_PATH={rocm_path}; fi
650if [ -z "${{CUDA_HOME:-}}" ]; then export CUDA_HOME={rocm_path}; fi
651if [ -z "${{ROCM_VERSION:-}}" ]; then export ROCM_VERSION={rocm_version}; fi
652if [ -z "${{ROCM_CHANNEL:-}}" ]; then export ROCM_CHANNEL={rocm_channel}; fi
653# GPU arch identity is MANAGED (unconditional): must match detected hardware.
654export GPU_ARCH={gpu_arch}
655export PYTORCH_ROCM_ARCH={gpu_arch}
656export GPU_ARCHS={gpu_arch}
657# libdrm amdgpu ASIC id table (ROCm's amdgpu.ids lookup)
658export AMDGPU_ASIC_ID_TABLE_PATH=/usr/share/libdrm/amdgpu.ids
659export AMDGPU_ASIC_ID_TABLE_PATHS=/usr/share/libdrm
660
661# Path Settings - Hardcoded safe paths to prevent "command not found" errors.
662# Idempotent: prepend the ROCm bin/lib blocks only if not already on the var, so
663# re-sourcing the env never grows PATH/LD_LIBRARY_PATH (the old un-guarded
664# `:$PATH` form accumulated a duplicate entry per source).
665case ":${{PATH:-}}:" in *:"{rocm_path}/bin":*) ;; *) export PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:{rocm_path}/bin:{rocm_path}/hip/bin:${{PATH:-}}";; esac
666case ":${{LD_LIBRARY_PATH:-}}:" in *:"{rocm_path}/lib":*) ;; *) export 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:-}}";; esac
667{rccl_block}
668
669# Performance Settings
670# HSA_OVERRIDE_GFX_VERSION is set based on detected GPU_ARCH
671{hsa_override_line}
672export HSA_ENABLE_SDMA=0
673export GPU_MAX_HEAP_SIZE=100
674export GPU_MAX_ALLOC_PERCENT=100
675export HSA_TOOLS_LIB=/opt/rocm/lib/rocprofiler-sdk/librocprofiler-sdk-tool.so
676
677# MIOpen Settings
678# Only set if not already set
679if [ -z "${{MIOPEN_DEBUG_CONV_IMPLICIT_GEMM:-}}" ]; then export MIOPEN_DEBUG_CONV_IMPLICIT_GEMM=1; fi
680if [ -z "${{MIOPEN_FIND_MODE:-}}" ]; then export MIOPEN_FIND_MODE=3; fi
681if [ -z "${{MIOPEN_FIND_ENFORCE:-}}" ]; then export MIOPEN_FIND_ENFORCE=3; fi
682
683# PyTorch Settings
684# Only set if not already set
685# (Stage 2: the CUDA build-arch env var is intentionally NOT exported on a
686# ROCm stack — ROCm uses PYTORCH_ROCM_ARCH. No CUDA env leakage.)
687if [ -z "${{PYTORCH_CUDA_ALLOC_CONF:-}}" ]; then export PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:512"; fi
688if [ -z "${{PYTORCH_HIP_ALLOC_CONF:-}}" ]; then export PYTORCH_HIP_ALLOC_CONF="max_split_size_mb:512"; fi
689if [ -z "${{VLLM_WORKER_MULTIPROC_METHOD:-}}" ]; then export VLLM_WORKER_MULTIPROC_METHOD=spawn; fi
690if [ -z "${{VLLM_ROCM_USE_AITER:-}}" ]; then export VLLM_ROCM_USE_AITER=0; fi
691if [ -z "${{MLSTACK_TRITON_HOME:-}}" ]; then export MLSTACK_TRITON_HOME="$HOME/.cache/mlstack/triton"; fi
692if [ -z "${{TRITON_HOME:-}}" ]; then export TRITON_HOME="$MLSTACK_TRITON_HOME"; fi
693if [ -z "${{TRITON_CACHE_DIR:-}}" ]; then export TRITON_CACHE_DIR="$TRITON_HOME/cache"; fi
694if [ -z "${{TRITON_DUMP_DIR:-}}" ]; then export TRITON_DUMP_DIR="$TRITON_HOME/dump"; fi
695if [ -z "${{TRITON_OVERRIDE_DIR:-}}" ]; then export TRITON_OVERRIDE_DIR="$TRITON_HOME/override"; fi
696mkdir -p "$TRITON_CACHE_DIR" "$TRITON_DUMP_DIR" "$TRITON_OVERRIDE_DIR" 2>/dev/null || true
697
698# MPI Settings
699# Only set if not already set
700# (Stage 2: the OMPI CUDA-opal support vars are intentionally NOT set — that is
701# the CUDA-built OpenMPI path; a ROCm stack uses the ROCm accelerator path.)
702if [ -z "${{OMPI_MCA_btl_openib_allow_ib:-}}" ]; then export OMPI_MCA_btl_openib_allow_ib=true; fi
703if [ -z "${{OMPI_MCA_btl_openib_warn_no_device_params_found:-}}" ]; then export OMPI_MCA_btl_openib_warn_no_device_params_found=0; fi
704if [ -z "${{OMPI_MCA_coll_hcoll_enable:-}}" ]; then export OMPI_MCA_coll_hcoll_enable=0; fi
705if [ -z "${{OMPI_MCA_pml:-}}" ]; then export OMPI_MCA_pml=ucx; fi
706if [ -z "${{OMPI_MCA_osc:-}}" ]; then export OMPI_MCA_osc=ucx; fi
707if [ -z "${{OMPI_MCA_btl:-}}" ]; then export OMPI_MCA_btl=^openib,uct; fi
708# Prevent UCX ROCm memory registration segfault on multi-GPU systems
709export UCX_MEMTYPE_CACHE=no
710export UCX_LOG_LEVEL=error
711
712# Global flags for seamless pip installs (PEP 668 override)
713export PIP_BREAK_SYSTEM_PACKAGES=1
714export UV_PIP_BREAK_SYSTEM_PACKAGES=1
715export UV_SYSTEM_PYTHON=1
716
717# Python Interpreter
718# Canonical Python binary — all ML components install to this interpreter.
719# Ensures a single unified environment regardless of how many Pythons exist.
720export MLSTACK_PYTHON_BIN={python_bin}
721export UV_PYTHON={python_bin}
722
723# Flash Attention AMD
724export FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE
725
726# ONNX Runtime Settings
727# Only add if not already in PYTHONPATH
728if ! echo "${{PYTHONPATH:-}}" | grep -q "{home}/onnxruntime_build/onnxruntime/build/Linux/Release"; then
729 export PYTHONPATH={home}/onnxruntime_build/onnxruntime/build/Linux/Release:${{PYTHONPATH:-}}
730fi
731"#,
732 date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
733 hip_visible_devices = hip_visible_devices,
734 rocm_path = rocm_path,
735 rocm_version = rocm_version,
736 rocm_channel = rocm_channel,
737 gpu_arch = gpu_arch,
738 hsa_override_line = hsa_override_line,
739 home = home,
740 python_bin = python_bin,
741 rccl_block = rccl_block,
742 )
743}
744
745pub fn generate_fish_env_file_content(
752 hip_visible_devices: &str,
753 rocm_path: &str,
754 rocm_version: &str,
755 rocm_channel: &str,
756 gpu_arch: &str,
757 hsa_override_gfx_version: &str,
758 python_bin: &str,
759) -> String {
760 let hsa_override_line_fish = if rocm_version_is_therock(rocm_version) {
761 "# TheRock runtime (7.14+/10.x): HSA_OVERRIDE_GFX_VERSION breaks HSA init; keep it unset.\nset -e HSA_OVERRIDE_GFX_VERSION".to_string()
762 } else {
763 format!("set -gx HSA_OVERRIDE_GFX_VERSION {hsa_override_gfx_version}")
764 };
765 let home = std::env::var("HOME").unwrap_or_else(|_| "$HOME".to_string());
766
767 let onnx_path = format!("{home}/onnxruntime_build/onnxruntime/build/Linux/Release");
769 let rccl_block = rccl_overlay_fish_block(&home);
771
772 format!(
773 r##"# ML Stack Environment File — Fish Shell Native
774# Generated by Enhanced ML Stack Environment Setup (Rust native)
775# Date: {date}
776#
777# This file is auto-generated. Edit via rusty-stack's env setup, not by hand.
778
779# --- GPU Selection — device filter is MANAGED (unconditional) so a stale
780# override can never re-expose an integrated GPU. ROCR_VISIBLE_DEVICES is the
781# authoritative ROCr (HSA) filter. ---
782# Discrete GPUs only (iGPUs filtered): {hip_visible_devices}
783set -gx ROCR_VISIBLE_DEVICES {hip_visible_devices}
784set -gx HIP_VISIBLE_DEVICES {hip_visible_devices}
785set -gx CUDA_VISIBLE_DEVICES {hip_visible_devices}
786set -gx PYTORCH_ROCM_DEVICE {hip_visible_devices}
787
788# --- ROCm Settings ---
789set -q ROCM_HOME; or set -gx ROCM_HOME {rocm_path}
790set -q ROCM_PATH; or set -gx ROCM_PATH {rocm_path}
791set -q HIP_PATH; or set -gx HIP_PATH {rocm_path}
792set -q CUDA_HOME; or set -gx CUDA_HOME {rocm_path}
793set -q ROCM_VERSION; or set -gx ROCM_VERSION {rocm_version}
794set -q ROCM_CHANNEL; or set -gx ROCM_CHANNEL {rocm_channel}
795# GPU arch identity is MANAGED (unconditional): must match detected hardware.
796set -gx GPU_ARCH {gpu_arch}
797set -gx PYTORCH_ROCM_ARCH {gpu_arch}
798set -gx GPU_ARCHS {gpu_arch}
799set -gx AMDGPU_ASIC_ID_TABLE_PATH /usr/share/libdrm/amdgpu.ids
800set -gx AMDGPU_ASIC_ID_TABLE_PATHS /usr/share/libdrm
801
802# --- Path Settings ---
803# Idempotent: prepend the ROCm bin/lib blocks only if not already on the var, so
804# re-sourcing never grows PATH/LD_LIBRARY_PATH (no per-source duplicate buildup).
805contains -- {rocm_path}/bin $PATH; or set -gx PATH /usr/local/bin /usr/bin /bin /usr/local/games /usr/games {rocm_path}/bin {rocm_path}/hip/bin $PATH
806contains -- {rocm_path}/lib $LD_LIBRARY_PATH; or set -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
807{rccl_block}
808
809# --- Performance Settings ---
810{hsa_override_line_fish}
811set -gx HSA_ENABLE_SDMA 0
812set -gx GPU_MAX_HEAP_SIZE 100
813set -gx GPU_MAX_ALLOC_PERCENT 100
814set -gx HSA_TOOLS_LIB /opt/rocm/lib/rocprofiler-sdk/librocprofiler-sdk-tool.so
815
816# --- MIOpen Settings ---
817set -q MIOPEN_DEBUG_CONV_IMPLICIT_GEMM; or set -gx MIOPEN_DEBUG_CONV_IMPLICIT_GEMM 1
818set -q MIOPEN_FIND_MODE; or set -gx MIOPEN_FIND_MODE 3
819set -q MIOPEN_FIND_ENFORCE; or set -gx MIOPEN_FIND_ENFORCE 3
820
821# --- PyTorch Settings ---
822# (Stage 2: the CUDA build-arch env var is intentionally NOT set on a ROCm
823# stack — ROCm uses PYTORCH_ROCM_ARCH. No CUDA env leakage.)
824set -q PYTORCH_CUDA_ALLOC_CONF; or set -gx PYTORCH_CUDA_ALLOC_CONF "max_split_size_mb:512"
825set -q PYTORCH_HIP_ALLOC_CONF; or set -gx PYTORCH_HIP_ALLOC_CONF "max_split_size_mb:512"
826set -q VLLM_WORKER_MULTIPROC_METHOD; or set -gx VLLM_WORKER_MULTIPROC_METHOD spawn
827set -q VLLM_ROCM_USE_AITER; or set -gx VLLM_ROCM_USE_AITER 0
828set -q MLSTACK_TRITON_HOME; or set -gx MLSTACK_TRITON_HOME "$HOME/.cache/mlstack/triton"
829set -q TRITON_HOME; or set -gx TRITON_HOME $MLSTACK_TRITON_HOME
830set -q TRITON_CACHE_DIR; or set -gx TRITON_CACHE_DIR "$TRITON_HOME/cache"
831set -q TRITON_DUMP_DIR; or set -gx TRITON_DUMP_DIR "$TRITON_HOME/dump"
832set -q TRITON_OVERRIDE_DIR; or set -gx TRITON_OVERRIDE_DIR "$TRITON_HOME/override"
833mkdir -p $TRITON_CACHE_DIR $TRITON_DUMP_DIR $TRITON_OVERRIDE_DIR 2>/dev/null; or true
834
835# --- Global flags for seamless pip installs (PEP 668 override) ---
836set -q PIP_BREAK_SYSTEM_PACKAGES; or set -gx PIP_BREAK_SYSTEM_PACKAGES 1
837set -q UV_PIP_BREAK_SYSTEM_PACKAGES; or set -gx UV_PIP_BREAK_SYSTEM_PACKAGES 1
838set -gx UV_SYSTEM_PYTHON 1
839
840# --- Python Interpreter ---
841# Canonical Python binary — all ML components install to this interpreter.
842# Ensures a single unified environment regardless of how many Pythons exist.
843set -gx MLSTACK_PYTHON_BIN {python_bin}
844set -gx UV_PYTHON {python_bin}
845
846# --- Flash Attention AMD ---
847set -gx FLASH_ATTENTION_TRITON_AMD_ENABLE TRUE
848
849# --- MPI/UCX Settings ---
850# (Stage 2: the OMPI CUDA-opal support vars are intentionally NOT set —
851# CUDA-built OpenMPI path; a ROCm stack uses the ROCm accelerator path.)
852set -q OMPI_MCA_btl_openib_allow_ib; or set -gx OMPI_MCA_btl_openib_allow_ib true
853set -q OMPI_MCA_btl_openib_warn_no_device_params_found; or set -gx OMPI_MCA_btl_openib_warn_no_device_params_found 0
854set -q OMPI_MCA_coll_hcoll_enable; or set -gx OMPI_MCA_coll_hcoll_enable 0
855set -q OMPI_MCA_pml; or set -gx OMPI_MCA_pml ucx
856set -q OMPI_MCA_osc; or set -gx OMPI_MCA_osc ucx
857set -q OMPI_MCA_btl; or set -gx OMPI_MCA_btl '^openib,uct'
858set -gx UCX_MEMTYPE_CACHE no
859set -gx UCX_LOG_LEVEL error
860
861# --- ONNX Runtime Settings ---
862if not string match -q '*{onnx_path}*' "$PYTHONPATH"
863 set -gx PYTHONPATH {onnx_path} $PYTHONPATH
864end
865"##,
866 date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
867 hip_visible_devices = hip_visible_devices,
868 rocm_path = rocm_path,
869 rocm_version = rocm_version,
870 rocm_channel = rocm_channel,
871 gpu_arch = gpu_arch,
872 hsa_override_line_fish = hsa_override_line_fish,
873 python_bin = python_bin,
874 onnx_path = onnx_path,
875 rccl_block = rccl_block,
876 )
877}
878
879fn rccl_active_dir(home: &str) -> String {
891 format!("{home}/.mlstack/components/rccl/active")
892}
893
894fn rccl_overlay_sha256(active: &str) -> Option<String> {
897 let manifest = std::fs::read_to_string(format!("{active}/manifest")).ok()?;
898 let raw = manifest.lines().find_map(|l| {
899 l.strip_prefix("sha256=")
900 .map(str::trim)
901 .filter(|s| !s.is_empty())
902 })?;
903 let is_hex64 = raw.len() == 64 && raw.chars().all(|c| c.is_ascii_hexdigit());
910 if is_hex64 {
911 Some(raw.to_string())
912 } else {
913 None
914 }
915}
916
917fn rccl_overlay_bash_block(home: &str) -> String {
926 if !std::path::Path::new(&rccl_active_dir(home)).exists() {
927 return String::new();
928 }
929 let sha_line = match rccl_overlay_sha256(&rccl_active_dir(home)) {
930 Some(sha) => format!(" export MLSTACK_RCCL_OVERLAY_SHA256=\"{sha}\""),
934 None => " # MLSTACK_RCCL_OVERLAY_SHA256 omitted (manifest missing or invalid)".to_string(),
935 };
936 format!(
937 r#"
938# Sealed RCCL overlay — MANAGED by the persistent-env generator (single source of
939# truth for env vars; the generator, not a component file, owns these). Follows
940# the `active` symlink; the PYTHONPATH prepend is guarded so re-sourcing the env
941# never accumulates duplicate entries.
942if [ -r "$HOME/.mlstack/components/rccl/active/lib/librccl.so.1.0" ]; then
943 export MLSTACK_RCCL_OVERLAY_LIB="$HOME/.mlstack/components/rccl/active/lib/librccl.so.1.0"
944{sha_line}
945 export NCCL_P2P_DISABLE=1
946 export RCCL_P2P_DISABLE=1
947 if ! echo "${{PYTHONPATH:-}}" | grep -qF "$HOME/.mlstack/components/rccl/active/python"; then
948 export PYTHONPATH="$HOME/.mlstack/components/rccl/active/python:${{PYTHONPATH:-}}"
949 fi
950fi
951"#
952 )
953}
954
955fn rccl_overlay_fish_block(home: &str) -> String {
958 if !std::path::Path::new(&rccl_active_dir(home)).exists() {
959 return String::new();
960 }
961 let sha_line = match rccl_overlay_sha256(&rccl_active_dir(home)) {
962 Some(sha) => format!(" set -gx MLSTACK_RCCL_OVERLAY_SHA256 \"{sha}\""),
963 None => {
964 " # MLSTACK_RCCL_OVERLAY_SHA256 omitted (manifest missing or invalid)".to_string()
965 }
966 };
967 format!(
968 r#"
969# --- Sealed RCCL overlay — MANAGED by the persistent-env generator (single
970# source of truth for env vars). Follows the `active` symlink; idempotent. ---
971if test -r $HOME/.mlstack/components/rccl/active/lib/librccl.so.1.0
972 set -gx MLSTACK_RCCL_OVERLAY_LIB $HOME/.mlstack/components/rccl/active/lib/librccl.so.1.0
973{sha_line}
974 set -gx NCCL_P2P_DISABLE 1
975 set -gx RCCL_P2P_DISABLE 1
976 if not contains -- $HOME/.mlstack/components/rccl/active/python $PYTHONPATH
977 set -gx PYTHONPATH $HOME/.mlstack/components/rccl/active/python $PYTHONPATH
978 end
979end
980"#
981 )
982}
983
984pub fn setup_environment() -> (EnvFileResult, GpuFilterResult, GpuArchInfo) {
1002 print_step("Enhanced ML Stack Environment Setup");
1003
1004 print_step("Detecting AMD GPUs...");
1006 let gpu_filter = detect_discrete_gpus();
1007 if gpu_filter.gpu_count > 0 {
1008 print_success(&format!(
1009 "Detected {} discrete GPU(s): {}",
1010 gpu_filter.gpu_count, gpu_filter.hip_visible_devices
1011 ));
1012 } else {
1013 print_warning("No discrete GPUs detected, using defaults");
1014 }
1015
1016 let rocm_env = crate::installers::common::rocm_env::RocmEnv::detect();
1018 let rocm_path = rocm_env
1019 .path()
1020 .map(|p| p.to_string_lossy().to_string())
1021 .unwrap_or_else(|| "/opt/rocm".to_string());
1022 let rocm_version = if rocm_env.version().is_empty() {
1023 "7.2.0".to_string()
1024 } else {
1025 rocm_env.version().to_string()
1026 };
1027 let rocm_channel = std::env::var("ROCM_CHANNEL").unwrap_or_else(|_| "latest".to_string());
1028
1029 if rocm_env.is_detected() {
1030 print_success(&format!("ROCm {rocm_version} detected at {rocm_path}"));
1031 } else {
1032 print_warning("ROCm not detected, using default paths");
1033 }
1034
1035 let gpu_arch_info = detect_gpu_arch_from_system();
1037
1038 print_step("Creating environment file...");
1040 let python_bin = crate::platform::environment::resolve_canonical_python_bin();
1041 let env_result = create_env_file(
1042 &gpu_filter.hip_visible_devices,
1043 &rocm_path,
1044 &rocm_version,
1045 &rocm_channel,
1046 &gpu_arch_info.gpu_arch,
1047 &gpu_arch_info.hsa_override_gfx_version,
1048 &python_bin,
1049 );
1050
1051 if env_result.success {
1052 print_success("Environment setup complete");
1053 }
1054
1055 (env_result, gpu_filter, gpu_arch_info)
1056}
1057
1058fn detect_gpu_arch_from_system() -> GpuArchInfo {
1060 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
1061 if !command_exists(&rocminfo_path) && !command_exists("rocminfo") {
1062 return GpuArchInfo::default();
1063 }
1064
1065 let output = match std::process::Command::new(&rocminfo_path).output() {
1066 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
1067 _ => return GpuArchInfo::default(),
1068 };
1069
1070 let mut marketing_name = String::new();
1071
1072 for line in output.lines() {
1073 let trimmed = line.trim();
1074 if trimmed.starts_with("Marketing Name:") {
1075 let name = trimmed
1076 .strip_prefix("Marketing Name:")
1077 .map(|s| s.trim())
1078 .unwrap_or("");
1079
1080 if is_integrated_gpu_name(name) {
1082 continue;
1083 }
1084
1085 if !name.is_empty() {
1086 marketing_name = name.to_string();
1087 break;
1088 }
1089 }
1090 }
1091
1092 detect_correct_gpu_arch(if marketing_name.is_empty() {
1093 None
1094 } else {
1095 Some(&marketing_name)
1096 })
1097}
1098
1099#[cfg(test)]
1104mod tests {
1105 use super::*;
1106
1107 #[test]
1110 fn test_gpu_arch_info_default() {
1111 let info = GpuArchInfo::default();
1112 assert_eq!(info.gpu_arch, "gfx1100");
1113 assert_eq!(info.hsa_override_gfx_version, "11.0.0");
1114 }
1115
1116 #[test]
1117 fn test_gpu_arch_info_from_arch_all_variants() {
1118 let cases = vec![
1119 ("gfx1100", "11.0.0"),
1120 ("gfx1101", "11.0.1"),
1121 ("gfx1102", "11.0.2"),
1122 ("gfx1200", "12.0.0"),
1123 ("gfx1201", "12.0.1"),
1124 ("gfx1030", "10.3.0"),
1125 ("gfx1031", "10.3.1"),
1126 ("gfx1032", "10.3.2"),
1127 ("gfx9999", "11.0.0"), ];
1129
1130 for (arch, expected_hsa) in cases {
1131 let info = GpuArchInfo::from_arch(arch);
1132 assert_eq!(info.gpu_arch, arch, "GPU arch mismatch for {arch}");
1133 assert_eq!(
1134 info.hsa_override_gfx_version, expected_hsa,
1135 "HSA version mismatch for {arch}"
1136 );
1137 }
1138 }
1139
1140 #[test]
1143 fn test_is_integrated_gpu_name_positive() {
1144 assert!(is_integrated_gpu_name("Raphael"));
1145 assert!(is_integrated_gpu_name("AMD Ryzen Graphics"));
1146 assert!(is_integrated_gpu_name("Integrated Graphics"));
1147 assert!(is_integrated_gpu_name("iGPU Device"));
1148 assert!(is_integrated_gpu_name("AMD Radeon Graphics"));
1149 }
1150
1151 #[test]
1152 fn test_is_integrated_gpu_name_negative() {
1153 assert!(!is_integrated_gpu_name("Radeon RX 7900 XTX"));
1154 assert!(!is_integrated_gpu_name("Radeon RX 7800 XT"));
1155 assert!(!is_integrated_gpu_name("Radeon RX 9070 XT"));
1156 assert!(!is_integrated_gpu_name("Unknown GPU"));
1157 }
1158
1159 #[test]
1160 fn test_is_integrated_gpu_name_case_insensitive() {
1161 assert!(is_integrated_gpu_name("raphael"));
1162 assert!(is_integrated_gpu_name("INTEGRATED"));
1163 assert!(is_integrated_gpu_name("AMD RADEON GRAPHICS"));
1164 }
1165
1166 #[test]
1169 fn test_detect_discrete_gpus_returns_result() {
1170 let result = detect_discrete_gpus();
1171 assert!(!result.hip_visible_devices.is_empty());
1173 assert!(result.gpu_count >= 1);
1174 }
1175
1176 #[test]
1177 fn test_detect_gpus_from_lspci_returns_vec() {
1178 let result = detect_gpus_from_lspci();
1180 let _ = result;
1182 }
1183
1184 #[test]
1185 fn test_detect_gpus_from_render_nodes_returns_vec() {
1186 let result = detect_gpus_from_render_nodes();
1187 let _ = result;
1188 }
1189
1190 #[test]
1193 fn test_detect_correct_gpu_arch_7900_xtx() {
1194 let info = detect_correct_gpu_arch(Some("Radeon RX 7900 XTX"));
1195 assert_eq!(info.gpu_arch, "gfx1100");
1196 assert_eq!(info.hsa_override_gfx_version, "11.0.0");
1197 }
1198
1199 #[test]
1200 fn test_detect_correct_gpu_arch_7800_xt() {
1201 let info = detect_correct_gpu_arch(Some("Radeon RX 7800 XT"));
1202 assert_eq!(info.gpu_arch, "gfx1101");
1203 }
1204
1205 #[test]
1206 fn test_detect_correct_gpu_arch_7600() {
1207 let info = detect_correct_gpu_arch(Some("Radeon RX 7600"));
1208 assert_eq!(info.gpu_arch, "gfx1102");
1209 }
1210
1211 #[test]
1212 fn test_detect_correct_gpu_arch_9070_xt() {
1213 let info = detect_correct_gpu_arch(Some("Radeon RX 9070 XT"));
1214 assert_eq!(info.gpu_arch, "gfx1201");
1215 }
1216
1217 #[test]
1218 fn test_detect_correct_gpu_arch_none() {
1219 let info = detect_correct_gpu_arch(None);
1220 assert_eq!(info.gpu_arch, "gfx1100"); }
1222
1223 #[test]
1224 fn test_detect_correct_gpu_arch_unknown() {
1225 let info = detect_correct_gpu_arch(Some("Unknown GPU Model"));
1226 assert_eq!(info.gpu_arch, "gfx1100"); }
1228
1229 #[test]
1230 fn test_detect_correct_gpu_arch_integrated_skipped() {
1231 let info = detect_correct_gpu_arch(Some("AMD Ryzen Graphics"));
1232 assert_eq!(info.gpu_arch, "gfx1100"); }
1234
1235 #[test]
1236 fn test_detect_correct_gpu_arch_case_insensitive() {
1237 let info = detect_correct_gpu_arch(Some("radeon rx 7900 xtx"));
1238 assert_eq!(info.gpu_arch, "gfx1100");
1239 }
1240
1241 #[test]
1244 fn test_system_dependency_required_packages() {
1245 let packages = SystemDependency::required_packages();
1246 assert!(!packages.is_empty());
1247 assert!(packages.contains(&"git"));
1248 assert!(packages.contains(&"cmake"));
1249 assert!(packages.contains(&"build-essential"));
1250 }
1251
1252 #[test]
1253 fn test_check_system_dependency_git() {
1254 let result = check_system_dependency("git");
1255 assert_eq!(result.package, "git");
1256 assert!(result.installed, "git should be installed");
1258 }
1259
1260 #[test]
1261 fn test_check_system_dependency_nonexistent() {
1262 let result = check_system_dependency("nonexistent-pkg-xyz-12345");
1263 assert!(!result.installed);
1264 }
1265
1266 #[test]
1267 fn test_check_all_dependencies_returns_results() {
1268 let results = check_all_dependencies();
1269 assert!(!results.is_empty(), "Should check at least one dependency");
1270 assert_eq!(results.len(), SystemDependency::required_packages().len());
1271 }
1272
1273 #[test]
1276 fn test_generate_env_file_has_header() {
1277 let content = generate_env_file_content(
1278 "0",
1279 "/opt/rocm",
1280 "7.2.0",
1281 "latest",
1282 "gfx1100",
1283 "11.0.0",
1284 "python3",
1285 );
1286 assert!(content.starts_with("# ML Stack Environment File"));
1287 }
1288
1289 #[test]
1290 fn test_generate_env_file_has_gpu_selection() {
1291 let content = generate_env_file_content(
1292 "0,1",
1293 "/opt/rocm",
1294 "7.2.0",
1295 "latest",
1296 "gfx1100",
1297 "11.0.0",
1298 "python3",
1299 );
1300 assert!(content.contains("HIP_VISIBLE_DEVICES"));
1301 assert!(content.contains("CUDA_VISIBLE_DEVICES"));
1302 assert!(content.contains("PYTORCH_ROCM_DEVICE"));
1303 assert!(content.contains("0,1"));
1304 }
1305
1306 #[test]
1307 fn test_generate_env_file_has_rocm_settings() {
1308 let content = generate_env_file_content(
1309 "0",
1310 "/opt/rocm",
1311 "7.2.0",
1312 "latest",
1313 "gfx1100",
1314 "11.0.0",
1315 "python3",
1316 );
1317 assert!(content.contains("ROCM_HOME"));
1318 assert!(content.contains("CUDA_HOME"));
1319 assert!(content.contains("ROCM_VERSION"));
1320 assert!(content.contains("ROCM_CHANNEL"));
1321 assert!(content.contains("GPU_ARCH"));
1322 assert!(content.contains("gfx1100"));
1323 }
1324
1325 #[test]
1326 fn test_generate_env_file_has_path_settings() {
1327 let content = generate_env_file_content(
1328 "0",
1329 "/opt/rocm",
1330 "7.2.0",
1331 "latest",
1332 "gfx1100",
1333 "11.0.0",
1334 "python3",
1335 );
1336 assert!(content.contains("PATH="));
1337 assert!(content.contains("LD_LIBRARY_PATH"));
1338 assert!(content.contains("/opt/rocm/bin"));
1339 }
1340
1341 #[test]
1342 fn test_generate_env_file_has_performance_settings() {
1343 let content = generate_env_file_content(
1344 "0",
1345 "/opt/rocm",
1346 "7.2.0",
1347 "latest",
1348 "gfx1100",
1349 "11.0.0",
1350 "python3",
1351 );
1352 assert!(content.contains("HSA_OVERRIDE_GFX_VERSION"));
1353 assert!(content.contains("HSA_ENABLE_SDMA"));
1354 assert!(content.contains("GPU_MAX_HEAP_SIZE"));
1355 assert!(content.contains("GPU_MAX_ALLOC_PERCENT"));
1356 assert!(content.contains("HSA_TOOLS_LIB"));
1357 }
1358
1359 #[test]
1360 fn test_generate_env_file_has_miopen_settings() {
1361 let content = generate_env_file_content(
1362 "0",
1363 "/opt/rocm",
1364 "7.2.0",
1365 "latest",
1366 "gfx1100",
1367 "11.0.0",
1368 "python3",
1369 );
1370 assert!(content.contains("MIOPEN_DEBUG_CONV_IMPLICIT_GEMM"));
1371 assert!(content.contains("MIOPEN_FIND_MODE"));
1372 assert!(content.contains("MIOPEN_FIND_ENFORCE"));
1373 }
1374
1375 #[test]
1376 fn test_generate_env_file_has_pytorch_settings() {
1377 let content = generate_env_file_content(
1378 "0",
1379 "/opt/rocm",
1380 "7.2.0",
1381 "latest",
1382 "gfx1100",
1383 "11.0.0",
1384 "python3",
1385 );
1386 assert!(!content.contains("TORCH_CUDA_ARCH_LIST"));
1387 assert!(content.contains("PYTORCH_CUDA_ALLOC_CONF"));
1388 assert!(content.contains("PYTORCH_HIP_ALLOC_CONF"));
1389 assert!(content.contains("VLLM_WORKER_MULTIPROC_METHOD"));
1390 }
1391
1392 #[test]
1393 fn test_generate_env_file_has_triton_settings() {
1394 let content = generate_env_file_content(
1395 "0",
1396 "/opt/rocm",
1397 "7.2.0",
1398 "latest",
1399 "gfx1100",
1400 "11.0.0",
1401 "python3",
1402 );
1403 assert!(content.contains("MLSTACK_TRITON_HOME"));
1404 assert!(content.contains("TRITON_HOME"));
1405 assert!(content.contains("TRITON_CACHE_DIR"));
1406 }
1407
1408 #[test]
1409 fn test_generate_env_file_has_mpi_settings() {
1410 let content = generate_env_file_content(
1411 "0",
1412 "/opt/rocm",
1413 "7.2.0",
1414 "latest",
1415 "gfx1100",
1416 "11.0.0",
1417 "python3",
1418 );
1419 assert!(
1420 !content.contains("opal_cuda_support"),
1421 "must NOT set any OMPI CUDA-opal MCA var (Stage 2: ROCm uses the ROCm accelerator path)"
1422 );
1423 assert!(content.contains("OMPI_MCA_pml"));
1424 assert!(content.contains("OMPI_MCA_osc"));
1425 assert!(content.contains("OMPI_MCA_btl"));
1426 }
1427
1428 #[test]
1429 fn test_generate_env_file_has_onnx_runtime() {
1430 let content = generate_env_file_content(
1431 "0",
1432 "/opt/rocm",
1433 "7.2.0",
1434 "latest",
1435 "gfx1100",
1436 "11.0.0",
1437 "python3",
1438 );
1439 assert!(content.contains("PYTHONPATH"));
1440 assert!(content.contains("onnxruntime"));
1441 }
1442
1443 #[test]
1444 fn test_generate_env_file_uses_guard_syntax() {
1445 let content = generate_env_file_content(
1446 "0",
1447 "/opt/rocm",
1448 "7.2.0",
1449 "latest",
1450 "gfx1100",
1451 "11.0.0",
1452 "python3",
1453 );
1454 assert!(content.contains("export ROCR_VISIBLE_DEVICES=0"));
1457 assert!(content.contains("export HIP_VISIBLE_DEVICES=0"));
1458 assert!(!content.contains("if [ -z \"${HIP_VISIBLE_DEVICES:-}\" ]"));
1459 assert!(content.contains("if [ -z \"${ROCM_HOME:-}\" ]"));
1461 }
1462
1463 #[test]
1464 fn test_generate_env_file_gpu_arch_not_guarded() {
1465 let content = generate_env_file_content(
1467 "0",
1468 "/opt/rocm",
1469 "7.2.0",
1470 "latest",
1471 "gfx1100",
1472 "11.0.0",
1473 "python3",
1474 );
1475 assert!(content.contains("export GPU_ARCH=gfx1100"));
1476 }
1477
1478 #[test]
1479 fn test_generate_env_file_custom_values() {
1480 let content = generate_env_file_content(
1481 "0,1",
1482 "/opt/rocm-7.0",
1483 "7.0.0",
1484 "legacy",
1485 "gfx1030",
1486 "10.3.0",
1487 "python3",
1488 );
1489 assert!(content.contains("0,1"));
1490 assert!(content.contains("/opt/rocm-7.0"));
1491 assert!(content.contains("7.0.0"));
1492 assert!(content.contains("legacy"));
1493 assert!(content.contains("gfx1030"));
1494 assert!(content.contains("10.3.0"));
1495 }
1496
1497 #[test]
1500 fn test_create_env_file_creates_file_and_fish() {
1501 let tmp_dir = tempfile::tempdir().expect("Failed to create temp dir");
1502 let _env_path = tmp_dir.path().join(".mlstack_env");
1503 let _fish_env_path = tmp_dir.path().join(".config/fish/conf.d/mlstack_env.fish");
1504
1505 let original_home = std::env::var("HOME").ok();
1507 std::env::set_var("HOME", tmp_dir.path());
1508
1509 let result = create_env_file(
1510 "0",
1511 "/opt/rocm",
1512 "7.2.0",
1513 "latest",
1514 "gfx1100",
1515 "11.0.0",
1516 "python3",
1517 );
1518
1519 if let Some(home) = original_home {
1521 std::env::set_var("HOME", home);
1522 } else {
1523 std::env::remove_var("HOME");
1524 }
1525
1526 assert!(result.success, "create_env_file should succeed");
1527 assert!(result.path.is_some());
1528
1529 let path = result.path.unwrap();
1531 let content = std::fs::read_to_string(&path).expect("Should be able to read env file");
1532 assert!(content.contains("HIP_VISIBLE_DEVICES"));
1533 assert!(content.contains("ROCM_HOME"));
1534 assert!(content.contains("PIP_BREAK_SYSTEM_PACKAGES=1"));
1535 assert!(content.contains("UV_SYSTEM_PYTHON=1"));
1536 }
1537
1538 #[test]
1541 fn test_generate_fish_env_file_has_fish_syntax() {
1542 let content = generate_fish_env_file_content(
1543 "0",
1544 "/opt/rocm",
1545 "7.2.0",
1546 "latest",
1547 "gfx1100",
1548 "11.0.0",
1549 "python3",
1550 );
1551 assert!(!content.contains("if [ -z"));
1553 assert!(!content.contains("; fi"));
1554 assert!(!content.contains("export "));
1555 assert!(!content.contains("${"));
1556 assert!(content.contains("set -gx"));
1558 assert!(content.contains("set -q"));
1559 assert!(content.contains("; or set -gx"));
1560 }
1561
1562 #[test]
1563 fn test_generate_fish_env_file_has_required_vars() {
1564 let content = generate_fish_env_file_content(
1565 "0,1",
1566 "/opt/rocm",
1567 "7.2.0",
1568 "latest",
1569 "gfx1100",
1570 "11.0.0",
1571 "python3",
1572 );
1573 assert!(content.contains("HIP_VISIBLE_DEVICES"));
1574 assert!(content.contains("ROCM_HOME"));
1575 assert!(content.contains("ROCM_VERSION"));
1576 assert!(content.contains("GPU_ARCH"));
1577 assert!(content.contains("HSA_OVERRIDE_GFX_VERSION"));
1578 assert!(content.contains("PIP_BREAK_SYSTEM_PACKAGES"));
1579 assert!(content.contains("UV_PIP_BREAK_SYSTEM_PACKAGES"));
1580 assert!(content.contains("UV_SYSTEM_PYTHON"));
1581 assert!(content.contains("FLASH_ATTENTION_TRITON_AMD_ENABLE"));
1582 assert!(!content.contains("source $HOME/.mlstack/components/rccl/active/env.fish"));
1586 assert!(content.contains("0,1"));
1587 }
1588
1589 #[test]
1590 fn test_generate_fish_env_file_has_no_bash_patterns() {
1591 let content = generate_fish_env_file_content(
1592 "0",
1593 "/opt/rocm",
1594 "7.2.0",
1595 "latest",
1596 "gfx1100",
1597 "11.0.0",
1598 "python3",
1599 );
1600 assert!(!content.contains("if ! "));
1602 assert!(!content.contains("fi\n"));
1603 assert!(!content.contains("; then"));
1604 }
1605
1606 #[test]
1607 fn test_generate_bash_env_file_has_pip_break_system_packages() {
1608 let content = generate_env_file_content(
1609 "0",
1610 "/opt/rocm",
1611 "7.2.0",
1612 "latest",
1613 "gfx1100",
1614 "11.0.0",
1615 "python3",
1616 );
1617 assert!(content.contains("export PIP_BREAK_SYSTEM_PACKAGES=1"));
1618 assert!(content.contains("export UV_PIP_BREAK_SYSTEM_PACKAGES=1"));
1619 assert!(content.contains("export UV_SYSTEM_PYTHON=1"));
1620 assert!(content.contains("export FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE"));
1621 assert!(!content.contains(". \"$HOME/.mlstack/components/rccl/active/env.sh\""));
1624 }
1625
1626 #[test]
1627 fn test_rccl_overlay_block_is_centralized_and_idempotent() {
1628 let tmp = std::env::temp_dir();
1630 let home_no_rccl = tmp.join("rusty_rccl_no_active_test");
1631 let _ = std::fs::remove_dir_all(&home_no_rccl);
1632 assert_eq!(rccl_overlay_bash_block(&home_no_rccl.to_string_lossy()), "");
1633 assert_eq!(rccl_overlay_fish_block(&home_no_rccl.to_string_lossy()), "");
1634
1635 let home = tmp.join("rusty_rccl_active_test");
1638 let active = home.join(".mlstack/components/rccl/active");
1639 let _ = std::fs::remove_dir_all(&home);
1640 std::fs::create_dir_all(active.join("python")).unwrap();
1641 std::fs::create_dir_all(active.join("lib")).unwrap();
1642 std::fs::write(active.join("lib/librccl.so.1.0"), b"").unwrap();
1643 let valid_sha = "a".repeat(64);
1647 std::fs::write(active.join("manifest"), format!("sha256={valid_sha}\n")).unwrap();
1648
1649 let bash = rccl_overlay_bash_block(&home.to_string_lossy());
1650 assert!(bash.contains("MLSTACK_RCCL_OVERLAY_LIB"));
1651 assert!(bash.contains(&format!("MLSTACK_RCCL_OVERLAY_SHA256=\"{valid_sha}\"")));
1653 assert!(bash.contains("grep -qF"));
1655 assert!(!bash.contains("env.sh"));
1657
1658 let fish = rccl_overlay_fish_block(&home.to_string_lossy());
1659 assert!(fish.contains("MLSTACK_RCCL_OVERLAY_LIB"));
1660 assert!(fish.contains(&format!("MLSTACK_RCCL_OVERLAY_SHA256 \"{valid_sha}\"")));
1661 assert!(fish.contains("contains --"));
1662 assert!(!fish.contains("env.fish"));
1663
1664 std::fs::write(active.join("manifest"), "sha256=NOT-HEX\n").unwrap();
1667 let bash_bad = rccl_overlay_bash_block(&home.to_string_lossy());
1668 assert!(
1669 !bash_bad.contains("MLSTACK_RCCL_OVERLAY_SHA256=\""),
1670 "invalid SHA must be omitted, not spliced: {bash_bad}"
1671 );
1672 assert!(bash_bad.contains("omitted"));
1673
1674 let _ = std::fs::remove_dir_all(&home);
1675 let _ = std::fs::remove_dir_all(&home_no_rccl);
1676 }
1677
1678 #[test]
1681 fn test_setup_environment_returns_results() {
1682 let (env_result, gpu_result, gpu_arch) = setup_environment();
1683 let _ = env_result.success;
1685 assert!(!gpu_result.hip_visible_devices.is_empty());
1686 assert!(!gpu_arch.gpu_arch.is_empty());
1687 }
1688 #[test]
1689 fn test_rocm_version_is_therock() {
1690 for v in ["7.14.0", "7.14.1", "7.15.0", "10.1.0", "10.1"] {
1691 assert!(rocm_version_is_therock(v), "{v}");
1692 }
1693 for v in ["6.4.3", "7.2.3", "7.2.4", "7.13.0", "", "garbage"] {
1694 assert!(!rocm_version_is_therock(v), "{v}");
1695 }
1696 }
1697
1698 #[test]
1699 fn test_env_file_omits_hsa_override_on_therock() {
1700 let classic = generate_env_file_content(
1701 "0,1",
1702 "/opt/rocm",
1703 "7.2.4",
1704 "latest",
1705 "gfx1100",
1706 "11.0.0",
1707 "python3",
1708 );
1709 assert!(classic.contains("export HSA_OVERRIDE_GFX_VERSION=11.0.0"));
1710 for v in ["7.14.1", "10.1.0"] {
1711 let c = generate_env_file_content(
1712 "0,1",
1713 "/opt/rocm",
1714 v,
1715 "latest",
1716 "gfx1100",
1717 "11.0.0",
1718 "python3",
1719 );
1720 assert!(!c.contains("export HSA_OVERRIDE_GFX_VERSION"), "{v}");
1721 assert!(c.contains("unset HSA_OVERRIDE_GFX_VERSION"), "{v}");
1722 let f = generate_fish_env_file_content(
1723 "0,1",
1724 "/opt/rocm",
1725 v,
1726 "latest",
1727 "gfx1100",
1728 "11.0.0",
1729 "python3",
1730 );
1731 assert!(!f.contains("set -gx HSA_OVERRIDE_GFX_VERSION"), "{v}");
1732 assert!(f.contains("set -e HSA_OVERRIDE_GFX_VERSION"), "{v}");
1733 }
1734 }
1735}