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_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_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    pub fn build_pip_install_command(&self) -> ShellCommand {
247        let is_global = self.config.method == InstallMethod::Global
248            || self.config.method == InstallMethod::Auto;
249
250        let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
251
252        if is_global {
253            args.push("--break-system-packages".to_string());
254        }
255
256        args.push("-e".to_string());
257        args.push(".".to_string());
258        args.push("--no-deps".to_string());
259
260        ShellCommand {
261            program: self.config.python_bin.clone(),
262            args,
263            env: vec![],
264            working_dir: Some(self.config.install_dir()),
265        }
266    }
267
268    /// Construct the pip install command for a specific dependency.
269    ///
270    /// Used for installing individual safe dependencies.
271    pub fn build_dep_install_command(&self, package: &str) -> ShellCommand {
272        let is_global = self.config.method == InstallMethod::Global
273            || self.config.method == InstallMethod::Auto;
274
275        let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
276
277        if is_global {
278            args.push("--break-system-packages".to_string());
279        }
280
281        args.push(package.to_string());
282
283        ShellCommand {
284            program: self.config.python_bin.clone(),
285            args,
286            env: vec![],
287            working_dir: None,
288        }
289    }
290
291    /// Construct a pip install command for a requirements file.
292    pub fn build_requirements_install_command(&self, requirements_path: &str) -> ShellCommand {
293        let is_global = self.config.method == InstallMethod::Global
294            || self.config.method == InstallMethod::Auto;
295
296        let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
297
298        if is_global {
299            args.push("--break-system-packages".to_string());
300        }
301
302        args.push("-r".to_string());
303        args.push(requirements_path.to_string());
304
305        ShellCommand {
306            program: self.config.python_bin.clone(),
307            args,
308            env: vec![],
309            working_dir: None,
310        }
311    }
312
313    // -----------------------------------------------------------------------
314    // Safe dependency filtering
315    // -----------------------------------------------------------------------
316
317    /// Check if a Python package is safe to install (non-CUDA/non-NVIDIA).
318    ///
319    /// Delegates to the single canonical blocklist
320    /// [`crate::installers::common::nvidia_blocklist::is_cuda_nvidia_package`]
321    /// (Stage 2: one unified rule for the "no nvidia deps ever" tenet).
322    pub fn is_safe_package(&self, package_name: &str) -> bool {
323        !crate::installers::common::nvidia_blocklist::is_cuda_nvidia_package(package_name)
324    }
325
326    // -----------------------------------------------------------------------
327    // Build error detection (VAL-INSTALL-014)
328    // -----------------------------------------------------------------------
329
330    /// Check build output for errors.
331    pub fn check_build_output(
332        &self,
333        stdout: &str,
334        stderr: &str,
335        exit_code: i32,
336    ) -> Result<(), String> {
337        if exit_code != 0 {
338            let error_patterns = [
339                "error:",
340                "Error:",
341                "FAILED:",
342                "fatal error:",
343                "undefined reference",
344                "No such file or directory",
345                "Permission denied",
346                "ImportError:",
347                "ModuleNotFoundError:",
348            ];
349
350            let combined = format!("{}\n{}", stdout, stderr);
351            let detected_errors: Vec<&str> = error_patterns
352                .iter()
353                .filter(|p| combined.contains(*p))
354                .copied()
355                .collect();
356
357            if detected_errors.is_empty() {
358                return Err(format!(
359                    "Megatron operation failed with exit code {}",
360                    exit_code
361                ));
362            }
363
364            return Err(format!(
365                "Megatron operation failed with exit code {}. Detected errors: {}",
366                exit_code,
367                detected_errors.join(", ")
368            ));
369        }
370
371        Ok(())
372    }
373}
374
375/// Get the home directory.
376fn dirs_home() -> PathBuf {
377    std::env::var("HOME")
378        .map(PathBuf::from)
379        .unwrap_or_else(|_| PathBuf::from("/root"))
380}
381
382// ===========================================================================
383// Tests
384// ===========================================================================
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use std::path::PathBuf;
390
391    // --- VAL-INSTALL-012: Megatron correct git clone ---
392
393    #[test]
394    fn test_git_clone_command() {
395        let installer = MegatronInstaller::with_defaults();
396        let cmd = installer.build_git_clone_command();
397
398        assert_eq!(cmd.program, "git");
399        assert!(cmd.args.contains(&"clone".to_string()));
400        assert!(cmd.args.iter().any(|a| a.contains("NVIDIA/Megatron-LM")));
401    }
402
403    #[test]
404    fn test_git_clone_target_dir() {
405        let installer = MegatronInstaller::new(MegatronConfig {
406            install_dir: Some(PathBuf::from("/home/user/Megatron-LM")),
407            ..Default::default()
408        });
409        let cmd = installer.build_git_clone_command();
410        assert!(cmd
411            .args
412            .iter()
413            .any(|a| a.contains("/home/user/Megatron-LM")));
414    }
415
416    #[test]
417    fn test_git_clone_retries() {
418        let installer = MegatronInstaller::with_defaults();
419        assert_eq!(installer.max_clone_retries(), 3);
420    }
421
422    // --- VAL-INSTALL-013: Megatron correct pip install post-clone ---
423
424    #[test]
425    fn test_pip_install_command() {
426        let installer = MegatronInstaller::new(MegatronConfig {
427            method: InstallMethod::Global,
428            ..Default::default()
429        });
430        let cmd = installer.build_pip_install_command();
431
432        assert_eq!(cmd.program, "python3");
433        assert!(cmd.args.contains(&"-m".to_string()));
434        assert!(cmd.args.contains(&"pip".to_string()));
435        assert!(cmd.args.contains(&"install".to_string()));
436        assert!(cmd.args.contains(&"--break-system-packages".to_string()));
437        assert!(cmd.args.contains(&"-e".to_string()));
438        assert!(cmd.args.contains(&".".to_string()));
439        assert!(cmd.args.contains(&"--no-deps".to_string()));
440    }
441
442    #[test]
443    fn test_pip_install_venv_no_break_system() {
444        let installer = MegatronInstaller::new(MegatronConfig {
445            method: InstallMethod::Venv,
446            ..Default::default()
447        });
448        let cmd = installer.build_pip_install_command();
449        assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
450    }
451
452    #[test]
453    fn test_pip_install_working_dir() {
454        let installer = MegatronInstaller::new(MegatronConfig {
455            install_dir: Some(PathBuf::from("/home/user/Megatron-LM")),
456            ..Default::default()
457        });
458        let cmd = installer.build_pip_install_command();
459        assert_eq!(
460            cmd.working_dir,
461            Some(PathBuf::from("/home/user/Megatron-LM"))
462        );
463    }
464
465    #[test]
466    fn test_dep_install_command() {
467        let installer = MegatronInstaller::new(MegatronConfig {
468            method: InstallMethod::Global,
469            ..Default::default()
470        });
471        let cmd = installer.build_dep_install_command("numpy");
472        assert!(cmd.args.contains(&"numpy".to_string()));
473        assert!(cmd.args.contains(&"--break-system-packages".to_string()));
474    }
475
476    #[test]
477    fn test_requirements_install_command() {
478        let installer = MegatronInstaller::new(MegatronConfig {
479            method: InstallMethod::Global,
480            ..Default::default()
481        });
482        let cmd = installer.build_requirements_install_command("/tmp/req.txt");
483        assert!(cmd.args.contains(&"-r".to_string()));
484        assert!(cmd.args.contains(&"/tmp/req.txt".to_string()));
485        assert!(cmd.args.contains(&"--break-system-packages".to_string()));
486    }
487
488    // --- VAL-INSTALL-041: Megatron declares dependency on PyTorch and MPI4Py ---
489
490    #[test]
491    fn test_dependencies() {
492        let installer = MegatronInstaller::with_defaults();
493        let deps = installer.dependencies();
494        assert!(deps.contains(&"pytorch"));
495        assert!(deps.contains(&"mpi4py"));
496    }
497
498    // --- ROCm environment ---
499
500    #[test]
501    fn test_rocm_env() {
502        let installer = MegatronInstaller::with_defaults();
503        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
504        let env = installer.rocm_env(&rocm_env);
505        assert!(env.iter().any(|(k, v)| k == "AMD_LOG_LEVEL" && v == "0"));
506        assert!(env
507            .iter()
508            .any(|(k, v)| k == "HSA_OVERRIDE_GFX_VERSION" && v == "11.0.0"));
509        assert!(env
510            .iter()
511            .any(|(k, v)| k == "PYTORCH_ROCM_ARCH" && v == "gfx1100"));
512        assert!(env
513            .iter()
514            .any(|(k, v)| k == "ROCM_PATH" && v == "/opt/rocm"));
515        assert!(env
516            .iter()
517            .any(|(k, v)| k == "PYTORCH_ALLOC_CONF" && v == "expandable_segments:True"));
518        assert!(env.iter().any(|(k, _)| k == "HSA_TOOLS_LIB"));
519    }
520
521    // --- Safe package filtering ---
522
523    #[test]
524    fn test_safe_package_allows_megatron_deps() {
525        let installer = MegatronInstaller::with_defaults();
526        // These should be safe
527        assert!(installer.is_safe_package("numpy"));
528        assert!(installer.is_safe_package("regex"));
529        assert!(installer.is_safe_package("sentencepiece"));
530        assert!(installer.is_safe_package("pyyaml"));
531        assert!(installer.is_safe_package("transformers"));
532        assert!(installer.is_safe_package("tokenizers"));
533        assert!(installer.is_safe_package("tqdm"));
534        assert!(installer.is_safe_package("einops"));
535        assert!(installer.is_safe_package("packaging"));
536        assert!(installer.is_safe_package("omegaconf"));
537    }
538
539    #[test]
540    fn test_safe_package_blocks_cuda_nvidia() {
541        let installer = MegatronInstaller::with_defaults();
542        // These should be blocked
543        assert!(!installer.is_safe_package("torch"));
544        assert!(!installer.is_safe_package("torchvision"));
545        assert!(!installer.is_safe_package("torchaudio"));
546        assert!(!installer.is_safe_package("triton"));
547        assert!(!installer.is_safe_package("nvidia-cublas-cu12"));
548        assert!(!installer.is_safe_package("nvidia-cuda-runtime-cu12"));
549        assert!(!installer.is_safe_package("pytorch-cuda"));
550        assert!(!installer.is_safe_package("torch-cuda"));
551        assert!(!installer.is_safe_package("xformers"));
552        assert!(!installer.is_safe_package("cupy-cuda12x"));
553    }
554
555    #[test]
556    fn test_safe_package_with_version_spec() {
557        let installer = MegatronInstaller::with_defaults();
558        assert!(installer.is_safe_package("numpy>=1.21"));
559        assert!(installer.is_safe_package("packaging>=20.0"));
560        assert!(!installer.is_safe_package("torch>=2.0"));
561    }
562
563    // --- Build error detection ---
564
565    #[test]
566    fn test_build_error_import() {
567        let installer = MegatronInstaller::with_defaults();
568        let result = installer.check_build_output("", "ImportError: No module named 'megatron'", 1);
569        assert!(result.is_err());
570        assert!(result.unwrap_err().contains("ImportError"));
571    }
572
573    #[test]
574    fn test_build_error_success() {
575        let installer = MegatronInstaller::with_defaults();
576        let result = installer.check_build_output("OK", "", 0);
577        assert!(result.is_ok());
578    }
579
580    // --- Config defaults ---
581
582    #[test]
583    fn test_config_defaults() {
584        let config = MegatronConfig::default();
585        assert_eq!(config.method, InstallMethod::Auto);
586        assert_eq!(config.python_bin, "python3");
587        assert!(!config.force_reinstall);
588        assert!(!config.dry_run);
589        assert!(config.install_dir.is_none());
590    }
591
592    #[test]
593    fn test_config_install_dir_default() {
594        let config = MegatronConfig::default();
595        let dir = config.install_dir();
596        assert!(dir.to_string_lossy().contains("Megatron-LM"));
597    }
598
599    #[test]
600    fn test_config_fallback_dir() {
601        let config = MegatronConfig::default();
602        let fallback = config.fallback_install_dir();
603        assert!(fallback
604            .to_string_lossy()
605            .contains(".mlstack/src/Megatron-LM"));
606    }
607
608    #[test]
609    fn test_command_string_format() {
610        let cmd = ShellCommand {
611            program: "python3".to_string(),
612            args: vec![
613                "-m".to_string(),
614                "pip".to_string(),
615                "install".to_string(),
616                "numpy".to_string(),
617            ],
618            env: vec![("ROCM_PATH".to_string(), "/opt/rocm".to_string())],
619            working_dir: None,
620        };
621        let s = cmd.to_command_string();
622        assert!(s.contains("ROCM_PATH=/opt/rocm"));
623        assert!(s.contains("python3 -m pip install numpy"));
624    }
625}