diff --git a/.clang-format b/.clang-format index 5503dd82..65c1b641 100644 --- a/.clang-format +++ b/.clang-format @@ -16,6 +16,8 @@ AllowShortLambdasOnASingleLine: All AllowShortLoopsOnASingleLine: false BinPackArguments: false BinPackParameters: false +# Preserve the existing initializer-list closing-brace layout on LLVM 23. +BreakBeforeCloseBracketBracedList: false BreakConstructorInitializers: BeforeComma ColumnLimit: 92 CompactNamespaces: false diff --git a/.clang-tidy b/.clang-tidy index 4419d66d..45f8ae34 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -23,9 +23,14 @@ Checks: > readability-inconsistent-declaration-parameter-name, readability-misleading-indentation, readability-redundant-control-flow, + -performance-enum-size, + -portability-avoid-pragma-once, + -portability-no-assembler, -portability-simd-intrinsics, -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling +# LLVM 23's new wildcard matches must not change enum ABI, replace the project's +# pragma-once convention, or reject the CPU detector's required inline assembly. # Keep the established checks blocking. Naming starts as warnings while existing # internal/parameter names are migrated; do not bulk-fix public interfaces. WarningsAsErrors: "*,-readability-identifier-naming" diff --git a/.github/actions/setup-clang/action.yml b/.github/actions/setup-clang/action.yml new file mode 100644 index 00000000..8e7bed18 --- /dev/null +++ b/.github/actions/setup-clang/action.yml @@ -0,0 +1,20 @@ +name: Set up Clang 23 +description: Install the pinned compiler, analysis, formatting and OpenMP tools on Ubuntu. +runs: + using: composite + steps: + - name: Install LLVM 23 tools + shell: bash + run: | + set -euo pipefail + # shellcheck source=/dev/null + source /etc/os-release + curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key \ + | sudo tee /usr/share/keyrings/apt.llvm.org.asc >/dev/null + echo "deb [signed-by=/usr/share/keyrings/apt.llvm.org.asc] https://apt.llvm.org/$VERSION_CODENAME/ llvm-toolchain-$VERSION_CODENAME-23 main" \ + | sudo tee /etc/apt/sources.list.d/llvm-23.list >/dev/null + sudo apt-get update -qq + sudo apt-get install -y clang-23 clang-format-23 clang-tidy-23 libomp-23-dev cmake ninja-build + clang++-23 --version + clang-format-23 --version + clang-tidy-23 --version diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 22c7f30a..685be714 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,14 +17,11 @@ jobs: needs: changes if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.cpp_format == 'true') }} name: C++ formatting - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - uses: actions/checkout@v7 - - name: Install clang-format - run: | - sudo apt-get update -qq - sudo apt-get install -y clang-format-15 + - uses: ./.github/actions/setup-clang - name: Check formatting run: ./scripts/check-format.sh @@ -32,7 +29,7 @@ jobs: needs: changes if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.cpp_tidy == 'true') }} name: C++ static analysis shard ${{ matrix.shard }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 strategy: fail-fast: false @@ -41,15 +38,15 @@ jobs: env: TIDY_SHARD_INDEX: ${{ matrix.shard }} TIDY_SHARD_COUNT: "2" + CXX: clang++-23 steps: - uses: actions/checkout@v7 + - uses: ./.github/actions/setup-clang - uses: actions/setup-python@v7 with: python-version: "3.11" - name: Install dependencies run: | - sudo apt-get update -qq - sudo apt-get install -y clang-tidy-15 libomp-15-dev cmake ninja-build python -m pip install "numpy>=1.23" "pybind11>=2.12" - name: Locate pybind11 run: echo "pybind11_DIR=$(python -m pybind11 --cmakedir)" >> "$GITHUB_ENV" @@ -59,6 +56,7 @@ jobs: -DRABITQ_BUILD_SAMPLES=OFF -DRABITQ_BUILD_TESTS=OFF -DRABITQ_BUILD_PYTHON_BINDINGS=ON + -DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF -DCMAKE_BUILD_TYPE=Release -Dpybind11_DIR="$pybind11_DIR" - name: Run clang-tidy @@ -134,6 +132,51 @@ jobs: cmake --build build-source-consumer ctest --test-dir build-source-consumer --output-on-failure + clang: + name: Clang 23 (OpenMP ${{ matrix.openmp }}) + needs: changes + if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.cpp == 'true') }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + openmp: ["ON", "OFF"] + env: + CC: clang-23 + CXX: clang++-23 + OMP_NUM_THREADS: "2" + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/setup-clang + - name: Configure portable build + run: >- + cmake -S . -B build-clang -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DRABITQ_BUILD_TESTS=ON + -DRABITQ_BUILD_SAMPLES=OFF + -DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF + -DRABITQ_USE_OPENMP=${{ matrix.openmp }} + -DRABITQ_BUILD_INDEX=${{ matrix.openmp }} + - name: Build and test + run: | + cmake --build build-clang --parallel 2 + ctest --test-dir build-clang --output-on-failure --no-tests=error + - name: Test installed index consumer + if: matrix.openmp == 'ON' + run: | + cmake --install build-clang --prefix "$RUNNER_TEMP/rabitq-clang-install" + cmake -S tests/consumer -B build-clang-consumer -G Ninja \ + -DCMAKE_PREFIX_PATH="$RUNNER_TEMP/rabitq-clang-install" + cmake --build build-clang-consumer --parallel 2 + ctest --test-dir build-clang-consumer --output-on-failure --no-tests=error + - name: Test core and FastScan integrations + run: >- + cmake -DRABITQ_SOURCE_DIR="$PWD" + -DWORK_DIR="$RUNNER_TEMP/rabitq-clang-core" + -DRABITQ_USE_OPENMP=${{ matrix.openmp }} + -P tests/core_consumer/run.cmake + windows: needs: changes if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.cpp == 'true') }} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 606276b2..a1e85f8f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -7,13 +7,33 @@ Run commands from the repository root in your activated the [source-build prerequisites](CONTRIBUTING.md#setup-for-code-changes). Refresh APT package lists with `sudo apt-get update` before installing tools. +## Compiler and tool versions + +Library builds require C++17; the LLVM 23 development tools below do not impose a +Clang 23 minimum on downstream users. CMake respects the caller's compiler choice. +OpenMP is required for ANN indexes and Python bindings, and optional for core/FastScan. + +Linux CI explicitly builds and tests with Clang 23, including installed consumers +and OpenMP ON/OFF numerical builds, alongside the existing GCC jobs. Native ARM64, +AppleClang and MSVC checks remain separate. Older GCC/Clang minimum versions have +not yet been established by a dedicated compatibility matrix. + +Formatting and static analysis use LLVM 23 as the shared local/CI baseline. Keep +this major version fixed rather than following unversioned distribution defaults. +The LLVM APT release branch supplies updated builds within that major version; +CI prints the exact tool versions, rather than promising a fixed patch build. +Upgrading them requires reviewing format differences and diagnostics together with +the scripts and CI configuration. The Ubuntu CI setup lives in +[setup-clang](.github/actions/setup-clang/action.yml). + ## C++ formatting RaBitQ uses the repository's `.clang-format` configuration and clang-format -15. Install that version on Ubuntu or Debian with: +23. After enabling the [LLVM 23 package repository](https://apt.llvm.org/) +for your distribution, install that version on Ubuntu or Debian with: ```bash -sudo apt-get install clang-format-15 +sudo apt-get install clang-format-23 ``` Format all project-maintained C and C++ files: @@ -28,14 +48,15 @@ Verify formatting without changing files: ./scripts/check-format.sh ``` -The scripts intentionally exclude vendored code in `include/rabitqlib/third/`. +The scripts intentionally exclude vendored code in `include/rabitqlib/third/` +and the frozen compatibility fixture `tests/fixtures/core_api_v1/consumer.cpp`. To use a nonstandard executable name, set `CLANG_FORMAT`; it must still identify -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 +itself as clang-format 23. On macOS, use Bash 4+ on PATH for the shell wrappers. +Apple SDKs newer than clang-tidy 23 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. +clang-tidy 23 baseline. -clangd embeds its own formatter, so use clangd 15 in editors such as VS Code +clangd embeds its own formatter, so use clangd 23 in editors such as VS Code if format-on-save must exactly match CI. If another clangd version is required, disable format-on-save and run the repository scripts before submitting. @@ -56,7 +77,7 @@ performance checks. Install the pinned analyzer and the dependencies needed to configure the core library and Python bindings: ```bash -sudo apt-get install clang-tidy-15 libomp-15-dev cmake ninja-build +sudo apt-get install clang-23 clang-tidy-23 libomp-23-dev cmake ninja-build python -m pip install "numpy>=1.23" "pybind11>=2.12" ``` @@ -66,18 +87,20 @@ and run the check with the same compiler used by CMake: ```bash export pybind11_DIR="$(python -m pybind11 --cmakedir)" cmake -S . -B build-tidy -G Ninja \ - -DCMAKE_CXX_COMPILER=c++ \ + -DCMAKE_CXX_COMPILER=clang++-23 \ -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" \ -DRABITQ_BUILD_SAMPLES=OFF \ -DRABITQ_BUILD_TESTS=OFF \ -DRABITQ_BUILD_PYTHON_BINDINGS=ON \ -DCMAKE_BUILD_TYPE=Release \ -Dpybind11_DIR="$pybind11_DIR" -CXX=c++ ./scripts/check-tidy.sh build-tidy +CXX=clang++-23 ./scripts/check-tidy.sh build-tidy ``` Use a fresh build directory when changing compilers. `CMAKE_CXX_COMPILER` -selects the compiler explicitly; `CXX=c++` on the check command must match it. +selects the compiler explicitly; `CXX` on the check command must match it. +Other supported compilers can still build the library; this command selects the +development analysis baseline only. The wrapper supplies clang-tidy with that compiler's standard-library include paths and reports diagnostics only for first-party files. Vendored Eigen and @@ -97,33 +120,26 @@ was focused or complete. The optional include dependency check uses clang-tidy's `misc-include-cleaner` to report missing and unused includes. Run it locally when -reviewing include changes. On Ubuntu, install unversioned distribution packages: +reviewing include changes. Use LLVM 23 to match the script's default analyzer +and the shared development baseline. After enabling +[LLVM's APT repository](https://apt.llvm.org/) for your distribution, install +the matching compiler, analysis and OpenMP packages: ```bash sudo apt-get update -sudo apt-get install clang-tidy clang libomp-dev cmake ninja-build -``` - -Clang-tidy 17 or newer is required. On older distributions such as Ubuntu 22.04, -use [LLVM's APT repository](https://apt.llvm.org/) to install a newer release. -For example, install LLVM 22 and its matching analysis and OpenMP packages: - -```bash -wget https://apt.llvm.org/llvm.sh -sudo bash llvm.sh 22 -sudo apt-get install clang-tidy-22 libomp-22-dev cmake ninja-build -export CLANG_TIDY=clang-tidy-22 -export CXX=clang++-22 +sudo apt-get install clang-23 clang-tidy-23 libomp-23-dev cmake ninja-build +export CLANG_TIDY=clang-tidy-23 +export CXX=clang++-23 ``` Configure and run the same check locally (use a fresh build directory when changing compilers). Clang needs its matching OpenMP development package; -for the LLVM 22 example above, that is `libomp-22-dev`. An older installed -`libomp-dev` alone may not be found by Clang 22: +for LLVM 23, that is `libomp-23-dev`. An older installed +`libomp-dev` alone may not be found by Clang 23: ```bash cmake -S . -B build-includes -G Ninja \ - -DCMAKE_CXX_COMPILER="${CXX:-clang++}" \ + -DCMAKE_CXX_COMPILER="${CXX:-clang++-23}" \ -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" \ -Dpybind11_DIR="$(python -m pybind11 --cmakedir)" \ -DRABITQ_BUILD_SAMPLES=ON \ @@ -175,7 +191,7 @@ run this example from the repository root using the existing project Python environment: ```bash -CXX=c++ python - <<'PY' +CXX=clang++-23 python - <<'PY' import json from pathlib import Path import subprocess @@ -201,7 +217,7 @@ translation units that include it directly or transitively, covering affected template instantiations and ISA variants. Configure any required test/sample targets first if they are absent from the database. Do not analyze a header as a standalone translation unit. If the consumer set is unclear, run the full -check. Replace `CXX=c++` in the example if you configured with another compiler. +check. Replace `CXX=clang++-23` in the example if you configured with another compiler. The wrapper's success message covers only the selected database entries in this mode; it does not mean the full pre-merge check passed. diff --git a/benchmarks/hotpaths/core.cpp b/benchmarks/hotpaths/core.cpp index 63a7933e..9ec22e55 100644 --- a/benchmarks/hotpaths/core.cpp +++ b/benchmarks/hotpaths/core.cpp @@ -60,8 +60,8 @@ void run(int argc, char** argv) { const double delta = static_cast(data[row * dim + d]) - centroid[d]; reference += delta * delta; } - if (!(std::abs(distances[row] - reference) <= 2e-6 * static_cast(dim) - )) { + if (!(std::abs(distances[row] - reference) <= + 2e-6 * static_cast(dim))) { throw std::runtime_error("L2 differs from float64 reference"); } } diff --git a/include/rabitqlib/clustering/detail/kmeans.hpp b/include/rabitqlib/clustering/detail/kmeans.hpp index 12ab4b4b..d92bf862 100644 --- a/include/rabitqlib/clustering/detail/kmeans.hpp +++ b/include/rabitqlib/clustering/detail/kmeans.hpp @@ -254,7 +254,8 @@ class LloydKMeans : public Parameters { ); bool in_range = true; bool finite = true; -#pragma omp parallel for num_threads(threads) reduction(&& : finite, in_range) schedule(static) +#pragma omp parallel for num_threads(threads) reduction(&& : finite, in_range) \ + schedule(static) for (std::ptrdiff_t loop_index = 0; loop_index < static_cast(n * d); ++loop_index) { const size_t i = static_cast(loop_index); @@ -330,8 +331,9 @@ class LloydKMeans : public Parameters { ); const auto [nsplit, shift] = consolidate_centroids(x, n, sums, cluster_sizes, previous_centroids); - iteration_stats.push_back(KMeansIterationStats{ - iteration + 1, obj, shift, nsplit}); + iteration_stats.push_back( + KMeansIterationStats{iteration + 1, obj, shift, nsplit} + ); if (verbose) { std::cout << Assignment::kName << " iteration " << iteration + 1 << "/" diff --git a/include/rabitqlib/clustering/qgkmeans.hpp b/include/rabitqlib/clustering/qgkmeans.hpp index 3882b3f4..5989692f 100644 --- a/include/rabitqlib/clustering/qgkmeans.hpp +++ b/include/rabitqlib/clustering/qgkmeans.hpp @@ -158,9 +158,11 @@ class QGAssigner { scratch_.reserve(num_threads_); for (uint32_t thread = 0; thread < num_threads_; ++thread) { - scratch_.emplace_back(std::make_unique( - graph_->padded_dim(), M_, ef_search_, 1 - )); + scratch_.emplace_back( + std::make_unique( + graph_->padded_dim(), M_, ef_search_, 1 + ) + ); } } diff --git a/include/rabitqlib/core/detail/rabitq_impl.hpp b/include/rabitqlib/core/detail/rabitq_impl.hpp index 9d4ef7a8..31742690 100644 --- a/include/rabitqlib/core/detail/rabitq_impl.hpp +++ b/include/rabitqlib/core/detail/rabitq_impl.hpp @@ -672,8 +672,8 @@ inline void ex_bits_code_with_factor_with_scratch( // Cauchy-Schwarz makes this nonnegative; collinear vectors can round below zero. const T normalized_error = - (((l2_sqr * l2norm_sqr(xu_cb.data(), dim)) / (ip_resi_xucb * ip_resi_xucb)) - 1 - ) / + (((l2_sqr * l2norm_sqr(xu_cb.data(), dim)) / (ip_resi_xucb * ip_resi_xucb)) - + 1) / (dim - 1); T tmp_error = l2_norm * kConstEpsilon * std::sqrt(std::max(normalized_error, T{0})); @@ -1003,8 +1003,8 @@ inline void code_factors( // Cauchy-Schwarz guarantees this value is nonnegative. Clamp small negative // round-off errors for collinear residual/code vectors before taking the square root. const T normalized_error = - (((l2_sqr * l2norm_sqr(xu_cb.data(), dim)) / (ip_resi_xucb * ip_resi_xucb)) - 1 - ) / + (((l2_sqr * l2norm_sqr(xu_cb.data(), dim)) / (ip_resi_xucb * ip_resi_xucb)) - + 1) / (dim - 1); T tmp_error = l2_norm * kConstEpsilon * std::sqrt(std::max(normalized_error, T{0})); diff --git a/include/rabitqlib/core/rotator.hpp b/include/rabitqlib/core/rotator.hpp index 49297aa7..49e833e8 100644 --- a/include/rabitqlib/core/rotator.hpp +++ b/include/rabitqlib/core/rotator.hpp @@ -32,7 +32,7 @@ class Rotator { public: explicit Rotator() = default; - explicit Rotator(size_t dim, size_t padded_dim) : dim_(dim), padded_dim_(padded_dim){}; + explicit Rotator(size_t dim, size_t padded_dim) : dim_(dim), padded_dim_(padded_dim) {}; virtual ~Rotator() = default; virtual void rotate(const T* src, T* dst) const = 0; virtual void load(std::ifstream&) = 0; @@ -93,8 +93,9 @@ class MatrixRotator : public Rotator { : MatrixRotator(dim, padded_dim, AllocateOnly{}) { RowMajorMatrix rand = random_gaussian_matrix(padded_dim, padded_dim); Eigen::HouseholderQR> qr(rand); - RowMajorMatrix q_inv = qr.householderQ().transpose( - ); // The inverse of an orthogonal matrix is its transpose. + RowMajorMatrix q_inv = + qr.householderQ() + .transpose(); // The inverse of an orthogonal matrix is its transpose. // the random matrix only need the first dim rows, since we just pad zeros for // the vector to be rotated to padded dimension diff --git a/include/rabitqlib/index/detail/parallel_for.hpp b/include/rabitqlib/index/detail/parallel_for.hpp index 6ae8fe53..b29691a7 100644 --- a/include/rabitqlib/index/detail/parallel_for.hpp +++ b/include/rabitqlib/index/detail/parallel_for.hpp @@ -76,7 +76,8 @@ inline void parallel_for(size_t start, size_t end, size_t num_threads, Function try { fn(id, thread_id); } catch (...) { - std::unique_lock last_except_lock(last_except_mutex + std::unique_lock last_except_lock( + last_except_mutex ); last_exception = std::current_exception(); /* diff --git a/include/rabitqlib/index/hnsw/hnsw.hpp b/include/rabitqlib/index/hnsw/hnsw.hpp index bb33c14b..55f7f8b8 100644 --- a/include/rabitqlib/index/hnsw/hnsw.hpp +++ b/include/rabitqlib/index/hnsw/hnsw.hpp @@ -299,8 +299,8 @@ class HierarchicalNSW { using EstimateRecord = rabitqlib::detail::DistanceEstimate; - float (*raw_dist_func_ - )(const float* __restrict__, const float* __restrict__, size_t){nullptr}; + float (*raw_dist_func_)(const float* __restrict__, const float* __restrict__, size_t){ + nullptr}; void set_ef(size_t ef) { ef_ = ef; } @@ -401,13 +401,14 @@ class HierarchicalNSW { // ANN Search template - void - get_bin_est_direct(std::vector&, SplitSingleQuery&, PID, HierarchicalNSW::EstimateRecord&); + void get_bin_est_direct( + std::vector&, SplitSingleQuery&, PID, HierarchicalNSW::EstimateRecord& + ); template - void - get_full_est_direct(std::vector&, SplitSingleQuery&, PID, HierarchicalNSW::EstimateRecord&) - const; + void get_full_est_direct( + std::vector&, SplitSingleQuery&, PID, HierarchicalNSW::EstimateRecord& + ) const; template std::vector> search_knn_sorted_direct( @@ -450,10 +451,12 @@ class HierarchicalNSW { void reconstruct_rotated(PID, float*) const; float get_quant_dist(PID target, PID query, detail::QuantQuery&) const; void add_point_quant(PID, int, LinkStorage&, detail::QuantQuery&); - maxheap> - search_base_layer_quant(PID, PID, int, detail::QuantQuery&); - PID - mutually_connect_quant(PID, maxheap>&, int, detail::QuantQuery&); + maxheap> search_base_layer_quant( + PID, PID, int, detail::QuantQuery& + ); + PID mutually_connect_quant( + PID, maxheap>&, int, detail::QuantQuery& + ); }; struct EstimateRecord { diff --git a/include/rabitqlib/index/ivf/initializer.hpp b/include/rabitqlib/index/ivf/initializer.hpp index 79213181..8260a6fb 100644 --- a/include/rabitqlib/index/ivf/initializer.hpp +++ b/include/rabitqlib/index/ivf/initializer.hpp @@ -52,8 +52,9 @@ class Initializer { virtual ~Initializer() = 0; [[nodiscard]] virtual const float* centroid(PID) const = 0; virtual void add_vectors(const float*, size_t) = 0; - virtual void - centroids_distances(const float*, size_t, std::vector>&) const = 0; + virtual void centroids_distances( + const float*, size_t, std::vector>& + ) const = 0; virtual void load(std::ifstream&, const char*) = 0; virtual void save(std::ofstream&, const char*) const = 0; }; @@ -195,7 +196,8 @@ class HNSWInitializer : public Initializer { void rebuild_label_lookup() { if (alg_hnsw_->cur_element_count != num_cluster_) { - throw std::runtime_error("HNSW centroid count does not match IVF cluster count" + throw std::runtime_error( + "HNSW centroid count does not match IVF cluster count" ); } std::vector label_to_internal(num_cluster_); @@ -235,10 +237,7 @@ class HNSWInitializer : public Initializer { size_t start = 0; size_t rows = num_cluster_; rabitqlib::index_detail::parallel_for( - start, - rows, - num_threads, - [&](size_t row, size_t /*thread_id*/) { + start, rows, num_threads, [&](size_t row, size_t /*thread_id*/) { alg_hnsw_->addPoint(cent + (row * dim_), row); } ); diff --git a/include/rabitqlib/index/ivf/ivf.hpp b/include/rabitqlib/index/ivf/ivf.hpp index 08b53085..ffe86d23 100644 --- a/include/rabitqlib/index/ivf/ivf.hpp +++ b/include/rabitqlib/index/ivf/ivf.hpp @@ -95,12 +95,28 @@ class IVF { static constexpr uint64_t kFormatMagic = 0x3158444951424152ULL; // "RABQIDX1" static constexpr uint32_t kFormatVersion = 2; - void - quantize_cluster(Cluster&, const std::vector&, const float*, const float*, float*, float*, const quant::RabitqConfig&); + void quantize_cluster( + Cluster&, + const std::vector&, + const float*, + const float*, + float*, + float*, + const quant::RabitqConfig& + ); - void - extend_cluster(const Cluster&, const std::vector&, const float*, const float*, PID, char*, char*, PID*, char*, const quant::RabitqConfig&) - const; + void extend_cluster( + const Cluster&, + const std::vector&, + const float*, + const float*, + PID, + char*, + char*, + PID*, + char*, + const quant::RabitqConfig& + ) const; // True if `n` points cannot be addressed by the storage size computations [[nodiscard]] bool exceeds_storage(size_t n) const; @@ -131,13 +147,24 @@ class IVF { void swap(IVF& other) noexcept; - inline void - search_cluster(const Cluster&, const SplitBatchQuery&, buffer::SearchBuffer&, bool, const float*) - const; + inline void search_cluster( + const Cluster&, + const SplitBatchQuery&, + buffer::SearchBuffer&, + bool, + const float* + ) const; - inline void - scan_one_batch(const char* batch_data, const char* ex_data, const PID* ids, const SplitBatchQuery& q_obj, buffer::SearchBuffer& knns, size_t num_points, bool, const float*) - const; + inline void scan_one_batch( + const char* batch_data, + const char* ex_data, + const PID* ids, + const SplitBatchQuery& q_obj, + buffer::SearchBuffer& knns, + size_t num_points, + bool, + const float* + ) const; inline void validate_search(const float*, size_t, size_t, const PID*) const; @@ -254,8 +281,9 @@ class IVF { float* distances = nullptr ) const; - void search(const float* query, size_t k, size_t nprobe, PID* results, bool use_hacc) - const; + void search( + const float* query, size_t k, size_t nprobe, PID* results, bool use_hacc + ) const; void search( const float* query, diff --git a/include/rabitqlib/index/symqg/qg.hpp b/include/rabitqlib/index/symqg/qg.hpp index 2e99abbf..fe0eeae4 100644 --- a/include/rabitqlib/index/symqg/qg.hpp +++ b/include/rabitqlib/index/symqg/qg.hpp @@ -69,8 +69,16 @@ void search_with_workspace( template <> class QuantizedGraph { friend class QGBuilder; - friend void detail:: - search_with_workspace(QuantizedGraph&, const float*, uint32_t, uint32_t*, float*, detail::SearchScratchView, PID, const float*); + friend void detail::search_with_workspace( + QuantizedGraph&, + const float*, + uint32_t, + uint32_t*, + float*, + detail::SearchScratchView, + PID, + const float* + ); friend struct QGConstructionTestAccess; private: @@ -145,13 +153,21 @@ class QuantizedGraph { return get_row_data(data_id); } - void validate_search(const float*, uint32_t, const uint32_t*, const float*, size_t) - const; + void validate_search( + const float*, uint32_t, const uint32_t*, const float*, size_t + ) const; void search_impl(const float*, uint32_t, uint32_t*, float*, size_t); - void - search_with_scratch_impl(const float*, uint32_t, uint32_t*, float*, detail::SearchScratchView, PID, const float*); + void search_with_scratch_impl( + const float*, + uint32_t, + uint32_t*, + float*, + detail::SearchScratchView, + PID, + const float* + ); float point_distance(const float*, const QuantizedQuery*, PID) const; @@ -173,7 +189,8 @@ class QuantizedGraph { ); } - [[nodiscard]] rabitqlib::detail::ConstPackedArrayView get_neighbors(PID data_id + [[nodiscard]] rabitqlib::detail::ConstPackedArrayView get_neighbors( + PID data_id ) const { return rabitqlib::detail::ConstPackedArrayView( get_row_data(data_id) + neighbor_offset_ @@ -189,8 +206,9 @@ class QuantizedGraph { const float* rotated_centroid ); - void - update_results(buffer::SearchBuffer&, VisitedSet&, const float*, const QuantizedQuery*); + void update_results( + buffer::SearchBuffer&, VisitedSet&, const float*, const QuantizedQuery* + ); void scan_neighbors( const BatchQuery&, @@ -324,7 +342,7 @@ class QuantizedGraph { }; // Preserve C++17 deduction for callers that omit the float template argument. -QuantizedGraph()->QuantizedGraph; +QuantizedGraph() -> QuantizedGraph; QuantizedGraph( size_t, size_t, @@ -333,7 +351,6 @@ QuantizedGraph( RotatorType = RotatorType::FhtKacRotator, size_t = 0, uint32_t = std::random_device{}() -) - ->QuantizedGraph; +) -> QuantizedGraph; } // namespace rabitqlib::symqg diff --git a/include/rabitqlib/utils/array.hpp b/include/rabitqlib/utils/array.hpp index fa09dd7d..1e87c363 100644 --- a/include/rabitqlib/utils/array.hpp +++ b/include/rabitqlib/utils/array.hpp @@ -120,7 +120,10 @@ class Array { if (this == &other) { return *this; } - if constexpr (!atraits::propagate_on_container_move_assignment::value && !atraits::is_always_equal::value) { + if constexpr ( + !atraits::propagate_on_container_move_assignment::value && + !atraits::is_always_equal::value + ) { if (allocator_ != other.allocator_ && other.pointer_ != nullptr) { // Allocate with the destination's allocator before releasing either // array. Unequal, non-propagating allocators cannot transfer storage. diff --git a/include/rabitqlib/utils/memory.hpp b/include/rabitqlib/utils/memory.hpp index 66b20fee..9a953f88 100644 --- a/include/rabitqlib/utils/memory.hpp +++ b/include/rabitqlib/utils/memory.hpp @@ -104,13 +104,15 @@ class AlignedAllocator { template constexpr explicit AlignedAllocator(const ReboundAllocator&) noexcept {} - friend constexpr bool - operator==(const AlignedAllocator&, const AlignedAllocator&) noexcept { + friend constexpr bool operator==( + const AlignedAllocator&, const AlignedAllocator& + ) noexcept { return true; } - friend constexpr bool - operator!=(const AlignedAllocator&, const AlignedAllocator&) noexcept { + friend constexpr bool operator!=( + const AlignedAllocator&, const AlignedAllocator& + ) noexcept { return false; } diff --git a/include/rabitqlib/utils/warmup_space.hpp b/include/rabitqlib/utils/warmup_space.hpp index e0b911dd..30544ec3 100644 --- a/include/rabitqlib/utils/warmup_space.hpp +++ b/include/rabitqlib/utils/warmup_space.hpp @@ -66,15 +66,3 @@ inline float warmup_ip_x0_q( } } // namespace rabitqlib - -template -inline float warmup_ip_x0_q( - const uint64_t* data, - const uint64_t* query, - float delta, - float vl, - size_t unused_padded_dim = 0, // not used - size_t unused_query_bits = 0 // not used -) { - return rabitqlib::warmup_ip_x0_q(data, query, delta, vl, padded_dim); -} diff --git a/python_bindings/bindings_common.hpp b/python_bindings/bindings_common.hpp index 852b6d44..b0525565 100644 --- a/python_bindings/bindings_common.hpp +++ b/python_bindings/bindings_common.hpp @@ -38,7 +38,8 @@ class IndexAccess { public: Guard(IndexAccess& access, bool writing) : access_(access), writing_(writing) { if (access.writing_ || (writing && access.readers_ != 0)) { - throw std::runtime_error("Index is busy: conflicting operation in progress" + throw std::runtime_error( + "Index is busy: conflicting operation in progress" ); } if (writing) { diff --git a/python_bindings/hnsw_bindings.cpp b/python_bindings/hnsw_bindings.cpp index 1fa8f856..fb404068 100644 --- a/python_bindings/hnsw_bindings.cpp +++ b/python_bindings/hnsw_bindings.cpp @@ -35,15 +35,17 @@ class HnswIndex { const std::string& metric = "l2", size_t random_seed = 100 ) - : index_(std::make_unique( - max_elements, - dim, - nbits, - M, - ef_construction, - random_seed, - metric_from_string(metric) - )) {} + : index_( + std::make_unique( + max_elements, + dim, + nbits, + M, + ef_construction, + random_seed, + metric_from_string(metric) + ) + ) {} void build( py::handle data, diff --git a/python_bindings/ivf_bindings.cpp b/python_bindings/ivf_bindings.cpp index a2eef5f2..c84f9611 100644 --- a/python_bindings/ivf_bindings.cpp +++ b/python_bindings/ivf_bindings.cpp @@ -94,7 +94,8 @@ class IvfIndex { throw std::invalid_argument("data dimension does not match index dim"); } if (static_cast(data_array.shape(0)) != index_->max_elements()) { - throw std::invalid_argument("number of data rows must match index max_elements" + throw std::invalid_argument( + "number of data rows must match index max_elements" ); } if (static_cast(centroids_array.shape(1)) != index_->dimension()) { diff --git a/sample/cpp/core/main.cpp b/sample/cpp/core/main.cpp index 30b8d65f..404b5558 100644 --- a/sample/cpp/core/main.cpp +++ b/sample/cpp/core/main.cpp @@ -25,9 +25,11 @@ int main() { } // Data, centroid and query must use the same rotation and padded domain. - std::unique_ptr> rotator(rabitqlib::choose_rotator( - kDim, rabitqlib::RotatorType::FhtKacRotator, 0, 42 - )); + std::unique_ptr> rotator( + rabitqlib::choose_rotator( + kDim, rabitqlib::RotatorType::FhtKacRotator, 0, 42 + ) + ); const size_t padded_dim = rotator->size(); std::vector rotated_vector(padded_dim), rotated_query(padded_dim); std::vector centroid(padded_dim, 0.0F); @@ -35,7 +37,8 @@ int main() { rotator->rotate(query.data(), rotated_query.data()); std::vector bin(rabitqlib::BinDataMap::data_bytes(padded_dim)); - std::vector extra(rabitqlib::ExDataMap::data_bytes(padded_dim, kExBits) + std::vector extra( + rabitqlib::ExDataMap::data_bytes(padded_dim, kExBits) ); const rabitqlib::quant::RabitqConfig config; rabitqlib::quant::quantize_split_single( diff --git a/sample/cpp/fastscan/main.cpp b/sample/cpp/fastscan/main.cpp index 7253b3af..37f28bda 100644 --- a/sample/cpp/fastscan/main.cpp +++ b/sample/cpp/fastscan/main.cpp @@ -21,9 +21,11 @@ int main() { try { constexpr size_t kDim = 65, kCount = 49, kExBits = 3; - std::unique_ptr> rotator(rabitqlib::choose_rotator( - kDim, rabitqlib::RotatorType::FhtKacRotator, 0, 42 - )); + std::unique_ptr> rotator( + rabitqlib::choose_rotator( + kDim, rabitqlib::RotatorType::FhtKacRotator, 0, 42 + ) + ); const size_t padded_dim = rotator->size(); std::vector vector(kDim), query(kDim), original_centroid(kDim); std::vector rotated_data(kCount * padded_dim), rotated_query(padded_dim); diff --git a/sample/cpp/kmeans_example.hpp b/sample/cpp/kmeans_example.hpp index 7a86ad93..a80ee269 100644 --- a/sample/cpp/kmeans_example.hpp +++ b/sample/cpp/kmeans_example.hpp @@ -50,7 +50,8 @@ int cluster_file(int argc, char** argv) { } const std::string metric = argc == 6 ? argv[5] : "l2"; if (metric != "l2" && metric != "ip") { - throw std::invalid_argument("metric must be l2 or ip; ip requires normalized input" + throw std::invalid_argument( + "metric must be l2 or ip; ip requires normalized input" ); } diff --git a/scripts/apply-format.sh b/scripts/apply-format.sh index cab35438..3397c813 100755 --- a/scripts/apply-format.sh +++ b/scripts/apply-format.sh @@ -4,15 +4,15 @@ set -euo pipefail repo_root="$(git rev-parse --show-toplevel)" readonly repo_root -readonly formatter="${CLANG_FORMAT:-clang-format-15}" +readonly formatter="${CLANG_FORMAT:-clang-format-23}" if ! command -v "$formatter" >/dev/null 2>&1; then - echo "error: $formatter was not found; install clang-format 15 or set CLANG_FORMAT" >&2 + echo "error: $formatter was not found; install clang-format 23 or set CLANG_FORMAT" >&2 exit 1 fi -if [[ "$($formatter --version)" != *"version 15."* ]]; then - echo "error: RaBitQ formatting requires clang-format 15" >&2 +if [[ "$($formatter --version)" != *"version 23."* ]]; then + echo "error: RaBitQ formatting requires clang-format 23" >&2 echo "found: $($formatter --version)" >&2 exit 1 fi diff --git a/scripts/check-format.sh b/scripts/check-format.sh index e70abb4c..f196075d 100755 --- a/scripts/check-format.sh +++ b/scripts/check-format.sh @@ -4,15 +4,15 @@ set -euo pipefail repo_root="$(git rev-parse --show-toplevel)" readonly repo_root -readonly formatter="${CLANG_FORMAT:-clang-format-15}" +readonly formatter="${CLANG_FORMAT:-clang-format-23}" if ! command -v "$formatter" >/dev/null 2>&1; then - echo "error: $formatter was not found; install clang-format 15 or set CLANG_FORMAT" >&2 + echo "error: $formatter was not found; install clang-format 23 or set CLANG_FORMAT" >&2 exit 1 fi -if [[ "$($formatter --version)" != *"version 15."* ]]; then - echo "error: RaBitQ formatting requires clang-format 15" >&2 +if [[ "$($formatter --version)" != *"version 23."* ]]; then + echo "error: RaBitQ formatting requires clang-format 23" >&2 echo "found: $($formatter --version)" >&2 exit 1 fi diff --git a/scripts/check-includes.sh b/scripts/check-includes.sh index f75ab33b..d9dcfc92 100755 --- a/scripts/check-includes.sh +++ b/scripts/check-includes.sh @@ -3,7 +3,7 @@ set -euo pipefail repo_root="$(git rev-parse --show-toplevel)" cd "$repo_root" -export CLANG_TIDY="${CLANG_TIDY:-clang-tidy}" +export CLANG_TIDY="${CLANG_TIDY:-clang-tidy-23}" include_build="$(realpath "${1:-build-includes}")" export include_build export include_root="$repo_root" diff --git a/scripts/check-tidy.sh b/scripts/check-tidy.sh index 2d2ff74b..cea25c6c 100755 --- a/scripts/check-tidy.sh +++ b/scripts/check-tidy.sh @@ -5,7 +5,7 @@ set -euo pipefail repo_root="$(git rev-parse --show-toplevel)" readonly repo_root readonly build_dir="${1:-$repo_root/build}" -readonly tidy_runner="${RUN_CLANG_TIDY:-run-clang-tidy-15}" +readonly tidy_runner="${RUN_CLANG_TIDY:-run-clang-tidy-23}" readonly compiler="${CXX:-c++}" if [[ ! -f "$build_dir/compile_commands.json" ]]; then @@ -15,7 +15,7 @@ if [[ ! -f "$build_dir/compile_commands.json" ]]; then fi if ! command -v "$tidy_runner" >/dev/null 2>&1; then - echo "error: $tidy_runner was not found; install clang-tidy 15" >&2 + echo "error: $tidy_runner was not found; install clang-tidy 23" >&2 exit 1 fi diff --git a/scripts/clang-format-files.sh b/scripts/clang-format-files.sh index 36cc51ea..278b959d 100755 --- a/scripts/clang-format-files.sh +++ b/scripts/clang-format-files.sh @@ -22,7 +22,8 @@ fi | while IFS= read -r -d '' file; do ../* | */../* | */..) continue ;; - include/rabitqlib/third/*) + include/rabitqlib/third/* | tests/fixtures/core_api_v1/consumer.cpp) + # The v1 consumer is frozen compatibility evidence, not maintained source. continue ;; esac diff --git a/scripts/format-changed.sh b/scripts/format-changed.sh index 233190b6..9d283a30 100755 --- a/scripts/format-changed.sh +++ b/scripts/format-changed.sh @@ -4,15 +4,15 @@ set -euo pipefail repo_root="$(git rev-parse --show-toplevel)" readonly repo_root -readonly formatter="${CLANG_FORMAT:-clang-format-15}" +readonly formatter="${CLANG_FORMAT:-clang-format-23}" if ! command -v "$formatter" >/dev/null 2>&1; then - echo "error: $formatter was not found; install clang-format 15 or set CLANG_FORMAT" >&2 + echo "error: $formatter was not found; install clang-format 23 or set CLANG_FORMAT" >&2 exit 1 fi -if [[ "$($formatter --version)" != *"version 15."* ]]; then - echo "error: RaBitQ formatting requires clang-format 15" >&2 +if [[ "$($formatter --version)" != *"version 23."* ]]; then + echo "error: RaBitQ formatting requires clang-format 23" >&2 echo "found: $($formatter --version)" >&2 exit 1 fi diff --git a/src/core/api.cpp b/src/core/api.cpp index 44b375dc..ee52c837 100644 --- a/src/core/api.cpp +++ b/src/core/api.cpp @@ -159,10 +159,15 @@ class ScratchStorage { float* zero; explicit ScratchStorage(rabitq_impl::ScratchAllocator& allocator) - : scratch{allocator.array(), allocator.array(), - allocator.array(), allocator.array(), - allocator.array(), allocator.array(), - allocator.array(), allocator.resource()} + : scratch{ + allocator.array(), + allocator.array(), + allocator.array(), + allocator.array(), + allocator.array(), + allocator.array(), + allocator.array(), + allocator.resource()} , codes(allocator.array()) , extra(allocator.array()) , zero(allocator.array()) {} @@ -179,7 +184,8 @@ void read_codes(const Parameters& p, CodeView input, ScratchStorage& s) { for (size_t i = 0; i < p.dim; ++i) { s.codes[i] = read_u16(bytes + 2 * i); if (s.codes[i] > maximum) { - throw std::invalid_argument("Unpacked code exceeds the configured bit width" + throw std::invalid_argument( + "Unpacked code exceeds the configured bit width" ); } } @@ -366,7 +372,8 @@ float QueryState::compute_checked( for (size_t i = 0; i < p.dim; ++i) { const uint16_t value = read_u16(bytes + 2 * i); if (value > maximum) { - throw std::invalid_argument("Unpacked code exceeds the configured bit width" + throw std::invalid_argument( + "Unpacked code exceeds the configured bit width" ); } ip += query.query_[i] * static_cast(value); diff --git a/src/fastscan/fastscan_avx512.cpp b/src/fastscan/fastscan_avx512.cpp index 0d494419..bafaf454 100644 --- a/src/fastscan/fastscan_avx512.cpp +++ b/src/fastscan/fastscan_avx512.cpp @@ -152,9 +152,9 @@ void transfer_lut_hacc_avx512(const uint16_t* lut, size_t dim, uint8_t* hc_lut) hc_lut + (i / kLutPerIter * kCodePerIter) + ((i % kLutPerIter) * kCodePerLine); uint8_t* fill_hi = fill_lo + (kRegBits / kByteBits); - __m512i tmp = - _mm512_cvtepi16_epi32(_mm256_loadu_si256(reinterpret_cast(lut)) - ); + __m512i tmp = _mm512_cvtepi16_epi32( + _mm256_loadu_si256(reinterpret_cast(lut)) + ); __m128i lo = _mm512_cvtepi32_epi8(tmp); __m128i hi = _mm512_cvtepi32_epi8(_mm512_srli_epi32(tmp, 8)); _mm_storeu_si128(reinterpret_cast<__m128i*>(fill_lo), lo); diff --git a/src/index/hnsw.cpp b/src/index/hnsw.cpp index fb0ec28b..2e705e80 100644 --- a/src/index/hnsw.cpp +++ b/src/index/hnsw.cpp @@ -67,7 +67,9 @@ HierarchicalNSW::HierarchicalNSW( , label_op_locks_(kMaxLabelOperationLock) , link_list_locks_(max_elements) , element_levels_(max_elements) - , raw_dist_func_((metric_type == METRIC_IP) ? dot_product_dis : euclidean_sqr) { + , raw_dist_func_( + (metric_type == METRIC_IP) ? dot_product_dis : euclidean_sqr + ) { static_assert( buffer::kSearchBufferMaxPointCount <= kRemovedMask, "cluster IDs must leave the removal bit free" @@ -78,9 +80,11 @@ HierarchicalNSW::HierarchicalNSW( } max_elements_ = max_elements; dim_ = dim; - rotator_.reset(choose_rotator( - dim, RotatorType::FhtKacRotator, round_up_to_multiple(dim_, 32) - )); + rotator_.reset( + choose_rotator( + dim, RotatorType::FhtKacRotator, round_up_to_multiple(dim_, 32) + ) + ); padded_dim_ = rotator_->size(); /* check size */ assert(padded_dim_ % 32 == 0); @@ -537,7 +541,8 @@ void HierarchicalNSW::construct( const size_t centroids_bytes = num_clusters_ * padded_dim_ * sizeof(float); centroids_memory_.reset(memory::huge_page_allocate(centroids_bytes)); if (centroids_memory_ == nullptr) { - throw std::runtime_error("Not enough memory: HNSW failed to allocate centroids" + throw std::runtime_error( + "Not enough memory: HNSW failed to allocate centroids" ); } @@ -555,10 +560,7 @@ void HierarchicalNSW::construct( construction_data_ = data; rabitqlib::index_detail::parallel_for( - 0, - num_points, - num_threads, - [&](size_t idx, size_t /*threadId*/) { + 0, num_points, num_threads, [&](size_t idx, size_t /*threadId*/) { add_point(idx, cluster_ids[idx], config); } ); @@ -949,10 +951,7 @@ std::vector>> HierarchicalNSW::search( } std::vector>> results(num_queries); rabitqlib::index_detail::parallel_for( - 0, - num_queries, - num_threads, - [&](size_t idx, size_t /*threadId*/) { + 0, num_queries, num_threads, [&](size_t idx, size_t /*threadId*/) { std::vector rotated_query(padded_dim_); this->rotator_->rotate(queries + (idx * dim_), rotated_query.data()); results[idx] = detail::search_knn_sorted( @@ -1052,8 +1051,9 @@ void HierarchicalNSW::reconstruct_rotated(PID id, float* out) const { // Only `query` is reconstructed; `target` is scored from its codes, as QGBuilder // does. Callers hold `query` fixed across a loop, so it is prepared once. -float HierarchicalNSW::get_quant_dist(PID target, PID query, detail::QuantQuery& cached) - const { +float HierarchicalNSW::get_quant_dist( + PID target, PID query, detail::QuantQuery& cached +) const { if (cached.id != query || !cached.prepared.has_value()) { cached.rotated.resize(padded_dim_); reconstruct_rotated(query, cached.rotated.data()); @@ -1370,9 +1370,9 @@ std::vector HierarchicalNSW::add( for (size_t i = 0; i < n; ++i) { char* raw = nullptr; if (levels[i] > 0) { - raw = - static_cast(std::calloc(1, (size_links_per_element_ * levels[i]) + 1) - ); + raw = static_cast( + std::calloc(1, (size_links_per_element_ * levels[i]) + 1) + ); if (raw == nullptr) { throw std::runtime_error( "Not enough memory: HNSW add failed to allocate a link list" diff --git a/src/index/ivf.cpp b/src/index/ivf.cpp index f0564836..54ee3323 100644 --- a/src/index/ivf.cpp +++ b/src/index/ivf.cpp @@ -382,10 +382,7 @@ void IVF::add( } } else { rabitqlib::index_detail::parallel_for( - 0, - n, - num_threads, - [&](size_t row, size_t /*thread_id*/) { + 0, n, num_threads, [&](size_t row, size_t /*thread_id*/) { std::vector rotated(padded_dim_); std::vector> nearest(1); rotator_->rotate(data + (row * dim_), rotated.data()); diff --git a/src/index/pipnn.cpp b/src/index/pipnn.cpp index 0b50cda8..c0f4fdc7 100644 --- a/src/index/pipnn.cpp +++ b/src/index/pipnn.cpp @@ -67,10 +67,9 @@ class CandidateTable { return; } auto* pos = std::lower_bound( - row, - row + size, - hash, - [](const Candidate& candidate, uint16_t key) { return candidate.hash < key; } + row, row + size, hash, [](const Candidate& candidate, uint16_t key) { + return candidate.hash < key; + } ); const size_t slot = static_cast(pos - row); if (slot < size && pos->hash == hash) { @@ -154,10 +153,12 @@ struct BucketJob { ScratchPool::ScratchPool(size_t count, size_t dim, size_t threads) : point_size(std::min(count, kTileSize) * dim) - , distance_size(std::max( - std::min(count, kTileSize) * std::min(count, size_t{512}), - std::min(count, kLeafSize) * std::min(count, kLeafSize) - )) + , distance_size( + std::max( + std::min(count, kTileSize) * std::min(count, size_t{512}), + std::min(count, kLeafSize) * std::min(count, kLeafSize) + ) + ) , stride(point_size + distance_size + std::min(count, kTileSize)) // Preserve uninitialized allocation and worker first-touch placement. , storage(new float[threads * stride]) {} @@ -329,7 +330,10 @@ std::vector cluster( small = Bucket(); } else if (child.size() <= kLeafSize) { finished[i].push_back(std::move(child)); - } else if (job.depth >= 4 || (job.depth >= 2 && child.size() * 5 > job.ids.size() * 4)) { + } else if ( + job.depth >= 4 || + (job.depth >= 2 && child.size() * 5 > job.ids.size() * 4) + ) { // Duplicates must not cause unbounded recursion/replication. std::mt19937 random(job.seed + static_cast(j)); std::shuffle(child.begin(), child.end(), random); diff --git a/src/index/qg.cpp b/src/index/qg.cpp index b1dcaf2a..bc21f59b 100644 --- a/src/index/qg.cpp +++ b/src/index/qg.cpp @@ -124,7 +124,9 @@ QuantizedGraph::QuantizedGraph( , degree_bound_(max_deg) , dim_(dim) , padded_dim_(dim) - , raw_dist_func_((metric_type == METRIC_IP) ? dot_product_dis : euclidean_sqr) + , raw_dist_func_( + (metric_type == METRIC_IP) ? dot_product_dis : euclidean_sqr + ) , metric_type_(metric_type) , rotator_type_(rotator_type) , quantization_bits_(quantization_bits) @@ -667,8 +669,9 @@ void QuantizedGraph::initialize(bool randomize_rotation) { } } -float QuantizedGraph::quantized_distance(const QuantizedQuery& query, PID data_id) - const { +float QuantizedGraph::quantized_distance( + const QuantizedQuery& query, PID data_id +) const { ConstExDataMap data( get_quantized_vector(data_id), padded_dim_, quantization_bits_ ); @@ -685,8 +688,9 @@ float QuantizedGraph::quantized_distance(const QuantizedQuery& query, PID ); } -void QuantizedGraph::reconstruct_quantized_vector(PID data_id, float* reconstructed) - const { +void QuantizedGraph::reconstruct_quantized_vector( + PID data_id, float* reconstructed +) const { ConstExDataMap data( get_quantized_vector(data_id), padded_dim_, quantization_bits_ ); diff --git a/src/simd/quantization_avx2.cpp b/src/simd/quantization_avx2.cpp index c0a137fe..88da2919 100644 --- a/src/simd/quantization_avx2.cpp +++ b/src/simd/quantization_avx2.cpp @@ -122,7 +122,8 @@ static RescaleSearchState evaluate_scale_state_avx2( if (fi[fk] != std::numeric_limits::max()) s.first = std::max( s.first, - static_cast(quantized_code_at_scale(magnitudes[fi[fk]], scale, max_code) + static_cast( + quantized_code_at_scale(magnitudes[fi[fk]], scale, max_code) ) / magnitudes[fi[fk]] ); if (ni[nk] != std::numeric_limits::max()) diff --git a/src/simd/quantization_avx512.cpp b/src/simd/quantization_avx512.cpp index 87f0636c..c0a06d72 100644 --- a/src/simd/quantization_avx512.cpp +++ b/src/simd/quantization_avx512.cpp @@ -102,18 +102,20 @@ static RescaleSearchState evaluate_scale_state_avx512( double f = _mm512_reduce_max_pd(vfirst), n = _mm512_reduce_min_pd(vnext); // At least one lane equals each reduced extremum. Select its first set bit; // tied boundary ratios describe the same event scale. - unsigned fk = bitops::countr_zero32(static_cast( - _mm512_cmp_pd_mask(vfirst, _mm512_set1_pd(f), _CMP_EQ_OQ) - )), - nk = bitops::countr_zero32(static_cast( - _mm512_cmp_pd_mask(vnext, _mm512_set1_pd(n), _CMP_EQ_OQ) - )); + unsigned + fk = bitops::countr_zero32( + static_cast(_mm512_cmp_pd_mask(vfirst, _mm512_set1_pd(f), _CMP_EQ_OQ)) + ), + nk = bitops::countr_zero32( + static_cast(_mm512_cmp_pd_mask(vnext, _mm512_set1_pd(n), _CMP_EQ_OQ)) + ); // Reciprocal products selected the coordinates; division now recovers the // exact heap event scales, clipped to the configured search interval. if (fi[fk] != std::numeric_limits::max()) s.first = std::max( s.first, - static_cast(quantized_code_at_scale(magnitudes[fi[fk]], scale, max_code) + static_cast( + quantized_code_at_scale(magnitudes[fi[fk]], scale, max_code) ) / magnitudes[fi[fk]] ); if (ni[nk] != std::numeric_limits::max()) diff --git a/src/simd/quantization_generic.cpp b/src/simd/quantization_generic.cpp index d3bd4527..9b55cff4 100644 --- a/src/simd/quantization_generic.cpp +++ b/src/simd/quantization_generic.cpp @@ -6,8 +6,9 @@ namespace rabitqlib::simd { // Request the existing scalar event sweep; it certifies ambiguous SIMD searches. double best_rescale_factor_generic(const float*, size_t, int, double, double) { return -1; } -double -best_rescale_factor_with_workspace_generic(const float*, size_t, int, double, double, double*, double*) { +double best_rescale_factor_with_workspace_generic( + const float*, size_t, int, double, double, double*, double* +) { return -1; } namespace detail { diff --git a/src/simd/rescale_search.hpp b/src/simd/rescale_search.hpp index 500778c2..ad402840 100644 --- a/src/simd/rescale_search.hpp +++ b/src/simd/rescale_search.hpp @@ -301,8 +301,10 @@ static bool try_find_best_scale_by_interval_search( if (!have_best || result.score_lower > best.score_upper) { best = result; have_best = true; - } else if (result.score_upper >= best.score_lower && - result.state.squared_norm != best.state.squared_norm) { + } else if ( + result.score_upper >= best.score_lower && + result.state.squared_norm != best.state.squared_norm + ) { // The event sweep resolves numerically indistinguishable winners // with its existing arithmetic and tie order. return false; diff --git a/tests/README.md b/tests/README.md index edf632df..8af6b18c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -17,6 +17,11 @@ and rejection of truncated or unsupported formats. - Git and network access during the first configuration so CMake can download GoogleTest 1.14.0 +Linux CI additionally selects Clang 23 explicitly for portable builds, full C++ +tests, installed consumers, and core/FastScan integrations with OpenMP ON/OFF. +This is a tested development toolchain, not a new minimum compiler requirement; +see [compiler and tool versions](../DEVELOPMENT.md#compiler-and-tool-versions). + OpenMP is optional for core/FastScan tests: configure `RABITQ_BUILD_INDEX=OFF` and `RABITQ_USE_OPENMP=OFF`. ANN/Python testing retains the full prerequisites above. diff --git a/tests/failures/compact_codes_failure_test.cpp b/tests/failures/compact_codes_failure_test.cpp index 693da78a..c74c077a 100644 --- a/tests/failures/compact_codes_failure_test.cpp +++ b/tests/failures/compact_codes_failure_test.cpp @@ -335,6 +335,11 @@ TEST(SplitBatchQueryAllocation, ReusesCapacityAcrossQueriesModesAndDimensions) { } TEST(SplitBatchQueryAllocation, InitialPreparationAndGrowthFailurePreserveState) { +#if RABITQ_USE_OPENMP + // The allocation hook queries OpenMP. Initialize its runtime before enabling + // injection so runtime startup allocations cannot re-enter initialization. + (void)omp_get_max_threads(); +#endif std::vector values(128, 0.5F), next(128, 0.25F), scratch(128 * 4); std::vector hacc_scratch(128 * 4); for (bool prepared : {false, true}) { diff --git a/tests/failures/construction_failure_test.cpp b/tests/failures/construction_failure_test.cpp index 66dcd3f1..777ddbdf 100644 --- a/tests/failures/construction_failure_test.cpp +++ b/tests/failures/construction_failure_test.cpp @@ -27,10 +27,12 @@ std::array live{}; size_t live_count = 0; bool should_fail() { + // OpenMP initialization may allocate through this hook. Do not re-enter the + // runtime unless parallel failure injection is actually enabled. #if _OPENMP >= 200805 - const bool in_region = omp_get_level() > 0; + const bool in_region = fail_parallel && omp_get_level() > 0; #else - const bool in_region = omp_in_parallel() != 0; + const bool in_region = fail_parallel && omp_in_parallel() != 0; #endif if (fail_constructor || (fail_parallel && in_region)) { if (remaining.fetch_sub(1) == 0) { diff --git a/tests/failures/hnsw_add_allocation_test.cpp b/tests/failures/hnsw_add_allocation_test.cpp index 5d42823e..f8003666 100644 --- a/tests/failures/hnsw_add_allocation_test.cpp +++ b/tests/failures/hnsw_add_allocation_test.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -75,6 +76,9 @@ struct Fixture { std::array clusters{}; Fixture() { + // Runtime/tool startup allocations are not index allocations and must + // happen before the global new hook starts injecting failures. + (void)omp_get_max_threads(); std::mt19937 generator(42); std::uniform_real_distribution distribution(-1, 1); for (auto& value : data) { diff --git a/tests/unit/rabitqlib/clustering/rabitqkmeans_test.cpp b/tests/unit/rabitqlib/clustering/rabitqkmeans_test.cpp index a13583dc..958bbda0 100644 --- a/tests/unit/rabitqlib/clustering/rabitqkmeans_test.cpp +++ b/tests/unit/rabitqlib/clustering/rabitqkmeans_test.cpp @@ -438,11 +438,11 @@ TEST(RaBitQKMeansTest, TrainsAtMaximumDimension) { ASSERT_EQ(model.assignments.size(), kPoints); ASSERT_EQ(model.distances.size(), kPoints); EXPECT_EQ(data, original); - EXPECT_TRUE(std::all_of( - model.centroids.begin(), - model.centroids.end(), - [](float value) { return std::isfinite(value); } - )); + EXPECT_TRUE( + std::all_of(model.centroids.begin(), model.centroids.end(), [](float value) { + return std::isfinite(value); + }) + ); double objective = 0; for (size_t point = 0; point < kPoints; ++point) { ASSERT_LT(model.assignments[point], kClusters); diff --git a/tests/unit/rabitqlib/fastscan/api_test.cpp b/tests/unit/rabitqlib/fastscan/api_test.cpp index 6a8d081c..93317ffa 100644 --- a/tests/unit/rabitqlib/fastscan/api_test.cpp +++ b/tests/unit/rabitqlib/fastscan/api_test.cpp @@ -514,7 +514,8 @@ TEST(FastScanApi, DoubleEncodingQueryAndScoringWithNonzeroCentroid) { ); EXPECT_EQ(reset_split.delta(), split.delta()); EXPECT_EQ(reset_split.kbxsumq(), split.kbxsumq()); - EXPECT_TRUE(std::equal(split.lut(), split.lut() + bytes, reset_split.lut()) + EXPECT_TRUE( + std::equal(split.lut(), split.lut() + bytes, reset_split.lut()) ); } } @@ -569,11 +570,13 @@ TEST(FastScanApi, DoubleLutMatchesIndependentSubsetSumQuantization) { const Lut zero(query.data(), kDim, hacc); EXPECT_EQ(zero.delta(), 0); EXPECT_EQ(zero.sum_vl(), 0); - EXPECT_TRUE(std::all_of( - zero.lut(), - zero.lut() + kDim * 4 * (hacc ? 2 : 1), - [](uint8_t value) { return value == 0; } - )); + EXPECT_TRUE( + std::all_of( + zero.lut(), zero.lut() + kDim * 4 * (hacc ? 2 : 1), [](uint8_t value) { + return value == 0; + } + ) + ); } } diff --git a/tests/unit/rabitqlib/quantization/core_api_test.cpp b/tests/unit/rabitqlib/quantization/core_api_test.cpp index 55d450cf..783ea5ed 100644 --- a/tests/unit/rabitqlib/quantization/core_api_test.cpp +++ b/tests/unit/rabitqlib/quantization/core_api_test.cpp @@ -450,8 +450,9 @@ TEST(CoreApi, ScoringRetainsBufferAndNumericalChecks) { constexpr size_t dim = 64; std::array values{}; std::array codes{}, extra{}; - float (*volatile compute_pointer - )(const api::QueryState&, api::CodeView, const api::Factors&) = &api::compute; + float (*volatile compute_pointer)( + const api::QueryState&, api::CodeView, const api::Factors& + ) = &api::compute; alignas(float) std::array misaligned{}; const auto* bad_alignment = reinterpret_cast(misaligned.data() + 1); EXPECT_THROW( diff --git a/tests/unit/rabitqlib/quantization/rescale_search_test.cpp b/tests/unit/rabitqlib/quantization/rescale_search_test.cpp index d13ba642..c0ff1064 100644 --- a/tests/unit/rabitqlib/quantization/rescale_search_test.cpp +++ b/tests/unit/rabitqlib/quantization/rescale_search_test.cpp @@ -185,14 +185,15 @@ TEST_P(RabitqRescaleBackendTest, MatchesExhaustiveObjectiveAndCanonicalThreshold std::vector promoted(dim + 2, -123.0); std::vector reciprocals(dim + 2, -123.0); EXPECT_EQ( - workspace_search( - )(magnitudes.data(), - dim, - max_code, - start, - end, - promoted.data() + 1, - reciprocals.data() + 1), + workspace_search()( + magnitudes.data(), + dim, + max_code, + start, + end, + promoted.data() + 1, + reciprocals.data() + 1 + ), t ); EXPECT_EQ(promoted.front(), -123.0); diff --git a/tests/unit/rabitqlib/utils/thread_count_test.cpp b/tests/unit/rabitqlib/utils/thread_count_test.cpp index 5ff7a7fa..78f38ac0 100644 --- a/tests/unit/rabitqlib/utils/thread_count_test.cpp +++ b/tests/unit/rabitqlib/utils/thread_count_test.cpp @@ -112,10 +112,7 @@ TEST(ThreadCount, ParallelForVisitsEachItemOnceWithBoundedWorkerIds) { visited.store(0); } rabitqlib::index_detail::parallel_for( - 0, - count, - requested, - [&](size_t i, size_t worker) { + 0, count, requested, [&](size_t i, size_t worker) { EXPECT_LT(worker, requested == 1 ? 1U : available); visits[i].fetch_add(1); } @@ -125,10 +122,7 @@ TEST(ThreadCount, ParallelForVisitsEachItemOnceWithBoundedWorkerIds) { } size_t calls = 0; rabitqlib::index_detail::parallel_for( - 7, - 8, - requested, - [&](size_t i, size_t worker) { + 7, 8, requested, [&](size_t i, size_t worker) { EXPECT_EQ(i, 7U); EXPECT_EQ(worker, 0U); ++calls; @@ -137,10 +131,7 @@ TEST(ThreadCount, ParallelForVisitsEachItemOnceWithBoundedWorkerIds) { EXPECT_EQ(calls, 1U); for (size_t start : {0U, 5U}) { rabitqlib::index_detail::parallel_for( - start, - start, - requested, - [](size_t, size_t) { + start, start, requested, [](size_t, size_t) { ADD_FAILURE() << "An empty range must not invoke the callback"; } );