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    /// Get the ROCm config file path.
175    pub fn rocm_config_path(&self) -> String {
176        format!(
177            "{}/.rocmrc",
178            std::env::var("HOME").unwrap_or_else(|_| "/root".to_string())
179        )
180    }
181
182    // -----------------------------------------------------------------------
183    // MIGraphX install command
184    // -----------------------------------------------------------------------
185
186    /// Construct the MIGraphX package install command per distro.
187    pub fn build_migraphx_install_command(&self, distro: &DistroFacade) -> SystemCommand {
188        if distro.uses_apt() {
189            SystemCommand {
190                program: "sudo".to_string(),
191                args: vec![
192                    "apt-get".to_string(),
193                    "install".to_string(),
194                    "-y".to_string(),
195                    "migraphx".to_string(),
196                    "python3-migraphx".to_string(),
197                ],
198            }
199        } else if distro.uses_dnf() {
200            SystemCommand {
201                program: "sudo".to_string(),
202                args: vec![
203                    "dnf".to_string(),
204                    "install".to_string(),
205                    "-y".to_string(),
206                    "migraphx".to_string(),
207                    "python3-migraphx".to_string(),
208                ],
209            }
210        } else if distro.uses_pacman() {
211            SystemCommand {
212                program: "sudo".to_string(),
213                args: vec![
214                    "pacman".to_string(),
215                    "-S".to_string(),
216                    "--noconfirm".to_string(),
217                    "migraphx".to_string(),
218                    "python-migraphx".to_string(),
219                ],
220            }
221        } else if distro.uses_zypper() {
222            SystemCommand {
223                program: "sudo".to_string(),
224                args: vec![
225                    "zypper".to_string(),
226                    "install".to_string(),
227                    "-y".to_string(),
228                    "migraphx".to_string(),
229                    "python3-migraphx".to_string(),
230                ],
231            }
232        } else {
233            SystemCommand {
234                program: "sudo".to_string(),
235                args: vec![
236                    "apt-get".to_string(),
237                    "install".to_string(),
238                    "-y".to_string(),
239                    "migraphx".to_string(),
240                    "python3-migraphx".to_string(),
241                ],
242            }
243        }
244    }
245
246    // -----------------------------------------------------------------------
247    // RCCL install command
248    // -----------------------------------------------------------------------
249
250    /// Construct the RCCL package install command per distro.
251    pub fn build_rccl_install_command(&self, distro: &DistroFacade) -> SystemCommand {
252        if distro.uses_apt() {
253            SystemCommand {
254                program: "sudo".to_string(),
255                args: vec![
256                    "apt-get".to_string(),
257                    "install".to_string(),
258                    "-y".to_string(),
259                    "rccl".to_string(),
260                ],
261            }
262        } else if distro.uses_dnf() {
263            SystemCommand {
264                program: "sudo".to_string(),
265                args: vec![
266                    "dnf".to_string(),
267                    "install".to_string(),
268                    "-y".to_string(),
269                    "rccl".to_string(),
270                ],
271            }
272        } else if distro.uses_pacman() {
273            SystemCommand {
274                program: "sudo".to_string(),
275                args: vec![
276                    "pacman".to_string(),
277                    "-S".to_string(),
278                    "--noconfirm".to_string(),
279                    "rccl".to_string(),
280                ],
281            }
282        } else {
283            SystemCommand {
284                program: "sudo".to_string(),
285                args: vec![
286                    "apt-get".to_string(),
287                    "install".to_string(),
288                    "-y".to_string(),
289                    "rccl".to_string(),
290                ],
291            }
292        }
293    }
294
295    // -----------------------------------------------------------------------
296    // MPI install command
297    // -----------------------------------------------------------------------
298
299    /// Construct the MPI system package install command per distro.
300    pub fn build_mpi_install_command(&self, distro: &DistroFacade) -> SystemCommand {
301        if distro.uses_apt() {
302            SystemCommand {
303                program: "sudo".to_string(),
304                args: vec![
305                    "apt-get".to_string(),
306                    "install".to_string(),
307                    "-y".to_string(),
308                    "openmpi-bin".to_string(),
309                    "libopenmpi-dev".to_string(),
310                ],
311            }
312        } else if distro.uses_dnf() {
313            SystemCommand {
314                program: "sudo".to_string(),
315                args: vec![
316                    "dnf".to_string(),
317                    "install".to_string(),
318                    "-y".to_string(),
319                    "openmpi".to_string(),
320                    "openmpi-devel".to_string(),
321                ],
322            }
323        } else if distro.uses_pacman() {
324            SystemCommand {
325                program: "sudo".to_string(),
326                args: vec![
327                    "pacman".to_string(),
328                    "-S".to_string(),
329                    "--noconfirm".to_string(),
330                    "openmpi".to_string(),
331                ],
332            }
333        } else {
334            SystemCommand {
335                program: "sudo".to_string(),
336                args: vec![
337                    "apt-get".to_string(),
338                    "install".to_string(),
339                    "-y".to_string(),
340                    "openmpi-bin".to_string(),
341                    "libopenmpi-dev".to_string(),
342                ],
343            }
344        }
345    }
346
347    // -----------------------------------------------------------------------
348    // Megatron-LM install commands
349    // -----------------------------------------------------------------------
350
351    /// Construct the Megatron-LM git clone command into `target_dir`.
352    pub fn build_megatron_clone_command(&self, target_dir: &str) -> SystemCommand {
353        SystemCommand {
354            program: "git".to_string(),
355            args: vec![
356                "clone".to_string(),
357                "https://github.com/NVIDIA/Megatron-LM.git".to_string(),
358                target_dir.to_string(),
359            ],
360            // Note: SystemCommand doesn't have env, but that's fine for git clone
361        }
362    }
363
364    /// Construct the Megatron-LM pip install command.
365    pub fn build_megatron_install_command(&self) -> PipCommand {
366        PipCommand {
367            program: self.config.python_bin.clone(),
368            args: vec![
369                "-m".to_string(),
370                "pip".to_string(),
371                "install".to_string(),
372                "--break-system-packages".to_string(),
373                "-e".to_string(),
374                ".".to_string(),
375            ],
376        }
377    }
378
379    // -----------------------------------------------------------------------
380    // Prerequisites check
381    // -----------------------------------------------------------------------
382
383    /// Get the list of prerequisite commands to check.
384    pub fn prerequisite_commands(&self) -> &[&str] {
385        &["rocminfo", "python3", "pip3", "git", "cmake"]
386    }
387
388    /// Check a single prerequisite.
389    pub fn check_prerequisite(&self, cmd: &str) -> bool {
390        command_exists(cmd)
391    }
392}
393
394// ===========================================================================
395// Tests
396// ===========================================================================
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401    use crate::platform::detection::{DistroFamily, DistroInfo, PackageManager};
402
403    fn ubuntu_distro() -> DistroFacade {
404        DistroFacade::from_info(DistroInfo {
405            id: "ubuntu".to_string(),
406            name: "Ubuntu 24.04".to_string(),
407            version: "24.04".to_string(),
408            codename: "noble".to_string(),
409            family: DistroFamily::Debian,
410            pkg_manager: PackageManager::Apt,
411            id_like: "debian".to_string(),
412        })
413    }
414
415    fn fedora_distro() -> DistroFacade {
416        DistroFacade::from_info(DistroInfo {
417            id: "fedora".to_string(),
418            name: "Fedora 41".to_string(),
419            version: "41".to_string(),
420            codename: String::new(),
421            family: DistroFamily::Rhel,
422            pkg_manager: PackageManager::Dnf,
423            id_like: "fedora".to_string(),
424        })
425    }
426
427    fn arch_distro() -> DistroFacade {
428        DistroFacade::from_info(DistroInfo {
429            id: "arch".to_string(),
430            name: "Arch Linux".to_string(),
431            version: "rolling".to_string(),
432            codename: String::new(),
433            family: DistroFamily::Arch,
434            pkg_manager: PackageManager::Pacman,
435            id_like: "arch".to_string(),
436        })
437    }
438
439    // --- VAL-INSTALL-007: ML Stack Core installer correct pip command ---
440
441    #[test]
442    fn test_install_order() {
443        let order = MlStackInstaller::install_order();
444        assert_eq!(order.len(), 8);
445        assert_eq!(order[0], MlStackComponent::RocmConfig);
446        assert_eq!(order[1], MlStackComponent::PyTorch);
447        assert_eq!(order[7], MlStackComponent::Mpi);
448    }
449
450    #[test]
451    fn test_rocm_config_path() {
452        let installer = MlStackInstaller::with_defaults();
453        let path = installer.rocm_config_path();
454        assert!(path.ends_with("/.rocmrc"));
455    }
456
457    #[test]
458    fn test_migraphx_install_apt() {
459        let installer = MlStackInstaller::with_defaults();
460        let distro = ubuntu_distro();
461        let cmd = installer.build_migraphx_install_command(&distro);
462        assert!(cmd.args.contains(&"migraphx".to_string()));
463        assert!(cmd.args.contains(&"python3-migraphx".to_string()));
464    }
465
466    #[test]
467    fn test_migraphx_install_dnf() {
468        let installer = MlStackInstaller::with_defaults();
469        let distro = fedora_distro();
470        let cmd = installer.build_migraphx_install_command(&distro);
471        assert!(cmd.args.contains(&"migraphx".to_string()));
472    }
473
474    #[test]
475    fn test_migraphx_install_pacman() {
476        let installer = MlStackInstaller::with_defaults();
477        let distro = arch_distro();
478        let cmd = installer.build_migraphx_install_command(&distro);
479        assert!(cmd.args.contains(&"python-migraphx".to_string()));
480    }
481
482    #[test]
483    fn test_rccl_install_apt() {
484        let installer = MlStackInstaller::with_defaults();
485        let distro = ubuntu_distro();
486        let cmd = installer.build_rccl_install_command(&distro);
487        assert!(cmd.args.contains(&"rccl".to_string()));
488    }
489
490    #[test]
491    fn test_mpi_install_apt() {
492        let installer = MlStackInstaller::with_defaults();
493        let distro = ubuntu_distro();
494        let cmd = installer.build_mpi_install_command(&distro);
495        assert!(cmd.args.contains(&"openmpi-bin".to_string()));
496        assert!(cmd.args.contains(&"libopenmpi-dev".to_string()));
497    }
498
499    #[test]
500    fn test_mpi_install_dnf() {
501        let installer = MlStackInstaller::with_defaults();
502        let distro = fedora_distro();
503        let cmd = installer.build_mpi_install_command(&distro);
504        assert!(cmd.args.contains(&"openmpi".to_string()));
505        assert!(cmd.args.contains(&"openmpi-devel".to_string()));
506    }
507
508    #[test]
509    fn test_megatron_clone_command() {
510        let installer = MlStackInstaller::with_defaults();
511        let cmd = installer.build_megatron_clone_command("/tmp/Megatron-LM");
512        assert_eq!(cmd.program, "git");
513        assert!(cmd.args.contains(&"clone".to_string()));
514        assert!(cmd.args.iter().any(|a| a.contains("NVIDIA/Megatron-LM")));
515        assert!(cmd.args.contains(&"/tmp/Megatron-LM".to_string()));
516    }
517
518    #[test]
519    fn test_megatron_install_command() {
520        let installer = MlStackInstaller::with_defaults();
521        let cmd = installer.build_megatron_install_command();
522        assert_eq!(cmd.program, "python3");
523        assert!(cmd.args.contains(&"-e".to_string()));
524        assert!(cmd.args.contains(&".".to_string()));
525    }
526
527    #[test]
528    fn test_prerequisite_commands() {
529        let installer = MlStackInstaller::with_defaults();
530        let cmds = installer.prerequisite_commands();
531        assert!(cmds.contains(&"python3"));
532        assert!(cmds.contains(&"git"));
533        assert!(cmds.contains(&"cmake"));
534    }
535
536    #[test]
537    fn test_component_display() {
538        assert_eq!(format!("{}", MlStackComponent::PyTorch), "pytorch");
539        assert_eq!(format!("{}", MlStackComponent::RocmConfig), "rocm_config");
540        assert_eq!(format!("{}", MlStackComponent::Mpi), "mpi");
541    }
542}