Skip to main content

rusty_stack/installers/components/
megatron.rs

1//! Megatron-LM installer — ports `scripts/install_megatron.sh`.
2//!
3//! Constructs correct git clone URL/branch and pip install commands.
4//! Megatron depends on PyTorch (ROCm-enabled) and MPI4Py.
5//!
6//! # Validation Assertions
7//!
8//! - **VAL-INSTALL-012**: Megatron correct git clone
9//! - **VAL-INSTALL-013**: Megatron correct pip install post-clone
10//! - **VAL-INSTALL-041**: Megatron declares dependency on PyTorch and MPI4Py
11//! - **VAL-INSTALL-043**: DeepSpeed declares dependency on PyTorch
12
13use crate::installers::common::RocmEnv;
14use std::fmt;
15use std::path::PathBuf;
16
17// ===========================================================================
18// Types
19// ===========================================================================
20
21/// Installation method.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum InstallMethod {
24    /// Install globally.
25    Global,
26    /// Install in a virtual environment.
27    Venv,
28    /// Try global, fallback to venv.
29    Auto,
30}
31
32impl fmt::Display for InstallMethod {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            InstallMethod::Global => write!(f, "global"),
36            InstallMethod::Venv => write!(f, "venv"),
37            InstallMethod::Auto => write!(f, "auto"),
38        }
39    }
40}
41
42/// Configuration for the Megatron-LM installer.
43#[derive(Debug, Clone)]
44pub struct MegatronConfig {
45    /// Installation method.
46    pub method: InstallMethod,
47    /// Python binary to use.
48    pub python_bin: String,
49    /// Whether to force reinstall.
50    pub force_reinstall: bool,
51    /// Whether to run in dry-run mode.
52    pub dry_run: bool,
53    /// Installation directory (defaults to `$HOME/Megatron-LM`).
54    pub install_dir: Option<PathBuf>,
55}
56
57impl Default for MegatronConfig {
58    fn default() -> Self {
59        Self {
60            method: InstallMethod::Auto,
61            python_bin: "python3".to_string(),
62            force_reinstall: false,
63            dry_run: false,
64            install_dir: None,
65        }
66    }
67}
68
69impl MegatronConfig {
70    /// Get the installation directory path.
71    ///
72    /// The original script uses `$HOME/Megatron-LM` as primary,
73    /// with `$HOME/.mlstack/src/Megatron-LM` as fallback.
74    pub fn install_dir(&self) -> PathBuf {
75        self.install_dir
76            .clone()
77            .unwrap_or_else(|| dirs_home().join("Megatron-LM"))
78    }
79
80    /// Get the fallback installation directory.
81    pub fn fallback_install_dir(&self) -> PathBuf {
82        dirs_home().join(".mlstack/src/Megatron-LM")
83    }
84}
85
86/// A constructed shell command.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ShellCommand {
89    /// The program to run.
90    pub program: String,
91    /// Arguments to pass.
92    pub args: Vec<String>,
93    /// Environment variables to set.
94    pub env: Vec<(String, String)>,
95    /// Working directory for the command.
96    pub working_dir: Option<PathBuf>,
97}
98
99impl ShellCommand {
100    /// Format as a shell command string.
101    pub fn to_command_string(&self) -> String {
102        let env_prefix = self
103            .env
104            .iter()
105            .map(|(k, v)| format!("{k}={v}"))
106            .collect::<Vec<_>>()
107            .join(" ");
108        let cmd = if self.args.is_empty() {
109            self.program.clone()
110        } else {
111            format!("{} {}", self.program, self.args.join(" "))
112        };
113        if env_prefix.is_empty() {
114            cmd
115        } else {
116            format!("{env_prefix} {cmd}")
117        }
118    }
119}
120
121/// The Megatron-LM installer.
122pub struct MegatronInstaller {
123    config: MegatronConfig,
124}
125
126impl MegatronInstaller {
127    /// Create a new Megatron installer with the given config.
128    pub fn new(config: MegatronConfig) -> Self {
129        Self { config }
130    }
131
132    /// Create with default config.
133    pub fn with_defaults() -> Self {
134        Self::new(MegatronConfig::default())
135    }
136
137    // -----------------------------------------------------------------------
138    // Dependencies (VAL-INSTALL-041)
139    // -----------------------------------------------------------------------
140
141    /// Get the list of required dependencies.
142    ///
143    /// Megatron-LM depends on:
144    /// - PyTorch (ROCm-enabled)
145    /// - MPI4Py
146    pub fn dependencies(&self) -> &[&str] {
147        &["pytorch", "mpi4py"]
148    }
149
150    // -----------------------------------------------------------------------
151    // ROCm environment
152    // -----------------------------------------------------------------------
153
154    /// Get the ROCm environment variables needed for Megatron.
155    ///
156    /// The original script sets:
157    /// - AMD_LOG_LEVEL=0
158    /// - HIP_VISIBLE_DEVICES
159    /// - ROCR_VISIBLE_DEVICES
160    /// - HSA_OVERRIDE_GFX_VERSION=11.0.0
161    /// - PYTORCH_ROCM_ARCH=gfx1100
162    /// - ROCM_PATH
163    /// - HSA_TOOLS_LIB
164    /// - PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
165    pub fn rocm_env(&self, rocm_env: &RocmEnv) -> Vec<(String, String)> {
166        let rocm_path = rocm_env
167            .path()
168            .map(|p| p.to_string_lossy().to_string())
169            .unwrap_or_else(|| "/opt/rocm".to_string());
170
171        let mut env = vec![
172            ("AMD_LOG_LEVEL".to_string(), "0".to_string()),
173            ("HSA_OVERRIDE_GFX_VERSION".to_string(), "11.0.0".to_string()),
174            ("PYTORCH_ROCM_ARCH".to_string(), "gfx1100".to_string()),
175            ("ROCM_PATH".to_string(), rocm_path.clone()),
176            (
177                "PYTORCH_CUDA_ALLOC_CONF".to_string(),
178                "expandable_segments:True".to_string(),
179            ),
180        ];
181
182        // HSA_TOOLS_LIB - check for rocprofiler library
183        // Try the ROCm 7.x layout first (lib/rocprofiler-sdk/), then the old layout (lib/)
184        let profiler_lib = rocm_env.path().and_then(|p| {
185            let new_layout = p.join("lib/rocprofiler-sdk/librocprofiler-sdk-tool.so");
186            let old_layout = p.join("lib/librocprofiler-sdk-tool.so");
187            if new_layout.exists() {
188                Some(new_layout)
189            } else if old_layout.exists() {
190                Some(old_layout)
191            } else {
192                None
193            }
194        });
195        if let Some(lib) = profiler_lib {
196            env.push((
197                "HSA_TOOLS_LIB".to_string(),
198                lib.to_string_lossy().to_string(),
199            ));
200        } else {
201            env.push(("HSA_TOOLS_LIB".to_string(), "0".to_string()));
202        }
203
204        env
205    }
206
207    // -----------------------------------------------------------------------
208    // Git clone command (VAL-INSTALL-012)
209    // -----------------------------------------------------------------------
210
211    /// Construct the git clone command for Megatron-LM.
212    ///
213    /// The original script clones:
214    /// `git clone https://github.com/NVIDIA/Megatron-LM.git <dir>`
215    ///
216    /// It retries up to 3 times on failure.
217    pub fn build_git_clone_command(&self) -> ShellCommand {
218        ShellCommand {
219            program: "git".to_string(),
220            args: vec![
221                "clone".to_string(),
222                "https://github.com/NVIDIA/Megatron-LM.git".to_string(),
223                self.config.install_dir().to_string_lossy().to_string(),
224            ],
225            env: vec![],
226            working_dir: None,
227        }
228    }
229
230    /// Get the maximum number of clone retries.
231    pub fn max_clone_retries(&self) -> u32 {
232        3
233    }
234
235    // -----------------------------------------------------------------------
236    // Pip install command (VAL-INSTALL-013)
237    // -----------------------------------------------------------------------
238
239    /// Construct the pip install command for Megatron-LM.
240    ///
241    /// The original script installs with:
242    /// `pip install -e . --no-deps`
243    ///
244    /// The editable install is done from the cloned directory.
245    /// Dependencies are resolved separately (safe requirements filtering).
246    ///
247    /// # Pip Target Directory
248    ///
249    /// The working directory is set to `install_dir()`, which is the git clone
250    /// target directory. The NVIDIA/Megatron-LM repository has `setup.py` at the
251    /// repository root (not in a subdirectory), so `pip install -e .` works
252    /// correctly from the clone root.
253    pub fn build_pip_install_command(&self) -> ShellCommand {
254        let is_global = self.config.method == InstallMethod::Global
255            || self.config.method == InstallMethod::Auto;
256
257        let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
258
259        if is_global {
260            args.push("--break-system-packages".to_string());
261        }
262
263        args.push("-e".to_string());
264        args.push(".".to_string());
265        args.push("--no-deps".to_string());
266
267        ShellCommand {
268            program: self.config.python_bin.clone(),
269            args,
270            env: vec![],
271            working_dir: Some(self.config.install_dir()),
272        }
273    }
274
275    /// Construct the pip install command for a specific dependency.
276    ///
277    /// Used for installing individual safe dependencies.
278    pub fn build_dep_install_command(&self, package: &str) -> ShellCommand {
279        let is_global = self.config.method == InstallMethod::Global
280            || self.config.method == InstallMethod::Auto;
281
282        let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
283
284        if is_global {
285            args.push("--break-system-packages".to_string());
286        }
287
288        args.push(package.to_string());
289
290        ShellCommand {
291            program: self.config.python_bin.clone(),
292            args,
293            env: vec![],
294            working_dir: None,
295        }
296    }
297
298    /// Construct a pip install command for a requirements file.
299    pub fn build_requirements_install_command(&self, requirements_path: &str) -> ShellCommand {
300        let is_global = self.config.method == InstallMethod::Global
301            || self.config.method == InstallMethod::Auto;
302
303        let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
304
305        if is_global {
306            args.push("--break-system-packages".to_string());
307        }
308
309        args.push("-r".to_string());
310        args.push(requirements_path.to_string());
311
312        ShellCommand {
313            program: self.config.python_bin.clone(),
314            args,
315            env: vec![],
316            working_dir: None,
317        }
318    }
319
320    // -----------------------------------------------------------------------
321    // Safe dependency filtering
322    // -----------------------------------------------------------------------
323
324    /// Check if a Python package is safe to install (non-CUDA/non-NVIDIA).
325    ///
326    /// Delegates to the single canonical blocklist
327    /// [`crate::installers::common::nvidia_blocklist::is_cuda_nvidia_package`]
328    /// (Stage 2: one unified rule for the "no nvidia deps ever" tenet).
329    pub fn is_safe_package(&self, package_name: &str) -> bool {
330        !crate::installers::common::nvidia_blocklist::is_cuda_nvidia_package(package_name)
331    }
332
333    // -----------------------------------------------------------------------
334    // Build error detection (VAL-INSTALL-014)
335    // -----------------------------------------------------------------------
336
337    /// Check build output for errors.
338    pub fn check_build_output(
339        &self,
340        stdout: &str,
341        stderr: &str,
342        exit_code: i32,
343    ) -> Result<(), String> {
344        if exit_code != 0 {
345            let error_patterns = [
346                "error:",
347                "Error:",
348                "FAILED:",
349                "fatal error:",
350                "undefined reference",
351                "No such file or directory",
352                "Permission denied",
353                "ImportError:",
354                "ModuleNotFoundError:",
355            ];
356
357            let combined = format!("{}\n{}", stdout, stderr);
358            let detected_errors: Vec<&str> = error_patterns
359                .iter()
360                .filter(|p| combined.contains(*p))
361                .copied()
362                .collect();
363
364            if detected_errors.is_empty() {
365                return Err(format!(
366                    "Megatron operation failed with exit code {}",
367                    exit_code
368                ));
369            }
370
371            return Err(format!(
372                "Megatron operation failed with exit code {}. Detected errors: {}",
373                exit_code,
374                detected_errors.join(", ")
375            ));
376        }
377
378        Ok(())
379    }
380}
381
382/// Get the home directory.
383fn dirs_home() -> PathBuf {
384    std::env::var("HOME")
385        .map(PathBuf::from)
386        .unwrap_or_else(|_| PathBuf::from("/root"))
387}
388
389// ===========================================================================
390// Tests
391// ===========================================================================
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use std::path::PathBuf;
397
398    // --- VAL-INSTALL-012: Megatron correct git clone ---
399
400    #[test]
401    fn test_git_clone_command() {
402        let installer = MegatronInstaller::with_defaults();
403        let cmd = installer.build_git_clone_command();
404
405        assert_eq!(cmd.program, "git");
406        assert!(cmd.args.contains(&"clone".to_string()));
407        assert!(cmd.args.iter().any(|a| a.contains("NVIDIA/Megatron-LM")));
408    }
409
410    #[test]
411    fn test_git_clone_target_dir() {
412        let installer = MegatronInstaller::new(MegatronConfig {
413            install_dir: Some(PathBuf::from("/home/user/Megatron-LM")),
414            ..Default::default()
415        });
416        let cmd = installer.build_git_clone_command();
417        assert!(cmd
418            .args
419            .iter()
420            .any(|a| a.contains("/home/user/Megatron-LM")));
421    }
422
423    #[test]
424    fn test_git_clone_retries() {
425        let installer = MegatronInstaller::with_defaults();
426        assert_eq!(installer.max_clone_retries(), 3);
427    }
428
429    // --- VAL-INSTALL-013: Megatron correct pip install post-clone ---
430
431    #[test]
432    fn test_pip_install_command() {
433        let installer = MegatronInstaller::new(MegatronConfig {
434            method: InstallMethod::Global,
435            ..Default::default()
436        });
437        let cmd = installer.build_pip_install_command();
438
439        assert_eq!(cmd.program, "python3");
440        assert!(cmd.args.contains(&"-m".to_string()));
441        assert!(cmd.args.contains(&"pip".to_string()));
442        assert!(cmd.args.contains(&"install".to_string()));
443        assert!(cmd.args.contains(&"--break-system-packages".to_string()));
444        assert!(cmd.args.contains(&"-e".to_string()));
445        assert!(cmd.args.contains(&".".to_string()));
446        assert!(cmd.args.contains(&"--no-deps".to_string()));
447    }
448
449    #[test]
450    fn test_pip_install_venv_no_break_system() {
451        let installer = MegatronInstaller::new(MegatronConfig {
452            method: InstallMethod::Venv,
453            ..Default::default()
454        });
455        let cmd = installer.build_pip_install_command();
456        assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
457    }
458
459    #[test]
460    fn test_pip_install_working_dir() {
461        let installer = MegatronInstaller::new(MegatronConfig {
462            install_dir: Some(PathBuf::from("/home/user/Megatron-LM")),
463            ..Default::default()
464        });
465        let cmd = installer.build_pip_install_command();
466        assert_eq!(
467            cmd.working_dir,
468            Some(PathBuf::from("/home/user/Megatron-LM"))
469        );
470    }
471
472    #[test]
473    fn test_dep_install_command() {
474        let installer = MegatronInstaller::new(MegatronConfig {
475            method: InstallMethod::Global,
476            ..Default::default()
477        });
478        let cmd = installer.build_dep_install_command("numpy");
479        assert!(cmd.args.contains(&"numpy".to_string()));
480        assert!(cmd.args.contains(&"--break-system-packages".to_string()));
481    }
482
483    #[test]
484    fn test_requirements_install_command() {
485        let installer = MegatronInstaller::new(MegatronConfig {
486            method: InstallMethod::Global,
487            ..Default::default()
488        });
489        let cmd = installer.build_requirements_install_command("/tmp/req.txt");
490        assert!(cmd.args.contains(&"-r".to_string()));
491        assert!(cmd.args.contains(&"/tmp/req.txt".to_string()));
492        assert!(cmd.args.contains(&"--break-system-packages".to_string()));
493    }
494
495    // --- VAL-INSTALL-041: Megatron declares dependency on PyTorch and MPI4Py ---
496
497    #[test]
498    fn test_dependencies() {
499        let installer = MegatronInstaller::with_defaults();
500        let deps = installer.dependencies();
501        assert!(deps.contains(&"pytorch"));
502        assert!(deps.contains(&"mpi4py"));
503    }
504
505    // --- ROCm environment ---
506
507    #[test]
508    fn test_rocm_env() {
509        let installer = MegatronInstaller::with_defaults();
510        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
511        let env = installer.rocm_env(&rocm_env);
512        assert!(env.iter().any(|(k, v)| k == "AMD_LOG_LEVEL" && v == "0"));
513        assert!(env
514            .iter()
515            .any(|(k, v)| k == "HSA_OVERRIDE_GFX_VERSION" && v == "11.0.0"));
516        assert!(env
517            .iter()
518            .any(|(k, v)| k == "PYTORCH_ROCM_ARCH" && v == "gfx1100"));
519        assert!(env
520            .iter()
521            .any(|(k, v)| k == "ROCM_PATH" && v == "/opt/rocm"));
522        assert!(env
523            .iter()
524            .any(|(k, v)| k == "PYTORCH_CUDA_ALLOC_CONF" && v == "expandable_segments:True"));
525        assert!(env.iter().any(|(k, _)| k == "HSA_TOOLS_LIB"));
526    }
527
528    // --- Safe package filtering ---
529
530    #[test]
531    fn test_safe_package_allows_megatron_deps() {
532        let installer = MegatronInstaller::with_defaults();
533        // These should be safe
534        assert!(installer.is_safe_package("numpy"));
535        assert!(installer.is_safe_package("regex"));
536        assert!(installer.is_safe_package("sentencepiece"));
537        assert!(installer.is_safe_package("pyyaml"));
538        assert!(installer.is_safe_package("transformers"));
539        assert!(installer.is_safe_package("tokenizers"));
540        assert!(installer.is_safe_package("tqdm"));
541        assert!(installer.is_safe_package("einops"));
542        assert!(installer.is_safe_package("packaging"));
543        assert!(installer.is_safe_package("omegaconf"));
544    }
545
546    #[test]
547    fn test_safe_package_blocks_cuda_nvidia() {
548        let installer = MegatronInstaller::with_defaults();
549        // These should be blocked
550        assert!(!installer.is_safe_package("torch"));
551        assert!(!installer.is_safe_package("torchvision"));
552        assert!(!installer.is_safe_package("torchaudio"));
553        assert!(!installer.is_safe_package("triton"));
554        assert!(!installer.is_safe_package("nvidia-cublas-cu12"));
555        assert!(!installer.is_safe_package("nvidia-cuda-runtime-cu12"));
556        assert!(!installer.is_safe_package("pytorch-cuda"));
557        assert!(!installer.is_safe_package("torch-cuda"));
558        assert!(!installer.is_safe_package("xformers"));
559        assert!(!installer.is_safe_package("cupy-cuda12x"));
560    }
561
562    #[test]
563    fn test_safe_package_with_version_spec() {
564        let installer = MegatronInstaller::with_defaults();
565        assert!(installer.is_safe_package("numpy>=1.21"));
566        assert!(installer.is_safe_package("packaging>=20.0"));
567        assert!(!installer.is_safe_package("torch>=2.0"));
568    }
569
570    // --- Build error detection ---
571
572    #[test]
573    fn test_build_error_import() {
574        let installer = MegatronInstaller::with_defaults();
575        let result = installer.check_build_output("", "ImportError: No module named 'megatron'", 1);
576        assert!(result.is_err());
577        assert!(result.unwrap_err().contains("ImportError"));
578    }
579
580    #[test]
581    fn test_build_error_success() {
582        let installer = MegatronInstaller::with_defaults();
583        let result = installer.check_build_output("OK", "", 0);
584        assert!(result.is_ok());
585    }
586
587    // --- Config defaults ---
588
589    #[test]
590    fn test_config_defaults() {
591        let config = MegatronConfig::default();
592        assert_eq!(config.method, InstallMethod::Auto);
593        assert_eq!(config.python_bin, "python3");
594        assert!(!config.force_reinstall);
595        assert!(!config.dry_run);
596        assert!(config.install_dir.is_none());
597    }
598
599    #[test]
600    fn test_config_install_dir_default() {
601        let config = MegatronConfig::default();
602        let dir = config.install_dir();
603        assert!(dir.to_string_lossy().contains("Megatron-LM"));
604    }
605
606    #[test]
607    fn test_config_fallback_dir() {
608        let config = MegatronConfig::default();
609        let fallback = config.fallback_install_dir();
610        assert!(fallback
611            .to_string_lossy()
612            .contains(".mlstack/src/Megatron-LM"));
613    }
614
615    #[test]
616    fn test_command_string_format() {
617        let cmd = ShellCommand {
618            program: "python3".to_string(),
619            args: vec![
620                "-m".to_string(),
621                "pip".to_string(),
622                "install".to_string(),
623                "numpy".to_string(),
624            ],
625            env: vec![("ROCM_PATH".to_string(), "/opt/rocm".to_string())],
626            working_dir: None,
627        };
628        let s = cmd.to_command_string();
629        assert!(s.contains("ROCM_PATH=/opt/rocm"));
630        assert!(s.contains("python3 -m pip install numpy"));
631    }
632}