diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc4fafec..0f3c3e99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,6 +100,52 @@ jobs: BUILD_RESULT: ${{ needs.linux-wheel-build.result }} run: test "$BUILD_RESULT" = success + linux-arm64-wheel-build: + name: Linux ARM64 ${{ matrix.abi }} wheel + 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: ubuntu-24.04-arm + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + abi: ${{ fromJSON(github.event_name == 'pull_request' && '["cp311","cp314"]' || '["cp311","cp312","cp313","cp314"]') }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.prepare.outputs.sha }} + - uses: actions/setup-python@v7 + with: + python-version: "3.14" + - name: Verify native ARM64 runner + run: test "$(uname -m)" = aarch64 + - name: Build and test repaired wheel + uses: pypa/cibuildwheel@v4.2.0 + env: + CIBW_BUILD: ${{ matrix.abi }}-manylinux_aarch64 + with: + output-dir: wheelhouse + - name: Store wheel artifact + uses: actions/upload-artifact@v7 + with: + name: linux-arm64-${{ matrix.abi }}-wheels + path: wheelhouse/*.whl + if-no-files-found: error + + linux-arm64-wheels: + name: Linux ARM64 wheels (CPython 3.11–3.14) + needs: linux-arm64-wheel-build + if: ${{ always() && !cancelled() && needs.linux-arm64-wheel-build.result != 'skipped' }} + runs-on: ubuntu-latest + steps: + - name: Require all selected wheels + env: + BUILD_RESULT: ${{ needs.linux-arm64-wheel-build.result }} + run: test "$BUILD_RESULT" = success + windows-wheel-build: name: Windows x86-64 ${{ matrix.abi }} wheel needs: [prepare, changes] @@ -193,7 +239,7 @@ jobs: release-tag: name: Create version tag - needs: [prepare, linux-wheels, windows-wheels, macos-wheels] + needs: [prepare, linux-wheels, linux-arm64-wheels, windows-wheels, macos-wheels] if: >- (github.event_name == 'push' || github.event_name == 'workflow_run') && needs.prepare.outputs.publish == 'true' @@ -222,7 +268,7 @@ jobs: if: >- (github.event_name == 'push' || github.event_name == 'workflow_run') && needs.prepare.outputs.publish == 'true' - needs: [prepare, linux-wheels, windows-wheels, macos-wheels, release-tag] + needs: [prepare, linux-wheels, linux-arm64-wheels, windows-wheels, macos-wheels, release-tag] runs-on: ubuntu-latest environment: name: pypi @@ -249,7 +295,7 @@ jobs: if: >- (github.event_name == 'push' || github.event_name == 'workflow_run') && needs.prepare.outputs.publish == 'true' - needs: [prepare, linux-wheels, windows-wheels, macos-wheels, pypi-publish] + needs: [prepare, linux-wheels, linux-arm64-wheels, windows-wheels, macos-wheels, pypi-publish] runs-on: ubuntu-latest permissions: contents: write diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f7f043cb..a77b368f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -182,6 +182,37 @@ jobs: - name: Test downstream consumer run: ctest --test-dir build-consumer --output-on-failure + linux-arm64: + name: Native Linux ARM64 C++ + needs: changes + if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.cpp == 'true') }} + runs-on: ubuntu-24.04-arm + timeout-minutes: 45 + env: + OMP_NUM_THREADS: "2" + steps: + - uses: actions/checkout@v7 + - name: Install build tools + run: sudo apt-get update -qq && sudo apt-get install -y cmake ninja-build + - name: Configure portable ARM64 build + run: | + test "$(uname -m)" = aarch64 + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DRABITQ_BUILD_TESTS=ON -DRABITQ_BUILD_SAMPLES=OFF \ + -DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF + ! grep -E -- '-mavx|_avx[25].*\.cpp' build/compile_commands.json + - name: Build and run C++ tests + run: | + cmake --build build --parallel 2 + 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 -G Ninja \ + -DCMAKE_PREFIX_PATH="$RUNNER_TEMP/rabitq-install" + cmake --build build-consumer --parallel 2 + ctest --test-dir build-consumer --output-on-failure + macos-arm64: name: Native Apple Silicon C++ needs: changes diff --git a/AGENTS.md b/AGENTS.md index c055059e..22aef9fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,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 and macOS ARM64 with NEON and portable scalar kernels. Linux AArch64 requires separate platform validation. +dispatch and Linux/macOS ARM64 with NEON and portable scalar kernels. Native Linux AArch64 +C++ and installed-wheel CI validate that platform. RaBitQ rotates vectors and quantizes residuals relative to centroids; compact codes and correction factors estimate L2 distance or inner product. diff --git a/CMakeLists.txt b/CMakeLists.txt index 3664b3ae..d39a376e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(RaBitQLib VERSION 0.3.9 LANGUAGES CXX) +project(RaBitQLib VERSION 0.3.10 LANGUAGES CXX) include(CMakePackageConfigHelpers) include(GNUInstallDirs) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 405692d1..efe9f676 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -317,9 +317,9 @@ covers its arithmetic kernels, not every scalar loop in construction and search. 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. +macOS and Linux ARM64 have native C++ and installed-wheel CI. 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 diff --git a/README.md b/README.md index 7d0fcfe1..5f2f6c1e 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ ## News +- **September 2026 — Linux ARM64 support:** Native AArch64 C++ tests and + CPython 3.11–3.14 wheel builds cover NEON kernels and all three indexes. + See the [platform requirements](docs/docs/quick_start.md#requirements). + - **September 2026 — Apple Silicon support:** macOS ARM64 joins Linux and Windows x86-64, with NEON kernels and CPython 3.11–3.14 wheel builds for macOS 14+. See the [platform requirements](docs/docs/quick_start.md#requirements). @@ -58,10 +62,12 @@ python -m pip install --upgrade rabitqlib ``` -Wheels: CPython 3.11–3.14 on Linux and Windows x86-64, and macOS 14+ ARM64 -(Apple Silicon). x86-64 uses AVX2/FMA with optional AVX-512 acceleration; -ARM64 uses NEON and portable scalar kernels. macOS wheels bundle OpenMP; -Intel Mac and universal2 wheels are not provided. +Wheels: CPython 3.11–3.14 on Linux x86-64 and ARM64, Windows x86-64, and +macOS 14+ ARM64 (Apple Silicon). x86-64 uses AVX2/FMA with optional AVX-512 +acceleration; ARM64 uses NEON and portable scalar kernels. Linux ARM64 and +macOS wheels bundle OpenMP. Linux ARM64 wheels use a `manylinux_2_28` build +image; check each wheel's tags for its glibc compatibility. Intel Mac and +universal2 wheels are not provided. ## Python quick start @@ -237,7 +243,7 @@ and HNSW implementations, with links to the source code. - CMake 3.20 or newer - a C++17 compiler with OpenMP support -- an x86-64 CPU with AVX2 and FMA, or an Apple Silicon Mac +- an x86-64 CPU with AVX2 and FMA, or an ARM64 CPU on Linux or macOS Clone and build the library and example programs: @@ -250,9 +256,8 @@ cmake --build build --parallel ``` For MSVC, follow the [Windows build instructions](tests/README.md#prerequisites). -For Apple Silicon source builds, see the [macOS ARM64 instructions](tests/README.md#macos-arm64). -The ARM kernel sources use portable AArch64 intrinsics, but macOS tests do not -establish Linux ARM64 support. +For ARM64 source builds, see the [Linux ARM64](tests/README.md#linux-arm64) +and [macOS ARM64](tests/README.md#macos-arm64) instructions. Local GCC/Clang builds enable `-march=native` by default; set `-DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF` for portable binaries, as release wheels do. See [CPU dispatch details](DEVELOPMENT.md#dispatch-conventions-and-coverage) diff --git a/docs/docs/index.md b/docs/docs/index.md index 7ecf087f..88ea6395 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -6,7 +6,8 @@ accurate vector quantization and approximate nearest-neighbor search. Build with the low-level quantizer or use complete IVF, HNSW, and SymphonyQG -indexes on Linux and Windows x86-64, or macOS ARM64 (Apple Silicon). +indexes on Linux x86-64 and ARM64, Windows x86-64, or macOS ARM64 +(Apple Silicon). [Get started](quick_start.md){ .md-button .md-button--primary } [View on GitHub](https://github.com/VectorDB-NTU/RaBitQ-Library){ .md-button } @@ -23,7 +24,7 @@ indexes on Linux and Windows x86-64, or macOS ARM64 (Apple Silicon).

Fast on modern CPUs

-

Use AVX2/AVX-512 kernels on x86-64 and NEON on Apple Silicon. IVF and +

Use AVX2/AVX-512 kernels on x86-64 and NEON on ARM64. IVF and SymphonyQG use FastScan for batched distance estimation.

diff --git a/docs/docs/quick_start.md b/docs/docs/quick_start.md index 02214a6a..4ee149aa 100644 --- a/docs/docs/quick_start.md +++ b/docs/docs/quick_start.md @@ -8,6 +8,7 @@ a C++17 API for both indexes and low-level quantization. | Platform | CPU baseline | Python wheel targets | | --- | --- | --- | | Linux x86-64 | AVX2 and FMA | CPython 3.11–3.14 | +| Linux ARM64 (AArch64) | NEON | CPython 3.11–3.14 | | Windows x86-64 | AVX2 and FMA | CPython 3.11–3.14 | | macOS 14+ ARM64 (Apple Silicon) | NEON | CPython 3.11–3.14 | @@ -16,8 +17,9 @@ Windows uses MSVC (Visual Studio 2026 with the Desktop development with C++ workload and CMake 4.2+ for its generator). Apple Silicon uses AppleClang and an external OpenMP runtime such as Homebrew `libomp`. -Linux AArch64 still requires separate platform validation. Intel Mac and -universal2 wheels are not provided. +Linux ARM64 source builds and repaired wheels run on native AArch64 CI. +The Linux ARM64 wheels use a `manylinux_2_28` build image; auditwheel assigns +the final glibc compatibility tags. Intel Mac and universal2 wheels are not provided.
CPU dispatch details @@ -42,7 +44,8 @@ python -m pip install rabitqlib ``` Wheels target the platforms above and require no compiler or CMake. -macOS ARM64 wheels bundle OpenMP, so wheel users do not need Homebrew. +Linux ARM64 and macOS ARM64 wheels bundle OpenMP, so wheel users do not +need a separate OpenMP installation. Release wheels disable native CPU tuning and select supported kernels at runtime. ### Build and search an IVF index @@ -146,7 +149,7 @@ CMake enables native CPU tuning by default where supported by the compiler. For portable binaries within a supported OS and architecture, configure with `-DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF`; runtime kernel selection remains active. See the [platform-specific build commands](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/tests/README.md#quick-start) -for Windows and Apple Silicon. +for Linux ARM64, Windows, and Apple Silicon. Example executables are written to `bin/`. Their source demonstrates complete indexing and querying workflows: diff --git a/pyproject.toml b/pyproject.toml index f5b20340..11566a15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "rabitqlib" -version = "0.3.9" +version = "0.3.10" description = "RaBitQ Python bindings for HNSW, IVF, and SymQG" readme = "README.md" requires-python = ">=3.11" @@ -41,12 +41,16 @@ cmake.args = [ ] [tool.cibuildwheel] -build = "cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64 cp314-manylinux_x86_64 cp311-win_amd64 cp312-win_amd64 cp313-win_amd64 cp314-win_amd64 cp311-macosx_arm64 cp312-macosx_arm64 cp313-macosx_arm64 cp314-macosx_arm64" +build = "cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64 cp314-manylinux_x86_64 cp311-manylinux_aarch64 cp312-manylinux_aarch64 cp313-manylinux_aarch64 cp314-manylinux_aarch64 cp311-win_amd64 cp312-win_amd64 cp313-win_amd64 cp314-win_amd64 cp311-macosx_arm64 cp312-macosx_arm64 cp313-macosx_arm64 cp314-macosx_arm64" build-verbosity = 1 environment = { CMAKE_ARGS = "-DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF" } test-requires = ["pytest", "faiss-cpu"] test-command = "pytest {project}/tests/python --tb=short -q" +[tool.cibuildwheel.linux] +manylinux-aarch64-image = "manylinux_2_28" +test-environment = { RABITQ_TEST_WHEEL = "1", OMP_NUM_THREADS = "2" } + [tool.cibuildwheel.macos] archs = ["arm64"] before-all = "brew install libomp" diff --git a/tests/README.md b/tests/README.md index 70e4b0c0..2e54d5d0 100644 --- a/tests/README.md +++ b/tests/README.md @@ -8,7 +8,7 @@ binding tests for RaBitQ Library. - CMake 3.20 or newer for the `ctest --test-dir` commands below (4.2 or newer for the Visual Studio 2026 generator) - A C++17 compiler with OpenMP support (GCC, Clang, or Visual Studio 2026) -- An x86-64 CPU with AVX2 and FMA (AVX-512 optional), or an Apple Silicon Mac +- An x86-64 CPU with AVX2 and FMA (AVX-512 optional), or an ARM64 CPU on Linux or macOS - Git and network access during the first configuration so CMake can download GoogleTest 1.14.0 @@ -45,6 +45,25 @@ cmake --build build --parallel ctest --test-dir build --output-on-failure ``` +#### Linux ARM64 + +Use a native AArch64 host with CMake, Ninja, a C++17 compiler, and OpenMP. +From the repository root: + +```bash +test "$(uname -m)" = aarch64 +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DRABITQ_BUILD_TESTS=ON -DRABITQ_BUILD_SAMPLES=OFF \ + -DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF +cmake --build build --parallel 2 +OMP_NUM_THREADS=2 ctest --test-dir build --output-on-failure +``` + +Native Linux ARM64 CI runs the C++ suite and installed CMake consumer. Its wheel +job builds and tests repaired CPython 3.11–3.14 wheels. Set +`RABITQ_TEST_WHEEL=1` after installing a repaired wheel locally to check the +AArch64 extension and bundled OpenMP runtime. AVX backend tests skip on ARM. + #### macOS ARM64 Use a native ARM64 project Python environment. Install CMake, Ninja and the existing @@ -62,8 +81,7 @@ OMP_NUM_THREADS=2 ctest --test-dir build --output-on-failure An environment providing `llvm-openmp` can use its prefix as `OpenMP_ROOT` instead. CMake excludes AVX source files on ARM64. Reference tests run through generic and -NEON kernels; explicit AVX backend tests are guarded or skipped. Linux AArch64 -needs its own build and execution validation. +NEON kernels; explicit AVX backend tests are guarded or skipped. For Python source builds, pass `-Ccmake.define.OpenMP_ROOT=` as well as the native-optimization setting below. Release CI uses native `macos-14` runners, diff --git a/tests/python/test_wheel_linux_arm64.py b/tests/python/test_wheel_linux_arm64.py new file mode 100644 index 00000000..e4656890 --- /dev/null +++ b/tests/python/test_wheel_linux_arm64.py @@ -0,0 +1,33 @@ +"""Check the installed, repaired Linux ARM64 wheel and its OpenMP runtime.""" + +import os +import platform +import subprocess +from pathlib import Path + +import pytest + + +@pytest.mark.skipif( + os.environ.get("RABITQ_TEST_WHEEL") != "1" + or platform.system() != "Linux" + or platform.machine() != "aarch64", + reason="Run with RABITQ_TEST_WHEEL=1 after installing a repaired Linux ARM64 wheel", +) +def test_native_aarch64_wheel_bundles_openmp(): + import rabitqlib._rabitqlib as extension + + path = Path(extension.__file__).resolve() + assert "site-packages" in path.parts + with path.open("rb") as binary: + header = binary.read(20) + assert header[:4] == b"\x7fELF" + assert int.from_bytes(header[18:20], "little") == 183 # EM_AARCH64 + + dependencies = subprocess.check_output(["ldd", str(path)], text=True) + openmp = [line for line in dependencies.splitlines() if "libgomp" in line] + assert len(openmp) == 1, dependencies + assert "=>" in openmp[0], dependencies + runtime = Path(openmp[0].split("=>", 1)[1].split("(", 1)[0].strip()) + assert "site-packages" in runtime.parts, dependencies + assert runtime.is_file(), dependencies