Skip to content

Commit 8061b24

Browse files
alibeklfcfacebook-github-bot
authored andcommitted
Add pip install support via scikit-build-core + cibuildwheel (facebookresearch#4862)
Summary: Add official pip wheel packaging for `faiss-cpu`, enabling `pip install faiss-cpu` from PyPI. Builds binary wheels across Linux x86_64/aarch64, macOS arm64/x86_64, and Windows x86_64 for Python 3.10-3.14. Linux and macOS use Python Stable ABI (abi3) — a single cp310-abi3 wheel per platform works on all Python 3.10+ versions without per-version rebuilds. **BLAS strategy:** - Linux (x86_64 and aarch64): Threaded OpenBLAS (`libopenblaso`). The CMake build prefers `libopenblaso` (OpenMP-threaded) or `libopenblasp` (pthreads-threaded) over the serial default on RHEL/Fedora-based manylinux containers. - macOS: Apple Accelerate (found automatically). - Windows: Prebuilt OpenBLAS from GitHub releases (v0.3.30). **SIMD optimization strategy:** - Linux and macOS: Dynamic Dispatch (`FAISS_OPT_LEVEL=dd`) compiles SIMD-hot kernel files at multiple ISA levels (generic + AVX2 + AVX-512 on x86_64, NEON + SVE on aarch64) into a single library. Runtime CPUID detection selects the optimal code path automatically. - Windows: MSVC does not support DD, so uses `FAISS_OPT_LEVEL=generic`. Shared libraries (`BUILD_SHARED_LIBS=ON`) are used to avoid MSVC's single-pass static archive scanning issue. **New files:** - `pyproject.toml`: scikit-build-core build backend config with cibuildwheel settings. Version is dynamically extracted from CMakeLists.txt. Enables abi3 on Linux/macOS via SWIG 4.2+ and NumPy 2.0+ Limited API support. Enables LTO and dead-code stripping for smaller wheel sizes. - `.github/workflows/build-pip.yml`: CI workflow using cibuildwheel to build binary wheels on 5 platform runners. All GitHub Actions are pinned to commit SHAs for supply chain security. Publishes wheels to PyPI via OIDC trusted publishers on tag push. - `THIRD_PARTY_NOTICES`: License notices for bundled dependencies (OpenBLAS, LLVM OpenMP). - `tests/test_wheel_smoke.py`: 12-test smoke suite validating import, OpenMP, BLAS (FlatL2/FlatIP), index factory (IVF+PQ), HNSW, serialization roundtrip, GC safety, contrib imports, and SIMD level detection. **Modified files:** - `faiss/CMakeLists.txt`: Added threaded OpenBLAS fallback — prefers `libopenblaso` (OpenMP-threaded) or `libopenblasp` (pthreads-threaded) over the serial default on RHEL/Fedora-based manylinux containers. - `python/CMakeLists.txt`: Added `install()` targets for scikit-build-core wheel packaging, including all Dynamic Dispatch SWIG variants (avx2/avx512/avx512_spr/sve). Added abi3 support: conditionally uses `Python::SABIModule`, defines `Py_LIMITED_API`, and sets `.abi3.so` suffix when `SKBUILD_SABI_VERSION` is set. - `.github/workflows/build.yml`: Wired `build-pip.yml` into the root CI trigger so pip builds run alongside conda builds. - `tests/BUCK`: Added `python_pytest` target for the smoke test. Reviewed By: mnorris11 Differential Revision: D95258115
1 parent 632427f commit 8061b24

7 files changed

Lines changed: 999 additions & 22 deletions

File tree

.github/workflows/build-pip.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
name: Build pip wheels
7+
8+
on:
9+
workflow_call:
10+
workflow_dispatch:
11+
12+
jobs:
13+
build-wheels:
14+
name: Build wheels on ${{ matrix.os }}
15+
runs-on: ${{ matrix.os }}
16+
timeout-minutes: 120
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
os: [ubuntu-latest, macos-14, macos-15-intel, windows-2022, 2-core-ubuntu-arm]
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
24+
with:
25+
fetch-depth: 0
26+
fetch-tags: true
27+
28+
- name: Build wheels
29+
uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0
30+
env:
31+
CIBW_BUILD_VERBOSITY: 1
32+
33+
- name: Upload wheels
34+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
35+
with:
36+
name: wheels-${{ matrix.os }}
37+
path: wheelhouse/*.whl
38+
39+
publish:
40+
name: Publish to PyPI
41+
needs: [build-wheels]
42+
runs-on: ubuntu-latest
43+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
44+
environment:
45+
name: pypi
46+
url: https://pypi.org/p/faiss-cpu
47+
permissions:
48+
id-token: write
49+
contents: read
50+
steps:
51+
- name: Download all artifacts
52+
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
53+
with:
54+
path: dist
55+
merge-multiple: true
56+
57+
- name: Publish to PyPI
58+
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ jobs:
1515
secrets:
1616
ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_API_TOKEN }}
1717
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
18+
build-pip:
19+
uses: ./.github/workflows/build-pip.yml

THIRD_PARTY_NOTICES

Lines changed: 410 additions & 0 deletions
Large diffs are not rendered by default.

faiss/CMakeLists.txt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,14 +583,28 @@ if(MKL_FOUND)
583583
target_link_libraries(faiss_avx512_spr PRIVATE ${MKL_LIBRARIES})
584584
target_link_libraries(faiss_sve PRIVATE ${MKL_LIBRARIES})
585585
else()
586-
find_package(BLAS REQUIRED)
586+
# Prefer threaded OpenBLAS (OpenMP or pthreads) over the serial default.
587+
# On RHEL/Fedora, the default libopenblas.so is the serial variant;
588+
# libopenblaso is OpenMP-threaded and libopenblasp is pthreads-threaded.
589+
find_library(BLAS_PREFER_THREADED NAMES openblaso openblasp)
590+
if(BLAS_PREFER_THREADED)
591+
message(STATUS "Using threaded OpenBLAS: ${BLAS_PREFER_THREADED}")
592+
set(BLAS_LIBRARIES "${BLAS_PREFER_THREADED}")
593+
# OpenBLAS bundles LAPACK routines, so no separate find_package(LAPACK)
594+
# is needed. Calling it would trigger FindBLAS internally, which
595+
# unconditionally resets BLAS_LIBRARIES to the serial libopenblas,
596+
# causing auditwheel to bundle both serial and threaded variants.
597+
set(LAPACK_LIBRARIES "${BLAS_PREFER_THREADED}")
598+
else()
599+
find_package(BLAS REQUIRED)
600+
find_package(LAPACK REQUIRED)
601+
endif()
587602
target_link_libraries(faiss PRIVATE ${BLAS_LIBRARIES})
588603
target_link_libraries(faiss_avx2 PRIVATE ${BLAS_LIBRARIES})
589604
target_link_libraries(faiss_avx512 PRIVATE ${BLAS_LIBRARIES})
590605
target_link_libraries(faiss_avx512_spr PRIVATE ${BLAS_LIBRARIES})
591606
target_link_libraries(faiss_sve PRIVATE ${BLAS_LIBRARIES})
592607

593-
find_package(LAPACK REQUIRED)
594608
target_link_libraries(faiss PRIVATE ${LAPACK_LIBRARIES})
595609
target_link_libraries(faiss_avx2 PRIVATE ${LAPACK_LIBRARIES})
596610
target_link_libraries(faiss_avx512 PRIVATE ${LAPACK_LIBRARIES})

faiss/python/CMakeLists.txt

Lines changed: 150 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -184,22 +184,23 @@ set_property(TARGET faiss_example_external_module PROPERTY SWIG_COMPILE_OPTIONS
184184
if(AIX)
185185
set_target_properties(swigfaiss PROPERTIES SUFFIX .a)
186186
set_target_properties(faiss_example_external_module PROPERTIES SUFFIX .a)
187-
elseif(NOT WIN32)
188-
# NOTE: Python does not recognize the dylib extension.
189-
set_target_properties(swigfaiss PROPERTIES SUFFIX .so)
190-
set_target_properties(swigfaiss_avx2 PROPERTIES SUFFIX .so)
191-
set_target_properties(swigfaiss_avx512 PROPERTIES SUFFIX .so)
192-
set_target_properties(swigfaiss_avx512_spr PROPERTIES SUFFIX .so)
193-
set_target_properties(swigfaiss_sve PROPERTIES SUFFIX .so)
194-
set_target_properties(faiss_example_external_module PROPERTIES SUFFIX .so)
195-
else()
187+
elseif(WIN32)
196188
# we need bigobj for the swig wrapper
197189
target_compile_options(swigfaiss PRIVATE /bigobj)
198190
target_compile_options(swigfaiss_avx2 PRIVATE /bigobj)
199191
target_compile_options(swigfaiss_avx512 PRIVATE /bigobj)
200192
target_compile_options(swigfaiss_avx512_spr PRIVATE /bigobj)
201193
target_compile_options(swigfaiss_sve PRIVATE /bigobj)
202194
target_compile_options(faiss_example_external_module PRIVATE /bigobj)
195+
elseif(NOT SKBUILD_SABI_VERSION)
196+
# NOTE: Python does not recognize the dylib extension.
197+
# When building abi3, the suffix is already set to .abi3.so above.
198+
set_target_properties(swigfaiss PROPERTIES SUFFIX .so)
199+
set_target_properties(swigfaiss_avx2 PROPERTIES SUFFIX .so)
200+
set_target_properties(swigfaiss_avx512 PROPERTIES SUFFIX .so)
201+
set_target_properties(swigfaiss_avx512_spr PROPERTIES SUFFIX .so)
202+
set_target_properties(swigfaiss_sve PROPERTIES SUFFIX .so)
203+
set_target_properties(faiss_example_external_module PROPERTIES SUFFIX .so)
203204
endif()
204205

205206
if(FAISS_ENABLE_SVS)
@@ -239,43 +240,80 @@ endif()
239240

240241
find_package(OpenMP REQUIRED)
241242

243+
# When scikit-build-core requests abi3 (Stable ABI), it passes
244+
# SKBUILD_SABI_VERSION. Use Development.SABIModule to link against the
245+
# stable Python ABI instead of the version-specific one.
246+
if(SKBUILD_SABI_VERSION)
247+
find_package(Python REQUIRED
248+
COMPONENTS Development.SABIModule NumPy
249+
)
250+
else()
251+
find_package(Python REQUIRED
252+
COMPONENTS Development.Module NumPy
253+
)
254+
endif()
255+
256+
# Select the Python module target based on whether abi3 is requested.
257+
if(SKBUILD_SABI_VERSION)
258+
set(FAISS_PYTHON_MODULE_TARGET Python::SABIModule)
259+
else()
260+
set(FAISS_PYTHON_MODULE_TARGET Python::Module)
261+
endif()
262+
263+
# When building for abi3, define Py_LIMITED_API and set the .abi3.so suffix
264+
# so the wheel is tagged as abi3-compatible.
265+
if(SKBUILD_SABI_VERSION)
266+
# 0x030A0000 = Python 3.10
267+
set(FAISS_PY_LIMITED_API "0x030A0000")
268+
foreach(tgt swigfaiss swigfaiss_avx2 swigfaiss_avx512
269+
swigfaiss_avx512_spr swigfaiss_sve)
270+
if(TARGET ${tgt})
271+
target_compile_definitions(${tgt}
272+
PRIVATE Py_LIMITED_API=${FAISS_PY_LIMITED_API})
273+
if(NOT WIN32)
274+
set_target_properties(${tgt} PROPERTIES SUFFIX ".abi3.so")
275+
endif()
276+
endif()
277+
endforeach()
278+
endif()
279+
242280
target_link_libraries(swigfaiss PRIVATE
243281
faiss
244-
Python::Module
282+
${FAISS_PYTHON_MODULE_TARGET}
245283
Python::NumPy
246284
OpenMP::OpenMP_CXX
247285
)
248286

249287
target_link_libraries(swigfaiss_avx2 PRIVATE
250288
faiss_avx2
251-
Python::Module
289+
${FAISS_PYTHON_MODULE_TARGET}
252290
Python::NumPy
253291
OpenMP::OpenMP_CXX
254292
)
255293

256294
target_link_libraries(swigfaiss_avx512 PRIVATE
257295
faiss_avx512
258-
Python::Module
296+
${FAISS_PYTHON_MODULE_TARGET}
259297
Python::NumPy
260298
OpenMP::OpenMP_CXX
261299
)
262300

263301
target_link_libraries(swigfaiss_avx512_spr PRIVATE
264302
faiss_avx512_spr
265-
Python::Module
303+
${FAISS_PYTHON_MODULE_TARGET}
266304
Python::NumPy
267305
OpenMP::OpenMP_CXX
268306
)
269307

270308
target_link_libraries(swigfaiss_sve PRIVATE
271309
faiss_sve
272-
Python::Module
310+
${FAISS_PYTHON_MODULE_TARGET}
273311
Python::NumPy
274312
OpenMP::OpenMP_CXX
275313
)
276314

277315
target_link_libraries(faiss_example_external_module PRIVATE
278-
Python::Module
316+
${FAISS_PYTHON_MODULE_TARGET}
279317
Python::NumPy
280318
OpenMP::OpenMP_CXX
281319
swigfaiss
@@ -291,16 +329,16 @@ target_include_directories(swigfaiss_avx512_spr PRIVATE ${PROJECT_SOURCE_DIR}/..
291329
target_include_directories(swigfaiss_sve PRIVATE ${PROJECT_SOURCE_DIR}/../..)
292330
target_include_directories(faiss_example_external_module PRIVATE ${PROJECT_SOURCE_DIR}/../..)
293331

294-
find_package(Python REQUIRED
295-
COMPONENTS Development.Module NumPy
296-
)
297-
298-
add_library(faiss_python_callbacks EXCLUDE_FROM_ALL
332+
add_library(faiss_python_callbacks STATIC EXCLUDE_FROM_ALL
299333
python_callbacks.cpp
300334
)
301335
set_property(TARGET faiss_python_callbacks
302336
PROPERTY POSITION_INDEPENDENT_CODE ON
303337
)
338+
if(SKBUILD_SABI_VERSION)
339+
target_compile_definitions(faiss_python_callbacks
340+
PRIVATE Py_LIMITED_API=${FAISS_PY_LIMITED_API})
341+
endif()
304342

305343
if ("${CMAKE_SYSTEM_NAME}" MATCHES "AIX")
306344
target_link_libraries(faiss_python_callbacks PRIVATE faiss)
@@ -328,3 +366,95 @@ configure_file(array_conversions.py array_conversions.py COPYONLY)
328366

329367
# file(GLOB files "${PROJECT_SOURCE_DIR}/../../contrib/*.py")
330368
file(COPY ${PROJECT_SOURCE_DIR}/../../contrib DESTINATION .)
369+
370+
# Install targets for scikit-build-core / pip wheel packaging.
371+
372+
# Set RPATH so the SWIG modules find libfaiss in the same directory.
373+
# auditwheel/delocate follow RPATH to locate libraries for bundling.
374+
foreach(tgt swigfaiss swigfaiss_avx2 swigfaiss_avx512
375+
swigfaiss_avx512_spr swigfaiss_sve)
376+
if(TARGET ${tgt})
377+
if(APPLE)
378+
set_target_properties(${tgt} PROPERTIES INSTALL_RPATH "@loader_path")
379+
else()
380+
set_target_properties(${tgt} PROPERTIES INSTALL_RPATH "$ORIGIN")
381+
endif()
382+
endif()
383+
endforeach()
384+
385+
# Install the SWIG extension modules (.so/.pyd).
386+
# In DD mode, swigfaiss_avx2/avx512/avx512_spr/sve are also built and
387+
# loader.py selects the best one at runtime based on CPU capabilities.
388+
foreach(tgt swigfaiss swigfaiss_avx2 swigfaiss_avx512
389+
swigfaiss_avx512_spr swigfaiss_sve)
390+
if(TARGET ${tgt})
391+
get_target_property(_excluded ${tgt} EXCLUDE_FROM_ALL)
392+
if(NOT _excluded)
393+
install(TARGETS ${tgt}
394+
LIBRARY DESTINATION faiss
395+
RUNTIME DESTINATION faiss
396+
)
397+
endif()
398+
endif()
399+
endforeach()
400+
401+
# Install the core faiss shared libraries into the Python package directory
402+
# so that auditwheel/delocate can find and bundle them with the SWIG modules.
403+
# Guard: when Python bindings are built standalone (e.g. conda), the faiss
404+
# target is an installed import library, not a build target.
405+
foreach(tgt faiss faiss_avx2 faiss_avx512 faiss_avx512_spr faiss_sve)
406+
if(TARGET ${tgt})
407+
get_target_property(_imported ${tgt} IMPORTED)
408+
get_target_property(_excluded ${tgt} EXCLUDE_FROM_ALL)
409+
if(NOT _imported AND NOT _excluded)
410+
install(TARGETS ${tgt}
411+
LIBRARY DESTINATION faiss
412+
RUNTIME DESTINATION faiss
413+
)
414+
endif()
415+
endif()
416+
endforeach()
417+
418+
# Install SWIG-generated Python wrappers.
419+
# In DD mode, each variant has its own .py wrapper that loader.py imports.
420+
foreach(mod swigfaiss swigfaiss_avx2 swigfaiss_avx512
421+
swigfaiss_avx512_spr swigfaiss_sve)
422+
if(EXISTS "${CMAKE_CURRENT_BINARY_DIR}/${mod}.py" OR TARGET ${mod})
423+
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${mod}.py"
424+
DESTINATION faiss
425+
OPTIONAL
426+
)
427+
endif()
428+
endforeach()
429+
430+
# Install hand-written Python source files
431+
install(FILES
432+
"${CMAKE_CURRENT_SOURCE_DIR}/__init__.py"
433+
"${CMAKE_CURRENT_SOURCE_DIR}/loader.py"
434+
"${CMAKE_CURRENT_SOURCE_DIR}/class_wrappers.py"
435+
"${CMAKE_CURRENT_SOURCE_DIR}/extra_wrappers.py"
436+
"${CMAKE_CURRENT_SOURCE_DIR}/gpu_wrappers.py"
437+
"${CMAKE_CURRENT_SOURCE_DIR}/array_conversions.py"
438+
DESTINATION faiss
439+
)
440+
441+
# Install type stubs if present
442+
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/__init__.pyi")
443+
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/__init__.pyi" DESTINATION faiss)
444+
endif()
445+
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/py.typed")
446+
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/py.typed" DESTINATION faiss)
447+
endif()
448+
449+
# Install contrib subpackage (excluding Meta-internal *_fb.py files)
450+
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../../contrib/"
451+
DESTINATION faiss/contrib
452+
FILES_MATCHING PATTERN "*.py"
453+
PATTERN "*_fb.py" EXCLUDE
454+
)
455+
456+
# Install contrib/torch subpackage data files
457+
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../../contrib/torch/"
458+
DESTINATION faiss/contrib/torch
459+
FILES_MATCHING PATTERN "*.md"
460+
)

0 commit comments

Comments
 (0)