From a3dae2512cb86d5054aa85b3962a3e35d92f7d35 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 28 Apr 2025 16:06:18 -0400 Subject: [PATCH] Disallow mixing requirements across PyTorch indexes (#13179) ## Summary If you use `--torch-backend=auto`, we want to avoid selecting (e.g.) a `+cu124` build of `torch` alongside a `+cu126` build of `torchvision`. --- Cargo.lock | 2 + crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/options.rs | 16 +++- .../uv-resolver/src/pubgrub/dependencies.rs | 5 +- crates/uv-resolver/src/pubgrub/package.rs | 23 +++-- crates/uv-resolver/src/pubgrub/priority.rs | 1 + crates/uv-resolver/src/resolver/mod.rs | 45 +++++++++- crates/uv-resolver/src/resolver/system.rs | 84 ++++++++++++++++++ crates/uv-torch/Cargo.toml | 1 + crates/uv-torch/src/backend.rs | 86 ++++++++++++++++++- crates/uv/src/commands/pip/compile.rs | 3 +- crates/uv/src/commands/pip/install.rs | 3 +- crates/uv/src/commands/pip/sync.rs | 3 +- crates/uv/tests/it/pip_compile.rs | 34 ++++++++ 14 files changed, 289 insertions(+), 18 deletions(-) create mode 100644 crates/uv-resolver/src/resolver/system.rs diff --git a/Cargo.lock b/Cargo.lock index 18d7393f5..910299b4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5689,6 +5689,7 @@ dependencies = [ "uv-requirements-txt", "uv-small-str", "uv-static", + "uv-torch", "uv-types", "uv-warnings", "uv-workspace", @@ -5823,6 +5824,7 @@ dependencies = [ "serde", "thiserror 2.0.12", "tracing", + "url", "uv-distribution-types", "uv-normalize", "uv-pep440", diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 4af1c95d9..79645906a 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -36,6 +36,7 @@ uv-python = { workspace = true } uv-requirements-txt = { workspace = true } uv-small-str = { workspace = true } uv-static = { workspace = true } +uv-torch = { workspace = true } uv-types = { workspace = true } uv-warnings = { workspace = true } uv-workspace = { workspace = true } diff --git a/crates/uv-resolver/src/options.rs b/crates/uv-resolver/src/options.rs index 34713110c..176b32910 100644 --- a/crates/uv-resolver/src/options.rs +++ b/crates/uv-resolver/src/options.rs @@ -1,7 +1,9 @@ -use crate::fork_strategy::ForkStrategy; -use crate::{DependencyMode, ExcludeNewer, PrereleaseMode, ResolutionMode}; use uv_configuration::{BuildOptions, IndexStrategy}; use uv_pypi_types::SupportedEnvironments; +use uv_torch::TorchStrategy; + +use crate::fork_strategy::ForkStrategy; +use crate::{DependencyMode, ExcludeNewer, PrereleaseMode, ResolutionMode}; /// Options for resolving a manifest. #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -15,6 +17,7 @@ pub struct Options { pub required_environments: SupportedEnvironments, pub flexibility: Flexibility, pub build_options: BuildOptions, + pub torch_backend: Option, } /// Builder for [`Options`]. @@ -29,6 +32,7 @@ pub struct OptionsBuilder { required_environments: SupportedEnvironments, flexibility: Flexibility, build_options: BuildOptions, + torch_backend: Option, } impl OptionsBuilder { @@ -100,6 +104,13 @@ impl OptionsBuilder { self } + /// Sets the [`TorchStrategy`]. + #[must_use] + pub fn torch_backend(mut self, torch_backend: Option) -> Self { + self.torch_backend = torch_backend; + self + } + /// Builds the options. pub fn build(self) -> Options { Options { @@ -112,6 +123,7 @@ impl OptionsBuilder { required_environments: self.required_environments, flexibility: self.flexibility, build_options: self.build_options, + torch_backend: self.torch_backend, } } } diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index 0c08ce819..f22754ead 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -127,10 +127,11 @@ impl PubGrubDependency { url, } } - PubGrubPackageInner::Root(_) => unreachable!("root package in dependencies"), + PubGrubPackageInner::Root(_) => unreachable!("Root package in dependencies"), PubGrubPackageInner::Python(_) => { - unreachable!("python package in dependencies") + unreachable!("Python package in dependencies") } + PubGrubPackageInner::System(_) => unreachable!("System package in dependencies"), } }) } diff --git a/crates/uv-resolver/src/pubgrub/package.rs b/crates/uv-resolver/src/pubgrub/package.rs index 0d6396002..d5285e840 100644 --- a/crates/uv-resolver/src/pubgrub/package.rs +++ b/crates/uv-resolver/src/pubgrub/package.rs @@ -44,6 +44,8 @@ pub(crate) enum PubGrubPackageInner { Root(Option), /// A Python version. Python(PubGrubPython), + /// A system package, which is used to represent a non-Python package. + System(PackageName), /// A Python package. /// /// Note that it is guaranteed that `extra` and `dev` are never both @@ -134,6 +136,7 @@ impl PubGrubPackage { // package is never returned by `get_dependencies`. So these cases never occur. PubGrubPackageInner::Root(None) | PubGrubPackageInner::Python(_) => None, PubGrubPackageInner::Root(Some(name)) + | PubGrubPackageInner::System(name) | PubGrubPackageInner::Package { name, .. } | PubGrubPackageInner::Extra { name, .. } | PubGrubPackageInner::Dev { name, .. } @@ -141,11 +144,13 @@ impl PubGrubPackage { } } - /// Returns the name of this PubGrub package, if it is not the root package or a Python version - /// constraint. + /// Returns the name of this PubGrub package, if it is not the root package, a Python version + /// constraint, or a system package. pub(crate) fn name_no_root(&self) -> Option<&PackageName> { match &**self { - PubGrubPackageInner::Root(_) | PubGrubPackageInner::Python(_) => None, + PubGrubPackageInner::Root(_) + | PubGrubPackageInner::Python(_) + | PubGrubPackageInner::System(_) => None, PubGrubPackageInner::Package { name, .. } | PubGrubPackageInner::Extra { name, .. } | PubGrubPackageInner::Dev { name, .. } @@ -159,7 +164,9 @@ impl PubGrubPackage { match &**self { // A root can never be a dependency of another package, and a `Python` pubgrub // package is never returned by `get_dependencies`. So these cases never occur. - PubGrubPackageInner::Root(_) | PubGrubPackageInner::Python(_) => MarkerTree::TRUE, + PubGrubPackageInner::Root(_) + | PubGrubPackageInner::Python(_) + | PubGrubPackageInner::System(_) => MarkerTree::TRUE, PubGrubPackageInner::Package { marker, .. } | PubGrubPackageInner::Extra { marker, .. } | PubGrubPackageInner::Dev { marker, .. } => *marker, @@ -177,6 +184,7 @@ impl PubGrubPackage { // package is never returned by `get_dependencies`. So these cases never occur. PubGrubPackageInner::Root(_) | PubGrubPackageInner::Python(_) + | PubGrubPackageInner::System(_) | PubGrubPackageInner::Package { extra: None, .. } | PubGrubPackageInner::Dev { .. } | PubGrubPackageInner::Marker { .. } => None, @@ -198,6 +206,7 @@ impl PubGrubPackage { // package is never returned by `get_dependencies`. So these cases never occur. PubGrubPackageInner::Root(_) | PubGrubPackageInner::Python(_) + | PubGrubPackageInner::System(_) | PubGrubPackageInner::Package { dev: None, .. } | PubGrubPackageInner::Extra { .. } | PubGrubPackageInner::Marker { .. } => None, @@ -256,7 +265,9 @@ impl PubGrubPackage { /// reporting where this routine is used. pub(crate) fn simplify_markers(&mut self, python_requirement: &PythonRequirement) { match *Arc::make_mut(&mut self.0) { - PubGrubPackageInner::Root(_) | PubGrubPackageInner::Python(_) => {} + PubGrubPackageInner::Root(_) + | PubGrubPackageInner::Python(_) + | PubGrubPackageInner::System(_) => {} PubGrubPackageInner::Package { ref mut marker, .. } | PubGrubPackageInner::Extra { ref mut marker, .. } | PubGrubPackageInner::Dev { ref mut marker, .. } @@ -272,6 +283,7 @@ impl PubGrubPackage { match &**self { PubGrubPackageInner::Root(_) => "root", PubGrubPackageInner::Python(_) => "python", + PubGrubPackageInner::System(_) => "system", PubGrubPackageInner::Package { .. } => "package", PubGrubPackageInner::Extra { .. } => "extra", PubGrubPackageInner::Dev { .. } => "dev", @@ -304,6 +316,7 @@ impl std::fmt::Display for PubGrubPackageInner { } } Self::Python(_) => write!(f, "Python"), + Self::System(name) => write!(f, "system:{name}"), Self::Package { name, extra: None, diff --git a/crates/uv-resolver/src/pubgrub/priority.rs b/crates/uv-resolver/src/pubgrub/priority.rs index c69adbae4..166c7a283 100644 --- a/crates/uv-resolver/src/pubgrub/priority.rs +++ b/crates/uv-resolver/src/pubgrub/priority.rs @@ -129,6 +129,7 @@ impl PubGrubPriorities { PubGrubPackageInner::Python(PubGrubPython::Target) => { (PubGrubPriority::Root, PubGrubTiebreaker::from(2)) } + PubGrubPackageInner::System(_) => (PubGrubPriority::Root, PubGrubTiebreaker::from(3)), PubGrubPackageInner::Marker { name, .. } | PubGrubPackageInner::Extra { name, .. } | PubGrubPackageInner::Dev { name, .. } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 8ecc115d8..fad0c71be 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -63,10 +63,6 @@ use crate::resolver::environment::{ fork_version_by_marker, fork_version_by_python_requirement, ForkingPossibility, }; pub(crate) use crate::resolver::fork_map::{ForkMap, ForkSet}; -pub(crate) use crate::resolver::urls::Urls; -use crate::universal_marker::{ConflictMarker, UniversalMarker}; -pub(crate) use provider::MetadataUnavailable; - pub use crate::resolver::index::InMemoryIndex; use crate::resolver::indexes::Indexes; pub use crate::resolver::provider::{ @@ -74,8 +70,13 @@ pub use crate::resolver::provider::{ VersionsResponse, WheelMetadataResult, }; pub use crate::resolver::reporter::{BuildId, Reporter}; +use crate::resolver::system::SystemDependency; +pub(crate) use crate::resolver::urls::Urls; +use crate::universal_marker::{ConflictMarker, UniversalMarker}; use crate::yanks::AllowedYanks; use crate::{marker, DependencyMode, Exclusions, FlatIndex, Options, ResolutionMode, VersionMap}; +pub(crate) use provider::MetadataUnavailable; +use uv_torch::TorchStrategy; mod availability; mod batch_prefetch; @@ -86,6 +87,7 @@ mod index; mod indexes; mod provider; mod reporter; +mod system; mod urls; /// The number of conflicts a package may accumulate before we re-prioritize and backtrack. @@ -598,6 +600,7 @@ impl ResolverState ResolverState { + // We don't care what the actual version is here, just that it's consistent across + // the dependency graph. + let Some(version) = range.as_singleton() else { + return Ok(None); + }; + Ok(Some(ResolverVersion::Unforked(version.clone()))) + } + PubGrubPackageInner::Marker { name, .. } | PubGrubPackageInner::Extra { name, .. } | PubGrubPackageInner::Dev { name, .. } @@ -1641,6 +1653,7 @@ impl ResolverState, package: &PubGrubPackage, version: &Version, + pins: &FilePins, fork_urls: &ForkUrls, env: &ResolverEnvironment, python_requirement: &PythonRequirement, @@ -1650,6 +1663,7 @@ impl ResolverState ResolverState, package: &PubGrubPackage, version: &Version, + pins: &FilePins, fork_urls: &ForkUrls, env: &ResolverEnvironment, python_requirement: &PythonRequirement, @@ -1781,6 +1796,24 @@ impl ResolverState ResolverState return Ok(Dependencies::Unforkable(Vec::default())), + PubGrubPackageInner::System(_) => return Ok(Dependencies::Unforkable(Vec::default())), + // Add a dependency on both the marker and base package. PubGrubPackageInner::Marker { name, marker } => { return Ok(Dependencies::Unforkable( @@ -2562,6 +2598,7 @@ impl ResolverState {} PubGrubPackageInner::Python(_) => {} + PubGrubPackageInner::System(_) => {} PubGrubPackageInner::Marker { .. } => {} PubGrubPackageInner::Extra { .. } => {} PubGrubPackageInner::Dev { .. } => {} diff --git a/crates/uv-resolver/src/resolver/system.rs b/crates/uv-resolver/src/resolver/system.rs new file mode 100644 index 000000000..49c23952b --- /dev/null +++ b/crates/uv-resolver/src/resolver/system.rs @@ -0,0 +1,84 @@ +use std::str::FromStr; + +use pubgrub::Ranges; +use url::Url; + +use uv_normalize::PackageName; +use uv_pep440::Version; +use uv_torch::TorchBackend; + +use crate::pubgrub::{PubGrubDependency, PubGrubPackage, PubGrubPackageInner}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct SystemDependency { + /// The name of the system dependency (e.g., `cuda`). + name: PackageName, + /// The version of the system dependency (e.g., `12.4`). + version: Version, +} + +impl SystemDependency { + /// Extract a [`SystemDependency`] from an index URL. + /// + /// For example, given `https://download.pytorch.org/whl/cu124`, returns CUDA 12.4. + pub(super) fn from_index(index: &Url) -> Option { + let backend = TorchBackend::from_index(index)?; + let cuda_version = backend.cuda_version()?; + Some(Self { + name: PackageName::from_str("cuda").unwrap(), + version: cuda_version, + }) + } +} + +impl std::fmt::Display for SystemDependency { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.name, self.version) + } +} + +impl From for PubGrubDependency { + fn from(value: SystemDependency) -> Self { + PubGrubDependency { + package: PubGrubPackage::from(PubGrubPackageInner::System(value.name)), + version: Ranges::singleton(value.version), + url: None, + } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use url::Url; + + use uv_normalize::PackageName; + use uv_pep440::Version; + + use crate::resolver::system::SystemDependency; + + #[test] + fn pypi() { + let url = Url::parse("https://pypi.org/simple").unwrap(); + assert_eq!(SystemDependency::from_index(&url), None); + } + + #[test] + fn pytorch_cuda_12_4() { + let url = Url::parse("https://download.pytorch.org/whl/cu124").unwrap(); + assert_eq!( + SystemDependency::from_index(&url), + Some(SystemDependency { + name: PackageName::from_str("cuda").unwrap(), + version: Version::new([12, 4]), + }) + ); + } + + #[test] + fn pytorch_cpu() { + let url = Url::parse("https://download.pytorch.org/whl/cpu").unwrap(); + assert_eq!(SystemDependency::from_index(&url), None); + } +} diff --git a/crates/uv-torch/Cargo.toml b/crates/uv-torch/Cargo.toml index fd1cb5aec..d173c6ede 100644 --- a/crates/uv-torch/Cargo.toml +++ b/crates/uv-torch/Cargo.toml @@ -23,6 +23,7 @@ schemars = { workspace = true, optional = true } serde = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } +url = { workspace = true } [lints] workspace = true diff --git a/crates/uv-torch/src/backend.rs b/crates/uv-torch/src/backend.rs index 9db252c77..d3e5afc55 100644 --- a/crates/uv-torch/src/backend.rs +++ b/crates/uv-torch/src/backend.rs @@ -41,6 +41,7 @@ use std::str::FromStr; use std::sync::LazyLock; use either::Either; +use url::Url; use uv_distribution_types::IndexUrl; use uv_normalize::PackageName; @@ -230,7 +231,7 @@ impl TorchStrategy { } /// The available backends for PyTorch. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum TorchBackend { Cpu, Cu128, @@ -261,7 +262,7 @@ pub enum TorchBackend { impl TorchBackend { /// Return the appropriate index URL for the given [`TorchBackend`]. - fn index_url(&self) -> &'static IndexUrl { + fn index_url(self) -> &'static IndexUrl { match self { Self::Cpu => &CPU_INDEX_URL, Self::Cu128 => &CU128_INDEX_URL, @@ -290,6 +291,87 @@ impl TorchBackend { Self::Cu80 => &CU80_INDEX_URL, } } + + /// Extract a [`TorchBackend`] from an index URL. + pub fn from_index(index: &Url) -> Option { + let backend_identifier = if index.host_str() == Some("download.pytorch.org") { + // E.g., `https://download.pytorch.org/whl/cu124` + let mut path_segments = index.path_segments()?; + if path_segments.next() != Some("whl") { + return None; + } + path_segments.next()? + } else { + return None; + }; + Self::from_str(backend_identifier).ok() + } + + /// Returns the CUDA [`Version`] for the given [`TorchBackend`]. + pub fn cuda_version(&self) -> Option { + match self { + TorchBackend::Cpu => None, + TorchBackend::Cu128 => Some(Version::new([12, 8])), + TorchBackend::Cu126 => Some(Version::new([12, 6])), + TorchBackend::Cu125 => Some(Version::new([12, 5])), + TorchBackend::Cu124 => Some(Version::new([12, 4])), + TorchBackend::Cu123 => Some(Version::new([12, 3])), + TorchBackend::Cu122 => Some(Version::new([12, 2])), + TorchBackend::Cu121 => Some(Version::new([12, 1])), + TorchBackend::Cu120 => Some(Version::new([12, 0])), + TorchBackend::Cu118 => Some(Version::new([11, 8])), + TorchBackend::Cu117 => Some(Version::new([11, 7])), + TorchBackend::Cu116 => Some(Version::new([11, 6])), + TorchBackend::Cu115 => Some(Version::new([11, 5])), + TorchBackend::Cu114 => Some(Version::new([11, 4])), + TorchBackend::Cu113 => Some(Version::new([11, 3])), + TorchBackend::Cu112 => Some(Version::new([11, 2])), + TorchBackend::Cu111 => Some(Version::new([11, 1])), + TorchBackend::Cu110 => Some(Version::new([11, 0])), + TorchBackend::Cu102 => Some(Version::new([10, 2])), + TorchBackend::Cu101 => Some(Version::new([10, 1])), + TorchBackend::Cu100 => Some(Version::new([10, 0])), + TorchBackend::Cu92 => Some(Version::new([9, 2])), + TorchBackend::Cu91 => Some(Version::new([9, 1])), + TorchBackend::Cu90 => Some(Version::new([9, 0])), + TorchBackend::Cu80 => Some(Version::new([8, 0])), + } + } +} + +impl FromStr for TorchBackend { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "cpu" => Ok(TorchBackend::Cpu), + "cu128" => Ok(TorchBackend::Cu128), + "cu126" => Ok(TorchBackend::Cu126), + "cu125" => Ok(TorchBackend::Cu125), + "cu124" => Ok(TorchBackend::Cu124), + "cu123" => Ok(TorchBackend::Cu123), + "cu122" => Ok(TorchBackend::Cu122), + "cu121" => Ok(TorchBackend::Cu121), + "cu120" => Ok(TorchBackend::Cu120), + "cu118" => Ok(TorchBackend::Cu118), + "cu117" => Ok(TorchBackend::Cu117), + "cu116" => Ok(TorchBackend::Cu116), + "cu115" => Ok(TorchBackend::Cu115), + "cu114" => Ok(TorchBackend::Cu114), + "cu113" => Ok(TorchBackend::Cu113), + "cu112" => Ok(TorchBackend::Cu112), + "cu111" => Ok(TorchBackend::Cu111), + "cu110" => Ok(TorchBackend::Cu110), + "cu102" => Ok(TorchBackend::Cu102), + "cu101" => Ok(TorchBackend::Cu101), + "cu100" => Ok(TorchBackend::Cu100), + "cu92" => Ok(TorchBackend::Cu92), + "cu91" => Ok(TorchBackend::Cu91), + "cu90" => Ok(TorchBackend::Cu90), + "cu80" => Ok(TorchBackend::Cu80), + _ => Err(format!("Unknown PyTorch backend: {s}")), + } + } } /// Linux CUDA driver versions and the corresponding CUDA versions. diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 3810be8e8..b14e2e89c 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -395,7 +395,7 @@ pub(crate) async fn pip_compile( .index_urls(index_locations.index_urls()) .index_strategy(index_strategy) .url_auth_policies(UrlAuthPolicies::from(&index_locations)) - .torch_backend(torch_backend) + .torch_backend(torch_backend.clone()) .markers(interpreter.markers()) .platform(interpreter.platform()) .build(); @@ -482,6 +482,7 @@ pub(crate) async fn pip_compile( .dependency_mode(dependency_mode) .exclude_newer(exclude_newer) .index_strategy(index_strategy) + .torch_backend(torch_backend) .build_options(build_options.clone()) .build(); diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 50c23b67f..ca238d01b 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -362,7 +362,7 @@ pub(crate) async fn pip_install( .index_urls(index_locations.index_urls()) .index_strategy(index_strategy) .url_auth_policies(UrlAuthPolicies::from(&index_locations)) - .torch_backend(torch_backend) + .torch_backend(torch_backend.clone()) .markers(interpreter.markers()) .platform(interpreter.platform()) .build(); @@ -456,6 +456,7 @@ pub(crate) async fn pip_install( .dependency_mode(dependency_mode) .exclude_newer(exclude_newer) .index_strategy(index_strategy) + .torch_backend(torch_backend) .build_options(build_options.clone()) .build(); diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 75d2e7b5a..163492886 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -294,7 +294,7 @@ pub(crate) async fn pip_sync( .index_urls(index_locations.index_urls()) .index_strategy(index_strategy) .url_auth_policies(UrlAuthPolicies::from(&index_locations)) - .torch_backend(torch_backend) + .torch_backend(torch_backend.clone()) .markers(interpreter.markers()) .platform(interpreter.platform()) .build(); @@ -391,6 +391,7 @@ pub(crate) async fn pip_sync( .dependency_mode(dependency_mode) .exclude_newer(exclude_newer) .index_strategy(index_strategy) + .torch_backend(torch_backend) .build_options(build_options.clone()) .build(); diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index f34c2b463..2dd6cbc7c 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -17300,3 +17300,37 @@ async fn index_has_no_requires_python() -> Result<()> { Ok(()) } + +/// Disallow resolving to multiple different PyTorch indexes. +#[test] +fn incompatible_cuda() -> Result<()> { + let context = TestContext::new("3.11"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str(indoc! {r" + torch==2.6.0+cu126 + torchvision==0.16.0+cu121 + "})?; + + uv_snapshot!(context + .pip_compile() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .env(EnvVars::UV_TORCH_BACKEND, "auto") + .env(EnvVars::UV_CUDA_DRIVER_VERSION, "525.60.13") + .arg("--preview") + .arg("requirements.in") + .arg("--python-platform") + .arg("x86_64-manylinux_2_28") + .arg("--python-version") + .arg("3.11"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because torchvision==0.16.0+cu121 depends on system:cuda==12.1 and torch==2.6.0+cu126 depends on system:cuda==12.6, we can conclude that torch==2.6.0+cu126 and torchvision==0.16.0+cu121 are incompatible. + And because you require torch==2.6.0+cu126 and torchvision==0.16.0+cu121, we can conclude that your requirements are unsatisfiable. + "); + + Ok(()) +}