use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TorchChannel {
Stable,
Latest,
Nightly,
}
impl fmt::Display for TorchChannel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TorchChannel::Stable => write!(f, "stable"),
TorchChannel::Latest => write!(f, "latest"),
TorchChannel::Nightly => write!(f, "nightly"),
}
}
}
impl TorchChannel {
pub fn from_choice(choice: u8) -> Option<Self> {
match choice {
1 => Some(TorchChannel::Stable),
2 => Some(TorchChannel::Latest),
3 => Some(TorchChannel::Nightly),
_ => None,
}
}
}
pub const AMD_WHL_NEXT_INDEX: &str = "https://stable.repo.amd.com/rocm/whl-next/";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstallMethod {
Global,
Venv,
Auto,
}
impl fmt::Display for InstallMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InstallMethod::Global => write!(f, "global"),
InstallMethod::Venv => write!(f, "venv"),
InstallMethod::Auto => write!(f, "auto"),
}
}
}
#[derive(Debug, Clone)]
pub struct PyTorchConfig {
pub channel: TorchChannel,
pub method: InstallMethod,
pub python_bin: String,
pub force_reinstall: bool,
pub dry_run: bool,
}
impl Default for PyTorchConfig {
fn default() -> Self {
Self {
channel: TorchChannel::Latest,
method: InstallMethod::Auto,
python_bin: "python3".to_string(),
force_reinstall: false,
dry_run: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PipCommand {
pub program: String,
pub args: Vec<String>,
}
impl PipCommand {
pub fn to_command_string(&self) -> String {
if self.args.is_empty() {
self.program.clone()
} else {
format!("{} {}", self.program, self.args.join(" "))
}
}
}
pub struct PyTorchInstaller {
config: PyTorchConfig,
}
impl PyTorchInstaller {
pub fn new(config: PyTorchConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::new(PyTorchConfig::default())
}
pub fn index_url_for_rocm(&self, rocm_mm: &str) -> String {
let mm: f32 = rocm_mm.parse().unwrap_or(7.2);
if mm >= 10.0 {
AMD_WHL_NEXT_INDEX.to_string()
} else if mm >= 7.0 {
format!("https://download.pytorch.org/whl/rocm{rocm_mm}")
} else if mm >= 6.4 {
format!("https://download.pytorch.org/whl/nightly/rocm{rocm_mm}")
} else if mm >= 6.3 {
format!("https://download.pytorch.org/whl/rocm{rocm_mm}")
} else if mm >= 6.0 {
"https://download.pytorch.org/whl/rocm6.2".to_string()
} else if mm >= 5.0 {
"https://download.pytorch.org/whl/rocm5.7".to_string()
} else {
"https://download.pytorch.org/whl/rocm6.3".to_string()
}
}
pub fn nightly_index_url(&self, rocm_mm: &str) -> String {
format!("https://download.pytorch.org/whl/nightly/rocm{rocm_mm}")
}
pub fn radeon_index_url(&self, rocm_mm: &str) -> String {
format!("https://repo.radeon.com/rocm/manylinux/rocm-rel-{rocm_mm}/")
}
pub fn fallback_index_url(&self) -> String {
"https://download.pytorch.org/whl/rocm7.0".to_string()
}
pub fn build_install_command(
&self,
rocm_mm: &str,
use_uv: bool,
target_version: Option<&str>,
) -> PipCommand {
let index_url = self.index_url_for_rocm(rocm_mm);
let is_global = self.config.method == InstallMethod::Global
|| self.config.method == InstallMethod::Auto;
let effective_use_uv = use_uv && !is_global;
let mut args = Vec::new();
if effective_use_uv {
args.push("pip".to_string());
args.push("install".to_string());
} else {
args.push("-m".to_string());
args.push("pip".to_string());
args.push("install".to_string());
if is_global {
args.push("--break-system-packages".to_string());
}
}
args.push("--no-cache".to_string());
let pin = target_version.map(str::trim).filter(|v| !v.is_empty());
if pin.is_some() {
args.push("--upgrade".to_string());
}
args.push("--index-url".to_string());
args.push(index_url);
let torch_name = if rocm_mm.parse::<f32>().map(|m| m >= 10.0).unwrap_or(false) {
"torch[device-all]"
} else {
"torch"
};
match pin {
Some(v) => args.push(format!("{torch_name}=={v}")),
None => args.push(torch_name.to_string()),
}
args.push("torchvision".to_string());
args.push("torchaudio".to_string());
let program = if effective_use_uv {
"uv".to_string()
} else {
self.config.python_bin.clone()
};
PipCommand { program, args }
}
pub fn build_fallback_command(&self, use_uv: bool) -> PipCommand {
let is_global = self.config.method == InstallMethod::Global
|| self.config.method == InstallMethod::Auto;
let effective_use_uv = use_uv && !is_global;
let mut args = Vec::new();
if effective_use_uv {
args.push("pip".to_string());
args.push("install".to_string());
} else {
args.push("-m".to_string());
args.push("pip".to_string());
args.push("install".to_string());
if is_global {
args.push("--break-system-packages".to_string());
}
}
args.push("--no-cache".to_string());
args.push("--index-url".to_string());
args.push(self.fallback_index_url());
args.push("torch".to_string());
args.push("torchvision".to_string());
args.push("torchaudio".to_string());
let program = if effective_use_uv {
"uv".to_string()
} else {
self.config.python_bin.clone()
};
PipCommand { program, args }
}
pub fn build_common_deps_command(&self, use_uv: bool) -> PipCommand {
let is_global = self.config.method == InstallMethod::Global
|| self.config.method == InstallMethod::Auto;
let effective_use_uv = use_uv && !is_global;
let mut args = Vec::new();
if effective_use_uv {
args.push("pip".to_string());
args.push("install".to_string());
} else {
args.push("-m".to_string());
args.push("pip".to_string());
args.push("install".to_string());
args.push("--break-system-packages".to_string());
}
args.push("--no-cache-dir".to_string());
args.push("--upgrade".to_string());
args.push("torchsde".to_string());
args.push("sentencepiece".to_string());
let program = if effective_use_uv {
"uv".to_string()
} else {
self.config.python_bin.clone()
};
PipCommand { program, args }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_index_url_rocm_72() {
let installer = PyTorchInstaller::with_defaults();
let url = installer.index_url_for_rocm("7.2");
assert_eq!(url, "https://download.pytorch.org/whl/rocm7.2");
}
#[test]
fn test_index_url_rocm_101_uses_amd_whl_next() {
let installer = PyTorchInstaller::with_defaults();
assert_eq!(installer.index_url_for_rocm("10.1"), AMD_WHL_NEXT_INDEX);
let cmd = installer.build_install_command("10.1", false, Some("2.14.0"));
assert!(cmd.args.contains(&AMD_WHL_NEXT_INDEX.to_string()));
assert!(cmd.args.contains(&"torch[device-all]==2.14.0".to_string()));
let old = installer.build_install_command("7.2", false, None);
assert!(old.args.contains(&"torch".to_string()));
}
#[test]
fn test_index_url_rocm_70() {
let installer = PyTorchInstaller::with_defaults();
let url = installer.index_url_for_rocm("7.0");
assert_eq!(url, "https://download.pytorch.org/whl/rocm7.0");
}
#[test]
fn test_index_url_rocm_64() {
let installer = PyTorchInstaller::with_defaults();
let url = installer.index_url_for_rocm("6.4");
assert_eq!(url, "https://download.pytorch.org/whl/nightly/rocm6.4");
}
#[test]
fn test_index_url_rocm_63() {
let installer = PyTorchInstaller::with_defaults();
let url = installer.index_url_for_rocm("6.3");
assert_eq!(url, "https://download.pytorch.org/whl/rocm6.3");
}
#[test]
fn test_index_url_rocm_62() {
let installer = PyTorchInstaller::with_defaults();
let url = installer.index_url_for_rocm("6.2");
assert_eq!(url, "https://download.pytorch.org/whl/rocm6.2");
}
#[test]
fn test_index_url_rocm_57() {
let installer = PyTorchInstaller::with_defaults();
let url = installer.index_url_for_rocm("5.7");
assert_eq!(url, "https://download.pytorch.org/whl/rocm5.7");
}
#[test]
fn test_index_url_fallback() {
let installer = PyTorchInstaller::with_defaults();
let url = installer.index_url_for_rocm("4.0");
assert_eq!(url, "https://download.pytorch.org/whl/rocm6.3");
}
#[test]
fn test_nightly_index_url() {
let installer = PyTorchInstaller::with_defaults();
let url = installer.nightly_index_url("7.2");
assert_eq!(url, "https://download.pytorch.org/whl/nightly/rocm7.2");
}
#[test]
fn test_radeon_index_url() {
let installer = PyTorchInstaller::with_defaults();
let url = installer.radeon_index_url("7.2");
assert_eq!(url, "https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/");
}
#[test]
fn test_build_install_command_uv_global_uses_pip() {
let installer = PyTorchInstaller::new(PyTorchConfig {
method: InstallMethod::Global,
..Default::default()
});
let cmd = installer.build_install_command("7.2", true, None);
assert_eq!(cmd.program, "python3");
assert!(cmd.args.contains(&"-m".to_string()));
assert!(cmd.args.contains(&"pip".to_string()));
assert!(cmd.args.contains(&"install".to_string()));
assert!(cmd.args.contains(&"--break-system-packages".to_string()));
assert!(cmd.args.contains(&"--index-url".to_string()));
assert!(cmd.args.iter().any(|a| a.contains("rocm7.2")));
assert!(cmd.args.contains(&"torch".to_string()));
assert!(!cmd.args.contains(&"--upgrade".to_string()));
assert!(!cmd.args.iter().any(|a| a.starts_with("torch==")));
}
#[test]
fn test_build_install_command_pip() {
let installer = PyTorchInstaller::new(PyTorchConfig {
method: InstallMethod::Global,
python_bin: "python3".to_string(),
..Default::default()
});
let cmd = installer.build_install_command("6.4", false, None);
assert_eq!(cmd.program, "python3");
assert!(cmd.args.contains(&"-m".to_string()));
assert!(cmd.args.contains(&"pip".to_string()));
assert!(cmd.args.contains(&"--break-system-packages".to_string()));
assert!(cmd.args.iter().any(|a| a.contains("rocm6.4")));
}
#[test]
fn test_build_install_command_venv_no_break_system() {
let installer = PyTorchInstaller::new(PyTorchConfig {
method: InstallMethod::Venv,
..Default::default()
});
let cmd = installer.build_install_command("7.2", true, None);
assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
assert!(!cmd.args.contains(&"--system".to_string()));
}
#[test]
fn test_build_install_command_pins_target_for_update() {
let installer = PyTorchInstaller::new(PyTorchConfig {
method: InstallMethod::Global,
..Default::default()
});
let cmd = installer.build_install_command("7.2", false, Some("2.13.0"));
assert!(cmd.args.contains(&"--upgrade".to_string()));
assert!(cmd.args.contains(&"torch==2.13.0".to_string()));
assert!(cmd.args.contains(&"torchvision".to_string()));
assert!(cmd.args.contains(&"torchaudio".to_string()));
let cmd_empty = installer.build_install_command("7.2", false, Some(" "));
assert!(!cmd_empty.args.contains(&"--upgrade".to_string()));
assert!(cmd_empty.args.contains(&"torch".to_string()));
}
#[test]
fn test_build_fallback_command() {
let installer = PyTorchInstaller::new(PyTorchConfig {
method: InstallMethod::Global,
..Default::default()
});
let cmd = installer.build_fallback_command(true);
assert!(cmd.args.iter().any(|a| a.contains("rocm7.0")));
assert!(cmd.args.contains(&"torch".to_string()));
}
#[test]
fn test_build_common_deps_command() {
let installer = PyTorchInstaller::with_defaults();
let cmd = installer.build_common_deps_command(true);
assert!(cmd.args.contains(&"torchsde".to_string()));
assert!(cmd.args.contains(&"sentencepiece".to_string()));
}
#[test]
fn test_channel_from_choice() {
assert_eq!(TorchChannel::from_choice(1), Some(TorchChannel::Stable));
assert_eq!(TorchChannel::from_choice(2), Some(TorchChannel::Latest));
assert_eq!(TorchChannel::from_choice(3), Some(TorchChannel::Nightly));
assert_eq!(TorchChannel::from_choice(0), None);
}
}