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