rusty_stack/installers/components/
pytorch.rs1use std::fmt;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum TorchChannel {
19 Stable,
21 Latest,
23 Nightly,
25}
26
27impl fmt::Display for TorchChannel {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 TorchChannel::Stable => write!(f, "stable"),
31 TorchChannel::Latest => write!(f, "latest"),
32 TorchChannel::Nightly => write!(f, "nightly"),
33 }
34 }
35}
36
37impl TorchChannel {
38 pub fn from_choice(choice: u8) -> Option<Self> {
40 match choice {
41 1 => Some(TorchChannel::Stable),
42 2 => Some(TorchChannel::Latest),
43 3 => Some(TorchChannel::Nightly),
44 _ => None,
45 }
46 }
47}
48
49pub const AMD_WHL_NEXT_INDEX: &str = "https://stable.repo.amd.com/rocm/whl-next/";
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum InstallMethod {
55 Global,
57 Venv,
59 Auto,
61}
62
63impl fmt::Display for InstallMethod {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 InstallMethod::Global => write!(f, "global"),
67 InstallMethod::Venv => write!(f, "venv"),
68 InstallMethod::Auto => write!(f, "auto"),
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct PyTorchConfig {
76 pub channel: TorchChannel,
78 pub method: InstallMethod,
80 pub python_bin: String,
82 pub force_reinstall: bool,
84 pub dry_run: bool,
86}
87
88impl Default for PyTorchConfig {
89 fn default() -> Self {
90 Self {
91 channel: TorchChannel::Latest,
92 method: InstallMethod::Auto,
93 python_bin: "python3".to_string(),
94 force_reinstall: false,
95 dry_run: false,
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct PipCommand {
103 pub program: String,
105 pub args: Vec<String>,
107}
108
109impl PipCommand {
110 pub fn to_command_string(&self) -> String {
112 if self.args.is_empty() {
113 self.program.clone()
114 } else {
115 format!("{} {}", self.program, self.args.join(" "))
116 }
117 }
118}
119
120pub struct PyTorchInstaller {
122 config: PyTorchConfig,
123}
124
125impl PyTorchInstaller {
126 pub fn new(config: PyTorchConfig) -> Self {
128 Self { config }
129 }
130
131 pub fn with_defaults() -> Self {
133 Self::new(PyTorchConfig::default())
134 }
135
136 pub fn index_url_for_rocm(&self, rocm_mm: &str) -> String {
150 let mm: f32 = rocm_mm.parse().unwrap_or(7.2);
151 if mm >= 10.0 {
152 AMD_WHL_NEXT_INDEX.to_string()
155 } else if mm >= 7.0 {
156 format!("https://download.pytorch.org/whl/rocm{rocm_mm}")
163 } else if mm >= 6.4 {
164 format!("https://download.pytorch.org/whl/nightly/rocm{rocm_mm}")
165 } else if mm >= 6.3 {
166 format!("https://download.pytorch.org/whl/rocm{rocm_mm}")
167 } else if mm >= 6.0 {
168 "https://download.pytorch.org/whl/rocm6.2".to_string()
169 } else if mm >= 5.0 {
170 "https://download.pytorch.org/whl/rocm5.7".to_string()
171 } else {
172 "https://download.pytorch.org/whl/rocm6.3".to_string()
173 }
174 }
175
176 pub fn nightly_index_url(&self, rocm_mm: &str) -> String {
178 format!("https://download.pytorch.org/whl/nightly/rocm{rocm_mm}")
179 }
180
181 pub fn radeon_index_url(&self, rocm_mm: &str) -> String {
183 format!("https://repo.radeon.com/rocm/manylinux/rocm-rel-{rocm_mm}/")
184 }
185
186 pub fn fallback_index_url(&self) -> String {
188 "https://download.pytorch.org/whl/rocm7.0".to_string()
189 }
190
191 pub fn build_install_command(
211 &self,
212 rocm_mm: &str,
213 use_uv: bool,
214 target_version: Option<&str>,
215 ) -> PipCommand {
216 let index_url = self.index_url_for_rocm(rocm_mm);
217 let is_global = self.config.method == InstallMethod::Global
218 || self.config.method == InstallMethod::Auto;
219
220 let effective_use_uv = use_uv && !is_global;
224
225 let mut args = Vec::new();
226
227 if effective_use_uv {
228 args.push("pip".to_string());
229 args.push("install".to_string());
230 } else {
231 args.push("-m".to_string());
232 args.push("pip".to_string());
233 args.push("install".to_string());
234 if is_global {
235 args.push("--break-system-packages".to_string());
236 }
237 }
238
239 args.push("--no-cache".to_string());
240 let pin = target_version.map(str::trim).filter(|v| !v.is_empty());
246 if pin.is_some() {
247 args.push("--upgrade".to_string());
248 }
249 args.push("--index-url".to_string());
250 args.push(index_url);
251 let torch_name = if rocm_mm.parse::<f32>().map(|m| m >= 10.0).unwrap_or(false) {
253 "torch[device-all]"
254 } else {
255 "torch"
256 };
257 match pin {
258 Some(v) => args.push(format!("{torch_name}=={v}")),
259 None => args.push(torch_name.to_string()),
260 }
261 args.push("torchvision".to_string());
262 args.push("torchaudio".to_string());
263
264 let program = if effective_use_uv {
265 "uv".to_string()
266 } else {
267 self.config.python_bin.clone()
268 };
269
270 PipCommand { program, args }
271 }
272
273 pub fn build_fallback_command(&self, use_uv: bool) -> PipCommand {
276 let is_global = self.config.method == InstallMethod::Global
277 || self.config.method == InstallMethod::Auto;
278
279 let effective_use_uv = use_uv && !is_global;
282
283 let mut args = Vec::new();
284 if effective_use_uv {
285 args.push("pip".to_string());
286 args.push("install".to_string());
287 } else {
288 args.push("-m".to_string());
289 args.push("pip".to_string());
290 args.push("install".to_string());
291 if is_global {
292 args.push("--break-system-packages".to_string());
293 }
294 }
295
296 args.push("--no-cache".to_string());
297 args.push("--index-url".to_string());
298 args.push(self.fallback_index_url());
299 args.push("torch".to_string());
300 args.push("torchvision".to_string());
301 args.push("torchaudio".to_string());
302
303 let program = if effective_use_uv {
304 "uv".to_string()
305 } else {
306 self.config.python_bin.clone()
307 };
308
309 PipCommand { program, args }
310 }
311
312 pub fn build_common_deps_command(&self, use_uv: bool) -> PipCommand {
316 let is_global = self.config.method == InstallMethod::Global
317 || self.config.method == InstallMethod::Auto;
318
319 let effective_use_uv = use_uv && !is_global;
322
323 let mut args = Vec::new();
324 if effective_use_uv {
325 args.push("pip".to_string());
326 args.push("install".to_string());
327 } else {
328 args.push("-m".to_string());
329 args.push("pip".to_string());
330 args.push("install".to_string());
331 args.push("--break-system-packages".to_string());
332 }
333 args.push("--no-cache-dir".to_string());
334 args.push("--upgrade".to_string());
335 args.push("torchsde".to_string());
336 args.push("sentencepiece".to_string());
337
338 let program = if effective_use_uv {
339 "uv".to_string()
340 } else {
341 self.config.python_bin.clone()
342 };
343 PipCommand { program, args }
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://download.pytorch.org/whl/rocm7.2");
362 }
363
364 #[test]
365 fn test_index_url_rocm_101_uses_amd_whl_next() {
366 let installer = PyTorchInstaller::with_defaults();
367 assert_eq!(installer.index_url_for_rocm("10.1"), AMD_WHL_NEXT_INDEX);
368 let cmd = installer.build_install_command("10.1", false, Some("2.14.0"));
369 assert!(cmd.args.contains(&AMD_WHL_NEXT_INDEX.to_string()));
370 assert!(cmd.args.contains(&"torch[device-all]==2.14.0".to_string()));
371 let old = installer.build_install_command("7.2", false, None);
372 assert!(old.args.contains(&"torch".to_string()));
373 }
374
375 #[test]
376 fn test_index_url_rocm_70() {
377 let installer = PyTorchInstaller::with_defaults();
378 let url = installer.index_url_for_rocm("7.0");
379 assert_eq!(url, "https://download.pytorch.org/whl/rocm7.0");
380 }
381
382 #[test]
383 fn test_index_url_rocm_64() {
384 let installer = PyTorchInstaller::with_defaults();
385 let url = installer.index_url_for_rocm("6.4");
386 assert_eq!(url, "https://download.pytorch.org/whl/nightly/rocm6.4");
387 }
388
389 #[test]
390 fn test_index_url_rocm_63() {
391 let installer = PyTorchInstaller::with_defaults();
392 let url = installer.index_url_for_rocm("6.3");
393 assert_eq!(url, "https://download.pytorch.org/whl/rocm6.3");
394 }
395
396 #[test]
397 fn test_index_url_rocm_62() {
398 let installer = PyTorchInstaller::with_defaults();
399 let url = installer.index_url_for_rocm("6.2");
400 assert_eq!(url, "https://download.pytorch.org/whl/rocm6.2");
401 }
402
403 #[test]
404 fn test_index_url_rocm_57() {
405 let installer = PyTorchInstaller::with_defaults();
406 let url = installer.index_url_for_rocm("5.7");
407 assert_eq!(url, "https://download.pytorch.org/whl/rocm5.7");
408 }
409
410 #[test]
411 fn test_index_url_fallback() {
412 let installer = PyTorchInstaller::with_defaults();
413 let url = installer.index_url_for_rocm("4.0");
414 assert_eq!(url, "https://download.pytorch.org/whl/rocm6.3");
415 }
416
417 #[test]
418 fn test_nightly_index_url() {
419 let installer = PyTorchInstaller::with_defaults();
420 let url = installer.nightly_index_url("7.2");
421 assert_eq!(url, "https://download.pytorch.org/whl/nightly/rocm7.2");
422 }
423
424 #[test]
425 fn test_radeon_index_url() {
426 let installer = PyTorchInstaller::with_defaults();
427 let url = installer.radeon_index_url("7.2");
428 assert_eq!(url, "https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/");
429 }
430
431 #[test]
432 fn test_build_install_command_uv_global_uses_pip() {
433 let installer = PyTorchInstaller::new(PyTorchConfig {
436 method: InstallMethod::Global,
437 ..Default::default()
438 });
439 let cmd = installer.build_install_command("7.2", true, None);
440 assert_eq!(cmd.program, "python3");
441 assert!(cmd.args.contains(&"-m".to_string()));
442 assert!(cmd.args.contains(&"pip".to_string()));
443 assert!(cmd.args.contains(&"install".to_string()));
444 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
445 assert!(cmd.args.contains(&"--index-url".to_string()));
446 assert!(cmd.args.iter().any(|a| a.contains("rocm7.2")));
447 assert!(cmd.args.contains(&"torch".to_string()));
448 assert!(!cmd.args.contains(&"--upgrade".to_string()));
450 assert!(!cmd.args.iter().any(|a| a.starts_with("torch==")));
451 }
452
453 #[test]
454 fn test_build_install_command_pip() {
455 let installer = PyTorchInstaller::new(PyTorchConfig {
456 method: InstallMethod::Global,
457 python_bin: "python3".to_string(),
458 ..Default::default()
459 });
460 let cmd = installer.build_install_command("6.4", false, None);
461 assert_eq!(cmd.program, "python3");
462 assert!(cmd.args.contains(&"-m".to_string()));
463 assert!(cmd.args.contains(&"pip".to_string()));
464 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
465 assert!(cmd.args.iter().any(|a| a.contains("rocm6.4")));
466 }
467
468 #[test]
469 fn test_build_install_command_venv_no_break_system() {
470 let installer = PyTorchInstaller::new(PyTorchConfig {
471 method: InstallMethod::Venv,
472 ..Default::default()
473 });
474 let cmd = installer.build_install_command("7.2", true, None);
475 assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
476 assert!(!cmd.args.contains(&"--system".to_string()));
477 }
478
479 #[test]
480 fn test_build_install_command_pins_target_for_update() {
481 let installer = PyTorchInstaller::new(PyTorchConfig {
485 method: InstallMethod::Global,
486 ..Default::default()
487 });
488 let cmd = installer.build_install_command("7.2", false, Some("2.13.0"));
489 assert!(cmd.args.contains(&"--upgrade".to_string()));
490 assert!(cmd.args.contains(&"torch==2.13.0".to_string()));
491 assert!(cmd.args.contains(&"torchvision".to_string()));
492 assert!(cmd.args.contains(&"torchaudio".to_string()));
493 let cmd_empty = installer.build_install_command("7.2", false, Some(" "));
495 assert!(!cmd_empty.args.contains(&"--upgrade".to_string()));
496 assert!(cmd_empty.args.contains(&"torch".to_string()));
497 }
498
499 #[test]
500 fn test_build_fallback_command() {
501 let installer = PyTorchInstaller::new(PyTorchConfig {
502 method: InstallMethod::Global,
503 ..Default::default()
504 });
505 let cmd = installer.build_fallback_command(true);
506 assert!(cmd.args.iter().any(|a| a.contains("rocm7.0")));
507 assert!(cmd.args.contains(&"torch".to_string()));
508 }
509
510 #[test]
511 fn test_build_common_deps_command() {
512 let installer = PyTorchInstaller::with_defaults();
513 let cmd = installer.build_common_deps_command(true);
514 assert!(cmd.args.contains(&"torchsde".to_string()));
515 assert!(cmd.args.contains(&"sentencepiece".to_string()));
516 }
517
518 #[test]
519 fn test_channel_from_choice() {
520 assert_eq!(TorchChannel::from_choice(1), Some(TorchChannel::Stable));
521 assert_eq!(TorchChannel::from_choice(2), Some(TorchChannel::Latest));
522 assert_eq!(TorchChannel::from_choice(3), Some(TorchChannel::Nightly));
523 assert_eq!(TorchChannel::from_choice(0), None);
524 }
525}