Skip to main content

rusty_stack/installers/components/
ml_stack.rs

1//! ML Stack Core installer — ports `scripts/install_ml_stack.sh`.
2//!
3//! Installs the full ML stack by orchestrating individual component
4//! installers. Constructs correct pip install paths for the ML stack
5//! Python package and delegates to sub-installers.
6//!
7//! # Validation Assertion
8//!
9//! - **VAL-INSTALL-007**: ML Stack Core installer correct pip command
10
11use crate::installers::common::{command_exists, DistroFacade};
12use std::fmt;
13
14// ===========================================================================
15// Types
16// ===========================================================================
17
18/// Component to install.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum MlStackComponent {
21    /// ROCm configuration.
22    RocmConfig,
23    /// PyTorch with ROCm.
24    PyTorch,
25    /// ONNX Runtime.
26    OnnxRuntime,
27    /// MIGraphX.
28    MigraphX,
29    /// Megatron-LM.
30    Megatron,
31    /// Flash Attention.
32    FlashAttention,
33    /// RCCL.
34    Rccl,
35    /// MPI (OpenMPI + mpi4py).
36    Mpi,
37}
38
39impl fmt::Display for MlStackComponent {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            MlStackComponent::RocmConfig => write!(f, "rocm_config"),
43            MlStackComponent::PyTorch => write!(f, "pytorch"),
44            MlStackComponent::OnnxRuntime => write!(f, "onnxruntime"),
45            MlStackComponent::MigraphX => write!(f, "migraphx"),
46            MlStackComponent::Megatron => write!(f, "megatron"),
47            MlStackComponent::FlashAttention => write!(f, "flash_attention"),
48            MlStackComponent::Rccl => write!(f, "rccl"),
49            MlStackComponent::Mpi => write!(f, "mpi"),
50        }
51    }
52}
53
54/// Installation mode.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum InstallMode {
57    /// Install all core components.
58    All,
59    /// Install only specific components.
60    Selective,
61}
62
63/// Configuration for the ML Stack installer.
64#[derive(Debug, Clone)]
65pub struct MlStackConfig {
66    /// Python binary to use.
67    pub python_bin: String,
68    /// Whether to force reinstall.
69    pub force_reinstall: bool,
70    /// Whether to run in dry-run mode.
71    pub dry_run: bool,
72    /// Installation mode.
73    pub mode: InstallMode,
74    /// Selected components (for selective mode).
75    pub selected_components: Vec<MlStackComponent>,
76}
77
78impl Default for MlStackConfig {
79    fn default() -> Self {
80        Self {
81            python_bin: "python3".to_string(),
82            force_reinstall: false,
83            dry_run: false,
84            mode: InstallMode::All,
85            selected_components: vec![],
86        }
87    }
88}
89
90/// A constructed pip command.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct PipCommand {
93    /// The program to run.
94    pub program: String,
95    /// Arguments to pass.
96    pub args: Vec<String>,
97}
98
99impl PipCommand {
100    /// Format as a shell command string.
101    pub fn to_command_string(&self) -> String {
102        if self.args.is_empty() {
103            self.program.clone()
104        } else {
105            format!("{} {}", self.program, self.args.join(" "))
106        }
107    }
108}
109
110/// A constructed system package command.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct SystemCommand {
113    /// The program to run.
114    pub program: String,
115    /// Arguments to pass.
116    pub args: Vec<String>,
117}
118
119impl SystemCommand {
120    /// Format as a shell command string.
121    pub fn to_command_string(&self) -> String {
122        if self.args.is_empty() {
123            self.program.clone()
124        } else {
125            format!("{} {}", self.program, self.args.join(" "))
126        }
127    }
128}
129
130/// The ML Stack installer.
131pub struct MlStackInstaller {
132    config: MlStackConfig,
133}
134
135impl MlStackInstaller {
136    /// Create a new ML Stack installer with the given config.
137    pub fn new(config: MlStackConfig) -> Self {
138        Self { config }
139    }
140
141    /// Create with default config.
142    pub fn with_defaults() -> Self {
143        Self::new(MlStackConfig::default())
144    }
145
146    // -----------------------------------------------------------------------
147    // Component install order
148    // -----------------------------------------------------------------------
149
150    /// Get the ordered list of core components to install.
151    ///
152    /// The order matches the original `install_ml_stack.sh`:
153    /// 1. ROCm Config
154    /// 2. PyTorch
155    /// 3. ONNX Runtime
156    /// 4. MIGraphX
157    /// 5. Megatron-LM
158    /// 6. Flash Attention
159    /// 7. RCCL
160    /// 8. MPI
161    pub fn install_order() -> Vec<MlStackComponent> {
162        vec![
163            MlStackComponent::RocmConfig,
164            MlStackComponent::PyTorch,
165            MlStackComponent::OnnxRuntime,
166            MlStackComponent::MigraphX,
167            MlStackComponent::Megatron,
168            MlStackComponent::FlashAttention,
169            MlStackComponent::Rccl,
170            MlStackComponent::Mpi,
171        ]
172    }
173
174    // -----------------------------------------------------------------------
175    // ROCm configuration (VAL-INSTALL-007)
176    // -----------------------------------------------------------------------
177
178    /// Construct the ROCm configuration file content.
179    ///
180    /// Creates a `.rocmrc` file with environment variable exports matching
181    /// the original script's `install_rocm_config()` function.
182    pub fn build_rocm_config_content(&self) -> String {
183        r#"# ROCm Configuration File
184# Created by ML Stack Installation Script
185
186export HSA_OVERRIDE_GFX_VERSION=11.0.0
187export PYTORCH_ROCM_ARCH=gfx1100
188export ROCM_PATH=/opt/rocm
189
190# Performance Settings
191export HSA_ENABLE_SDMA=0
192export GPU_MAX_HEAP_SIZE=100
193export GPU_MAX_ALLOC_PERCENT=100
194
195# MIOpen Settings
196export MIOPEN_DEBUG_CONV_IMPLICIT_GEMM=1
197export MIOPEN_FIND_MODE=3
198export MIOPEN_FIND_ENFORCE=3
199"#
200        .to_string()
201    }
202
203    /// Get the ROCm config file path.
204    pub fn rocm_config_path(&self) -> String {
205        format!(
206            "{}/.rocmrc",
207            std::env::var("HOME").unwrap_or_else(|_| "/root".to_string())
208        )
209    }
210
211    // -----------------------------------------------------------------------
212    // MIGraphX install command
213    // -----------------------------------------------------------------------
214
215    /// Construct the MIGraphX package install command per distro.
216    pub fn build_migraphx_install_command(&self, distro: &DistroFacade) -> SystemCommand {
217        if distro.uses_apt() {
218            SystemCommand {
219                program: "sudo".to_string(),
220                args: vec![
221                    "apt-get".to_string(),
222                    "install".to_string(),
223                    "-y".to_string(),
224                    "migraphx".to_string(),
225                    "python3-migraphx".to_string(),
226                ],
227            }
228        } else if distro.uses_dnf() {
229            SystemCommand {
230                program: "sudo".to_string(),
231                args: vec![
232                    "dnf".to_string(),
233                    "install".to_string(),
234                    "-y".to_string(),
235                    "migraphx".to_string(),
236                    "python3-migraphx".to_string(),
237                ],
238            }
239        } else if distro.uses_pacman() {
240            SystemCommand {
241                program: "sudo".to_string(),
242                args: vec![
243                    "pacman".to_string(),
244                    "-S".to_string(),
245                    "--noconfirm".to_string(),
246                    "migraphx".to_string(),
247                    "python-migraphx".to_string(),
248                ],
249            }
250        } else if distro.uses_zypper() {
251            SystemCommand {
252                program: "sudo".to_string(),
253                args: vec![
254                    "zypper".to_string(),
255                    "install".to_string(),
256                    "-y".to_string(),
257                    "migraphx".to_string(),
258                    "python3-migraphx".to_string(),
259                ],
260            }
261        } else {
262            SystemCommand {
263                program: "sudo".to_string(),
264                args: vec![
265                    "apt-get".to_string(),
266                    "install".to_string(),
267                    "-y".to_string(),
268                    "migraphx".to_string(),
269                    "python3-migraphx".to_string(),
270                ],
271            }
272        }
273    }
274
275    // -----------------------------------------------------------------------
276    // RCCL install command
277    // -----------------------------------------------------------------------
278
279    /// Construct the RCCL package install command per distro.
280    pub fn build_rccl_install_command(&self, distro: &DistroFacade) -> SystemCommand {
281        if distro.uses_apt() {
282            SystemCommand {
283                program: "sudo".to_string(),
284                args: vec![
285                    "apt-get".to_string(),
286                    "install".to_string(),
287                    "-y".to_string(),
288                    "rccl".to_string(),
289                ],
290            }
291        } else if distro.uses_dnf() {
292            SystemCommand {
293                program: "sudo".to_string(),
294                args: vec![
295                    "dnf".to_string(),
296                    "install".to_string(),
297                    "-y".to_string(),
298                    "rccl".to_string(),
299                ],
300            }
301        } else if distro.uses_pacman() {
302            SystemCommand {
303                program: "sudo".to_string(),
304                args: vec![
305                    "pacman".to_string(),
306                    "-S".to_string(),
307                    "--noconfirm".to_string(),
308                    "rccl".to_string(),
309                ],
310            }
311        } else {
312            SystemCommand {
313                program: "sudo".to_string(),
314                args: vec![
315                    "apt-get".to_string(),
316                    "install".to_string(),
317                    "-y".to_string(),
318                    "rccl".to_string(),
319                ],
320            }
321        }
322    }
323
324    // -----------------------------------------------------------------------
325    // MPI install command
326    // -----------------------------------------------------------------------
327
328    /// Construct the MPI system package install command per distro.
329    pub fn build_mpi_install_command(&self, distro: &DistroFacade) -> SystemCommand {
330        if distro.uses_apt() {
331            SystemCommand {
332                program: "sudo".to_string(),
333                args: vec![
334                    "apt-get".to_string(),
335                    "install".to_string(),
336                    "-y".to_string(),
337                    "openmpi-bin".to_string(),
338                    "libopenmpi-dev".to_string(),
339                ],
340            }
341        } else if distro.uses_dnf() {
342            SystemCommand {
343                program: "sudo".to_string(),
344                args: vec![
345                    "dnf".to_string(),
346                    "install".to_string(),
347                    "-y".to_string(),
348                    "openmpi".to_string(),
349                    "openmpi-devel".to_string(),
350                ],
351            }
352        } else if distro.uses_pacman() {
353            SystemCommand {
354                program: "sudo".to_string(),
355                args: vec![
356                    "pacman".to_string(),
357                    "-S".to_string(),
358                    "--noconfirm".to_string(),
359                    "openmpi".to_string(),
360                ],
361            }
362        } else {
363            SystemCommand {
364                program: "sudo".to_string(),
365                args: vec![
366                    "apt-get".to_string(),
367                    "install".to_string(),
368                    "-y".to_string(),
369                    "openmpi-bin".to_string(),
370                    "libopenmpi-dev".to_string(),
371                ],
372            }
373        }
374    }
375
376    // -----------------------------------------------------------------------
377    // Megatron-LM install commands
378    // -----------------------------------------------------------------------
379
380    /// Construct the Megatron-LM git clone command.
381    pub fn build_megatron_clone_command(&self) -> SystemCommand {
382        SystemCommand {
383            program: "git".to_string(),
384            args: vec![
385                "clone".to_string(),
386                "https://github.com/NVIDIA/Megatron-LM.git".to_string(),
387            ],
388            // Note: SystemCommand doesn't have env, but that's fine for git clone
389        }
390    }
391
392    /// Construct the Megatron-LM pip install command.
393    pub fn build_megatron_install_command(&self) -> PipCommand {
394        PipCommand {
395            program: self.config.python_bin.clone(),
396            args: vec![
397                "-m".to_string(),
398                "pip".to_string(),
399                "install".to_string(),
400                "--break-system-packages".to_string(),
401                "-e".to_string(),
402                ".".to_string(),
403            ],
404        }
405    }
406
407    // -----------------------------------------------------------------------
408    // Prerequisites check
409    // -----------------------------------------------------------------------
410
411    /// Get the list of prerequisite commands to check.
412    pub fn prerequisite_commands(&self) -> &[&str] {
413        &["rocminfo", "python3", "pip3", "git", "cmake"]
414    }
415
416    /// Check a single prerequisite.
417    pub fn check_prerequisite(&self, cmd: &str) -> bool {
418        command_exists(cmd)
419    }
420}
421
422// ===========================================================================
423// Tests
424// ===========================================================================
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use crate::platform::detection::{DistroFamily, DistroInfo, PackageManager};
430
431    fn ubuntu_distro() -> DistroFacade {
432        DistroFacade::from_info(DistroInfo {
433            id: "ubuntu".to_string(),
434            name: "Ubuntu 24.04".to_string(),
435            version: "24.04".to_string(),
436            codename: "noble".to_string(),
437            family: DistroFamily::Debian,
438            pkg_manager: PackageManager::Apt,
439            id_like: "debian".to_string(),
440        })
441    }
442
443    fn fedora_distro() -> DistroFacade {
444        DistroFacade::from_info(DistroInfo {
445            id: "fedora".to_string(),
446            name: "Fedora 41".to_string(),
447            version: "41".to_string(),
448            codename: String::new(),
449            family: DistroFamily::Rhel,
450            pkg_manager: PackageManager::Dnf,
451            id_like: "fedora".to_string(),
452        })
453    }
454
455    fn arch_distro() -> DistroFacade {
456        DistroFacade::from_info(DistroInfo {
457            id: "arch".to_string(),
458            name: "Arch Linux".to_string(),
459            version: "rolling".to_string(),
460            codename: String::new(),
461            family: DistroFamily::Arch,
462            pkg_manager: PackageManager::Pacman,
463            id_like: "arch".to_string(),
464        })
465    }
466
467    // --- VAL-INSTALL-007: ML Stack Core installer correct pip command ---
468
469    #[test]
470    fn test_install_order() {
471        let order = MlStackInstaller::install_order();
472        assert_eq!(order.len(), 8);
473        assert_eq!(order[0], MlStackComponent::RocmConfig);
474        assert_eq!(order[1], MlStackComponent::PyTorch);
475        assert_eq!(order[7], MlStackComponent::Mpi);
476    }
477
478    #[test]
479    fn test_rocm_config_content() {
480        let installer = MlStackInstaller::with_defaults();
481        let content = installer.build_rocm_config_content();
482        assert!(content.contains("HSA_OVERRIDE_GFX_VERSION"));
483        assert!(content.contains("PYTORCH_ROCM_ARCH"));
484        assert!(content.contains("ROCM_PATH"));
485        assert!(content.contains("MIOPEN_DEBUG_CONV_IMPLICIT_GEMM"));
486    }
487
488    #[test]
489    fn test_rocm_config_path() {
490        let installer = MlStackInstaller::with_defaults();
491        let path = installer.rocm_config_path();
492        assert!(path.ends_with("/.rocmrc"));
493    }
494
495    #[test]
496    fn test_migraphx_install_apt() {
497        let installer = MlStackInstaller::with_defaults();
498        let distro = ubuntu_distro();
499        let cmd = installer.build_migraphx_install_command(&distro);
500        assert!(cmd.args.contains(&"migraphx".to_string()));
501        assert!(cmd.args.contains(&"python3-migraphx".to_string()));
502    }
503
504    #[test]
505    fn test_migraphx_install_dnf() {
506        let installer = MlStackInstaller::with_defaults();
507        let distro = fedora_distro();
508        let cmd = installer.build_migraphx_install_command(&distro);
509        assert!(cmd.args.contains(&"migraphx".to_string()));
510    }
511
512    #[test]
513    fn test_migraphx_install_pacman() {
514        let installer = MlStackInstaller::with_defaults();
515        let distro = arch_distro();
516        let cmd = installer.build_migraphx_install_command(&distro);
517        assert!(cmd.args.contains(&"python-migraphx".to_string()));
518    }
519
520    #[test]
521    fn test_rccl_install_apt() {
522        let installer = MlStackInstaller::with_defaults();
523        let distro = ubuntu_distro();
524        let cmd = installer.build_rccl_install_command(&distro);
525        assert!(cmd.args.contains(&"rccl".to_string()));
526    }
527
528    #[test]
529    fn test_mpi_install_apt() {
530        let installer = MlStackInstaller::with_defaults();
531        let distro = ubuntu_distro();
532        let cmd = installer.build_mpi_install_command(&distro);
533        assert!(cmd.args.contains(&"openmpi-bin".to_string()));
534        assert!(cmd.args.contains(&"libopenmpi-dev".to_string()));
535    }
536
537    #[test]
538    fn test_mpi_install_dnf() {
539        let installer = MlStackInstaller::with_defaults();
540        let distro = fedora_distro();
541        let cmd = installer.build_mpi_install_command(&distro);
542        assert!(cmd.args.contains(&"openmpi".to_string()));
543        assert!(cmd.args.contains(&"openmpi-devel".to_string()));
544    }
545
546    #[test]
547    fn test_megatron_clone_command() {
548        let installer = MlStackInstaller::with_defaults();
549        let cmd = installer.build_megatron_clone_command();
550        assert_eq!(cmd.program, "git");
551        assert!(cmd.args.contains(&"clone".to_string()));
552        assert!(cmd.args.iter().any(|a| a.contains("NVIDIA/Megatron-LM")));
553    }
554
555    #[test]
556    fn test_megatron_install_command() {
557        let installer = MlStackInstaller::with_defaults();
558        let cmd = installer.build_megatron_install_command();
559        assert_eq!(cmd.program, "python3");
560        assert!(cmd.args.contains(&"-e".to_string()));
561        assert!(cmd.args.contains(&".".to_string()));
562    }
563
564    #[test]
565    fn test_prerequisite_commands() {
566        let installer = MlStackInstaller::with_defaults();
567        let cmds = installer.prerequisite_commands();
568        assert!(cmds.contains(&"python3"));
569        assert!(cmds.contains(&"git"));
570        assert!(cmds.contains(&"cmake"));
571    }
572
573    #[test]
574    fn test_component_display() {
575        assert_eq!(format!("{}", MlStackComponent::PyTorch), "pytorch");
576        assert_eq!(format!("{}", MlStackComponent::RocmConfig), "rocm_config");
577        assert_eq!(format!("{}", MlStackComponent::Mpi), "mpi");
578    }
579}