Why Installing GPU Python Packages Is So Complicated
Python wheels have no way to express GPU hardware requirements. A wheel’s three metadata tags (Python version, ABI, and platform) tell an installer everything about the CPU environment, but nothing about the GPU or which CUDA version is available.
PyTorch ships different binaries for CUDA 12.6, 12.8, 13.0, and CPU-only builds. Without GPU metadata in wheels, it has no standard way to publish all four variants to PyPI and let installers pick the right one. Every CUDA-dependent project has had to invent its own distribution workaround.
How packages work around this today
Without standard metadata for GPU support, every CUDA-dependent project has invented its own distribution scheme.
Separate index URLs (the PyTorch approach)
PyTorch hosts its own package index at download.pytorch.org/whl/. Users select their CUDA version by pointing pip or uv at the matching URL:
# CUDA 12.8
pip install torch --index-url https://download.pytorch.org/whl/cu128
# CPU only
pip install torch --index-url https://download.pytorch.org/whl/cpuThe package name stays torch everywhere. The index determines which binary gets installed. PyTorch 2.11.0 ships variants for CUDA 12.6, 12.8, and 13.0, plus ROCm 7.2 and CPU-only builds.
This approach keeps the package name clean but pushes complexity into project configuration. Every developer on a team needs to use the same index URL, and CI pipelines need separate configurations for GPU and CPU runners.
Package name suffixes (the RAPIDS approach)
NVIDIA’s RAPIDS libraries encode the CUDA version in the package name itself: cudf-cu12, cuml-cu12, cudf-cu13. These are hosted on NVIDIA’s own index at pypi.nvidia.com. NVIDIA’s lower-level runtime libraries use the same convention on PyPI: nvidia-cuda-runtime-cu12.
pip install cudf-cu12 --extra-index-url=https://pypi.nvidia.comUsers must know their CUDA major version and select the matching package. Dependency resolution becomes awkward because cudf-cu12 and cudf-cu13 look like unrelated packages to the installer, even though they provide the same Python API.
Extras syntax (the JAX approach)
JAX uses pip extras to pull in the right backend:
pip install "jax[cuda13]"The extras trigger installation of CUDA-specific dependencies. This keeps the base package name intact and uses a standard pip mechanism, though it still requires the user to specify the CUDA version manually.
How uv handles this today
uv has built-in support for routing packages to different indexes based on platform markers. A pyproject.toml can encode the PyTorch index dance declaratively:
[[tool.uv.index]]
name = "pytorch-cu128"
url = "https://download.pytorch.org/whl/cu128"
explicit = true
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
[tool.uv.sources]
torch = [
{ index = "pytorch-cpu", marker = "sys_platform != 'linux'" },
{ index = "pytorch-cu128", marker = "sys_platform == 'linux'" },
]This gives teams a single lockable configuration that routes GPU builds to Linux and CPU builds to macOS/Windows. uv also provides a --torch-backend=auto flag (and UV_TORCH_BACKEND environment variable) in its uv pip interface that auto-detects GPU hardware, though this feature is not yet available in the project-level workflow.
What conda and pixi do differently
Conda sidesteps this problem by managing CUDA as a non-Python dependency. A conda environment file can pin a specific CUDA toolkit version alongside pytorch and numpy, and conda’s solver resolves all of them together. The CUDA toolkit is just another package in the dependency graph.
Pixi inherits this capability through conda-forge. For teams already using conda-based tooling, CUDA versioning is a solved problem. The tradeoff is that you must commit to the conda ecosystem for your entire dependency stack, or carefully manage the boundary between conda and pip packages.
What wheel variants will change
Two draft PEPs aim to fix this at the packaging standard level. PEP 817, created December 10, 2025, describes the full variant model, including the provider plugin API and the variant ordering algorithm. PEP 825, created February 17, 2026, specifies the wheel format extensions: where variant metadata lives in the wheel, and the namespace :: feature :: value property structure. The same 11 authors sign both PEPs, with contributors associated with NVIDIA, Quansight, Astral, Meta, and PyPA projects.
The key idea: wheels gain structured metadata like nvidia :: cuda_version_lower_bound :: 12.8. At install time, a provider plugin queries the local hardware (GPU model, driver version) and reports what variants the machine supports. The installer then picks the best match from available wheels.
Variants are not a GPU-only mechanism. The same property system describes CPU microarchitecture levels (x86_64 :: level :: v3 covers the AVX2-and-FMA baseline) and other compatibility dimensions the current platform tags cannot express. For the full explanation of how the format and plugin model work, see What are wheel variants?.
If adopted, pip install torch or uv add torch could select the correct CUDA build automatically, with no index URL configuration and no package name suffixes. Astral published a variant-enabled build of uv on August 13, 2025, though this remains separate from mainline uv.
What to do now
For PyTorch-centric projects, configure index URLs in pyproject.toml using uv’s source routing. This gives reproducible builds without manual index URL flags. For mixed GPU workloads spanning PyTorch and RAPIDS, consider whether conda or pixi would reduce the configuration burden, since both manage CUDA versions as first-class dependencies.
Watch PEP 817 and PEP 825. If wheel variants reach acceptance, the index URL workarounds become unnecessary, and CUDA packages can distribute through PyPI like any other wheel.
Note
PEP 817 and PEP 825 are in Draft status as of early 2026. Draft PEPs can change or be withdrawn. Check the PEP index for current status.