Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/scripts/ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ def classify(paths):
or path == "tests/CMakeLists.txt"
):
selected.add("cpp")
elif path.startswith("tests/python/"):
elif path.startswith(("tests/python/", "sample/python/")):
selected.update(("python_quality", "python_build", "wheels"))
elif path.startswith(("python/", "sample/python/", ".github/scripts/")):
elif path.startswith(("python/", ".github/scripts/")):
selected.add("python_quality")
elif path == ".github/workflows/test.yaml":
selected.update(("cpp", "shell"))
Expand Down
8 changes: 6 additions & 2 deletions .github/scripts/test_ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ def test_language_specific_changes(self):
"python_build",
"wheels",
},
"sample/python/ivf_rabitq_indexing.py": {"python_quality"},
"sample/python/ivf_rabitq_indexing.py": {
"python_quality",
"python_build",
"wheels",
},
".github/scripts/prepare_release.py": {"python_quality"},
"scripts/check-python.sh": {"shell", "python_quality"},
"scripts/check-tidy.sh": {"shell", "cpp"},
Expand All @@ -68,7 +72,7 @@ def test_mixed_changes_and_renamed_source(self):
self.assertIn("cpp", classify(["docs/new.md", "src/deleted.cpp"]))
self.assertEqual(
classify(["sample/python/tool.py", "tests/unit/example.cpp"]),
{"cpp", "python_quality"},
{"cpp", "python_quality", "python_build", "wheels"},
)

def test_unknown_inputs_and_filter_edits_run_everything(self):
Expand Down
35 changes: 32 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,38 @@ jobs:
path: wheelhouse/*.whl
if-no-files-found: error

macos-wheels:
name: macOS arm64 wheels (CPython 3.11–3.14)
needs: [prepare, changes]
if: >-
!cancelled() && needs.prepare.result == 'success' &&
needs.prepare.outputs.build == 'true' &&
(needs.changes.result != 'success' || needs.changes.outputs.wheels == 'true')
runs-on: macos-14
timeout-minutes: 90
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.sha }}
- uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Verify native Apple Silicon runner
run: test "$(uname -m)" = arm64
# All four ABIs are built, repaired, installed and tested on PRs as well.
- name: Build and test wheels with bundled OpenMP
uses: pypa/cibuildwheel@v4.2.0
with:
output-dir: wheelhouse
- uses: actions/upload-artifact@v7
with:
name: macos-arm64-wheels
path: wheelhouse/*.whl
if-no-files-found: error

release-tag:
name: Create version tag
needs: [prepare, linux-wheels, windows-wheels]
needs: [prepare, linux-wheels, windows-wheels, macos-wheels]
if: >-
(github.event_name == 'push' || github.event_name == 'workflow_run') &&
needs.prepare.outputs.publish == 'true'
Expand Down Expand Up @@ -161,7 +190,7 @@ jobs:
if: >-
(github.event_name == 'push' || github.event_name == 'workflow_run') &&
needs.prepare.outputs.publish == 'true'
needs: [prepare, linux-wheels, windows-wheels, release-tag]
needs: [prepare, linux-wheels, windows-wheels, macos-wheels, release-tag]
runs-on: ubuntu-latest
environment:
name: pypi
Expand All @@ -188,7 +217,7 @@ jobs:
if: >-
(github.event_name == 'push' || github.event_name == 'workflow_run') &&
needs.prepare.outputs.publish == 'true'
needs: [prepare, linux-wheels, windows-wheels, pypi-publish]
needs: [prepare, linux-wheels, windows-wheels, macos-wheels, pypi-publish]
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,36 @@ jobs:
cmake --build build-consumer
- name: Test downstream consumer
run: ctest --test-dir build-consumer --output-on-failure

macos-arm64:
name: Native Apple Silicon C++
needs: changes
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.cpp == 'true') }}
runs-on: macos-14
timeout-minutes: 45
env:
OMP_NUM_THREADS: "2"
steps:
- uses: actions/checkout@v7
- name: Install build tools and existing OpenMP dependency
run: brew install cmake ninja libomp
- name: Configure portable ARM64 source groups
run: |
test "$(uname -m)" = arm64
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DRABITQ_BUILD_TESTS=ON -DRABITQ_BUILD_SAMPLES=OFF \
-DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF \
-DOpenMP_ROOT="$(brew --prefix libomp)"
! grep -E -- '-mavx|_avx[25].*\.cpp' build/compile_commands.json
- name: Build and run C++ reference and index tests
run: |
cmake --build build --parallel 3
ctest --test-dir build --output-on-failure
- name: Test installed CMake package
run: |
cmake --install build --prefix "$RUNNER_TEMP/rabitq-install"
cmake -S tests/consumer -B build-consumer \
-DCMAKE_PREFIX_PATH="$RUNNER_TEMP/rabitq-install" \
-DOpenMP_ROOT="$(brew --prefix libomp)"
cmake --build build-consumer --parallel 3
ctest --test-dir build-consumer --output-on-failure
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ where present. Keep this file current when the paths, commands, or contracts bel

RaBitQ-Library is a C++17 library with Python bindings (`rabitqlib`) for compact vector
quantization and approximate nearest-neighbor search. It provides one-bit and multi-bit RaBitQ
encoding and IVF, HNSW, and SymphonyQG indexes. It targets x86-64 with runtime AVX2/AVX-512 dispatch.
encoding and IVF, HNSW, and SymphonyQG indexes. It targets x86-64 with runtime AVX2/AVX-512
dispatch and macOS ARM64 with NEON and portable scalar kernels. Linux AArch64 requires separate platform validation.
RaBitQ rotates vectors and quantizes residuals relative to centroids; compact codes and correction
factors estimate L2 distance or inner product.

Expand All @@ -19,7 +20,7 @@ factors estimate L2 distance or inner product.
| `include/rabitqlib/index/{query,estimator}.hpp` | Query state and distance estimation |
| `include/rabitqlib/simd/`, `src/simd/` | Kernel declarations, implementations, and dispatch |
| `src/index/` | Compiled SymphonyQG implementation, HNSW search kernels, and IVF candidate insertion |
| `src/utils/cpu_features.cpp` | Generic x86 feature detection |
| `src/utils/cpu_features.cpp` | Generic CPU feature detection |
| `include/rabitqlib/utils/` | Rotation, allocation, buffers, I/O, and helpers |
| `python_bindings/` | pybind11 extension and index wrappers |
| `sample/cpp/`, `sample/python/` | Usage examples |
Expand Down Expand Up @@ -110,13 +111,16 @@ Recommended:
- Public/generic code calls centralized dispatch entry points. Keep ISA-specific translation units
and their flags in `CMakeLists.txt` synchronized with feature predicates in
`src/simd/dispatch.cpp` and detection in `src/utils/cpu_features.cpp`, including HNSW source groups.
ARM64 builds must exclude all x86 ISA source groups; ARM kernels use portable AArch64
NEON intrinsics.
- Use the shared resolver in `src/simd/dispatch.cpp` for cached selection, including HNSW.
Keep calculations in backend source files; see the dispatch coverage table in
[DEVELOPMENT.md](DEVELOPMENT.md#dispatch-conventions-and-coverage).
- Dispatch resolves function pointers during static initialization. Detection must use safe generic
code; never execute a high-ISA kernel to find out whether the CPU supports it.
- Semantic kernel changes must cover every implementation and a backend-independent reference
test, including dimensions around vector-width/packing boundaries and supported bit widths.
Keep portable reference tests enabled on ARM; guard only explicit x86 backend calls.
- Native tuning can affect generic code independently of dispatch. Keep native optimization off
for portable binaries and wheels; runtime dispatch alone does not make a native build portable.
A portable build tested on the current CPU does not establish AVX2 backend coverage. Report
Expand Down
39 changes: 33 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ option(
ON
)

include(cmake/rabitqOpenMP.cmake)
find_package(OpenMP REQUIRED)

add_library(rabitq_compile_options INTERFACE)
Expand Down Expand Up @@ -104,14 +105,39 @@ set(RABITQ_HNSW_AVX512_POPCNT_SOURCES
src/index/hnsw_search_avx512_popcnt.cpp
)

# Select source groups from the target architecture, never the build host's ISA.
set(RABITQ_TARGET_PROCESSOR "${CMAKE_SYSTEM_PROCESSOR}")
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
set(RABITQ_TARGET_PROCESSOR "${CMAKE_OSX_ARCHITECTURES}")
endif()
string(TOLOWER "${RABITQ_TARGET_PROCESSOR}" RABITQ_TARGET_PROCESSOR)
if(RABITQ_TARGET_PROCESSOR MATCHES "^(x86_64|amd64)$")
set(RABITQ_ARCH_SOURCES
${RABITQ_AVX2_SOURCES}
${RABITQ_AVX512_SOURCES}
${RABITQ_AVX512_POPCNT_SOURCES}
${RABITQ_HNSW_AVX2_SOURCES}
${RABITQ_HNSW_AVX512_CORE_SOURCES}
${RABITQ_HNSW_AVX512_POPCNT_SOURCES}
)
elseif(RABITQ_TARGET_PROCESSOR MATCHES "^(arm64|aarch64)$")
set(RABITQ_ARCH_SOURCES
src/simd/space_neon.cpp
src/simd/fastscan_neon.cpp
src/simd/space_packed_neon.cpp
src/simd/rotator_neon.cpp
src/simd/query_neon.cpp
src/index/hnsw_search_neon.cpp
)
else()
message(FATAL_ERROR "Supported targets are x86-64 and ARM64 (single architecture)")
endif()

add_library(rabitq_core STATIC
${RABITQ_COMMON_SOURCES}
${RABITQ_AVX2_SOURCES}
${RABITQ_AVX512_SOURCES}
${RABITQ_AVX512_POPCNT_SOURCES}
${RABITQ_HNSW_AVX2_SOURCES}
${RABITQ_HNSW_AVX512_CORE_SOURCES}
${RABITQ_HNSW_AVX512_POPCNT_SOURCES}
${RABITQ_ARCH_SOURCES}
src/simd/portable_generic.cpp
src/index/hnsw_search_generic.cpp
)

target_include_directories(rabitq_core PUBLIC
Expand Down Expand Up @@ -194,6 +220,7 @@ install(EXPORT rabitqlibTargets
DESTINATION "${rabitqlib_cmake_dir}"
)
install(FILES
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/rabitqOpenMP.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/rabitqlibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/rabitqlibConfigVersion.cmake"
DESTINATION "${rabitqlib_cmake_dir}"
Expand Down
30 changes: 21 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ Run any examples you add or change, including examples inside Markdown.

## Setup for code changes

Source builds need an x86-64 CPU supported by RaBitQ's AVX2 or AVX-512 dispatch,
CMake 3.20 or newer for the commands below, and a GCC- or Clang-compatible C++17 compiler with OpenMP.
The current build does not support MSVC or AArch64 (including Apple Silicon).
Source builds support x86-64 with AVX2/FMA (AVX-512 optional) and macOS ARM64
with NEON. They need CMake 3.20 or newer, a C++17 compiler, and OpenMP.
Windows x86-64 uses Visual Studio 2026; Linux AArch64 needs separate platform
validation. See the [platform-specific build instructions](tests/README.md#quick-start)
for Windows and [Apple Silicon](tests/README.md#macos-arm64).
Documentation-only contributions do not need this hardware or compiler setup.

On Ubuntu or Debian:
Expand Down Expand Up @@ -75,13 +77,23 @@ python -m pip install ".[test]"
python -m pytest tests/python/test_ivf.py::test_search_output_shape -q
```

On Apple Silicon, pass the OpenMP prefix and disable native tuning when building
the extension in the same environment:

```bash
python -m pip install ".[test]" \
-Ccmake.define.OpenMP_ROOT="$(brew --prefix libomp)" \
-Ccmake.define.RABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF
python -m pytest tests/python/test_ivf.py::test_search_output_shape -q
```

This builds the C++ extension, so the source-build prerequisites above apply.
Re-run the install command after changing package sources, bindings, or C++ code;
this is not an editable install. Tests should use the newly installed package.

### C++ changes

Configure, build, and run the tests:
On Linux, configure, build, and run the tests:

```bash
cmake -S . -B build -DRABITQ_BUILD_TESTS=ON \
Expand All @@ -93,8 +105,8 @@ ctest --test-dir build --output-on-failure

The first configuration needs Git and network access to download GoogleTest.
Increase `--parallel 2` if your machine has enough memory for more compiler jobs.
Native CPU tuning is disabled here so the build can run on other supported
x86-64 machines. See [the test guide](tests/README.md) for test layout and options.
Native CPU tuning is disabled here to keep generic code portable. On Windows
or Apple Silicon, use the matching commands in [the test guide](tests/README.md#quick-start).

## Before opening a pull request

Expand Down Expand Up @@ -137,9 +149,9 @@ for details.

| Problem | What to check |
| --- | --- |
| CMake cannot find OpenMP or a compiler | Install the source-build prerequisites above. Include the compiler version and CMake error when asking for help. |
| Unsupported CPU or architecture | Code tests require a supported x86-64 CPU; disabling native tuning does not add AArch64 or generic-CPU quantized search support. Report your CPU model in the issue or PR. |
| Tests do not reflect your Python or C++ edits | Re-run `python -m pip install ".[test]"` in the active environment, then check the import paths below. Re-run the regression test that exercises your change. |
| CMake cannot find OpenMP or a compiler | Check the [platform-specific prerequisites](tests/README.md#prerequisites). On Apple Silicon, install `libomp` and pass its prefix as `OpenMP_ROOT`. Include the compiler version and CMake error when asking for help. |
| Unsupported CPU or architecture | Use x86-64 with AVX2/FMA or macOS ARM64 with NEON. Linux AArch64 still needs platform validation; disabling native tuning alone does not establish support. Report your CPU model in the issue or PR. |
| Tests do not reflect your Python or C++ edits | Re-run the platform-specific install command above in the active environment, then check the import paths below. Re-run the regression test that exercises your change. |
| Formatting or analysis reports the wrong tool version | Use the pinned versions and executable overrides in the [development reference](DEVELOPMENT.md). |

Check which installed package and extension Python imports:
Expand Down
32 changes: 22 additions & 10 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ Verify formatting without changing files:

The scripts intentionally exclude vendored code in `include/rabitqlib/third/`.
To use a nonstandard executable name, set `CLANG_FORMAT`; it must still identify
itself as clang-format 15.
itself as clang-format 15. On macOS, use Bash 4+ on PATH for the shell wrappers.
Apple SDKs newer than clang-tidy 15 may require a matching newer analyzer; set
`RUN_CLANG_TIDY` to its runner and report that version separately from the Linux
clang-tidy 15 baseline.

clangd embeds its own formatter, so use clangd 15 in editors such as VS Code
if format-on-save must exactly match CI. If another clangd version is required,
Expand Down Expand Up @@ -222,7 +225,7 @@ This matches CI's script set. Also pass any changed shell scripts outside
- Preserve existing public headers, aliases, and index formats unless a change
is explicitly documented as breaking.
- Add backend-independent tests when introducing or changing SIMD kernels.
- Keep scalar, AVX2, and AVX-512 implementations behaviorally equivalent.
- Keep scalar, NEON, AVX2, and AVX-512 implementations behaviorally equivalent.
- Benchmark allocations or algorithm changes in search and quantization hot
paths, and substantiate performance claims with benchmarks. Run correctness
tests first and include commands, dataset, CPU, compiler, ISA, thread count,
Expand Down Expand Up @@ -258,11 +261,18 @@ must happen in generic code first.
#### Dispatch conventions and coverage

- Keep backend-neutral declarations in `include/rabitqlib/simd/*_dispatch.hpp` and
implementations in `src/simd/*_{generic,avx2,avx512}.cpp`. HNSW keeps its existing
implementations in `src/simd/*_{generic,neon,avx2,avx512}.cpp`. HNSW keeps its existing
`src/index/` implementations and compatibility namespaces.
- Select a cached function pointer through `resolve_kernel` in `src/simd/dispatch.cpp`.
The order is AVX-512, AVX2/FMA, then the existing generic implementation or a descriptive
unsupported-operation exception. Public wrappers do not repeat feature checks.
The x86 order is AVX-512, AVX2/FMA, then the portable generic implementation. ARM64
selects NEON for raw float distances, FastScan, packed-code dot products (1–8 extra
bits), masked sums, binary warmup, HNSW search, and FHT/Kac rotation including
sign flipping, query-bit transposition, uint8/uint16 scalar quantization, and
FastScan LUT construction. Extra-code packing retains a portable scalar implementation.
Public wrappers do not repeat feature checks.
Standard FastScan accumulation requires AVX2/FMA, AVX-512, or NEON and throws
`std::runtime_error` when no supported SIMD backend is available. Its scalar kernel
is retained as a correctness reference only, never as a runtime fallback.
- Preserve stricter predicates: population-count kernels need AVX512_VPOPCNTDQ, and HNSW's
AVX-512 core variant also needs AVX2 for its warmup implementation. Do not infer support
from a backend name or from `__AVX*__` macros in a public header.
Expand Down Expand Up @@ -294,10 +304,10 @@ covers its arithmetic kernels, not every scalar loop in construction and search.

| Area | Dispatch coverage / deliberate boundary |
| --- | --- |
| Raw float distances, norms, packed-code products | Runtime AVX2/AVX-512 selection; generic raw-float fallback |
| Raw float distances, norms, packed-code products | Runtime AVX2/AVX-512/NEON selection; portable generic fallback |
| Quantizer rescale search | SIMD bounded search; the certified scalar event sweep remains the fallback |
| Integer scalar quantization, extra-code packing, transpose, sign masks | Existing runtime selection; byte layouts and rounding rules are unchanged |
| Standard and high-accuracy FastScan | Runtime selection; generic LUT construction stays separate from selection |
| Standard and high-accuracy FastScan | Runtime selection, including NEON float LUT construction; HACC LUT byte conversion remains generic on ARM |
| IVF and float SymphonyQG batch correction | Complete estimator runs in the selected backend; non-float template paths remain generic |
| FHT/Kac rotation | Complete rotation and scaling run in selected ISA translation units; shared AVX intrinsics preserve the FFHT butterfly order |
| Float matrix rotation and PiPNN construction | Matrix products, row norms, and lower-triangle pairwise distances use isolated matrix backends |
Expand All @@ -306,9 +316,11 @@ covers its arithmetic kernels, not every scalar loop in construction and search.
| Graph scheduling, candidate queues, I/O, allocation, random initialization | Generic control code; IVF one-bit candidate insertion stays in a small compiled function to avoid inlining-induced register spills; thread scheduling and seeds remain caller-owned |
| Example KMeans training | Uses external FAISS, whose build and dispatch are independent of this package |

Portable wheels continue to disable `RABITQ_ENABLE_NATIVE_OPTIMIZATION`. Adding an optimized
backend does not add support for generic-CPU quantized search or AArch64; those require
complete implementations and separate compatibility validation.
Portable wheels disable `RABITQ_ENABLE_NATIVE_OPTIMIZATION`. ARM64 builds exclude all
x86 source groups and use standard AArch64 NEON intrinsics, without Apple-only APIs.
macOS ARM64 has native C++ and installed-wheel CI. Linux ARM64 still requires separate
platform verification. The reference suites also exercise portable scalar kernels on x86, including
byte-for-byte packing parity with the selected x86 backend.

### Change quantization or packing

Expand Down
Loading
Loading