You pip install torch and watch 2.3 GB download. Then you realize that was the CPU build.
Regular Python dependencies are polite. Flask, requests, click — pure Python wheels that install in seconds and never ask about your hardware. ML dependencies are a different species. They drag in CUDA runtimes, GPU-specific binaries, native C extensions, and version-locked pairs where torch 2.2 demands exactly CUDA 12.1. Not 12.0. Not 12.2.
The dependency problem scales with org size. One data scientist can conda install pytorch and move on. Twenty teams sharing a monorepo with different GPU requirements? That’s where it gets interesting.
The Mega Lockfile
The simplest approach: one giant requirements.txt for the whole monorepo. Every team, every service, one resolution.
torch==2.2.0+cu121
numpy==1.26.4
transformers==4.38.1
# ... 847 more lines
It works until it doesn’t. One team upgrades torch for the new flash attention API. Numpy gets yanked to a version that breaks the recommendation service. Nobody notices for two weeks because CI only tests the training jobs.
The failure mode is always the same — a transitive dependency shifts and something downstream silently breaks. At GPU scale, “silently breaks” can mean incorrect model outputs instead of a clean crash.
Per-Project with Shared Constraints
Each service gets its own pyproject.toml and lockfile. An org-wide constraints.txt pins the versions that must stay aligned across teams.
# constraints.txt — org-wide pins
numpy>=1.26.0,<1.27
protobuf>=4.25.0,<5
cuda-toolkit==12.1
pip install -c constraints.txt -r requirements.txt
This is what most orgs actually ship. HuggingFace, large ML platform teams — they converge on this pattern because it balances autonomy with coherence. Each team owns their deps. The constraints file prevents the numpy-goes-sideways scenario.
The downside: someone has to maintain that constraints file. It becomes a bottleneck. PRs to update it get contentious.
uv Workspaces
The newer answer. A single lockfile across all workspace members, but with per-project dependency declarations.
# Root pyproject.toml
[tool.uv.workspace]
members = ["training", "inference", "data-pipeline"]
# training/pyproject.toml
[project]
dependencies = ["torch>=2.2", "transformers>=4.38"]
uv resolves everything together — one coherent uv.lock that satisfies all members simultaneously. No constraints file to maintain. No two-team collision because the resolver sees the whole picture upfront.
For medium-to-large teams doing pure Python ML work, this is genuinely the best option right now. The resolution is fast enough that you’re not waiting minutes for a solve, and the lockfile is cross-platform by default.
The catch: uv doesn’t manage CUDA itself.
Conda Environments
Conda’s killer feature isn’t Python packaging. It’s native binary resolution.
conda install pytorch cuda-toolkit=12.1 -c pytorch -c nvidia
That actually installs the right CUDA runtime. The right cuDNN. The right NCCL for multi-GPU. pip and uv just hope those exist somewhere on your system — in /usr/local/cuda, in a Docker layer, wherever.
For teams where the GPU stack matters more than the Python stack — training infrastructure, custom CUDA kernels, anything touching NCCL — conda is still the only tool that treats native binaries as first-class deps.
The tradeoff: conda’s resolver is slow. Painfully slow. And the ecosystem is smaller. Half your Python deps won’t have conda packages.
The Hybrid: Conda + pip/uv
This is what most ML teams actually run in production.
# Conda for the hard stuff
conda install cuda-toolkit=12.1 cudnn nccl mkl ffmpeg
# pip/uv for everything else
uv pip install torch transformers wandb
It works. Mostly. The problem is fundamental: two resolvers that don’t talk to each other.
pip doesn’t know what conda installed. It can silently overwrite a conda-managed numpy with a pip-managed one — same version number, different binary linkage. Your code imports fine. Tests pass. Then at runtime, you get a segfault in libopenblas because pip’s numpy links against a different BLAS than the one conda configured.
# The classic symptom
Intel MKL FATAL ERROR: Cannot load libmkl_intel_thread.so
The standard mitigation: install conda deps first, then pip deps, and never run conda install after pip install. Order matters. It’s fragile. Everyone does it anyway.
So What Do You Actually Pick?
No clean answer.
Pure Python ML work — uv workspaces. The resolution is coherent, the tooling is fast, and you avoid the two-resolver problem entirely.
Heavy CUDA/native deps — conda, or the hybrid. Conda is the only tool that actually resolves binary compatibility. The hybrid is common but demands discipline.
Large org, many teams — per-project with shared constraints. Boring. Works. The constraints file is annoying but manageable.
The real trend: uv is eating this space fast, and conda-forge is getting better at keeping up with PyTorch releases. The gap between “pure Python resolver” and “native binary resolver” is shrinking — but it’s not closed yet.
Pick your pain. At least now you know which nerve each option hits.