Skip to main content

rusty_stack/bootstrap/
env_setup.rs

1//! enhanced_setup_environment.sh equivalent — GPU filtering, system deps, env file, ROCm optimization.
2//!
3//! Ports `scripts/enhanced_setup_environment.sh` (1411 lines) to native Rust.
4//! This is the most complex bootstrap script, handling:
5//! - GPU detection and filtering (discrete vs integrated)
6//! - System dependency checking (kernel, ROCm, packages, Python)
7//! - Environment file creation (`~/.mlstack_env`)
8//! - ROCm optimization settings (HSA_OVERRIDE_GFX_VERSION, MIOpen, channel selection)
9//!
10//! # Validation Assertions
11//!
12//! - **VAL-VBA-014**: GPU filtering detects AMD GPUs via ROCm, sets HIP_VISIBLE_DEVICES
13//! - **VAL-VBA-015**: System dependency checking covers kernel, ROCm, packages, Python
14//! - **VAL-VBA-016**: Environment file creation produces shell-sourceable ~/.mlstack_env
15//! - **VAL-VBA-017**: ROCm optimization: HSA_OVERRIDE_GFX_VERSION, ROCM_MATH_LIBS, MIOPEN_DEBUG, channel selection
16
17use crate::installers::common::utils::{
18    command_exists, print_error, print_step, print_success, print_warning,
19};
20use std::fmt;
21
22// ===========================================================================
23// Types
24// ===========================================================================
25
26/// True when `version` is a TheRock-based ROCm runtime (7.14+ or 10.x+).
27///
28/// The TheRock runtime fails HSA init (`HSA_STATUS_ERROR_OUT_OF_RESOURCES`, so
29/// `torch.cuda.is_available()` is False) when `HSA_OVERRIDE_GFX_VERSION` is
30/// set: it enumerates the GPUs natively and the spoofed ISA matches no device
31/// code object. Classic 7.2.x still needs the override (e.g. gfx1101 as 11.0.0).
32pub 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/// GPU architecture information detected from the system.
49#[derive(Debug, Clone)]
50pub struct GpuArchInfo {
51    /// The GPU architecture string (e.g., "gfx1100").
52    pub gpu_arch: String,
53    /// The HSA override GFX version (e.g., "11.0.0").
54    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    /// Create GpuArchInfo from a GPU architecture string.
68    ///
69    /// Maps the architecture to the correct HSA override version.
70    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", // default fallback
81        };
82        Self {
83            gpu_arch: arch.to_string(),
84            hsa_override_gfx_version: hsa_version.to_string(),
85        }
86    }
87}
88
89/// Result of GPU detection and filtering.
90#[derive(Debug, Clone)]
91pub struct GpuFilterResult {
92    /// Indices of discrete GPUs (integrated GPUs filtered out).
93    pub discrete_gpu_indices: Vec<u32>,
94    /// Comma-separated string for HIP_VISIBLE_DEVICES.
95    pub hip_visible_devices: String,
96    /// Number of detected GPUs.
97    pub gpu_count: usize,
98}
99
100/// A system dependency to check.
101#[derive(Debug, Clone)]
102pub struct SystemDependency {
103    /// The package name (generic, will be mapped per distro).
104    pub package: &'static str,
105    /// Human-readable description.
106    pub description: &'static str,
107}
108
109impl SystemDependency {
110    /// Returns the list of required system packages.
111    ///
112    /// Mirrors the `required_packages` array from enhanced_setup_environment.sh.
113    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/// Result of checking a single system dependency.
135#[derive(Debug, Clone)]
136pub struct DependencyCheckResult {
137    /// The generic package name.
138    pub package: String,
139    /// Whether the package is installed.
140    pub installed: bool,
141    /// The distro-mapped package name.
142    pub mapped_name: String,
143}
144
145/// Result of environment file creation.
146#[derive(Debug, Clone)]
147pub struct EnvFileResult {
148    /// Whether the operation was successful.
149    pub success: bool,
150    /// Path to the created environment file.
151    pub path: Option<String>,
152    /// Any errors encountered.
153    pub errors: Vec<String>,
154}
155
156impl EnvFileResult {
157    /// Create a successful result.
158    pub fn success(path: String) -> Self {
159        Self {
160            success: true,
161            path: Some(path),
162            errors: Vec::new(),
163        }
164    }
165
166    /// Create a failure result.
167    pub fn failure(error: String) -> Self {
168        Self {
169            success: false,
170            path: None,
171            errors: vec![error],
172        }
173    }
174}
175
176// ===========================================================================
177// GPU Detection and Filtering (VAL-VBA-014)
178// ===========================================================================
179
180/// Detect discrete AMD GPUs, filtering out integrated GPUs.
181///
182/// Uses rocminfo to enumerate GPUs and filters out known integrated GPUs
183/// (Raphael, Ryzen Graphics, etc.). Falls back to lspci if rocminfo is
184/// unavailable.
185///
186/// # Validation
187///
188/// - **VAL-VBA-014**: GPU filtering detects AMD GPUs via ROCm, sets HIP_VISIBLE_DEVICES
189pub fn detect_discrete_gpus() -> GpuFilterResult {
190    let mut discrete_indices: Vec<u32> = Vec::new();
191
192    // Try rocminfo first
193    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    // Fallback to lspci
199    if discrete_indices.is_empty() {
200        discrete_indices = detect_gpus_from_lspci();
201    }
202
203    // Fallback to render nodes
204    if discrete_indices.is_empty() {
205        discrete_indices = detect_gpus_from_render_nodes();
206    }
207
208    // Default to GPU 0 if nothing detected
209    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
227/// Detect GPUs from rocminfo output, filtering out integrated GPUs.
228fn 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; // Fallback counter for systems without GPU ID lines
239
240    for line in output.lines() {
241        let trimmed = line.trim();
242
243        // Detect new Agent sections (lines like "Agent N")
244        if trimmed.starts_with("Agent") && trimmed.contains(char::is_numeric) {
245            // Don't increment here — only for GPU agents
246        }
247
248        if trimmed.starts_with("GPU ID:") {
249            // Save previous GPU if it was discrete
250            if let Some(id) = current_gpu_id {
251                if !is_igpu && !discrete_indices.contains(&id) {
252                    discrete_indices.push(id);
253                }
254            }
255
256            // Parse new GPU ID
257            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            // Check if this is an integrated GPU
268            if is_integrated_gpu_name(marketing_name) {
269                is_igpu = true;
270            }
271
272            // If no GPU ID lines exist, track by agent counter
273            if current_gpu_id.is_none() {
274                // Only count GPU agents (those with Device Type: GPU)
275                // We'll finalize on Device Type line
276            }
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            // CPU devices are also not discrete GPUs
284            if device_type.contains("CPU") {
285                is_igpu = true;
286            }
287
288            // Finalize this GPU
289            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                // No GPU ID lines in this rocminfo output — use counter
295                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    // Handle last GPU
304    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
315/// Detect GPUs from lspci output.
316fn 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        // Filter out integrated GPUs
337        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
350/// Detect GPUs from /dev/dri/render* nodes.
351/// Filters out iGPU render nodes by checking the associated device description.
352fn 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    // Try to filter iGPUs via sysfs device description
367    let discrete_count = render_entries
368        .iter()
369        .filter(|entry| {
370            let name = entry.file_name().to_string_lossy().to_string();
371            // Extract render node number
372            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; // Can't determine, include it
379            }
380
381            // Check sysfs for the associated device description
382            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                // Look for PCI_ID to identify the device
385                for line in uevent.lines() {
386                    if line.starts_with("PCI_ID=") {
387                        // Could check specific IDs here if needed
388                        return true;
389                    }
390                }
391            }
392
393            // Alternative: check via /sys/class/drm/card<N>/device/device
394            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                // 0x164e = Raphael iGPU (7800X3D)
398                if pci_id == "0x164e" {
399                    return false; // Skip iGPU
400                }
401            }
402
403            true // Include by default
404        })
405        .count();
406
407    if discrete_count > 0 {
408        (0..discrete_count as u32).collect()
409    } else if !render_entries.is_empty() {
410        // Fallback: use all render nodes (safer than returning empty)
411        (0..render_entries.len() as u32).collect()
412    } else {
413        Vec::new()
414    }
415}
416
417/// Check if a GPU name indicates an integrated GPU.
418///
419/// Delegates to the canonical classifier `crate::gpu::is_integrated_gpu_name`
420/// (Stage 1: single source of truth for dGPU/iGPU classification).
421pub fn is_integrated_gpu_name(name: &str) -> bool {
422    crate::gpu::is_integrated_gpu_name(name)
423}
424
425// ===========================================================================
426// GPU Architecture Detection (VAL-VBA-017)
427// ===========================================================================
428
429/// Detect the correct GPU architecture from marketing name.
430///
431/// This corrects rocminfo bugs where RDNA3 cards report gfx1030 instead of
432/// gfx1100. Maps marketing names to correct architectures.
433///
434/// # Validation
435///
436/// - **VAL-VBA-017**: ROCm optimization — correct GPU arch detection
437pub 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    // Skip integrated GPUs
444    if is_integrated_gpu_name(name) {
445        return GpuArchInfo::default();
446    }
447
448    // Delegate to canonical name→gfx lookup; default to gfx1100 for unknown
449    let arch = crate::gpu::gfx_from_marketing_name(name).unwrap_or("gfx1100");
450
451    GpuArchInfo::from_arch(arch)
452}
453
454// ===========================================================================
455// System Dependency Checking (VAL-VBA-015)
456// ===========================================================================
457
458/// Check a single system dependency.
459///
460/// Returns whether the package is installed and the mapped name.
461/// Uses distro detection to map generic package names to distro-specific names.
462///
463/// # Validation
464///
465/// - **VAL-VBA-015**: System dependency checking covers kernel, ROCm, packages, Python
466pub 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
482/// Check all required system dependencies.
483///
484/// Returns results for each dependency, indicating which are missing.
485pub fn check_all_dependencies() -> Vec<DependencyCheckResult> {
486    SystemDependency::required_packages()
487        .into_iter()
488        .map(check_system_dependency)
489        .collect()
490}
491
492/// Check if a package is installed (best-effort, distro-agnostic).
493fn check_package_installed(package: &str) -> bool {
494    // Try dpkg first (Debian/Ubuntu)
495    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    // Try rpm (Fedora/RHEL/SUSE)
508    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    // Try pacman (Arch)
520    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
534// ===========================================================================
535// Environment File Creation (VAL-VBA-016)
536// ===========================================================================
537
538/// Create the `~/.mlstack_env` file with all required variables.
539///
540/// Generates a shell-sourceable file with GPU settings, ROCm paths,
541/// performance optimizations, and PyTorch/MPI settings.
542///
543/// # Validation
544///
545/// - **VAL-VBA-016**: Environment file creation produces shell-sourceable ~/.mlstack_env
546pub 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    // Write the bash env file
570    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    // Write the fish-compatible env file
577    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    // Ensure fish conf.d directory exists
587    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        // Non-fatal — fish env is best-effort
592        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
601/// Generate the environment file content.
602///
603/// This produces a shell-sourceable file with all required environment
604/// variables. The content uses `if [ -z ... ]` guards to avoid overriding
605/// existing values, matching the original script behavior exactly.
606///
607/// # Validation
608///
609/// - **VAL-VBA-016**: Produces shell-sourceable content
610/// - **VAL-VBA-017**: Includes ROCm optimization settings
611pub 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    // RCCL overlay env is MANAGED here (the persistent-env generator is the
627    // single source of truth for env vars — components no longer write env files).
628    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
745/// Generate a fish-compatible environment file.
746///
747/// Produces `~/.config/fish/conf.d/mlstack_env.fish` that natively loads
748/// all ML Stack environment variables without needing to source the bash file.
749/// Fish doesn't support bash syntax (`if [ -z ... ]`, `${VAR:-}`, `fi`, etc.),
750/// so this uses idiomatic fish constructs.
751pub 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    // Build ONNX Runtime PYTHONPATH check
768    let onnx_path = format!("{home}/onnxruntime_build/onnxruntime/build/Linux/Release");
769    // RCCL overlay env is MANAGED here (single source of truth — no component env files).
770    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
879// ===========================================================================
880// Component env contributions — MANAGED by the persistent-env generator.
881// The generator is the SINGLE source of truth for env vars; components must
882// NOT write their own env files (the old rccl `active/env.sh` did an un-guarded
883// `export PYTHONPATH=X${PYTHONPATH:+:$PYTHONPATH}` that accumulated a duplicate
884// entry on every `source ~/.mlstack_env`). Components instead declare state via
885// the filesystem (the `active` symlink + manifest); the generator reads it and
886// emits an idempotent block here.
887// ===========================================================================
888
889/// Path to the sealed RCCL overlay's `active` dir for `home`.
890fn rccl_active_dir(home: &str) -> String {
891    format!("{home}/.mlstack/components/rccl/active")
892}
893
894/// Read the RCCL overlay sha256 from `<active>/manifest` (line `sha256=…`), if
895/// the overlay is installed and the manifest is readable.
896fn 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    // Defense-in-depth: the SHA is written by the installer (always a 64-char
904    // hex string), but this value is interpolated unquoted into a file that is
905    // sourced on EVERY shell login. A corrupted/tampered manifest containing
906    // shell metacharacters (`;`, backticks, `$()`, …) would execute when the
907    // env file is sourced. Accept ONLY a 64-char lowercase-hex SHA-256; any
908    // other shape is omitted (preserving the missing-manifest behavior).
909    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
917/// Emit the **idempotent** bash block for the sealed RCCL overlay, or an empty
918/// string when no overlay is installed.
919///
920/// Uses the `$HOME`-relative `active` symlink path in the emitted exports so
921/// the block tracks the current RCCL build without an env refresh. The
922/// PYTHONPATH prepend is guarded (`grep -qF`) so re-sourcing the env never
923/// accumulates duplicates — the generator, not a component-written file, owns
924/// these vars.
925fn 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        // Quoted for defense-in-depth (the value is already validated 64-hex
931        // upstream, so it carries no metacharacters — but quoting keeps the
932        // sourced line robust if the validation contract ever loosens).
933        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
955/// Emit the **idempotent** fish block for the sealed RCCL overlay, or empty.
956/// Fish analogue of [`rccl_overlay_bash_block`]; `contains` is the membership guard.
957fn 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
984// ===========================================================================
985// Full Setup Orchestration
986// ===========================================================================
987
988/// Run the complete enhanced setup environment.
989///
990/// This is the Rust equivalent of the `main()` function in
991/// `enhanced_setup_environment.sh`:
992/// 1. Check system dependencies
993/// 2. Detect AMD GPUs
994/// 3. Detect ROCm
995/// 4. Configure environment variables
996/// 5. Create environment file
997///
998/// # Returns
999///
1000/// A tuple of (env_file_result, gpu_filter_result, gpu_arch_info).
1001pub fn setup_environment() -> (EnvFileResult, GpuFilterResult, GpuArchInfo) {
1002    print_step("Enhanced ML Stack Environment Setup");
1003
1004    // Step 1: Detect GPUs and filter
1005    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    // Step 2: Detect ROCm
1017    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    // Step 3: Detect GPU architecture
1036    let gpu_arch_info = detect_gpu_arch_from_system();
1037
1038    // Step 4: Resolve canonical Python and create environment file
1039    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
1058/// Detect GPU architecture from the system (rocminfo-based).
1059fn 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            // Skip integrated GPUs
1081            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// ===========================================================================
1100// Tests
1101// ===========================================================================
1102
1103#[cfg(test)]
1104mod tests {
1105    use super::*;
1106
1107    // --- GPU Arch Info (VAL-VBA-017) ---
1108
1109    #[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"), // unknown defaults
1128        ];
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    // --- Integrated GPU Detection (VAL-VBA-014) ---
1141
1142    #[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    // --- GPU Detection (VAL-VBA-014) ---
1167
1168    #[test]
1169    fn test_detect_discrete_gpus_returns_result() {
1170        let result = detect_discrete_gpus();
1171        // Should always return at least the default GPU 0
1172        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        // Just verify it doesn't panic
1179        let result = detect_gpus_from_lspci();
1180        // May be empty if no AMD GPUs, that's fine
1181        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    // --- Marketing Name to Architecture (VAL-VBA-017) ---
1191
1192    #[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"); // default
1221    }
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"); // default fallback
1227    }
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"); // default (skipped iGPU)
1233    }
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    // --- System Dependencies (VAL-VBA-015) ---
1242
1243    #[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        // git should be installed on any dev system
1257        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    // --- Environment File Content (VAL-VBA-016, VAL-VBA-017) ---
1274
1275    #[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        // Device filter is MANAGED/unconditional (stale override must never
1455        // re-expose an iGPU): ROCR/HIP/CUDA_VISIBLE_DEVICES are bare exports.
1456        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        // Tuning vars still use "if [ -z ... ]" guards (respect user overrides).
1460        assert!(content.contains("if [ -z \"${ROCM_HOME:-}\" ]"));
1461    }
1462
1463    #[test]
1464    fn test_generate_env_file_gpu_arch_not_guarded() {
1465        // GPU_ARCH should be exported unconditionally (not guarded)
1466        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    // --- create_env_file (VAL-VBA-016) ---
1498
1499    #[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        // Temporarily set HOME to the temp dir
1506        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        // Restore HOME
1520        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        // Verify the bash file exists and has content
1530        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    // --- Fish env file generation ---
1539
1540    #[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        // Must NOT contain bash syntax
1552        assert!(!content.contains("if [ -z"));
1553        assert!(!content.contains("; fi"));
1554        assert!(!content.contains("export "));
1555        assert!(!content.contains("${"));
1556        // Must use fish syntax
1557        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        // Centralization: the generator must NOT source a component-written
1583        // env.fish — RCCL overlay env is emitted inline + idempotently by the
1584        // generator (rccl_overlay_fish_block), the single source of truth.
1585        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        // Should never contain bash-specific constructs
1601        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        // Centralization: the generator must NOT source a component-written
1622        // env.sh — RCCL overlay env is emitted inline + idempotently here.
1623        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        // No active RCCL overlay → generator emits nothing for it.
1629        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        // Active RCCL overlay → generator emits a guarded, idempotent block that
1636        // follows the `active` symlink (no component-written env file).
1637        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        // A valid SHA-256 (64 lowercase hex chars) — the installer always writes
1644        // this shape. The generator validates 64-hex before splicing (defense-
1645        // in-depth against a tampered manifest), so use a realistic value here.
1646        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        // SHA is quoted + validated (64-hex). An invalid SHA would be omitted.
1652        assert!(bash.contains(&format!("MLSTACK_RCCL_OVERLAY_SHA256=\"{valid_sha}\"")));
1653        // Idempotent guard present (no bare un-guarded prepend).
1654        assert!(bash.contains("grep -qF"));
1655        // Single source of truth: does NOT delegate to a component env file.
1656        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        // Invalid SHA (not 64-hex) → omitted, never spliced into the sourced
1665        // block. Guards against a corrupted/tampered manifest injecting shell.
1666        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    // --- setup_environment integration ---
1679
1680    #[test]
1681    fn test_setup_environment_returns_results() {
1682        let (env_result, gpu_result, gpu_arch) = setup_environment();
1683        // Should always return results without panicking
1684        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}