Why Installing GPU Python Packages Is So Complicated
Python wheels describe themselves with three tags: Python version, ABI, and platform. A wheel tagged cp312-cp312-linux_x86_64 tells an installer everything it needs to select the right binary for CPython 3.12 on 64-bit Linux. But it says nothing about the GPU sitting in that Linux machine, or which version of CUDA is installed alongside it.
This gap means that a package like PyTorch, which ships different binaries for CUDA 12.6, CUDA 12.8, CUDA 13.0, and CPU-only builds, 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 825 defines a new wheel format that can carry “variant properties,” and PEP 817 specifies how installers should select among variants. Both were created in December 2025, with authors from NVIDIA, Astral, and the conda community.
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.
If adopted, this means 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 released an experimental build of uv with variant support in August 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.
Get Python tooling updates
Subscribe to the newsletter