Pin faiss-gpu-cuvs RAPIDS deps to the 26.06 build series (#5643)

Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5643

`faiss-gpu-cuvs` 1.15.0 fails at import on a clean install:

```
ImportError: libfaiss.so: undefined symbol: _ZTIN3rmm9bad_allocE
```

## Root cause, measured

RAPIDS 26.08 wrapped RMM in a per-release ABI inline namespace. `nm -D` on the published `librmm-cu13` wheels:

| Version | `typeinfo for rmm::bad_alloc` | Exported instead |
| --- | --- | --- |
| 26.2.0 | present | |
| 26.4.0 | present | |
| 26.6.0 | present | |
| 26.8.0 | **absent** | `typeinfo for rmm::_RMM_26_8::bad_alloc` |

`pyproject-gpu-cuvs.toml` builds against `==26.06.*` in `before-all` but declares `>=26.06,<27` at runtime and in `test-requires`. A fresh install therefore pairs a 26.06-built `libfaiss.so` with a 26.8 `librmm.so` that renamed every `rmm::` symbol. Nothing vendored covers the gap: `repair-wheel-command` hands every RAPIDS `lib*.so*` under `/opt/cuvs` to `auditwheel --exclude`.

The 1.15.0 wheel was uploaded 2026-08-03, two days before RAPIDS 26.08 reached PyPI. It published green and began failing for every new installer afterwards, which matches `build-pip-gpu / Build & test faiss-gpu-cuvs` going red on 2026-08-05 after 68 consecutive passes.

An exact-series pin rather than a narrower range is required: the inline namespace encodes the release, so **any** range spanning more than one RAPIDS minor series is broken by construction.

## This diff

- Pin `libcuvs-cu13`, `libraft-cu13`, and `librmm-cu13` to `==26.06.*` in both `dependencies` and `test-requires`.
- Declare `libraft-cu13`. `faiss/python/__init__.py` imports `libraft` and calls `libraft.load_library()`, but only `before-all` installed it, so at runtime its version floated on `libcuvs-cu13` transitive metadata.
- Correct the cuSOLVER soname in the `auditwheel` exclude list from `libcusolver.so.11` to `libcusolver.so.12`, which is what CUDA 13.2 ships.
- Update the `_preload_gpu_libs` fix-it message, which told users to install the range this diff removes.
- Add `TestCuvsRapidsPinning` to `tests/test_wheel_smoke_gpu.py`.

## Regression guard

`tests/test_wheel_smoke_gpu.py` is already the `test-command` for the cuVS wheel, so the new class runs inside `build-pip-gpu.yml` with no workflow change and no new test file. It asserts three properties of the built wheel's own `Requires-Dist`:

1. every RAPIDS distribution `_preload_gpu_libs` dlopens is declared
2. each is pinned to `==26.06.*` rather than an open range
3. the installed versions sit in that series

`_RAPIDS_SERIES` duplicates the series from `pyproject-gpu-cuvs.toml` because the wheel does not ship that file. The duplication is the tripwire: a RAPIDS bump has to touch both places deliberately.

## Prior attempt

D117099095 made the version-pin change on 2026-08-23 and was abandoned on 2026-09-04 without landing. It never failed review. `faiss/public_tld/` is ShipIt-mirrored with `requireExportConfig`, so `github-export-checks` held it Unpublished pending a `facebookresearch/faiss` PR that a bot cannot open, and three pings over 12 days went unanswered. This diff needs that same export.

## Notes

The published 1.15.0 metadata declares only `libcuvs-cu13`, not `librmm-cu13`. That is not a fbsource/GitHub divergence: `librmm-cu13` was added to `dependencies` on 2026-08-14, after the 08-03 release, and has never shipped.

`rapids-logger` is installed by `before-all` but stays undeclared here. `librmm-cu13` owns that link-time dependency and carries it in its own metadata; an unversioned duplicate would not make the resolve safer.

Reviewed By: mnorris11

Differential Revision: D120162004

fbshipit-source-id: eb6dc316a4fba2a4228eab62d7b86ac8ea94a1b7
This commit is contained in:
Alibek Zhakubayev
2026-09-15 14:42:20 -07:00
committed by meta-codesync[bot]
parent 7c3b55c01b
commit 3bb48643ab
3 changed files with 118 additions and 6 deletions
+2 -1
View File
@@ -121,7 +121,8 @@ def _preload_gpu_libs():
raise RuntimeError(
"faiss-gpu-cuvs installed but the cuVS runtime wheels are "
"missing — "
"pip install 'libcuvs-cu13>=26.06,<27' "
"pip install 'libcuvs-cu13==26.06.*' 'libraft-cu13==26.06.*' "
"'librmm-cu13==26.06.*' "
"--extra-index-url https://pypi.nvidia.com"
) from e
+11 -5
View File
@@ -44,8 +44,13 @@ dependencies = [
"nvidia-cublas>=13.2,<14",
"nvidia-curand>=10.3.7,<11",
"nvidia-nvjitlink>=13.2,<14",
"libcuvs-cu13>=26.06,<27",
"librmm-cu13>=26.06,<27",
# Exact-series pin, not a range: RAPIDS >=26.08 wraps RMM in a per-release
# inline namespace (rmm::_RMM_26_8::), so each minor series renames the
# symbols libfaiss.so is linked against. libraft is listed because
# _preload_gpu_libs dlopens it directly.
"libcuvs-cu13==26.06.*",
"libraft-cu13==26.06.*",
"librmm-cu13==26.06.*",
]
[project.urls]
@@ -114,8 +119,9 @@ test-requires = [
"nvidia-cublas>=13.2,<14",
"nvidia-curand>=10.3.7,<11",
"nvidia-nvjitlink>=13.2,<14",
"libcuvs-cu13>=26.06,<27",
"librmm-cu13>=26.06,<27",
"libcuvs-cu13==26.06.*",
"libraft-cu13==26.06.*",
"librmm-cu13==26.06.*",
]
test-command = "python -m pytest {project}/tests/test_wheel_smoke_gpu.py -v"
@@ -135,7 +141,7 @@ before-all = "dnf install -y gcc-toolset-14 epel-release && dnf install -y openb
# auditwheel excludes (not vendored; supplied at runtime by nvidia-* / libcuvs-cu13
# wheels): host driver libcuda, CUDA runtime/math libs (cusparse/cusolver are explicit —
# under the cuda-toolkit RPM, not /opt/cuvs), and every RAPIDS .so found under /opt/cuvs.
repair-wheel-command = "bash -c 'EXCLUDES=\"--exclude libcuda.so.1 --exclude libcudart.so.13 --exclude libcublas.so.13 --exclude libcublasLt.so.13 --exclude libcurand.so.10 --exclude libnvJitLink.so.13 --exclude libcusparse.so.12 --exclude libcusolver.so.11\"; for f in $(find /opt/cuvs -name \"lib*.so*\" -printf \"%f\\n\" | sort -u); do EXCLUDES=\"$EXCLUDES --exclude $f\"; done; auditwheel repair -w {dest_dir} {wheel} $EXCLUDES'"
repair-wheel-command = "bash -c 'EXCLUDES=\"--exclude libcuda.so.1 --exclude libcudart.so.13 --exclude libcublas.so.13 --exclude libcublasLt.so.13 --exclude libcurand.so.10 --exclude libnvJitLink.so.13 --exclude libcusparse.so.12 --exclude libcusolver.so.12\"; for f in $(find /opt/cuvs -name \"lib*.so*\" -printf \"%f\\n\" | sort -u); do EXCLUDES=\"$EXCLUDES --exclude $f\"; done; auditwheel repair -w {dest_dir} {wheel} $EXCLUDES'"
# Canary: ensure the host GPU is actually visible to the container before tests
# run. Without this, smoke tests would silently skip on broken --gpus passthrough.
before-test = "nvidia-smi || (echo 'GPU not visible to container; check container-engine create-args' && exit 1)"
+105
View File
@@ -18,8 +18,12 @@ runs separately in the cmake-based CI.
"""
import functools
import importlib.metadata
import re
import unittest
import numpy as np
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
import faiss
@@ -247,3 +251,104 @@ class TestCuvsCagra(unittest.TestCase):
self.assertEqual(I.shape, (nq, 10))
# CAGRA is approximate, so allow itself-as-first-NN as the contract.
np.testing.assert_array_equal(I[:, 0], np.arange(nq))
# Keep in step with the `libcuvs-cu13==<series>.*` pins in
# pyproject-gpu-cuvs.toml. The wheel does not ship that file, so nothing else
# ties the two together. Compared via SpecifierSet, never by string prefix:
# PEP 440 strips the leading zero, so the series pinned as `26.06` installs as
# `26.6.0`.
_RAPIDS_SERIES = SpecifierSet("==26.06.*")
# RAPIDS >=26.08 wraps RMM in a per-release inline namespace
# (rmm::_RMM_26_8::), so a neighbouring series renames every rmm:: symbol
# libfaiss.so is linked against. RAPIDS ships six series a year, YY.02 to
# YY.12; probing them lets the declared spec be compared against
# _RAPIDS_SERIES by behaviour, so bumping the series needs no edit here.
_RAPIDS_PROBES = tuple(
f"{year}.{month}.0"
for year in (25, 26, 27)
for month in (2, 4, 6, 8, 10, 12)
)
# RAPIDS distributions faiss dlopens at import; see _preload_gpu_libs in
# faiss/python/__init__.py.
_RAPIDS_DISTS = ("librmm-cu13", "libraft-cu13", "libcuvs-cu13")
def _canonical(name):
return re.sub(r"[-_.]+", "-", name).lower()
def _cuvs_wheel_requirements():
"""Requires-Dist of the installed faiss-gpu-cuvs wheel, grouped by
canonical name, or None when faiss did not come from that wheel.
Values are lists: one name can appear several times under different
markers or extras, and collapsing them would hide a bad pin.
"""
try:
specs = importlib.metadata.requires("faiss-gpu-cuvs")
except importlib.metadata.PackageNotFoundError:
return None
grouped = {}
for req in map(Requirement, specs or ()):
grouped.setdefault(_canonical(req.name), []).append(req)
return grouped
_CUVS_WHEEL_REQS = _cuvs_wheel_requirements() if _CUVS_BUILD else None
_skip_unless_cuvs_wheel = unittest.skipUnless(
_CUVS_WHEEL_REQS is not None,
"Skipping cuVS packaging tests: not a faiss-gpu-cuvs wheel install",
)
@_skip_unless_cuvs_wheel
class TestCuvsRapidsPinning(unittest.TestCase):
"""Catch: RAPIDS build/runtime skew, undeclared RAPIDS deps.
The wheel is compiled against one RAPIDS series but resolves RAPIDS at
install time. A declared range wider than that series lets the installer
pick a librmm that no longer exports the symbols libfaiss.so was linked
against; `import faiss` then dies during collection, before any other
test in this file runs.
"""
def _declared(self, dist):
reqs = _CUVS_WHEEL_REQS.get(_canonical(dist))
self.assertTrue(
reqs,
f"{dist} is dlopened at import but not declared by the wheel, "
f"so its version floats on other packages' metadata",
)
return reqs
def test_preloaded_rapids_dists_are_declared(self):
for dist in _RAPIDS_DISTS:
self._declared(dist)
def test_rapids_dists_admit_only_the_build_series(self):
for dist in _RAPIDS_DISTS:
for req in self._declared(dist):
for probe in _RAPIDS_PROBES:
self.assertEqual(
_RAPIDS_SERIES.contains(probe),
req.specifier.contains(probe),
f"{dist} spec '{req.specifier}' disagrees with the "
f"build series {_RAPIDS_SERIES} on RAPIDS {probe}; "
f"a spec spanning series renames the rmm:: symbols "
f"libfaiss.so is linked against",
)
def test_installed_rapids_matches_the_build_series(self):
for dist in _RAPIDS_DISTS:
try:
version = importlib.metadata.version(dist)
except importlib.metadata.PackageNotFoundError:
self.fail(f"{dist} is declared but not installed")
self.assertTrue(
_RAPIDS_SERIES.contains(version),
f"{dist} {version} is outside the {_RAPIDS_SERIES} series "
f"the wheel was built against",
)