Skip to content

feat(build): cuda + torch dev shell variants - #167

Draft
yeetypete wants to merge 13 commits into
feat/nix-flakefrom
feat/torch-cuda-matrix
Draft

yeetypete wants to merge 13 commits into
feat/nix-flakefrom
feat/torch-cuda-matrix

Conversation

@yeetypete

@yeetypete yeetypete commented Aug 16, 2026

Copy link
Copy Markdown
Owner

NOTE: This PR is a PoC of using Nix in vision3d and may not be merged.

To test using the vision3d flake first install nix (can be done on any system): https://nixos.org/download/

What

  • Provide one dev shell per (CUDA toolkit, torch) pair instead of a single hardcoded toolkit. Currently torch 2.10 on CUDA 12.8 and 2.11 through 2.13 on CUDA 13.0.
  • Build the environments from uv.lock with uv2nix instead of uv sync doing it at runtime. The test suite, pyrefly, clang-tidy, the git hooks and the release artifacts become flake outputs. In CI we can just run nix build .#checks.x86_64-linux.<name>.

Why Nix

Building this project needs a CUDA toolkit, a host gcc its nvcc accepts, clang 22 for clang-tidy, and a torch built against that same toolkit. Installing these dependencies can be tricky and until now was only documented in the vision3d README. CI had to use containers (slow) to guarantee the right versions.

Reasons to move to a flake:

  • Nobody has to install any development dependencies by hand anymore. The flake provides a reproducible development environment.
  • A host can only have one CUDA toolkit installed at a time. vision3d can be more easily tested across different CUDA toolkit versions on a single host.
  • Release wheels need the manylinux_2_28 toolchain, which used to mean building inside a specialized RHEL 8 docker image. The flake provides that toolchain as well, so nix build .#dist produces the sdist and the wheel on any host.

Testing

# Run CPU test suite, linting, git hooks on all build variants.
nix flake check
# Run CPU test suite, linting, git hooks on a single build variant.
nix build .#checks.x86_64-linux.tests-torch210-cu128
# Run a command in the dev shell for a given variant.
nix develop .#torch210-cu128 --command just test -m cpu
# Enter the development environment (torch213-cu130 default variant).
direnv allow
# Build release sdist and the manylinux_2_28 wheel. It lands in result/.
nix build .#dist

Experience with Nix

The Good:

  • Full reproducibility of the dev environment and release artifacts.
  • Easy to add another toolchain variant which we want to build / test vision3d against. Before each one would have required its own container and CI job.
  • CI is a thin wrapper around commands anyone can run locally.
  • No containers anywhere, not even for the manylinux wheel which requires a RHEL 8 toolchain / older glibc.
  • Cachix makes ci significantly faster than before with containers.

The Bad:

  • Most of the work is not writing Nix. It is patching software that assumes an FHS layout. CUDA, the torch wheels, manylinux and the GPU drivers all expect a system compiler and libraries in /usr/lib, and each one needs its own workaround to function outside of that.
  • Debugging the toolchain is hard. Even with a good understanding of CUDA, linking and packaging debugging the build without an LLM would be tough and things that "just work" on a standard distro will not.
  • The ecosystem still feels pretty young with everyone authoring and sharing various workarounds in their flakes, especially when working with CUDA. The driver discovery here is adapted from NVlabs/cutile-rs, the manylinux toolchain from huggingface/kernels, and the general community answer for drivers is a whole separate wrapper, nixGL.
  • uv already provides a pretty good development environment in most cases. vision3d also uses the torch stable ABI which erases some of the benefits of needing multiple different build variants with different CUDA toolkits (they are mostly just for runtime testing).
  • Nix wants to own the language package manager. This is why we use uv2nix.
  • Letting uv install the environment itself is simpler, but then it is resolved at runtime and outside the store, so the reproducibility and the Cachix speedup mostly go away. The dev environment likely won't work on NixOS in that case because the wheels uv installs are prebuilt binaries linked against an FHS layout. Here we have to patch them with autoPatchelfHook to make them work.

for testing vision3d across torch / torchvision versions
- flake.nix declares only inputs, imports and systems
- nix/cuda.nix holds the CUDA toolkit options and the torch builds
  they pair with
- nix/devshells.nix holds the per-variant dev shells and the wheel shell
- nix/cuda-home.nix builds the joined toolkit prefix with callPackage,
  renamed cuda-merged after the nixpkgs derivations that build the same
  symlinkJoin
- Pair each torch release with the CUDA build PyPI publishes for it, moving
  2.11 and 2.13 to cudaPackages_13_0 and dropping the cu132 lane. The three
  PyTorch wheel indexes and `index-strategy` go with them, and the lock now
  carries a hash for every wheel it names.
- Build the dev environments from `uv.lock` with uv2nix, one editable
  virtual environment per variant. There is no `.venv`, no `uv sync`, and no
  `UV_NO_GROUP`: entering a shell selects the variant.
- Put the GPU driver on the library path in the dev shells, after
  https://github.com/NVlabs/cutile-rs, so torch finds `libcuda.so.1` on
  distributions that keep it outside the store.
- Run the CPU suite of every variant, pyrefly, clang-tidy, and the git hooks
  as flake checks. Each test check's output is that variant's HTML coverage
  report.
- Name the GCC and libc headers clang-tidy analyses against, rather than
  letting clang scan the machine for a toolchain it may not find.
- Move what `just tidy` and `just compile-db` run into `scripts/`, so the
  checks of those names run the same thing.
- Build the release sdist and wheel as the `dist` package, against torch
  2.10, CUDA 12.8, and the manylinux_2_28 toolchain. `nix build --rebuild`
  reproduces them byte for byte.
- Keep the store out of the wheel: the wrapped linker no longer writes an
  rpath or the toolchain's loader into the extension, `auditwheel repair`
  strips the debug info naming the build machine, and
  `scripts/check-wheel.sh` fails the build if a run path survives.
- Drive CI with `nix build` and `nix flake check` instead of dev shells,
  which retires the uv cache. The docs job stays a shell, since the gallery
  downloads the dataset samples it renders.
- Drop the instructions for developing without nix, and name the release
  pair rather than describing it.
- Build the release artifacts with `nix build` in the `build` job, which no
  longer enters a shell and so has no `just`.
- Realise the source the manylinux toolchain imports before checking the
  flake. Evaluating the release artifacts pulls in a pinned nixpkgs through
  import-from-derivation, which `--no-build` refuses to fetch.
- Ignore the `typing_extensions.ReadOnly` cross-reference in the docs. It
  reaches the annotations on Python 3.12, and its own inventory documents it
  as data rather than a class, so nitpicky mode has nothing to resolve.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant