Skip to main content

rusty_stack/installers/components/
deepspeed.rs

1//! DeepSpeed installer — ports `scripts/install_deepspeed.sh`.
2//!
3//! Constructs correct pip install with ROCm build flags.
4//! DeepSpeed depends on PyTorch (validated via preflight check).
5//!
6//! # Validation Assertion
7//!
8//! - **VAL-INSTALL-006**: DeepSpeed installer correct pip command
9
10use std::fmt;
11
12// ===========================================================================
13// Types
14// ===========================================================================
15
16/// Installation method.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum InstallMethod {
19    /// Install globally.
20    Global,
21    /// Install in a virtual environment.
22    Venv,
23    /// Try global, fallback to venv.
24    Auto,
25}
26
27impl fmt::Display for InstallMethod {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            InstallMethod::Global => write!(f, "global"),
31            InstallMethod::Venv => write!(f, "venv"),
32            InstallMethod::Auto => write!(f, "auto"),
33        }
34    }
35}
36
37/// Configuration for the DeepSpeed installer.
38#[derive(Debug, Clone)]
39pub struct DeepSpeedConfig {
40    /// Installation method.
41    pub method: InstallMethod,
42    /// Python binary to use.
43    pub python_bin: String,
44    /// Whether to force reinstall.
45    pub force_reinstall: bool,
46    /// Whether to run in dry-run mode.
47    pub dry_run: bool,
48}
49
50impl Default for DeepSpeedConfig {
51    fn default() -> Self {
52        Self {
53            method: InstallMethod::Auto,
54            python_bin: "python3".to_string(),
55            force_reinstall: false,
56            dry_run: false,
57        }
58    }
59}
60
61/// A constructed pip command.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct PipCommand {
64    /// The program to run.
65    pub program: String,
66    /// Arguments to pass.
67    pub args: Vec<String>,
68    /// Environment variables to set.
69    pub env: Vec<(String, String)>,
70}
71
72impl PipCommand {
73    /// Format as a shell command string.
74    pub fn to_command_string(&self) -> String {
75        let env_prefix = self
76            .env
77            .iter()
78            .map(|(k, v)| format!("{k}={v}"))
79            .collect::<Vec<_>>()
80            .join(" ");
81        let cmd = if self.args.is_empty() {
82            self.program.clone()
83        } else {
84            format!("{} {}", self.program, self.args.join(" "))
85        };
86        if env_prefix.is_empty() {
87            cmd
88        } else {
89            format!("{env_prefix} {cmd}")
90        }
91    }
92}
93
94/// The DeepSpeed installer.
95pub struct DeepSpeedInstaller {
96    config: DeepSpeedConfig,
97}
98
99impl DeepSpeedInstaller {
100    /// Create a new DeepSpeed installer with the given config.
101    pub fn new(config: DeepSpeedConfig) -> Self {
102        Self { config }
103    }
104
105    /// Create with default config.
106    pub fn with_defaults() -> Self {
107        Self::new(DeepSpeedConfig::default())
108    }
109
110    // -----------------------------------------------------------------------
111    // Command construction
112    // -----------------------------------------------------------------------
113
114    /// Construct the pip install command for DeepSpeed dependencies.
115    ///
116    /// Installs packaging, ninja, pydantic, jsonschema, einops.
117    pub fn build_deps_install_command(&self) -> PipCommand {
118        let is_global = self.config.method == InstallMethod::Global
119            || self.config.method == InstallMethod::Auto;
120
121        let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
122        if is_global {
123            args.push("--break-system-packages".to_string());
124        }
125        args.extend([
126            "packaging".to_string(),
127            "ninja".to_string(),
128            "pydantic".to_string(),
129            "jsonschema".to_string(),
130            "einops".to_string(),
131        ]);
132
133        PipCommand {
134            program: self.config.python_bin.clone(),
135            args,
136            env: vec![],
137        }
138    }
139
140    /// Construct the pip install command for DeepSpeed.
141    ///
142    /// The original script uses `pip install deepspeed einops` with
143    /// ROCM_HOME and HIP_PATH set. It does NOT set DS_ACCELERATOR=rocm
144    /// as that caused ValueError in newer versions.
145    pub fn build_install_command(&self) -> PipCommand {
146        let is_global = self.config.method == InstallMethod::Global
147            || self.config.method == InstallMethod::Auto;
148
149        let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
150        if is_global {
151            args.push("--break-system-packages".to_string());
152        }
153        if self.config.force_reinstall {
154            // --force-reinstall would reinstall ALL deps including torch, pulling
155            // a CUDA torch from PyPI and overriding the ROCm build. --no-deps
156            // scopes the reinstall to deepspeed itself (deps are already present
157            // from the initial non-force install).
158            args.push("--force-reinstall".to_string());
159            args.push("--no-deps".to_string());
160        }
161        // NOTE: the normal (non-force) path deliberately does NOT use --no-deps.
162        // PyTorch (ROCm torch) is guaranteed pre-installed by install order
163        // (pytorch runs before deepspeed), so pip sees torch satisfied and will
164        // NOT pull a CUDA torch. Previously --no-deps was always set to block the
165        // CUDA-torch pull, but it ALSO skipped deepspeed's real runtime deps
166        // (py-cpuinfo → the `cpuinfo` module), so `import deepspeed` failed with
167        // "No module named 'cpuinfo'".
168        // --upgrade (default only-if-needed strategy) so an update apply
169        // actually moves an older installed deepspeed to the target.
170        args.push("--upgrade".to_string());
171        args.push("deepspeed".to_string());
172        args.push("einops".to_string());
173
174        PipCommand {
175            program: self.config.python_bin.clone(),
176            args,
177            env: vec![], // ROCm env set separately
178        }
179    }
180
181    /// Construct the retry install command with --no-deps.
182    ///
183    /// Used when the primary install fails.
184    pub fn build_no_deps_command(&self) -> PipCommand {
185        let mut args = vec![
186            "-m".to_string(),
187            "pip".to_string(),
188            "install".to_string(),
189            "--break-system-packages".to_string(),
190            "--no-deps".to_string(),
191        ];
192        args.push("deepspeed".to_string());
193
194        PipCommand {
195            program: self.config.python_bin.clone(),
196            args,
197            env: vec![],
198        }
199    }
200
201    /// Construct the force-reinstall retry command.
202    ///
203    /// Uses `--no-deps` so a force-reinstall never pulls/overrides the ROCm
204    /// torch (Stage 3: never override a core component).
205    pub fn build_force_reinstall_command(&self) -> PipCommand {
206        let mut args = vec![
207            "-m".to_string(),
208            "pip".to_string(),
209            "install".to_string(),
210            "--break-system-packages".to_string(),
211            "--force-reinstall".to_string(),
212            "--no-deps".to_string(),
213        ];
214        args.push("deepspeed".to_string());
215
216        PipCommand {
217            program: self.config.python_bin.clone(),
218            args,
219            env: vec![],
220        }
221    }
222
223    /// Get the list of required dependencies to verify after install.
224    pub fn required_dependencies(&self) -> &[&str] {
225        &[
226            "packaging",
227            "ninja",
228            "pydantic",
229            "jsonschema",
230            "einops",
231            "deepspeed",
232        ]
233    }
234}
235
236// ===========================================================================
237// Tests
238// ===========================================================================
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    // --- VAL-INSTALL-006: DeepSpeed installer correct pip command ---
245
246    #[test]
247    fn test_build_install_command_global() {
248        let installer = DeepSpeedInstaller::new(DeepSpeedConfig {
249            method: InstallMethod::Global,
250            ..Default::default()
251        });
252        let cmd = installer.build_install_command();
253        assert_eq!(cmd.program, "python3");
254        assert!(cmd.args.contains(&"-m".to_string()));
255        assert!(cmd.args.contains(&"pip".to_string()));
256        assert!(cmd.args.contains(&"install".to_string()));
257        assert!(cmd.args.contains(&"--break-system-packages".to_string()));
258        assert!(cmd.args.contains(&"deepspeed".to_string()));
259        assert!(cmd.args.contains(&"einops".to_string()));
260    }
261
262    #[test]
263    fn test_build_install_command_venv() {
264        let installer = DeepSpeedInstaller::new(DeepSpeedConfig {
265            method: InstallMethod::Venv,
266            ..Default::default()
267        });
268        let cmd = installer.build_install_command();
269        // Venv should NOT have --break-system-packages
270        assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
271        assert!(cmd.args.contains(&"deepspeed".to_string()));
272    }
273
274    #[test]
275    fn test_build_install_command_force_reinstall() {
276        let installer = DeepSpeedInstaller::new(DeepSpeedConfig {
277            force_reinstall: true,
278            ..Default::default()
279        });
280        let cmd = installer.build_install_command();
281        assert!(cmd.args.contains(&"--force-reinstall".to_string()));
282    }
283
284    #[test]
285    fn test_build_deps_command() {
286        let installer = DeepSpeedInstaller::with_defaults();
287        let cmd = installer.build_deps_install_command();
288        assert!(cmd.args.contains(&"packaging".to_string()));
289        assert!(cmd.args.contains(&"ninja".to_string()));
290        assert!(cmd.args.contains(&"pydantic".to_string()));
291        assert!(cmd.args.contains(&"jsonschema".to_string()));
292        assert!(cmd.args.contains(&"einops".to_string()));
293    }
294
295    #[test]
296    fn test_build_no_deps_command() {
297        let installer = DeepSpeedInstaller::with_defaults();
298        let cmd = installer.build_no_deps_command();
299        assert!(cmd.args.contains(&"--no-deps".to_string()));
300        assert!(cmd.args.contains(&"deepspeed".to_string()));
301    }
302
303    #[test]
304    fn test_build_force_reinstall_command() {
305        let installer = DeepSpeedInstaller::with_defaults();
306        let cmd = installer.build_force_reinstall_command();
307        assert!(cmd.args.contains(&"--force-reinstall".to_string()));
308        assert!(cmd.args.contains(&"deepspeed".to_string()));
309    }
310
311    #[test]
312    fn test_required_dependencies() {
313        let installer = DeepSpeedInstaller::with_defaults();
314        let deps = installer.required_dependencies();
315        assert!(deps.contains(&"deepspeed"));
316        assert!(deps.contains(&"einops"));
317        assert!(deps.contains(&"ninja"));
318    }
319
320    #[test]
321    fn test_command_string_format() {
322        let cmd = PipCommand {
323            program: "python3".to_string(),
324            args: vec![
325                "-m".to_string(),
326                "pip".to_string(),
327                "install".to_string(),
328                "deepspeed".to_string(),
329            ],
330            env: vec![("ROCM_HOME".to_string(), "/opt/rocm".to_string())],
331        };
332        let s = cmd.to_command_string();
333        assert!(s.contains("ROCM_HOME=/opt/rocm"));
334        assert!(s.contains("python3 -m pip install deepspeed"));
335    }
336}