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/// Integrated GPUs include: Raphael, Ryzen Graphics, Integrated Graphics, iGPU,
398/// AMD Radeon Graphics (when not a discrete card).
399pub fn is_integrated_gpu_name(name: &str) -> bool {
400    let lower = name.to_lowercase();
401    lower.contains("raphael")
402        || lower.contains("integrated")
403        || lower.contains("igpu")
404        || lower.contains("ryzen") && !lower.contains("rx")
405        || lower == "amd radeon graphics"
406}
407
408// ===========================================================================
409// GPU Architecture Detection (VAL-VBA-017)
410// ===========================================================================
411
412/// Detect the correct GPU architecture from marketing name.
413///
414/// This corrects rocminfo bugs where RDNA3 cards report gfx1030 instead of
415/// gfx1100. Maps marketing names to correct architectures.
416///
417/// # Validation
418///
419/// - **VAL-VBA-017**: ROCm optimization — correct GPU arch detection
420pub fn detect_correct_gpu_arch(marketing_name: Option<&str>) -> GpuArchInfo {
421    let name = match marketing_name {
422        Some(n) => n,
423        None => return GpuArchInfo::default(),
424    };
425
426    let lower = name.to_lowercase();
427
428    // Skip integrated GPUs
429    if is_integrated_gpu_name(name) {
430        return GpuArchInfo::default();
431    }
432
433    // Map marketing names to correct architectures
434    // RDNA3 cards are commonly misreported as gfx1030
435    let arch = if lower.contains("7900 xtx")
436        || lower.contains("7900xtx")
437        || lower.contains("7900 xt")
438        || lower.contains("7900xt")
439        || lower.contains("7900 gre")
440        || lower.contains("7900gre")
441    {
442        "gfx1100"
443    } else if lower.contains("7800 xt")
444        || lower.contains("7800xt")
445        || lower.contains("7800 gre")
446        || lower.contains("7800gre")
447        || lower.contains("7700 xt")
448        || lower.contains("7700xt")
449    {
450        "gfx1101"
451    } else if lower.contains("7600 xt") || lower.contains("7600xt") || lower.contains("7600") {
452        "gfx1102"
453    } else if lower.contains("9070 xt") || lower.contains("9070xt") {
454        "gfx1200"
455    } else {
456        "gfx1100" // default fallback
457    };
458
459    GpuArchInfo::from_arch(arch)
460}
461
462// ===========================================================================
463// System Dependency Checking (VAL-VBA-015)
464// ===========================================================================
465
466/// Check a single system dependency.
467///
468/// Returns whether the package is installed and the mapped name.
469/// Uses distro detection to map generic package names to distro-specific names.
470///
471/// # Validation
472///
473/// - **VAL-VBA-015**: System dependency checking covers kernel, ROCm, packages, Python
474pub fn check_system_dependency(package: &str) -> DependencyCheckResult {
475    use crate::installers::common::distro::DistroFacade;
476    use crate::installers::common::package_mappings::map_package_name;
477
478    let distro = DistroFacade::detect();
479    let pkg_mgr = distro.package_manager();
480    let mapped = map_package_name(package, pkg_mgr);
481    let installed = check_package_installed(&mapped);
482
483    DependencyCheckResult {
484        package: package.to_string(),
485        installed,
486        mapped_name: mapped,
487    }
488}
489
490/// Check all required system dependencies.
491///
492/// Returns results for each dependency, indicating which are missing.
493pub fn check_all_dependencies() -> Vec<DependencyCheckResult> {
494    SystemDependency::required_packages()
495        .into_iter()
496        .map(check_system_dependency)
497        .collect()
498}
499
500/// Check if a package is installed (best-effort, distro-agnostic).
501fn check_package_installed(package: &str) -> bool {
502    // Try dpkg first (Debian/Ubuntu)
503    if command_exists("dpkg-query") {
504        let output = std::process::Command::new("dpkg-query")
505            .args(["-W", "-f=${Status}", package])
506            .output();
507        if let Ok(o) = output {
508            let stdout = String::from_utf8_lossy(&o.stdout);
509            if stdout.contains("install ok installed") {
510                return true;
511            }
512        }
513    }
514
515    // Try rpm (Fedora/RHEL/SUSE)
516    if command_exists("rpm") {
517        let output = std::process::Command::new("rpm")
518            .args(["-q", package])
519            .output();
520        if let Ok(o) = output {
521            if o.status.success() {
522                return true;
523            }
524        }
525    }
526
527    // Try pacman (Arch)
528    if command_exists("pacman") {
529        let output = std::process::Command::new("pacman")
530            .args(["-Q", package])
531            .output();
532        if let Ok(o) = output {
533            if o.status.success() {
534                return true;
535            }
536        }
537    }
538
539    false
540}
541
542// ===========================================================================
543// Environment File Creation (VAL-VBA-016)
544// ===========================================================================
545
546/// Create the `~/.mlstack_env` file with all required variables.
547///
548/// Generates a shell-sourceable file with GPU settings, ROCm paths,
549/// performance optimizations, and PyTorch/MPI settings.
550///
551/// # Validation
552///
553/// - **VAL-VBA-016**: Environment file creation produces shell-sourceable ~/.mlstack_env
554pub fn create_env_file(
555    hip_visible_devices: &str,
556    rocm_path: &str,
557    rocm_version: &str,
558    rocm_channel: &str,
559    gpu_arch: &str,
560    hsa_override_gfx_version: &str,
561    python_bin: &str,
562) -> EnvFileResult {
563    let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
564    let env_path = format!("{home}/.mlstack_env");
565    let fish_env_path = format!("{home}/.config/fish/conf.d/mlstack_env.fish");
566
567    let content = generate_env_file_content(
568        hip_visible_devices,
569        rocm_path,
570        rocm_version,
571        rocm_channel,
572        gpu_arch,
573        hsa_override_gfx_version,
574        python_bin,
575    );
576
577    // Write the bash env file
578    if let Err(e) = std::fs::write(&env_path, &content) {
579        let msg = format!("Failed to write environment file: {e}");
580        print_error(&msg);
581        return EnvFileResult::failure(msg);
582    }
583
584    // Write the fish-compatible env file
585    let fish_content = generate_fish_env_file_content(
586        hip_visible_devices,
587        rocm_path,
588        rocm_version,
589        rocm_channel,
590        gpu_arch,
591        hsa_override_gfx_version,
592        python_bin,
593    );
594    // Ensure fish conf.d directory exists
595    if let Some(parent) = std::path::Path::new(&fish_env_path).parent() {
596        let _ = std::fs::create_dir_all(parent);
597    }
598    if let Err(e) = std::fs::write(&fish_env_path, &fish_content) {
599        // Non-fatal — fish env is best-effort
600        print_warning(&format!("Failed to write fish env file: {e}"));
601    } else {
602        print_success(&format!("Fish env file created: {fish_env_path}"));
603    }
604
605    print_success(&format!("Environment file created: {env_path}"));
606    EnvFileResult::success(env_path)
607}
608
609/// Generate the environment file content.
610///
611/// This produces a shell-sourceable file with all required environment
612/// variables. The content uses `if [ -z ... ]` guards to avoid overriding
613/// existing values, matching the original script behavior exactly.
614///
615/// # Validation
616///
617/// - **VAL-VBA-016**: Produces shell-sourceable content
618/// - **VAL-VBA-017**: Includes ROCm optimization settings
619pub fn generate_env_file_content(
620    hip_visible_devices: &str,
621    rocm_path: &str,
622    rocm_version: &str,
623    rocm_channel: &str,
624    gpu_arch: &str,
625    hsa_override_gfx_version: &str,
626    python_bin: &str,
627) -> String {
628    let home = std::env::var("HOME").unwrap_or_else(|_| "$HOME".to_string());
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
636# Only set if not already set
637if [ -z "${{HIP_VISIBLE_DEVICES:-}}" ]; then export HIP_VISIBLE_DEVICES={hip_visible_devices}; fi
638if [ -z "${{CUDA_VISIBLE_DEVICES:-}}" ]; then export CUDA_VISIBLE_DEVICES={hip_visible_devices}; fi
639if [ -z "${{PYTORCH_ROCM_DEVICE:-}}" ]; then export PYTORCH_ROCM_DEVICE={hip_visible_devices}; fi
640
641# ROCm Settings
642# Only set if not already set
643if [ -z "${{ROCM_HOME:-}}" ]; then export ROCM_HOME={rocm_path}; fi
644if [ -z "${{CUDA_HOME:-}}" ]; then export CUDA_HOME={rocm_path}; fi
645if [ -z "${{ROCM_VERSION:-}}" ]; then export ROCM_VERSION={rocm_version}; fi
646if [ -z "${{ROCM_CHANNEL:-}}" ]; then export ROCM_CHANNEL={rocm_channel}; fi
647# GPU_ARCH is set based on detected hardware (corrected for rocminfo bugs)
648export GPU_ARCH={gpu_arch}
649
650# Path Settings - Hardcoded safe paths to prevent "command not found" errors
651export PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:{rocm_path}/bin:{rocm_path}/hip/bin:$PATH"
652export 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:-}}"
653
654# Performance Settings
655# HSA_OVERRIDE_GFX_VERSION is set based on detected GPU_ARCH
656export HSA_OVERRIDE_GFX_VERSION={hsa_override_gfx_version}
657export HSA_ENABLE_SDMA=0
658export GPU_MAX_HEAP_SIZE=100
659export GPU_MAX_ALLOC_PERCENT=100
660export HSA_TOOLS_LIB=/opt/rocm/lib/rocprofiler-sdk/librocprofiler-sdk-tool.so
661
662# MIOpen Settings
663# Only set if not already set
664if [ -z "${{MIOPEN_DEBUG_CONV_IMPLICIT_GEMM:-}}" ]; then export MIOPEN_DEBUG_CONV_IMPLICIT_GEMM=1; fi
665if [ -z "${{MIOPEN_FIND_MODE:-}}" ]; then export MIOPEN_FIND_MODE=3; fi
666if [ -z "${{MIOPEN_FIND_ENFORCE:-}}" ]; then export MIOPEN_FIND_ENFORCE=3; fi
667
668# PyTorch Settings
669# Only set if not already set
670if [ -z "${{TORCH_CUDA_ARCH_LIST:-}}" ]; then export TORCH_CUDA_ARCH_LIST="7.0;8.0;9.0"; fi
671# Use PYTORCH_ALLOC_CONF instead of deprecated PYTORCH_CUDA_ALLOC_CONF
672if [ -z "${{PYTORCH_ALLOC_CONF:-}}" ]; then export PYTORCH_ALLOC_CONF="max_split_size_mb:512"; fi
673if [ -z "${{PYTORCH_HIP_ALLOC_CONF:-}}" ]; then export PYTORCH_HIP_ALLOC_CONF="max_split_size_mb:512"; fi
674if [ -z "${{VLLM_WORKER_MULTIPROC_METHOD:-}}" ]; then export VLLM_WORKER_MULTIPROC_METHOD=spawn; fi
675if [ -z "${{VLLM_ROCM_USE_AITER:-}}" ]; then export VLLM_ROCM_USE_AITER=0; fi
676if [ -z "${{MLSTACK_TRITON_HOME:-}}" ]; then export MLSTACK_TRITON_HOME="$HOME/.cache/mlstack/triton"; fi
677if [ -z "${{TRITON_HOME:-}}" ]; then export TRITON_HOME="$MLSTACK_TRITON_HOME"; fi
678if [ -z "${{TRITON_CACHE_DIR:-}}" ]; then export TRITON_CACHE_DIR="$TRITON_HOME/cache"; fi
679if [ -z "${{TRITON_DUMP_DIR:-}}" ]; then export TRITON_DUMP_DIR="$TRITON_HOME/dump"; fi
680if [ -z "${{TRITON_OVERRIDE_DIR:-}}" ]; then export TRITON_OVERRIDE_DIR="$TRITON_HOME/override"; fi
681mkdir -p "$TRITON_CACHE_DIR" "$TRITON_DUMP_DIR" "$TRITON_OVERRIDE_DIR" 2>/dev/null || true
682
683# MPI Settings
684# Only set if not already set
685if [ -z "${{OMPI_MCA_opal_cuda_support:-}}" ]; then export OMPI_MCA_opal_cuda_support=true; fi
686if [ -z "${{OMPI_MCA_pml_ucx_opal_cuda_support:-}}" ]; then export OMPI_MCA_pml_ucx_opal_cuda_support=true; fi
687if [ -z "${{OMPI_MCA_btl_openib_allow_ib:-}}" ]; then export OMPI_MCA_btl_openib_allow_ib=true; fi
688if [ -z "${{OMPI_MCA_btl_openib_warn_no_device_params_found:-}}" ]; then export OMPI_MCA_btl_openib_warn_no_device_params_found=0; fi
689if [ -z "${{OMPI_MCA_coll_hcoll_enable:-}}" ]; then export OMPI_MCA_coll_hcoll_enable=0; fi
690if [ -z "${{OMPI_MCA_pml:-}}" ]; then export OMPI_MCA_pml=ucx; fi
691if [ -z "${{OMPI_MCA_osc:-}}" ]; then export OMPI_MCA_osc=ucx; fi
692if [ -z "${{OMPI_MCA_btl:-}}" ]; then export OMPI_MCA_btl=^openib,uct; fi
693# Prevent UCX ROCm memory registration segfault on multi-GPU systems
694export UCX_MEMTYPE_CACHE=no
695export UCX_LOG_LEVEL=error
696
697# Global flags for seamless pip installs (PEP 668 override)
698export PIP_BREAK_SYSTEM_PACKAGES=1
699export UV_PIP_BREAK_SYSTEM_PACKAGES=1
700export UV_SYSTEM_PYTHON=1
701
702# Python Interpreter
703# Canonical Python binary — all ML components install to this interpreter.
704# Ensures a single unified environment regardless of how many Pythons exist.
705export MLSTACK_PYTHON_BIN={python_bin}
706export UV_PYTHON={python_bin}
707
708# Flash Attention AMD
709export FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE
710
711# ONNX Runtime Settings
712# Only add if not already in PYTHONPATH
713if ! echo "${{PYTHONPATH:-}}" | grep -q "{home}/onnxruntime_build/onnxruntime/build/Linux/Release"; then
714  export PYTHONPATH={home}/onnxruntime_build/onnxruntime/build/Linux/Release:${{PYTHONPATH:-}}
715fi
716"#,
717        date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
718        hip_visible_devices = hip_visible_devices,
719        rocm_path = rocm_path,
720        rocm_version = rocm_version,
721        rocm_channel = rocm_channel,
722        gpu_arch = gpu_arch,
723        hsa_override_gfx_version = hsa_override_gfx_version,
724        home = home,
725        python_bin = python_bin,
726    )
727}
728
729/// Generate a fish-compatible environment file.
730///
731/// Produces `~/.config/fish/conf.d/mlstack_env.fish` that natively loads
732/// all ML Stack environment variables without needing to source the bash file.
733/// Fish doesn't support bash syntax (`if [ -z ... ]`, `${VAR:-}`, `fi`, etc.),
734/// so this uses idiomatic fish constructs.
735pub fn generate_fish_env_file_content(
736    hip_visible_devices: &str,
737    rocm_path: &str,
738    rocm_version: &str,
739    rocm_channel: &str,
740    gpu_arch: &str,
741    hsa_override_gfx_version: &str,
742    python_bin: &str,
743) -> String {
744    let home = std::env::var("HOME").unwrap_or_else(|_| "$HOME".to_string());
745
746    // Build ONNX Runtime PYTHONPATH check
747    let onnx_path = format!("{home}/onnxruntime_build/onnxruntime/build/Linux/Release");
748
749    format!(
750        r##"# ML Stack Environment File — Fish Shell Native
751# Generated by Enhanced ML Stack Environment Setup (Rust native)
752# Date: {date}
753#
754# This file is auto-generated. Edit via rusty-stack's env setup, not by hand.
755
756# --- GPU Selection ---
757set -q HIP_VISIBLE_DEVICES; or set -gx HIP_VISIBLE_DEVICES {hip_visible_devices}
758set -q CUDA_VISIBLE_DEVICES; or set -gx CUDA_VISIBLE_DEVICES {hip_visible_devices}
759set -q PYTORCH_ROCM_DEVICE; or set -gx PYTORCH_ROCM_DEVICE {hip_visible_devices}
760
761# --- ROCm Settings ---
762set -q ROCM_HOME; or set -gx ROCM_HOME {rocm_path}
763set -q CUDA_HOME; or set -gx CUDA_HOME {rocm_path}
764set -q ROCM_VERSION; or set -gx ROCM_VERSION {rocm_version}
765set -q ROCM_CHANNEL; or set -gx ROCM_CHANNEL {rocm_channel}
766# GPU_ARCH is set based on detected hardware (corrected for rocminfo bugs)
767set -gx GPU_ARCH {gpu_arch}
768
769# --- Path Settings ---
770set -gx PATH /usr/local/bin /usr/bin /bin /usr/local/games /usr/games {rocm_path}/bin {rocm_path}/hip/bin $PATH
771set -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
772
773# --- Performance Settings ---
774set -gx HSA_OVERRIDE_GFX_VERSION {hsa_override_gfx_version}
775set -gx HSA_ENABLE_SDMA 0
776set -gx GPU_MAX_HEAP_SIZE 100
777set -gx GPU_MAX_ALLOC_PERCENT 100
778set -gx HSA_TOOLS_LIB /opt/rocm/lib/rocprofiler-sdk/librocprofiler-sdk-tool.so
779
780# --- MIOpen Settings ---
781set -q MIOPEN_DEBUG_CONV_IMPLICIT_GEMM; or set -gx MIOPEN_DEBUG_CONV_IMPLICIT_GEMM 1
782set -q MIOPEN_FIND_MODE; or set -gx MIOPEN_FIND_MODE 3
783set -q MIOPEN_FIND_ENFORCE; or set -gx MIOPEN_FIND_ENFORCE 3
784
785# --- PyTorch Settings ---
786set -q TORCH_CUDA_ARCH_LIST; or set -gx TORCH_CUDA_ARCH_LIST "7.0;8.0;9.0"
787set -q PYTORCH_ALLOC_CONF; or set -gx PYTORCH_ALLOC_CONF "max_split_size_mb:512"
788set -q PYTORCH_HIP_ALLOC_CONF; or set -gx PYTORCH_HIP_ALLOC_CONF "max_split_size_mb:512"
789set -q VLLM_WORKER_MULTIPROC_METHOD; or set -gx VLLM_WORKER_MULTIPROC_METHOD spawn
790set -q VLLM_ROCM_USE_AITER; or set -gx VLLM_ROCM_USE_AITER 0
791set -q MLSTACK_TRITON_HOME; or set -gx MLSTACK_TRITON_HOME "$HOME/.cache/mlstack/triton"
792set -q TRITON_HOME; or set -gx TRITON_HOME $MLSTACK_TRITON_HOME
793set -q TRITON_CACHE_DIR; or set -gx TRITON_CACHE_DIR "$TRITON_HOME/cache"
794set -q TRITON_DUMP_DIR; or set -gx TRITON_DUMP_DIR "$TRITON_HOME/dump"
795set -q TRITON_OVERRIDE_DIR; or set -gx TRITON_OVERRIDE_DIR "$TRITON_HOME/override"
796mkdir -p $TRITON_CACHE_DIR $TRITON_DUMP_DIR $TRITON_OVERRIDE_DIR 2>/dev/null; or true
797
798# --- Global flags for seamless pip installs (PEP 668 override) ---
799set -q PIP_BREAK_SYSTEM_PACKAGES; or set -gx PIP_BREAK_SYSTEM_PACKAGES 1
800set -q UV_PIP_BREAK_SYSTEM_PACKAGES; or set -gx UV_PIP_BREAK_SYSTEM_PACKAGES 1
801set -gx UV_SYSTEM_PYTHON 1
802
803# --- Python Interpreter ---
804# Canonical Python binary — all ML components install to this interpreter.
805# Ensures a single unified environment regardless of how many Pythons exist.
806set -gx MLSTACK_PYTHON_BIN {python_bin}
807set -gx UV_PYTHON {python_bin}
808
809# --- Flash Attention AMD ---
810set -gx FLASH_ATTENTION_TRITON_AMD_ENABLE TRUE
811
812# --- MPI/UCX Settings ---
813set -q OMPI_MCA_opal_cuda_support; or set -gx OMPI_MCA_opal_cuda_support true
814set -q OMPI_MCA_pml_ucx_opal_cuda_support; or set -gx OMPI_MCA_pml_ucx_opal_cuda_support true
815set -q OMPI_MCA_btl_openib_allow_ib; or set -gx OMPI_MCA_btl_openib_allow_ib true
816set -q OMPI_MCA_btl_openib_warn_no_device_params_found; or set -gx OMPI_MCA_btl_openib_warn_no_device_params_found 0
817set -q OMPI_MCA_coll_hcoll_enable; or set -gx OMPI_MCA_coll_hcoll_enable 0
818set -q OMPI_MCA_pml; or set -gx OMPI_MCA_pml ucx
819set -q OMPI_MCA_osc; or set -gx OMPI_MCA_osc ucx
820set -q OMPI_MCA_btl; or set -gx OMPI_MCA_btl '^openib,uct'
821set -gx UCX_MEMTYPE_CACHE no
822set -gx UCX_LOG_LEVEL error
823
824# --- ONNX Runtime Settings ---
825if not string match -q '*{onnx_path}*' "$PYTHONPATH"
826    set -gx PYTHONPATH {onnx_path} $PYTHONPATH
827end
828"##,
829        date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
830        hip_visible_devices = hip_visible_devices,
831        rocm_path = rocm_path,
832        rocm_version = rocm_version,
833        rocm_channel = rocm_channel,
834        gpu_arch = gpu_arch,
835        hsa_override_gfx_version = hsa_override_gfx_version,
836        python_bin = python_bin,
837        onnx_path = onnx_path,
838    )
839}
840
841// ===========================================================================
842// Full Setup Orchestration
843// ===========================================================================
844
845/// Run the complete enhanced setup environment.
846///
847/// This is the Rust equivalent of the `main()` function in
848/// `enhanced_setup_environment.sh`:
849/// 1. Check system dependencies
850/// 2. Detect AMD GPUs
851/// 3. Detect ROCm
852/// 4. Configure environment variables
853/// 5. Create environment file
854///
855/// # Returns
856///
857/// A tuple of (env_file_result, gpu_filter_result, gpu_arch_info).
858pub fn setup_environment() -> (EnvFileResult, GpuFilterResult, GpuArchInfo) {
859    print_step("Enhanced ML Stack Environment Setup");
860
861    // Step 1: Detect GPUs and filter
862    print_step("Detecting AMD GPUs...");
863    let gpu_filter = detect_discrete_gpus();
864    if gpu_filter.gpu_count > 0 {
865        print_success(&format!(
866            "Detected {} discrete GPU(s): {}",
867            gpu_filter.gpu_count, gpu_filter.hip_visible_devices
868        ));
869    } else {
870        print_warning("No discrete GPUs detected, using defaults");
871    }
872
873    // Step 2: Detect ROCm
874    let rocm_env = crate::installers::common::rocm_env::RocmEnv::detect();
875    let rocm_path = rocm_env
876        .path()
877        .map(|p| p.to_string_lossy().to_string())
878        .unwrap_or_else(|| "/opt/rocm".to_string());
879    let rocm_version = if rocm_env.version().is_empty() {
880        "7.2.0".to_string()
881    } else {
882        rocm_env.version().to_string()
883    };
884    let rocm_channel = std::env::var("ROCM_CHANNEL").unwrap_or_else(|_| "latest".to_string());
885
886    if rocm_env.is_detected() {
887        print_success(&format!("ROCm {rocm_version} detected at {rocm_path}"));
888    } else {
889        print_warning("ROCm not detected, using default paths");
890    }
891
892    // Step 3: Detect GPU architecture
893    let gpu_arch_info = detect_gpu_arch_from_system();
894
895    // Step 4: Resolve canonical Python and create environment file
896    print_step("Creating environment file...");
897    let python_bin = crate::platform::environment::resolve_canonical_python_bin();
898    let env_result = create_env_file(
899        &gpu_filter.hip_visible_devices,
900        &rocm_path,
901        &rocm_version,
902        &rocm_channel,
903        &gpu_arch_info.gpu_arch,
904        &gpu_arch_info.hsa_override_gfx_version,
905        &python_bin,
906    );
907
908    if env_result.success {
909        print_success("Environment setup complete");
910    }
911
912    (env_result, gpu_filter, gpu_arch_info)
913}
914
915/// Detect GPU architecture from the system (rocminfo-based).
916fn detect_gpu_arch_from_system() -> GpuArchInfo {
917    let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
918    if !command_exists(&rocminfo_path) && !command_exists("rocminfo") {
919        return GpuArchInfo::default();
920    }
921
922    let output = match std::process::Command::new(&rocminfo_path).output() {
923        Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
924        _ => return GpuArchInfo::default(),
925    };
926
927    let mut marketing_name = String::new();
928
929    for line in output.lines() {
930        let trimmed = line.trim();
931        if trimmed.starts_with("Marketing Name:") {
932            let name = trimmed
933                .strip_prefix("Marketing Name:")
934                .map(|s| s.trim())
935                .unwrap_or("");
936
937            // Skip integrated GPUs
938            if is_integrated_gpu_name(name) {
939                continue;
940            }
941
942            if !name.is_empty() {
943                marketing_name = name.to_string();
944                break;
945            }
946        }
947    }
948
949    detect_correct_gpu_arch(if marketing_name.is_empty() {
950        None
951    } else {
952        Some(&marketing_name)
953    })
954}
955
956// ===========================================================================
957// Tests
958// ===========================================================================
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963
964    // --- GPU Arch Info (VAL-VBA-017) ---
965
966    #[test]
967    fn test_gpu_arch_info_default() {
968        let info = GpuArchInfo::default();
969        assert_eq!(info.gpu_arch, "gfx1100");
970        assert_eq!(info.hsa_override_gfx_version, "11.0.0");
971    }
972
973    #[test]
974    fn test_gpu_arch_info_from_arch_all_variants() {
975        let cases = vec![
976            ("gfx1100", "11.0.0"),
977            ("gfx1101", "11.0.1"),
978            ("gfx1102", "11.0.2"),
979            ("gfx1200", "12.0.0"),
980            ("gfx1201", "12.0.1"),
981            ("gfx1030", "10.3.0"),
982            ("gfx1031", "10.3.1"),
983            ("gfx1032", "10.3.2"),
984            ("gfx9999", "11.0.0"), // unknown defaults
985        ];
986
987        for (arch, expected_hsa) in cases {
988            let info = GpuArchInfo::from_arch(arch);
989            assert_eq!(info.gpu_arch, arch, "GPU arch mismatch for {arch}");
990            assert_eq!(
991                info.hsa_override_gfx_version, expected_hsa,
992                "HSA version mismatch for {arch}"
993            );
994        }
995    }
996
997    // --- Integrated GPU Detection (VAL-VBA-014) ---
998
999    #[test]
1000    fn test_is_integrated_gpu_name_positive() {
1001        assert!(is_integrated_gpu_name("Raphael"));
1002        assert!(is_integrated_gpu_name("AMD Ryzen Graphics"));
1003        assert!(is_integrated_gpu_name("Integrated Graphics"));
1004        assert!(is_integrated_gpu_name("iGPU Device"));
1005        assert!(is_integrated_gpu_name("AMD Radeon Graphics"));
1006    }
1007
1008    #[test]
1009    fn test_is_integrated_gpu_name_negative() {
1010        assert!(!is_integrated_gpu_name("Radeon RX 7900 XTX"));
1011        assert!(!is_integrated_gpu_name("Radeon RX 7800 XT"));
1012        assert!(!is_integrated_gpu_name("Radeon RX 9070 XT"));
1013        assert!(!is_integrated_gpu_name("Unknown GPU"));
1014    }
1015
1016    #[test]
1017    fn test_is_integrated_gpu_name_case_insensitive() {
1018        assert!(is_integrated_gpu_name("raphael"));
1019        assert!(is_integrated_gpu_name("INTEGRATED"));
1020        assert!(is_integrated_gpu_name("AMD RADEON GRAPHICS"));
1021    }
1022
1023    // --- GPU Detection (VAL-VBA-014) ---
1024
1025    #[test]
1026    fn test_detect_discrete_gpus_returns_result() {
1027        let result = detect_discrete_gpus();
1028        // Should always return at least the default GPU 0
1029        assert!(!result.hip_visible_devices.is_empty());
1030        assert!(result.gpu_count >= 1);
1031    }
1032
1033    #[test]
1034    fn test_detect_gpus_from_lspci_returns_vec() {
1035        // Just verify it doesn't panic
1036        let result = detect_gpus_from_lspci();
1037        // May be empty if no AMD GPUs, that's fine
1038        let _ = result;
1039    }
1040
1041    #[test]
1042    fn test_detect_gpus_from_render_nodes_returns_vec() {
1043        let result = detect_gpus_from_render_nodes();
1044        let _ = result;
1045    }
1046
1047    // --- Marketing Name to Architecture (VAL-VBA-017) ---
1048
1049    #[test]
1050    fn test_detect_correct_gpu_arch_7900_xtx() {
1051        let info = detect_correct_gpu_arch(Some("Radeon RX 7900 XTX"));
1052        assert_eq!(info.gpu_arch, "gfx1100");
1053        assert_eq!(info.hsa_override_gfx_version, "11.0.0");
1054    }
1055
1056    #[test]
1057    fn test_detect_correct_gpu_arch_7800_xt() {
1058        let info = detect_correct_gpu_arch(Some("Radeon RX 7800 XT"));
1059        assert_eq!(info.gpu_arch, "gfx1101");
1060    }
1061
1062    #[test]
1063    fn test_detect_correct_gpu_arch_7600() {
1064        let info = detect_correct_gpu_arch(Some("Radeon RX 7600"));
1065        assert_eq!(info.gpu_arch, "gfx1102");
1066    }
1067
1068    #[test]
1069    fn test_detect_correct_gpu_arch_9070_xt() {
1070        let info = detect_correct_gpu_arch(Some("Radeon RX 9070 XT"));
1071        assert_eq!(info.gpu_arch, "gfx1200");
1072    }
1073
1074    #[test]
1075    fn test_detect_correct_gpu_arch_none() {
1076        let info = detect_correct_gpu_arch(None);
1077        assert_eq!(info.gpu_arch, "gfx1100"); // default
1078    }
1079
1080    #[test]
1081    fn test_detect_correct_gpu_arch_unknown() {
1082        let info = detect_correct_gpu_arch(Some("Unknown GPU Model"));
1083        assert_eq!(info.gpu_arch, "gfx1100"); // default fallback
1084    }
1085
1086    #[test]
1087    fn test_detect_correct_gpu_arch_integrated_skipped() {
1088        let info = detect_correct_gpu_arch(Some("AMD Ryzen Graphics"));
1089        assert_eq!(info.gpu_arch, "gfx1100"); // default (skipped iGPU)
1090    }
1091
1092    #[test]
1093    fn test_detect_correct_gpu_arch_case_insensitive() {
1094        let info = detect_correct_gpu_arch(Some("radeon rx 7900 xtx"));
1095        assert_eq!(info.gpu_arch, "gfx1100");
1096    }
1097
1098    // --- System Dependencies (VAL-VBA-015) ---
1099
1100    #[test]
1101    fn test_system_dependency_required_packages() {
1102        let packages = SystemDependency::required_packages();
1103        assert!(!packages.is_empty());
1104        assert!(packages.contains(&"git"));
1105        assert!(packages.contains(&"cmake"));
1106        assert!(packages.contains(&"build-essential"));
1107    }
1108
1109    #[test]
1110    fn test_check_system_dependency_git() {
1111        let result = check_system_dependency("git");
1112        assert_eq!(result.package, "git");
1113        // git should be installed on any dev system
1114        assert!(result.installed, "git should be installed");
1115    }
1116
1117    #[test]
1118    fn test_check_system_dependency_nonexistent() {
1119        let result = check_system_dependency("nonexistent-pkg-xyz-12345");
1120        assert!(!result.installed);
1121    }
1122
1123    #[test]
1124    fn test_check_all_dependencies_returns_results() {
1125        let results = check_all_dependencies();
1126        assert!(!results.is_empty(), "Should check at least one dependency");
1127        assert_eq!(results.len(), SystemDependency::required_packages().len());
1128    }
1129
1130    // --- Environment File Content (VAL-VBA-016, VAL-VBA-017) ---
1131
1132    #[test]
1133    fn test_generate_env_file_has_header() {
1134        let content = generate_env_file_content(
1135            "0",
1136            "/opt/rocm",
1137            "7.2.0",
1138            "latest",
1139            "gfx1100",
1140            "11.0.0",
1141            "python3",
1142        );
1143        assert!(content.starts_with("# ML Stack Environment File"));
1144    }
1145
1146    #[test]
1147    fn test_generate_env_file_has_gpu_selection() {
1148        let content = generate_env_file_content(
1149            "0,1",
1150            "/opt/rocm",
1151            "7.2.0",
1152            "latest",
1153            "gfx1100",
1154            "11.0.0",
1155            "python3",
1156        );
1157        assert!(content.contains("HIP_VISIBLE_DEVICES"));
1158        assert!(content.contains("CUDA_VISIBLE_DEVICES"));
1159        assert!(content.contains("PYTORCH_ROCM_DEVICE"));
1160        assert!(content.contains("0,1"));
1161    }
1162
1163    #[test]
1164    fn test_generate_env_file_has_rocm_settings() {
1165        let content = generate_env_file_content(
1166            "0",
1167            "/opt/rocm",
1168            "7.2.0",
1169            "latest",
1170            "gfx1100",
1171            "11.0.0",
1172            "python3",
1173        );
1174        assert!(content.contains("ROCM_HOME"));
1175        assert!(content.contains("CUDA_HOME"));
1176        assert!(content.contains("ROCM_VERSION"));
1177        assert!(content.contains("ROCM_CHANNEL"));
1178        assert!(content.contains("GPU_ARCH"));
1179        assert!(content.contains("gfx1100"));
1180    }
1181
1182    #[test]
1183    fn test_generate_env_file_has_path_settings() {
1184        let content = generate_env_file_content(
1185            "0",
1186            "/opt/rocm",
1187            "7.2.0",
1188            "latest",
1189            "gfx1100",
1190            "11.0.0",
1191            "python3",
1192        );
1193        assert!(content.contains("PATH="));
1194        assert!(content.contains("LD_LIBRARY_PATH"));
1195        assert!(content.contains("/opt/rocm/bin"));
1196    }
1197
1198    #[test]
1199    fn test_generate_env_file_has_performance_settings() {
1200        let content = generate_env_file_content(
1201            "0",
1202            "/opt/rocm",
1203            "7.2.0",
1204            "latest",
1205            "gfx1100",
1206            "11.0.0",
1207            "python3",
1208        );
1209        assert!(content.contains("HSA_OVERRIDE_GFX_VERSION"));
1210        assert!(content.contains("HSA_ENABLE_SDMA"));
1211        assert!(content.contains("GPU_MAX_HEAP_SIZE"));
1212        assert!(content.contains("GPU_MAX_ALLOC_PERCENT"));
1213        assert!(content.contains("HSA_TOOLS_LIB"));
1214    }
1215
1216    #[test]
1217    fn test_generate_env_file_has_miopen_settings() {
1218        let content = generate_env_file_content(
1219            "0",
1220            "/opt/rocm",
1221            "7.2.0",
1222            "latest",
1223            "gfx1100",
1224            "11.0.0",
1225            "python3",
1226        );
1227        assert!(content.contains("MIOPEN_DEBUG_CONV_IMPLICIT_GEMM"));
1228        assert!(content.contains("MIOPEN_FIND_MODE"));
1229        assert!(content.contains("MIOPEN_FIND_ENFORCE"));
1230    }
1231
1232    #[test]
1233    fn test_generate_env_file_has_pytorch_settings() {
1234        let content = generate_env_file_content(
1235            "0",
1236            "/opt/rocm",
1237            "7.2.0",
1238            "latest",
1239            "gfx1100",
1240            "11.0.0",
1241            "python3",
1242        );
1243        assert!(content.contains("TORCH_CUDA_ARCH_LIST"));
1244        assert!(content.contains("PYTORCH_ALLOC_CONF"));
1245        assert!(content.contains("PYTORCH_HIP_ALLOC_CONF"));
1246        assert!(content.contains("VLLM_WORKER_MULTIPROC_METHOD"));
1247    }
1248
1249    #[test]
1250    fn test_generate_env_file_has_triton_settings() {
1251        let content = generate_env_file_content(
1252            "0",
1253            "/opt/rocm",
1254            "7.2.0",
1255            "latest",
1256            "gfx1100",
1257            "11.0.0",
1258            "python3",
1259        );
1260        assert!(content.contains("MLSTACK_TRITON_HOME"));
1261        assert!(content.contains("TRITON_HOME"));
1262        assert!(content.contains("TRITON_CACHE_DIR"));
1263    }
1264
1265    #[test]
1266    fn test_generate_env_file_has_mpi_settings() {
1267        let content = generate_env_file_content(
1268            "0",
1269            "/opt/rocm",
1270            "7.2.0",
1271            "latest",
1272            "gfx1100",
1273            "11.0.0",
1274            "python3",
1275        );
1276        assert!(content.contains("OMPI_MCA_opal_cuda_support"));
1277        assert!(content.contains("OMPI_MCA_pml"));
1278        assert!(content.contains("OMPI_MCA_osc"));
1279        assert!(content.contains("OMPI_MCA_btl"));
1280    }
1281
1282    #[test]
1283    fn test_generate_env_file_has_onnx_runtime() {
1284        let content = generate_env_file_content(
1285            "0",
1286            "/opt/rocm",
1287            "7.2.0",
1288            "latest",
1289            "gfx1100",
1290            "11.0.0",
1291            "python3",
1292        );
1293        assert!(content.contains("PYTHONPATH"));
1294        assert!(content.contains("onnxruntime"));
1295    }
1296
1297    #[test]
1298    fn test_generate_env_file_uses_guard_syntax() {
1299        let content = generate_env_file_content(
1300            "0",
1301            "/opt/rocm",
1302            "7.2.0",
1303            "latest",
1304            "gfx1100",
1305            "11.0.0",
1306            "python3",
1307        );
1308        // Should use "if [ -z ... ]" guards for conditional exports
1309        assert!(content.contains("if [ -z \"${HIP_VISIBLE_DEVICES:-}\" ]"));
1310        assert!(content.contains("if [ -z \"${ROCM_HOME:-}\" ]"));
1311    }
1312
1313    #[test]
1314    fn test_generate_env_file_gpu_arch_not_guarded() {
1315        // GPU_ARCH should be exported unconditionally (not guarded)
1316        let content = generate_env_file_content(
1317            "0",
1318            "/opt/rocm",
1319            "7.2.0",
1320            "latest",
1321            "gfx1100",
1322            "11.0.0",
1323            "python3",
1324        );
1325        assert!(content.contains("export GPU_ARCH=gfx1100"));
1326    }
1327
1328    #[test]
1329    fn test_generate_env_file_custom_values() {
1330        let content = generate_env_file_content(
1331            "0,1",
1332            "/opt/rocm-7.0",
1333            "7.0.0",
1334            "legacy",
1335            "gfx1030",
1336            "10.3.0",
1337            "python3",
1338        );
1339        assert!(content.contains("0,1"));
1340        assert!(content.contains("/opt/rocm-7.0"));
1341        assert!(content.contains("7.0.0"));
1342        assert!(content.contains("legacy"));
1343        assert!(content.contains("gfx1030"));
1344        assert!(content.contains("10.3.0"));
1345    }
1346
1347    // --- create_env_file (VAL-VBA-016) ---
1348
1349    #[test]
1350    fn test_create_env_file_creates_file_and_fish() {
1351        let tmp_dir = tempfile::tempdir().expect("Failed to create temp dir");
1352        let _env_path = tmp_dir.path().join(".mlstack_env");
1353        let _fish_env_path = tmp_dir.path().join(".config/fish/conf.d/mlstack_env.fish");
1354
1355        // Temporarily set HOME to the temp dir
1356        let original_home = std::env::var("HOME").ok();
1357        std::env::set_var("HOME", tmp_dir.path());
1358
1359        let result = create_env_file(
1360            "0",
1361            "/opt/rocm",
1362            "7.2.0",
1363            "latest",
1364            "gfx1100",
1365            "11.0.0",
1366            "python3",
1367        );
1368
1369        // Restore HOME
1370        if let Some(home) = original_home {
1371            std::env::set_var("HOME", home);
1372        } else {
1373            std::env::remove_var("HOME");
1374        }
1375
1376        assert!(result.success, "create_env_file should succeed");
1377        assert!(result.path.is_some());
1378
1379        // Verify the bash file exists and has content
1380        let path = result.path.unwrap();
1381        let content = std::fs::read_to_string(&path).expect("Should be able to read env file");
1382        assert!(content.contains("HIP_VISIBLE_DEVICES"));
1383        assert!(content.contains("ROCM_HOME"));
1384        assert!(content.contains("PIP_BREAK_SYSTEM_PACKAGES=1"));
1385        assert!(content.contains("UV_SYSTEM_PYTHON=1"));
1386    }
1387
1388    // --- Fish env file generation ---
1389
1390    #[test]
1391    fn test_generate_fish_env_file_has_fish_syntax() {
1392        let content = generate_fish_env_file_content(
1393            "0",
1394            "/opt/rocm",
1395            "7.2.0",
1396            "latest",
1397            "gfx1100",
1398            "11.0.0",
1399            "python3",
1400        );
1401        // Must NOT contain bash syntax
1402        assert!(!content.contains("if [ -z"));
1403        assert!(!content.contains("; fi"));
1404        assert!(!content.contains("export "));
1405        assert!(!content.contains("${"));
1406        // Must use fish syntax
1407        assert!(content.contains("set -gx"));
1408        assert!(content.contains("set -q"));
1409        assert!(content.contains("; or set -gx"));
1410    }
1411
1412    #[test]
1413    fn test_generate_fish_env_file_has_required_vars() {
1414        let content = generate_fish_env_file_content(
1415            "0,1",
1416            "/opt/rocm",
1417            "7.2.0",
1418            "latest",
1419            "gfx1100",
1420            "11.0.0",
1421            "python3",
1422        );
1423        assert!(content.contains("HIP_VISIBLE_DEVICES"));
1424        assert!(content.contains("ROCM_HOME"));
1425        assert!(content.contains("ROCM_VERSION"));
1426        assert!(content.contains("GPU_ARCH"));
1427        assert!(content.contains("HSA_OVERRIDE_GFX_VERSION"));
1428        assert!(content.contains("PIP_BREAK_SYSTEM_PACKAGES"));
1429        assert!(content.contains("UV_PIP_BREAK_SYSTEM_PACKAGES"));
1430        assert!(content.contains("UV_SYSTEM_PYTHON"));
1431        assert!(content.contains("FLASH_ATTENTION_TRITON_AMD_ENABLE"));
1432        assert!(content.contains("0,1"));
1433    }
1434
1435    #[test]
1436    fn test_generate_fish_env_file_has_no_bash_patterns() {
1437        let content = generate_fish_env_file_content(
1438            "0",
1439            "/opt/rocm",
1440            "7.2.0",
1441            "latest",
1442            "gfx1100",
1443            "11.0.0",
1444            "python3",
1445        );
1446        // Should never contain bash-specific constructs
1447        assert!(!content.contains("if ! "));
1448        assert!(!content.contains("fi\n"));
1449        assert!(!content.contains("; then"));
1450    }
1451
1452    #[test]
1453    fn test_generate_bash_env_file_has_pip_break_system_packages() {
1454        let content = generate_env_file_content(
1455            "0",
1456            "/opt/rocm",
1457            "7.2.0",
1458            "latest",
1459            "gfx1100",
1460            "11.0.0",
1461            "python3",
1462        );
1463        assert!(content.contains("export PIP_BREAK_SYSTEM_PACKAGES=1"));
1464        assert!(content.contains("export UV_PIP_BREAK_SYSTEM_PACKAGES=1"));
1465        assert!(content.contains("export UV_SYSTEM_PYTHON=1"));
1466        assert!(content.contains("export FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE"));
1467    }
1468
1469    // --- setup_environment integration ---
1470
1471    #[test]
1472    fn test_setup_environment_returns_results() {
1473        let (env_result, gpu_result, gpu_arch) = setup_environment();
1474        // Should always return results without panicking
1475        let _ = env_result.success;
1476        assert!(!gpu_result.hip_visible_devices.is_empty());
1477        assert!(!gpu_arch.gpu_arch.is_empty());
1478    }
1479}