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
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum InstallMethod {
52 Global,
54 Venv,
56 Auto,
58}
59
60impl fmt::Display for InstallMethod {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 InstallMethod::Global => write!(f, "global"),
64 InstallMethod::Venv => write!(f, "venv"),
65 InstallMethod::Auto => write!(f, "auto"),
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct PyTorchConfig {
73 pub channel: TorchChannel,
75 pub method: InstallMethod,
77 pub python_bin: String,
79 pub force_reinstall: bool,
81 pub dry_run: bool,
83}
84
85impl Default for PyTorchConfig {
86 fn default() -> Self {
87 Self {
88 channel: TorchChannel::Latest,
89 method: InstallMethod::Auto,
90 python_bin: "python3".to_string(),
91 force_reinstall: false,
92 dry_run: false,
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct PipCommand {
100 pub program: String,
102 pub args: Vec<String>,
104}
105
106impl PipCommand {
107 pub fn to_command_string(&self) -> String {
109 if self.args.is_empty() {
110 self.program.clone()
111 } else {
112 format!("{} {}", self.program, self.args.join(" "))
113 }
114 }
115}
116
117pub struct PyTorchInstaller {
119 config: PyTorchConfig,
120}
121
122impl PyTorchInstaller {
123 pub fn new(config: PyTorchConfig) -> Self {
125 Self { config }
126 }
127
128 pub fn with_defaults() -> Self {
130 Self::new(PyTorchConfig::default())
131 }
132
133 pub fn index_url_for_rocm(&self, rocm_mm: &str) -> String {
147 let mm: f32 = rocm_mm.parse().unwrap_or(7.2);
148 if mm >= 7.0 {
149 format!("https://download.pytorch.org/whl/rocm{rocm_mm}")
156 } else if mm >= 6.4 {
157 format!("https://download.pytorch.org/whl/nightly/rocm{rocm_mm}")
158 } else if mm >= 6.3 {
159 format!("https://download.pytorch.org/whl/rocm{rocm_mm}")
160 } else if mm >= 6.0 {
161 "https://download.pytorch.org/whl/rocm6.2".to_string()
162 } else if mm >= 5.0 {
163 "https://download.pytorch.org/whl/rocm5.7".to_string()
164 } else {
165 "https://download.pytorch.org/whl/rocm6.3".to_string()
166 }
167 }
168
169 pub fn nightly_index_url(&self, rocm_mm: &str) -> String {
171 format!("https://download.pytorch.org/whl/nightly/rocm{rocm_mm}")
172 }
173
174 pub fn radeon_index_url(&self, rocm_mm: &str) -> String {
176 format!("https://repo.radeon.com/rocm/manylinux/rocm-rel-{rocm_mm}/")
177 }
178
179 pub fn fallback_index_url(&self) -> String {
181 "https://download.pytorch.org/whl/rocm7.0".to_string()
182 }
183
184 pub fn build_install_command(
204 &self,
205 rocm_mm: &str,
206 use_uv: bool,
207 target_version: Option<&str>,
208 ) -> PipCommand {
209 let index_url = self.index_url_for_rocm(rocm_mm);
210 let is_global = self.config.method == InstallMethod::Global
211 || self.config.method == InstallMethod::Auto;
212
213 let effective_use_uv = use_uv && !is_global;
217
218 let mut args = Vec::new();
219
220 if effective_use_uv {
221 args.push("pip".to_string());
222 args.push("install".to_string());
223 } else {
224 args.push("-m".to_string());
225 args.push("pip".to_string());
226 args.push("install".to_string());
227 if is_global {
228 args.push("--break-system-packages".to_string());
229 }
230 }
231
232 args.push("--no-cache".to_string());
233 let pin = target_version.map(str::trim).filter(|v| !v.is_empty());
239 if pin.is_some() {
240 args.push("--upgrade".to_string());
241 }
242 args.push("--index-url".to_string());
243 args.push(index_url);
244 match pin {
245 Some(v) => args.push(format!("torch=={v}")),
246 None => args.push("torch".to_string()),
247 }
248 args.push("torchvision".to_string());
249 args.push("torchaudio".to_string());
250
251 let program = if effective_use_uv {
252 "uv".to_string()
253 } else {
254 self.config.python_bin.clone()
255 };
256
257 PipCommand { program, args }
258 }
259
260 pub fn build_fallback_command(&self, use_uv: bool) -> PipCommand {
263 let is_global = self.config.method == InstallMethod::Global
264 || self.config.method == InstallMethod::Auto;
265
266 let effective_use_uv = use_uv && !is_global;
269
270 let mut args = Vec::new();
271 if effective_use_uv {
272 args.push("pip".to_string());
273 args.push("install".to_string());
274 } else {
275 args.push("-m".to_string());
276 args.push("pip".to_string());
277 args.push("install".to_string());
278 if is_global {
279 args.push("--break-system-packages".to_string());
280 }
281 }
282
283 args.push("--no-cache".to_string());
284 args.push("--index-url".to_string());
285 args.push(self.fallback_index_url());
286 args.push("torch".to_string());
287 args.push("torchvision".to_string());
288 args.push("torchaudio".to_string());
289
290 let program = if effective_use_uv {
291 "uv".to_string()
292 } else {
293 self.config.python_bin.clone()
294 };
295
296 PipCommand { program, args }
297 }
298
299 pub fn build_common_deps_command(&self, use_uv: bool) -> PipCommand {
303 let is_global = self.config.method == InstallMethod::Global
304 || self.config.method == InstallMethod::Auto;
305
306 let effective_use_uv = use_uv && !is_global;
309
310 let mut args = Vec::new();
311 if effective_use_uv {
312 args.push("pip".to_string());
313 args.push("install".to_string());
314 } else {
315 args.push("-m".to_string());
316 args.push("pip".to_string());
317 args.push("install".to_string());
318 args.push("--break-system-packages".to_string());
319 }
320 args.push("--no-cache-dir".to_string());
321 args.push("--upgrade".to_string());
322 args.push("torchsde".to_string());
323 args.push("sentencepiece".to_string());
324
325 let program = if effective_use_uv {
326 "uv".to_string()
327 } else {
328 self.config.python_bin.clone()
329 };
330 PipCommand { program, args }
331 }
332}
333
334#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
345 fn test_index_url_rocm_72() {
346 let installer = PyTorchInstaller::with_defaults();
347 let url = installer.index_url_for_rocm("7.2");
348 assert_eq!(url, "https://download.pytorch.org/whl/rocm7.2");
349 }
350
351 #[test]
352 fn test_index_url_rocm_70() {
353 let installer = PyTorchInstaller::with_defaults();
354 let url = installer.index_url_for_rocm("7.0");
355 assert_eq!(url, "https://download.pytorch.org/whl/rocm7.0");
356 }
357
358 #[test]
359 fn test_index_url_rocm_64() {
360 let installer = PyTorchInstaller::with_defaults();
361 let url = installer.index_url_for_rocm("6.4");
362 assert_eq!(url, "https://download.pytorch.org/whl/nightly/rocm6.4");
363 }
364
365 #[test]
366 fn test_index_url_rocm_63() {
367 let installer = PyTorchInstaller::with_defaults();
368 let url = installer.index_url_for_rocm("6.3");
369 assert_eq!(url, "https://download.pytorch.org/whl/rocm6.3");
370 }
371
372 #[test]
373 fn test_index_url_rocm_62() {
374 let installer = PyTorchInstaller::with_defaults();
375 let url = installer.index_url_for_rocm("6.2");
376 assert_eq!(url, "https://download.pytorch.org/whl/rocm6.2");
377 }
378
379 #[test]
380 fn test_index_url_rocm_57() {
381 let installer = PyTorchInstaller::with_defaults();
382 let url = installer.index_url_for_rocm("5.7");
383 assert_eq!(url, "https://download.pytorch.org/whl/rocm5.7");
384 }
385
386 #[test]
387 fn test_index_url_fallback() {
388 let installer = PyTorchInstaller::with_defaults();
389 let url = installer.index_url_for_rocm("4.0");
390 assert_eq!(url, "https://download.pytorch.org/whl/rocm6.3");
391 }
392
393 #[test]
394 fn test_nightly_index_url() {
395 let installer = PyTorchInstaller::with_defaults();
396 let url = installer.nightly_index_url("7.2");
397 assert_eq!(url, "https://download.pytorch.org/whl/nightly/rocm7.2");
398 }
399
400 #[test]
401 fn test_radeon_index_url() {
402 let installer = PyTorchInstaller::with_defaults();
403 let url = installer.radeon_index_url("7.2");
404 assert_eq!(url, "https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/");
405 }
406
407 #[test]
408 fn test_build_install_command_uv_global_uses_pip() {
409 let installer = PyTorchInstaller::new(PyTorchConfig {
412 method: InstallMethod::Global,
413 ..Default::default()
414 });
415 let cmd = installer.build_install_command("7.2", true, None);
416 assert_eq!(cmd.program, "python3");
417 assert!(cmd.args.contains(&"-m".to_string()));
418 assert!(cmd.args.contains(&"pip".to_string()));
419 assert!(cmd.args.contains(&"install".to_string()));
420 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
421 assert!(cmd.args.contains(&"--index-url".to_string()));
422 assert!(cmd.args.iter().any(|a| a.contains("rocm7.2")));
423 assert!(cmd.args.contains(&"torch".to_string()));
424 assert!(!cmd.args.contains(&"--upgrade".to_string()));
426 assert!(!cmd.args.iter().any(|a| a.starts_with("torch==")));
427 }
428
429 #[test]
430 fn test_build_install_command_pip() {
431 let installer = PyTorchInstaller::new(PyTorchConfig {
432 method: InstallMethod::Global,
433 python_bin: "python3".to_string(),
434 ..Default::default()
435 });
436 let cmd = installer.build_install_command("6.4", false, None);
437 assert_eq!(cmd.program, "python3");
438 assert!(cmd.args.contains(&"-m".to_string()));
439 assert!(cmd.args.contains(&"pip".to_string()));
440 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
441 assert!(cmd.args.iter().any(|a| a.contains("rocm6.4")));
442 }
443
444 #[test]
445 fn test_build_install_command_venv_no_break_system() {
446 let installer = PyTorchInstaller::new(PyTorchConfig {
447 method: InstallMethod::Venv,
448 ..Default::default()
449 });
450 let cmd = installer.build_install_command("7.2", true, None);
451 assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
452 assert!(!cmd.args.contains(&"--system".to_string()));
453 }
454
455 #[test]
456 fn test_build_install_command_pins_target_for_update() {
457 let installer = PyTorchInstaller::new(PyTorchConfig {
461 method: InstallMethod::Global,
462 ..Default::default()
463 });
464 let cmd = installer.build_install_command("7.2", false, Some("2.13.0"));
465 assert!(cmd.args.contains(&"--upgrade".to_string()));
466 assert!(cmd.args.contains(&"torch==2.13.0".to_string()));
467 assert!(cmd.args.contains(&"torchvision".to_string()));
468 assert!(cmd.args.contains(&"torchaudio".to_string()));
469 let cmd_empty = installer.build_install_command("7.2", false, Some(" "));
471 assert!(!cmd_empty.args.contains(&"--upgrade".to_string()));
472 assert!(cmd_empty.args.contains(&"torch".to_string()));
473 }
474
475 #[test]
476 fn test_build_fallback_command() {
477 let installer = PyTorchInstaller::new(PyTorchConfig {
478 method: InstallMethod::Global,
479 ..Default::default()
480 });
481 let cmd = installer.build_fallback_command(true);
482 assert!(cmd.args.iter().any(|a| a.contains("rocm7.0")));
483 assert!(cmd.args.contains(&"torch".to_string()));
484 }
485
486 #[test]
487 fn test_build_common_deps_command() {
488 let installer = PyTorchInstaller::with_defaults();
489 let cmd = installer.build_common_deps_command(true);
490 assert!(cmd.args.contains(&"torchsde".to_string()));
491 assert!(cmd.args.contains(&"sentencepiece".to_string()));
492 }
493
494 #[test]
495 fn test_channel_from_choice() {
496 assert_eq!(TorchChannel::from_choice(1), Some(TorchChannel::Stable));
497 assert_eq!(TorchChannel::from_choice(2), Some(TorchChannel::Latest));
498 assert_eq!(TorchChannel::from_choice(3), Some(TorchChannel::Nightly));
499 assert_eq!(TorchChannel::from_choice(0), None);
500 }
501}