diff --git a/.github/scripts/ci_changes.py b/.github/scripts/ci_changes.py index 1393a5d3..c39bca10 100644 --- a/.github/scripts/ci_changes.py +++ b/.github/scripts/ci_changes.py @@ -72,8 +72,12 @@ def classify(paths): selected.add("cpp") if path.endswith(CPP_SUFFIXES): selected.add("cpp_format") + elif path == "tests/python/test_cpp_clustering_examples.py": + selected.update(("cpp", "python_quality")) elif path.startswith("tests/python/"): selected.update(("python_build", "wheels")) + if path == "tests/python/conftest.py": + selected.add("cpp") if path.endswith(".py"): selected.add("python_quality") elif path.startswith("sample/python/"): diff --git a/.github/scripts/test_ci_changes.py b/.github/scripts/test_ci_changes.py index 1dc9b3d8..9d3d15a7 100644 --- a/.github/scripts/test_ci_changes.py +++ b/.github/scripts/test_ci_changes.py @@ -44,6 +44,13 @@ def test_language_specific_changes(self): }, "python_bindings/__init__.py": {"python_quality", "python_build", "wheels"}, "python_bindings/licenses/LLVM-OpenMP.txt": {"python_build", "wheels"}, + "tests/python/test_cpp_clustering_examples.py": {"cpp", "python_quality"}, + "tests/python/conftest.py": { + "cpp", + "python_quality", + "python_build", + "wheels", + }, "tests/python/test_ivf.py": {"python_quality", "python_build", "wheels"}, "tests/python/fixtures/ivf_legacy_4bit.index": { "python_build", diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index e44b92ec..391cc83d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -67,8 +67,16 @@ jobs: - name: Build and install wheel run: pip install ".[test]" -v - - name: Run pytest - run: pytest --tb=short -q + # Repaired-wheel jobs run the full Python suite. + - name: Smoke-test source installation + run: >- + python -m pytest --tb=short -q + tests/python/test_import.py + tests/python/test_ivf.py::test_save_load_roundtrip + tests/python/test_hnsw.py::test_save_load_roundtrip + tests/python/test_symqg.py::test_save_load_roundtrip + tests/python/test_qgkmeans.py::test_train_populates_faiss_style_outputs + tests/python/test_rabitqkmeans.py::test_flat_parameters_and_outputs python-windows: needs: changes @@ -84,5 +92,33 @@ jobs: cache: pip - name: Build and install package run: python -m pip install ".[test]" -v - - name: Test Python bindings - run: python -m pytest tests/python --tb=short -q + - name: Smoke-test source installation + run: >- + python -m pytest --tb=short -q + tests/python/test_import.py + tests/python/test_ivf.py::test_save_load_roundtrip + tests/python/test_hnsw.py::test_save_load_roundtrip + tests/python/test_symqg.py::test_save_load_roundtrip + tests/python/test_qgkmeans.py::test_train_populates_faiss_style_outputs + tests/python/test_rabitqkmeans.py::test_flat_parameters_and_outputs + + faiss-comparison: + needs: changes + if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.python_build == 'true') }} + name: FAISS comparison script + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + OMP_NUM_THREADS: "2" + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.11" + cache: pip + - name: Install system dependencies + run: sudo apt-get update -qq && sudo apt-get install -y cmake + - name: Install comparison dependencies + run: python -m pip install ".[test]" faiss-cpu + - name: Test comparison script + run: python -m pytest tests/python/test_compare_with_faiss.py --tb=short -q diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a77b368f..6d97338d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -98,14 +98,26 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.11" - name: Install dependencies run: sudo apt-get update -qq && sudo apt-get install -y cmake ninja-build - name: Configure - run: cmake -S . -B build -G Ninja -DRABITQ_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release + run: cmake -S . -B build -G Ninja -DRABITQ_BUILD_TESTS=ON -DRABITQ_BUILD_SAMPLES=ON -DCMAKE_BUILD_TYPE=Release - name: Build run: cmake --build build - - name: Test - run: ctest --test-dir build --output-on-failure + # The installed-consumer job runs the full portable Release suite. + - name: Test native build smoke coverage + run: >- + ctest --test-dir build --output-on-failure --no-tests=error + -R 'MatrixDispatchTest|FloatDistance|FastScan|QGKMeansTest.TrainsAndReturnsAssignmentsForFinalCentroids|RaBitQKMeansTest.FinalModesKeepFlatLloydAssignmentsAcrossClusterCounts' + - name: Install example test dependencies + run: python -m pip install numpy pytest + - name: Test C++ examples + env: + RABITQ_REQUIRE_CPP_EXAMPLES: "1" + run: python -m pytest tests/python/test_cpp_clustering_examples.py --tb=short -q windows: needs: changes @@ -135,6 +147,7 @@ jobs: - uses: actions/checkout@v7 - name: Install dependencies run: sudo apt-get update -qq && sudo apt-get install -y cmake ninja-build + # Keep assertions/debug symbols while avoiding unoptimized sanitizer loops. - name: Configure run: >- cmake -S . -B build-sanitizers -G Ninja @@ -143,6 +156,7 @@ jobs: -DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF -DRABITQ_ENABLE_SANITIZERS=ON -DCMAKE_BUILD_TYPE=Debug + -DCMAKE_CXX_FLAGS_DEBUG="-O1 -g -fno-optimize-sibling-calls" - name: Build run: cmake --build build-sanitizers - name: Test @@ -172,6 +186,30 @@ jobs: cmake --build build-install - name: Test portable Release build run: ctest --test-dir build-install --output-on-failure + - name: Download pinned Intel SDE + run: | + curl --fail --location --retry 3 --max-time 120 \ + https://downloadmirror.intel.com/924984/sde-external-10.13.1-2026-07-28-lin.tar.xz \ + -o "$RUNNER_TEMP/sde.tar.xz" + echo "94e97d623fec54385686e1e7ba65ebc9941748c05ee451423948334892bf2b50 $RUNNER_TEMP/sde.tar.xz" | sha256sum --check + mkdir -p "$RUNNER_TEMP/sde" + tar -xJf "$RUNNER_TEMP/sde.tar.xz" -C "$RUNNER_TEMP/sde" --strip-components=1 + - name: Test AVX2-only and AVX-512 dispatch under emulation + env: + OMP_NUM_THREADS: "2" + run: | + simd_filter='CpuFeatures.*:FloatDistance.*:MatrixDispatchTest.*:FastScan*.*:BatchEstimatorTest.*' + simd_filter+=':FhtDispatchTest.*:FlipSignTest.*:KacsWalkTest.*:ExcodeIp.*:MaskIpX0Q.*' + simd_filter+=':PortableKernels.*:QueryPreparation.*:ScalarQuantize.*:WarmupIpX0Q.*:PackBinary.*' + simd_filter+=':ExplicitSimd/RabitqRescaleBackendTest.*:QGEstimatorTest.*' + simd_filter+=':QGKMeansTest.TrainsAndReturnsAssignmentsForFinalCentroids' + simd_filter+=':RaBitQKMeansTest.FinalModesKeepFlatLloydAssignmentsAcrossClusterCounts' + simd_filter+=':IvfPersistenceTest.ReloadsRawAndQuantizedStorage' + simd_filter+=':HnswSaveTest.SuccessfulSavePreservesSearchAfterLoading' + for cpu_model in hsw icl; do + RABITQ_TEST_CPU="$cpu_model" "$RUNNER_TEMP/sde/sde64" "-$cpu_model" -- \ + build-install/tests/rabitq_tests --gtest_filter="$simd_filter" + done - name: Install RaBitQ-Library run: cmake --install build-install - name: Build downstream consumer diff --git a/pyproject.toml b/pyproject.toml index 9008db1f..d1a73b02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,8 +44,8 @@ cmake.args = [ 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" +test-requires = ["pytest"] +test-command = "pytest {project}/tests/python --ignore={project}/tests/python/test_compare_with_faiss.py --ignore={project}/tests/python/test_cpp_clustering_examples.py --tb=short -q" [tool.cibuildwheel.linux] manylinux-aarch64-image = "manylinux_2_28" diff --git a/tests/README.md b/tests/README.md index e5fa9b6b..d62fe882 100644 --- a/tests/README.md +++ b/tests/README.md @@ -98,6 +98,27 @@ AVX-512 tests skip when the CPU or OS lacks the required features. Tests using `/dev/full` skip on Windows; native POSIX path tests compile only on their applicable platforms. Passing on an AVX2 machine does not verify AVX-512 execution. +## CI coverage + +The installed-consumer job runs the full portable Linux Release suite. The Ubuntu +native build runs SIMD and clustering smoke tests; Windows, ARM64, and ASan/UBSan +jobs retain their full C++ suites. The sanitizer job uses Debug with +`-O1 -g -fno-optimize-sibling-calls`, retaining assertions, frame pointers, leak +checking, and both ASan/UBSan while avoiding unoptimized clustering loops. + +The portable Linux job also reuses its test binary under pinned +[Intel SDE](https://www.intel.com/content/www/us/en/download/684897/intel-software-development-emulator.html) +CPU models: Haswell (`-hsw`, AVX2 only) and Ice Lake (`-icl`, AVX-512 including +VPOPCNTDQ). Kernel reference tests and small clustering/index tests exercise real +runtime dispatch under emulation. `RABITQ_TEST_CPU` checks the emulated features +and prints them; an incorrect CPU model fails instead of silently losing coverage. +These are correctness checks, not hardware performance measurements. + +Source-install Python jobs check imports, index persistence, and both clustering +APIs. Repaired-wheel jobs run the full core Python suite. Large-dimension Python +cases check representative binding/persistence paths; C++ retains the exhaustive +clustering dimension/storage matrix. + ## Python tests Use an activated [project environment](../CONTRIBUTING.md#python-environment). @@ -117,16 +138,20 @@ The [Python example tests](python/test_examples.py) cover RaBitQKMeans and QGKMeans clustering, L2/IP indexing, saved cluster files, and queries without importing FAISS. The [FAISS comparison tests](python/test_compare_with_faiss.py) cover both clustering methods and skip FAISS-dependent cases when `faiss-cpu` -is absent; wheel CI includes it as a test dependency. +is absent. CI runs them in a dedicated Linux job; wheel tests do not install FAISS. The [C++ clustering example tests](python/test_cpp_clustering_examples.py) check saved vectors, exact labels, and compatibility with the C++ index builders. -Build the corresponding examples first; cases skip when their executables are absent. +The Ubuntu C++ job runs them against its sample binaries, with +`RABITQ_REQUIRE_CPP_EXAMPLES=1` so missing executables fail. Wheel jobs exclude +these tests. Locally, build the examples first; missing executables otherwise skip. ## Installed CMake package test After building the library, verify that another project can consume its installed -headers and library. Use an absolute path for ``: +headers and library. The consumer trains QGKMeans and RaBitQKMeans and checks +final assignments, distances, and objective against a scalar L2 reference. +Use an absolute path for ``: ```text cmake --install build --config Release --prefix "" diff --git a/tests/consumer/main.cpp b/tests/consumer/main.cpp index 76851cca..c4a68e5b 100644 --- a/tests/consumer/main.cpp +++ b/tests/consumer/main.cpp @@ -1,6 +1,93 @@ +#include +#include +#include +#include +#include +#include + +#include "rabitqlib/clustering/qgkmeans.hpp" +#include "rabitqlib/clustering/rabitqkmeans.hpp" #include "rabitqlib/utils/cpu_features.hpp" +namespace { +constexpr size_t kDimension = 65; +constexpr size_t kPoints = 67; + +// Use public results and an independent scalar reference. Checks must also run +// in Release builds, where assert() would be disabled. +template +bool check_clustering(Model& model, const std::vector& data) { + model.train(kPoints, data.data()); + if (model.centroids.size() != model.k * kDimension || + model.assignments.size() != kPoints || model.distances.size() != kPoints || + model.iteration_stats.empty()) { + return false; + } + for (float value : model.centroids) { + if (!std::isfinite(value)) { + return false; + } + } + double objective = 0; + for (size_t point = 0; point < kPoints; ++point) { + if (model.assignments[point] >= model.k) { + return false; + } + double nearest = std::numeric_limits::infinity(); + double selected = 0; + for (size_t cluster = 0; cluster < model.k; ++cluster) { + double distance = 0; + for (size_t dim = 0; dim < kDimension; ++dim) { + const double delta = static_cast(data[point * kDimension + dim]) - + model.centroids[cluster * kDimension + dim]; + distance += delta * delta; + } + nearest = std::min(nearest, distance); + if (cluster == model.assignments[point]) { + selected = distance; + } + } + const double tolerance = 1e-4 * std::max(1.0, nearest); + if (!(std::abs(selected - nearest) <= tolerance) || + !(std::abs(model.distances[point] - selected) <= tolerance)) { + return false; + } + objective += selected; + } + return std::abs(model.final_obj - objective) <= 1e-4 * std::max(1.0, objective); +} +} // namespace + int main() { const auto& features = rabitqlib::cpu::features(); - return features.avx512vpopcntdq && !features.avx512f ? 1 : 0; + if (features.avx512vpopcntdq && !features.avx512f) { + return 1; + } + std::vector data(kPoints * kDimension); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = std::sin(static_cast(i) * 0.17F); + } + + rabitqlib::qgkmeans::QGKMeansParameters graph_parameters; + graph_parameters.niter = 2; + graph_parameters.num_threads = 2; + graph_parameters.ef_build = 33; + graph_parameters.ef_search = 33; + graph_parameters.final_assignment = rabitqlib::clustering::FinalAssignmentMode::Exact; + rabitqlib::qgkmeans::QGKMeans graph(kDimension, 33, graph_parameters); + if (!check_clustering(graph, data)) { + std::cerr << "Installed QGKMeans returned invalid clustering results\n"; + return 1; + } + + rabitqlib::rabitqkmeans::RaBitQKMeansParameters flat_parameters; + flat_parameters.niter = 2; + flat_parameters.num_threads = 2; + flat_parameters.final_assignment = rabitqlib::clustering::FinalAssignmentMode::Exact; + rabitqlib::rabitqkmeans::RaBitQKMeans flat(kDimension, 4, flat_parameters); + if (!check_clustering(flat, data)) { + std::cerr << "Installed RaBitQKMeans returned invalid clustering results\n"; + return 1; + } + return 0; } diff --git a/tests/python/test_cpp_clustering_examples.py b/tests/python/test_cpp_clustering_examples.py index 22dee2c5..e331d583 100644 --- a/tests/python/test_cpp_clustering_examples.py +++ b/tests/python/test_cpp_clustering_examples.py @@ -13,7 +13,10 @@ def run_cpp(name, *args): executable = BIN / (name + (".exe" if os.name == "nt" else "")) if not executable.is_file(): - pytest.skip(f"Build the C++ example target {name} first") + message = f"Build the C++ example target {name} first" + if os.environ.get("RABITQ_REQUIRE_CPP_EXAMPLES") == "1": + pytest.fail(message) + pytest.skip(message) return subprocess.run( [str(executable), *map(str, args)], env=dict(os.environ, OMP_THREAD_LIMIT="2", OMP_NUM_THREADS="2"), @@ -119,3 +122,14 @@ def test_cpp_clustering_usage_and_errors(tmp_path, method): result = run_cpp(method, *args) assert result.returncode == 1 assert "File does not exist:" in result.stderr + + +@pytest.mark.parametrize("required", [False, True]) +def test_missing_cpp_example_fails_when_required(tmp_path, monkeypatch, required): + monkeypatch.setitem(run_cpp.__globals__, "BIN", tmp_path) + monkeypatch.setenv("RABITQ_REQUIRE_CPP_EXAMPLES", "1" if required else "0") + expected = pytest.fail.Exception if required else pytest.skip.Exception + with pytest.raises( + expected, match=r"Build the C\+\+ example target qgkmeans first" + ): + run_cpp("qgkmeans") diff --git a/tests/python/test_large_dimensions.py b/tests/python/test_large_dimensions.py index 162c7cba..d03b56ee 100644 --- a/tests/python/test_large_dimensions.py +++ b/tests/python/test_large_dimensions.py @@ -5,7 +5,8 @@ from rabitqlib import HnswIndex, IvfIndex, SymqgIndex -@pytest.mark.parametrize("dim", [4096, 16384, 16385, 65536]) +# Keep a padded dimension and the maximum across every index and metric. +@pytest.mark.parametrize("dim", [16385, 65536]) @pytest.mark.parametrize("metric", ["l2", "ip"]) @pytest.mark.parametrize("kind", ["ivf", "hnsw", "qg"]) def test_large_dimension_round_trip(tmp_path, dim, metric, kind): diff --git a/tests/python/test_qgkmeans.py b/tests/python/test_qgkmeans.py index ff855770..d29719fa 100644 --- a/tests/python/test_qgkmeans.py +++ b/tests/python/test_qgkmeans.py @@ -411,8 +411,8 @@ def test_training_size_warning_is_opt_in(capfd, verbose): assert ("please provide at least" in captured.err) == verbose -@pytest.mark.parametrize("dim", [4096, 4097, 65535, 65536]) -@pytest.mark.parametrize("bits", [0, 4, 8]) +# C++ covers the full dimension/storage matrix; check binding outputs here. +@pytest.mark.parametrize("dim,bits", [(4097, 0), (65535, 4), (65536, 8)]) def test_large_dimensions(dim, bits): x = np.zeros((33, dim), dtype=np.float32) x[np.arange(33), np.arange(33)] = 1 diff --git a/tests/unit/rabitqlib/clustering/rabitqkmeans_test.cpp b/tests/unit/rabitqlib/clustering/rabitqkmeans_test.cpp index 5a967e68..79b972a5 100644 --- a/tests/unit/rabitqlib/clustering/rabitqkmeans_test.cpp +++ b/tests/unit/rabitqlib/clustering/rabitqkmeans_test.cpp @@ -411,6 +411,66 @@ TEST(RaBitQKMeansTest, FinalModesKeepFlatLloydAssignmentsAcrossClusterCounts) { } } +TEST(RaBitQKMeansTest, TrainsAtMaximumDimension) { + // A partial FastScan batch keeps the maximum-width training check small. + constexpr size_t kDim = 65536, kPoints = 5, kClusters = 2; + std::vector data(kPoints * kDim); + for (size_t point = 0; point < kPoints; ++point) { + // Exercise the full extent, including the last coordinate, without + // making the distance check depend on long float accumulation chains. + for (size_t dim = 1023; dim < kDim; dim += 1024) { + data[point * kDim + dim] = + static_cast(static_cast((dim + point * 7) % 19) - 9) / 256.0F; + } + } + const auto original = data; + for (const auto mode : {FinalAssignmentMode::Approximate, FinalAssignmentMode::Exact}) { + SCOPED_TRACE(static_cast(mode)); + RaBitQKMeansParameters parameters; + parameters.niter = 2; + parameters.num_threads = 2; + parameters.final_assignment = mode; + RaBitQKMeans model(kDim, kClusters, parameters); + model.train(kPoints, data.data()); + ASSERT_EQ(model.iteration_stats.size(), 2U); + ASSERT_EQ(model.centroids.size(), kClusters * kDim); + 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); } + )); + double objective = 0; + for (size_t point = 0; point < kPoints; ++point) { + ASSERT_LT(model.assignments[point], kClusters); + const double selected = scalar_centroid_distance( + data.data() + point * kDim, + model.centroids.data() + model.assignments[point] * kDim, + kDim, + false + ); + EXPECT_NEAR(model.distances[point], selected, 2e-6 * std::max(1.0, selected)); + objective += selected; + if (mode == FinalAssignmentMode::Exact) { + for (size_t cluster = 0; cluster < kClusters; ++cluster) { + EXPECT_LE( + selected, + scalar_centroid_distance( + data.data() + point * kDim, + model.centroids.data() + cluster * kDim, + kDim, + false + ) + ); + } + } + } + EXPECT_NEAR(model.final_obj, objective, 2e-6 * std::max(1.0, objective)); + } +} + TEST(RaBitQKMeansTest, PointCodesPreserveBorrowedCentroidsDuringRetraining) { constexpr size_t kDim = 65, kPoints = 129; std::mt19937 rng(945); diff --git a/tests/unit/rabitqlib/index/qg_test.cpp b/tests/unit/rabitqlib/index/qg_test.cpp index 637411d2..f4f5ba1f 100644 --- a/tests/unit/rabitqlib/index/qg_test.cpp +++ b/tests/unit/rabitqlib/index/qg_test.cpp @@ -377,9 +377,11 @@ TEST(BatchQueryTest, PreparedSumPreservesLookupTableAndMetric) { } TEST(QuantizedGraphSearchTest, ScratchSearchMatchesRegularSearch) { - constexpr size_t kNumPoints = 257; for (const size_t kDim : {64U, 65U, 1025U, 4097U}) { SCOPED_TRACE(kDim); + // Cover IDs beyond 255 at small dimensions; wide vectors need only + // a non-complete graph with the same degree and search window. + const size_t kNumPoints = kDim <= 65 ? 257 : 65; constexpr size_t kDegree = 32; constexpr size_t kEfSearch = 1; const size_t kNumQueries = kDim == 64 ? 256 : 16; @@ -395,7 +397,9 @@ TEST(QuantizedGraphSearchTest, ScratchSearchMatchesRegularSearch) { QuantizedGraph graph( kNumPoints, kDim, kDegree, metric, RotatorType::FhtKacRotator, bits, 42 ); - QGBuilder builder(graph, kDegree, data.data(), 1, 42); + // Graph construction is setup here; cache vectors to avoid repeated + // reconstruction. CachedResetMatchesFreshConstruction checks parity. + QGBuilder builder(graph, kDegree, data.data(), 1, 42, true); graph.set_ef(kEfSearch); std::vector rotated_query(graph.padded_dim()); @@ -512,7 +516,7 @@ TEST(QuantizedGraphSearchTest, SingleResultMatchesTopKWithTiesAndHints) { QuantizedGraph graph( kCount, dim, 32, metric, RotatorType::FhtKacRotator, bits, 42 ); - QGBuilder builder(graph, 64, data.data(), 1, 42); + QGBuilder builder(graph, 64, data.data(), 1, 42, true); builder.build(); graph.set_ef(kCount); std::vector rotated(graph.padded_dim()); diff --git a/tests/unit/rabitqlib/utils/cpu_features_test.cpp b/tests/unit/rabitqlib/utils/cpu_features_test.cpp index cc755699..1beeb51a 100644 --- a/tests/unit/rabitqlib/utils/cpu_features_test.cpp +++ b/tests/unit/rabitqlib/utils/cpu_features_test.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include namespace { @@ -30,6 +33,28 @@ TEST(CpuFeatures, ReturnsStableResult) { ); } +// The emulated CI runs must fail instead of silently skipping optional backends. +TEST(CpuFeatures, MatchesRequiredEmulatedCpu) { + const bool avx2 = rabitqlib::cpu::has_avx2(); + const bool avx512 = rabitqlib::cpu::has_avx512_core(); + const bool popcnt = rabitqlib::cpu::has_avx512_popcnt(); + const bool neon = rabitqlib::cpu::has_neon(); + std::cout << "SIMD capabilities: AVX2/FMA=" << avx2 << " AVX512=" << avx512 + << " AVX512_VPOPCNTDQ=" << popcnt << " NEON=" << neon << '\n'; + const char* required = std::getenv("RABITQ_TEST_CPU"); + if (required == nullptr) { + return; + } + SCOPED_TRACE(required); + ASSERT_TRUE(std::strcmp(required, "hsw") == 0 || std::strcmp(required, "icl") == 0) + << "RABITQ_TEST_CPU must be hsw or icl"; + EXPECT_TRUE(avx2); + EXPECT_FALSE(neon); + const bool require_avx512 = std::strcmp(required, "icl") == 0; + EXPECT_EQ(avx512, require_avx512); + EXPECT_EQ(popcnt, require_avx512); +} + TEST(CpuFeatures, RequiresOsManagedAvxState) { constexpr uint64_t kAvxState = 0x6; const auto hardware = all_hardware_features();