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        args.push("deepspeed".to_string());
169        args.push("einops".to_string());
170
171        PipCommand {
172            program: self.config.python_bin.clone(),
173            args,
174            env: vec![], // ROCm env set separately
175        }
176    }
177
178    /// Construct the retry install command with --no-deps.
179    ///
180    /// Used when the primary install fails.
181    pub fn build_no_deps_command(&self) -> PipCommand {
182        let mut args = vec![
183            "-m".to_string(),
184            "pip".to_string(),
185            "install".to_string(),
186            "--break-system-packages".to_string(),
187            "--no-deps".to_string(),
188        ];
189        args.push("deepspeed".to_string());
190
191        PipCommand {
192            program: self.config.python_bin.clone(),
193            args,
194            env: vec![],
195        }
196    }
197
198    /// Construct the force-reinstall retry command.
199    ///
200    /// Uses `--no-deps` so a force-reinstall never pulls/overrides the ROCm
201    /// torch (Stage 3: never override a core component).
202    pub fn build_force_reinstall_command(&self) -> PipCommand {
203        let mut args = vec![
204            "-m".to_string(),
205            "pip".to_string(),
206            "install".to_string(),
207            "--break-system-packages".to_string(),
208            "--force-reinstall".to_string(),
209            "--no-deps".to_string(),
210        ];
211        args.push("deepspeed".to_string());
212
213        PipCommand {
214            program: self.config.python_bin.clone(),
215            args,
216            env: vec![],
217        }
218    }
219
220    /// Get the list of required dependencies to verify after install.
221    pub fn required_dependencies(&self) -> &[&str] {
222        &[
223            "packaging",
224            "ninja",
225            "pydantic",
226            "jsonschema",
227            "einops",
228            "deepspeed",
229        ]
230    }
231}
232
233// ===========================================================================
234// Tests
235// ===========================================================================
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    // --- VAL-INSTALL-006: DeepSpeed installer correct pip command ---
242
243    #[test]
244    fn test_build_install_command_global() {
245        let installer = DeepSpeedInstaller::new(DeepSpeedConfig {
246            method: InstallMethod::Global,
247            ..Default::default()
248        });
249        let cmd = installer.build_install_command();
250        assert_eq!(cmd.program, "python3");
251        assert!(cmd.args.contains(&"-m".to_string()));
252        assert!(cmd.args.contains(&"pip".to_string()));
253        assert!(cmd.args.contains(&"install".to_string()));
254        assert!(cmd.args.contains(&"--break-system-packages".to_string()));
255        assert!(cmd.args.contains(&"deepspeed".to_string()));
256        assert!(cmd.args.contains(&"einops".to_string()));
257    }
258
259    #[test]
260    fn test_build_install_command_venv() {
261        let installer = DeepSpeedInstaller::new(DeepSpeedConfig {
262            method: InstallMethod::Venv,
263            ..Default::default()
264        });
265        let cmd = installer.build_install_command();
266        // Venv should NOT have --break-system-packages
267        assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
268        assert!(cmd.args.contains(&"deepspeed".to_string()));
269    }
270
271    #[test]
272    fn test_build_install_command_force_reinstall() {
273        let installer = DeepSpeedInstaller::new(DeepSpeedConfig {
274            force_reinstall: true,
275            ..Default::default()
276        });
277        let cmd = installer.build_install_command();
278        assert!(cmd.args.contains(&"--force-reinstall".to_string()));
279    }
280
281    #[test]
282    fn test_build_deps_command() {
283        let installer = DeepSpeedInstaller::with_defaults();
284        let cmd = installer.build_deps_install_command();
285        assert!(cmd.args.contains(&"packaging".to_string()));
286        assert!(cmd.args.contains(&"ninja".to_string()));
287        assert!(cmd.args.contains(&"pydantic".to_string()));
288        assert!(cmd.args.contains(&"jsonschema".to_string()));
289        assert!(cmd.args.contains(&"einops".to_string()));
290    }
291
292    #[test]
293    fn test_build_no_deps_command() {
294        let installer = DeepSpeedInstaller::with_defaults();
295        let cmd = installer.build_no_deps_command();
296        assert!(cmd.args.contains(&"--no-deps".to_string()));
297        assert!(cmd.args.contains(&"deepspeed".to_string()));
298    }
299
300    #[test]
301    fn test_build_force_reinstall_command() {
302        let installer = DeepSpeedInstaller::with_defaults();
303        let cmd = installer.build_force_reinstall_command();
304        assert!(cmd.args.contains(&"--force-reinstall".to_string()));
305        assert!(cmd.args.contains(&"deepspeed".to_string()));
306    }
307
308    #[test]
309    fn test_required_dependencies() {
310        let installer = DeepSpeedInstaller::with_defaults();
311        let deps = installer.required_dependencies();
312        assert!(deps.contains(&"deepspeed"));
313        assert!(deps.contains(&"einops"));
314        assert!(deps.contains(&"ninja"));
315    }
316
317    #[test]
318    fn test_command_string_format() {
319        let cmd = PipCommand {
320            program: "python3".to_string(),
321            args: vec![
322                "-m".to_string(),
323                "pip".to_string(),
324                "install".to_string(),
325                "deepspeed".to_string(),
326            ],
327            env: vec![("ROCM_HOME".to_string(), "/opt/rocm".to_string())],
328        };
329        let s = cmd.to_command_string();
330        assert!(s.contains("ROCM_HOME=/opt/rocm"));
331        assert!(s.contains("python3 -m pip install deepspeed"));
332    }
333}