rusty_stack/installers/components/
pytorch.rs1use crate::installers::common::RocmEnv;
11use std::fmt;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum TorchChannel {
20 Stable,
22 Latest,
24 Nightly,
26}
27
28impl fmt::Display for TorchChannel {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 TorchChannel::Stable => write!(f, "stable"),
32 TorchChannel::Latest => write!(f, "latest"),
33 TorchChannel::Nightly => write!(f, "nightly"),
34 }
35 }
36}
37
38impl TorchChannel {
39 pub fn from_choice(choice: u8) -> Option<Self> {
41 match choice {
42 1 => Some(TorchChannel::Stable),
43 2 => Some(TorchChannel::Latest),
44 3 => Some(TorchChannel::Nightly),
45 _ => None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum InstallMethod {
53 Global,
55 Venv,
57 Auto,
59}
60
61impl fmt::Display for InstallMethod {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 InstallMethod::Global => write!(f, "global"),
65 InstallMethod::Venv => write!(f, "venv"),
66 InstallMethod::Auto => write!(f, "auto"),
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct PyTorchConfig {
74 pub channel: TorchChannel,
76 pub method: InstallMethod,
78 pub python_bin: String,
80 pub force_reinstall: bool,
82 pub dry_run: bool,
84}
85
86impl Default for PyTorchConfig {
87 fn default() -> Self {
88 Self {
89 channel: TorchChannel::Latest,
90 method: InstallMethod::Auto,
91 python_bin: "python3".to_string(),
92 force_reinstall: false,
93 dry_run: false,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct PipCommand {
101 pub program: String,
103 pub args: Vec<String>,
105}
106
107impl PipCommand {
108 pub fn to_command_string(&self) -> String {
110 if self.args.is_empty() {
111 self.program.clone()
112 } else {
113 format!("{} {}", self.program, self.args.join(" "))
114 }
115 }
116}
117
118pub struct PyTorchInstaller {
120 config: PyTorchConfig,
121}
122
123impl PyTorchInstaller {
124 pub fn new(config: PyTorchConfig) -> Self {
126 Self { config }
127 }
128
129 pub fn with_defaults() -> Self {
131 Self::new(PyTorchConfig::default())
132 }
133
134 pub fn index_url_for_rocm(&self, rocm_mm: &str) -> String {
148 let mm: f32 = rocm_mm.parse().unwrap_or(7.2);
149 if mm >= 7.0 {
150 format!("https://repo.radeon.com/rocm/manylinux/rocm-rel-{rocm_mm}/")
152 } else if mm >= 6.4 {
153 format!("https://download.pytorch.org/whl/nightly/rocm{rocm_mm}")
154 } else if mm >= 6.3 {
155 format!("https://download.pytorch.org/whl/rocm{rocm_mm}")
156 } else if mm >= 6.0 {
157 "https://download.pytorch.org/whl/rocm6.2".to_string()
158 } else if mm >= 5.0 {
159 "https://download.pytorch.org/whl/rocm5.7".to_string()
160 } else {
161 "https://download.pytorch.org/whl/rocm6.3".to_string()
162 }
163 }
164
165 pub fn nightly_index_url(&self, rocm_mm: &str) -> String {
167 format!("https://download.pytorch.org/whl/nightly/rocm{rocm_mm}")
168 }
169
170 pub fn radeon_index_url(&self, rocm_mm: &str) -> String {
172 format!("https://repo.radeon.com/rocm/manylinux/rocm-rel-{rocm_mm}/")
173 }
174
175 pub fn fallback_index_url(&self) -> String {
177 "https://download.pytorch.org/whl/rocm7.0".to_string()
178 }
179
180 pub fn build_install_command(&self, rocm_mm: &str, use_uv: bool) -> PipCommand {
193 let index_url = self.index_url_for_rocm(rocm_mm);
194 let is_global = self.config.method == InstallMethod::Global
195 || self.config.method == InstallMethod::Auto;
196
197 let effective_use_uv = use_uv && !is_global;
201
202 let mut args = Vec::new();
203
204 if effective_use_uv {
205 args.push("pip".to_string());
206 args.push("install".to_string());
207 } else {
208 args.push("-m".to_string());
209 args.push("pip".to_string());
210 args.push("install".to_string());
211 if is_global {
212 args.push("--break-system-packages".to_string());
213 }
214 }
215
216 args.push("--no-cache".to_string());
217 args.push("--index-url".to_string());
218 args.push(index_url);
219 args.push("torch".to_string());
220 args.push("torchvision".to_string());
221 args.push("torchaudio".to_string());
222
223 let program = if effective_use_uv {
224 "uv".to_string()
225 } else {
226 self.config.python_bin.clone()
227 };
228
229 PipCommand { program, args }
230 }
231
232 pub fn build_fallback_command(&self, use_uv: bool) -> PipCommand {
235 let is_global = self.config.method == InstallMethod::Global
236 || self.config.method == InstallMethod::Auto;
237
238 let effective_use_uv = use_uv && !is_global;
241
242 let mut args = Vec::new();
243 if effective_use_uv {
244 args.push("pip".to_string());
245 args.push("install".to_string());
246 } else {
247 args.push("-m".to_string());
248 args.push("pip".to_string());
249 args.push("install".to_string());
250 if is_global {
251 args.push("--break-system-packages".to_string());
252 }
253 }
254
255 args.push("--no-cache".to_string());
256 args.push("--index-url".to_string());
257 args.push(self.fallback_index_url());
258 args.push("torch".to_string());
259 args.push("torchvision".to_string());
260 args.push("torchaudio".to_string());
261
262 let program = if effective_use_uv {
263 "uv".to_string()
264 } else {
265 self.config.python_bin.clone()
266 };
267
268 PipCommand { program, args }
269 }
270
271 pub fn build_common_deps_command(&self, use_uv: bool) -> PipCommand {
275 let is_global = self.config.method == InstallMethod::Global
276 || self.config.method == InstallMethod::Auto;
277
278 let effective_use_uv = use_uv && !is_global;
281
282 let mut args = Vec::new();
283 if effective_use_uv {
284 args.push("pip".to_string());
285 args.push("install".to_string());
286 } else {
287 args.push("-m".to_string());
288 args.push("pip".to_string());
289 args.push("install".to_string());
290 args.push("--break-system-packages".to_string());
291 }
292 args.push("--no-cache-dir".to_string());
293 args.push("--upgrade".to_string());
294 args.push("torchsde".to_string());
295 args.push("sentencepiece".to_string());
296
297 let program = if effective_use_uv {
298 "uv".to_string()
299 } else {
300 self.config.python_bin.clone()
301 };
302 PipCommand { program, args }
303 }
304
305 pub fn rocm_env_exports(&self, rocm_env: &RocmEnv) -> Vec<(String, String)> {
309 let mut exports = vec![
310 ("HSA_OVERRIDE_GFX_VERSION".to_string(), "11.0.0".to_string()),
311 ("PYTORCH_ROCM_ARCH".to_string(), "gfx1100".to_string()),
312 (
313 "ROCM_PATH".to_string(),
314 rocm_env
315 .path()
316 .map(|p| p.to_string_lossy().to_string())
317 .unwrap_or_else(|| "/opt/rocm".to_string()),
318 ),
319 ];
320
321 let rocm_lib = rocm_env.path().and_then(|p| {
324 let new_layout = p.join("lib/rocprofiler-sdk/librocprofiler-sdk-tool.so");
325 let old_layout = p.join("lib/librocprofiler-sdk-tool.so");
326 if new_layout.exists() {
327 Some(new_layout)
328 } else if old_layout.exists() {
329 Some(old_layout)
330 } else {
331 None
332 }
333 });
334 if let Some(lib) = rocm_lib {
335 exports.push((
336 "HSA_TOOLS_LIB".to_string(),
337 lib.to_string_lossy().to_string(),
338 ));
339 } else {
340 exports.push(("HSA_TOOLS_LIB".to_string(), "0".to_string()));
341 }
342
343 exports
344 }
345}
346
347#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
358 fn test_index_url_rocm_72() {
359 let installer = PyTorchInstaller::with_defaults();
360 let url = installer.index_url_for_rocm("7.2");
361 assert_eq!(url, "https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/");
362 }
363
364 #[test]
365 fn test_index_url_rocm_70() {
366 let installer = PyTorchInstaller::with_defaults();
367 let url = installer.index_url_for_rocm("7.0");
368 assert_eq!(url, "https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0/");
369 }
370
371 #[test]
372 fn test_index_url_rocm_64() {
373 let installer = PyTorchInstaller::with_defaults();
374 let url = installer.index_url_for_rocm("6.4");
375 assert_eq!(url, "https://download.pytorch.org/whl/nightly/rocm6.4");
376 }
377
378 #[test]
379 fn test_index_url_rocm_63() {
380 let installer = PyTorchInstaller::with_defaults();
381 let url = installer.index_url_for_rocm("6.3");
382 assert_eq!(url, "https://download.pytorch.org/whl/rocm6.3");
383 }
384
385 #[test]
386 fn test_index_url_rocm_62() {
387 let installer = PyTorchInstaller::with_defaults();
388 let url = installer.index_url_for_rocm("6.2");
389 assert_eq!(url, "https://download.pytorch.org/whl/rocm6.2");
390 }
391
392 #[test]
393 fn test_index_url_rocm_57() {
394 let installer = PyTorchInstaller::with_defaults();
395 let url = installer.index_url_for_rocm("5.7");
396 assert_eq!(url, "https://download.pytorch.org/whl/rocm5.7");
397 }
398
399 #[test]
400 fn test_index_url_fallback() {
401 let installer = PyTorchInstaller::with_defaults();
402 let url = installer.index_url_for_rocm("4.0");
403 assert_eq!(url, "https://download.pytorch.org/whl/rocm6.3");
404 }
405
406 #[test]
407 fn test_nightly_index_url() {
408 let installer = PyTorchInstaller::with_defaults();
409 let url = installer.nightly_index_url("7.2");
410 assert_eq!(url, "https://download.pytorch.org/whl/nightly/rocm7.2");
411 }
412
413 #[test]
414 fn test_radeon_index_url() {
415 let installer = PyTorchInstaller::with_defaults();
416 let url = installer.radeon_index_url("7.2");
417 assert_eq!(url, "https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/");
418 }
419
420 #[test]
421 fn test_build_install_command_uv_global_uses_pip() {
422 let installer = PyTorchInstaller::new(PyTorchConfig {
425 method: InstallMethod::Global,
426 ..Default::default()
427 });
428 let cmd = installer.build_install_command("7.2", true);
429 assert_eq!(cmd.program, "python3");
430 assert!(cmd.args.contains(&"-m".to_string()));
431 assert!(cmd.args.contains(&"pip".to_string()));
432 assert!(cmd.args.contains(&"install".to_string()));
433 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
434 assert!(cmd.args.contains(&"--index-url".to_string()));
435 assert!(cmd.args.iter().any(|a| a.contains("rocm-rel-7.2")));
436 assert!(cmd.args.contains(&"torch".to_string()));
437 }
438
439 #[test]
440 fn test_build_install_command_pip() {
441 let installer = PyTorchInstaller::new(PyTorchConfig {
442 method: InstallMethod::Global,
443 python_bin: "python3".to_string(),
444 ..Default::default()
445 });
446 let cmd = installer.build_install_command("6.4", false);
447 assert_eq!(cmd.program, "python3");
448 assert!(cmd.args.contains(&"-m".to_string()));
449 assert!(cmd.args.contains(&"pip".to_string()));
450 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
451 assert!(cmd.args.iter().any(|a| a.contains("rocm6.4")));
452 }
453
454 #[test]
455 fn test_build_install_command_venv_no_break_system() {
456 let installer = PyTorchInstaller::new(PyTorchConfig {
457 method: InstallMethod::Venv,
458 ..Default::default()
459 });
460 let cmd = installer.build_install_command("7.2", true);
461 assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
462 assert!(!cmd.args.contains(&"--system".to_string()));
463 }
464
465 #[test]
466 fn test_build_fallback_command() {
467 let installer = PyTorchInstaller::new(PyTorchConfig {
468 method: InstallMethod::Global,
469 ..Default::default()
470 });
471 let cmd = installer.build_fallback_command(true);
472 assert!(cmd.args.iter().any(|a| a.contains("rocm7.0")));
473 assert!(cmd.args.contains(&"torch".to_string()));
474 }
475
476 #[test]
477 fn test_build_common_deps_command() {
478 let installer = PyTorchInstaller::with_defaults();
479 let cmd = installer.build_common_deps_command(true);
480 assert!(cmd.args.contains(&"torchsde".to_string()));
481 assert!(cmd.args.contains(&"sentencepiece".to_string()));
482 }
483
484 #[test]
485 fn test_rocm_env_exports() {
486 let installer = PyTorchInstaller::with_defaults();
487 let rocm_env = RocmEnv::from_known(
488 Some(std::path::PathBuf::from("/opt/rocm")),
489 "7.2.0".to_string(),
490 );
491 let exports = installer.rocm_env_exports(&rocm_env);
492 assert!(exports
493 .iter()
494 .any(|(k, v)| k == "HSA_OVERRIDE_GFX_VERSION" && v == "11.0.0"));
495 assert!(exports
496 .iter()
497 .any(|(k, v)| k == "PYTORCH_ROCM_ARCH" && v == "gfx1100"));
498 assert!(exports
499 .iter()
500 .any(|(k, v)| k == "ROCM_PATH" && v == "/opt/rocm"));
501 assert!(exports.iter().any(|(k, _)| k == "HSA_TOOLS_LIB"));
502 }
503
504 #[test]
505 fn test_channel_from_choice() {
506 assert_eq!(TorchChannel::from_choice(1), Some(TorchChannel::Stable));
507 assert_eq!(TorchChannel::from_choice(2), Some(TorchChannel::Latest));
508 assert_eq!(TorchChannel::from_choice(3), Some(TorchChannel::Nightly));
509 assert_eq!(TorchChannel::from_choice(0), None);
510 }
511}