From b450f16ef1b93425499d6121de99954f70bc6e19 Mon Sep 17 00:00:00 2001 From: Aminsed <35748194+Aminsed@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:46:43 -0400 Subject: [PATCH] Make pip install work on a stock CUDA machine The 0.1.0 sdist on PyPI contains no CUDA sources at all -- no .cu, no headers, no CMakeLists -- so `pip install culof` could never build. Nothing short of a new release fixes that, and 0.1.0 cannot be reuploaded, so this prepares 0.2.0. Finding nvcc is the other half. CMake searches only PATH for it, while the usual Linux install puts the toolkit under /usr/local/cuda and leaves only the driver on PATH. project(... LANGUAGES CUDA) then aborts with "No CMAKE_CUDA_COMPILER could be found" on a machine with a perfectly good toolkit -- reproduced here on an A6000. Look in the standard roots before enabling the language. Also: the README pointed at its images with relative paths, which resolve on GitHub and break on the PyPI project page, so every figure was a broken image there. Absolute raw.githubusercontent.com URLs render in both places. The release workflow publishes via PyPI Trusted Publishing, so no API token is stored in the repository, and it refuses to publish an sdist that is missing any file needed to build -- the exact defect that shipped in 0.1.0. Verified: sdist builds, installs into a clean venv with nvcc off PATH, cuda_available() is true, and all 45 tests pass against the installed package. --- .github/workflows/release.yml | 66 +++++++++++++++++++++++++++++++++++ CMakeLists.txt | 20 +++++++++++ README.md | 6 ++-- src/culof/_version.py | 3 +- 4 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8f875f2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,66 @@ +name: release + +# Publishes to PyPI when a GitHub Release is published. +# +# Authentication is PyPI Trusted Publishing (OIDC): GitHub mints a short-lived +# token for this workflow and PyPI verifies it. No API token is stored in the +# repository, so there is nothing to leak or rotate. +on: + release: + types: [published] + workflow_dispatch: + +jobs: + sdist: + name: Build sdist + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Build + run: | + python -m pip install --upgrade build twine + python -m build --sdist + + - name: Check metadata renders on PyPI + run: python -m twine check --strict dist/* + + - name: Verify the sdist actually carries the sources + # cuLOF ships source-only: pip compiles the CUDA extension on the + # target machine. That makes a complete sdist load-bearing, and the + # 0.1.0 release on PyPI shipped without a single .cu file, so it could + # never build. Fail the release rather than publish that again. + run: | + set -euo pipefail + for f in CMakeLists.txt src/cuda/knn.cu src/cuda/lof.cu \ + src/cuda/pipeline.cu src/cuda/preprocess.cu \ + src/cuda/binding.cpp include/culof.h \ + include/culof_internal.cuh src/culof/__init__.py; do + tar tzf dist/*.tar.gz | grep -q "/$f$" \ + || { echo "::error::sdist is missing $f"; exit 1; } + done + echo "sdist contains every file needed to build" + + - uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + + publish: + name: Publish to PyPI + needs: sdist + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write # required for trusted publishing + steps: + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CMakeLists.txt b/CMakeLists.txt index a978fd2..1a4010f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,24 @@ cmake_minimum_required(VERSION 3.18) + +# --------------------------------------------------------------------------- +# Find nvcc before the CUDA language is enabled. +# +# CMake searches only PATH for nvcc. The usual Linux install puts the toolkit +# under /usr/local/cuda and only the driver on PATH, which would otherwise be +# indistinguishable from having no CUDA at all -- this is why `pip install` +# failed on machines that had a perfectly good toolkit. +# --------------------------------------------------------------------------- +if(NOT DEFINED CMAKE_CUDA_COMPILER AND NOT DEFINED ENV{CUDACXX}) + file(GLOB _cuda_roots /usr/local/cuda-*) + find_program(_nvcc nvcc + HINTS ENV CUDA_HOME ENV CUDA_PATH ENV CUDA_ROOT + /usr/local/cuda ${_cuda_roots} + PATH_SUFFIXES bin) + if(_nvcc) + set(CMAKE_CUDA_COMPILER "${_nvcc}") + endif() +endif() + project(culof LANGUAGES CXX CUDA) set(CMAKE_CXX_STANDARD 17) diff --git a/README.md b/README.md index 2cfa030..0dc3f07 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ answers. Changing the import is the whole migration. ## Performance -![Speedup over scikit-learn](img/benchmark_speed.png) +![Speedup over scikit-learn](https://raw.githubusercontent.com/Aminsed/cuLOF/main/img/benchmark_speed.png) | samples | scikit-learn | cuLOF | speedup | |--------:|-------------:|------:|--------:| @@ -74,7 +74,7 @@ float32 distance matrix would be 149 GiB. ## Accuracy -![Agreement with scikit-learn](img/accuracy.png) +![Agreement with scikit-learn](https://raw.githubusercontent.com/Aminsed/cuLOF/main/img/accuracy.png) cuLOF computes distances in float32 and scikit-learn in float64, so agreement is measured rather than assumed. At n = 20,000, d = 8, k = 20: **median relative @@ -89,7 +89,7 @@ which points were flagged. [`test_near_ties_are_the_only_disagreement`](tests/python/test_parity.py) asserts exactly this. -![Detection on three datasets](img/detection.png) +![Detection on three datasets](https://raw.githubusercontent.com/Aminsed/cuLOF/main/img/detection.png) ## Install diff --git a/src/culof/_version.py b/src/culof/_version.py index 517b022..48b0fe2 100644 --- a/src/culof/_version.py +++ b/src/culof/_version.py @@ -1,6 +1,7 @@ """Single source of truth for the package version. -pyproject.toml reads it via `tool.setuptools.dynamic`, and `culof.__version__` +pyproject.toml reads it back through the scikit-build-core regex +metadata provider, and `culof.__version__` re-exports it, so there is exactly one place to bump. """