From b8b193602877f3b276631f9cbb5849a9c9903fdb Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Fri, 23 Oct 2026 19:57:56 +0800 Subject: [PATCH 1/2] ci: deduplicate builds and parallelize sanitizer tests --- .github/scripts/test_ci_aggregates.py | 7 +++++ .github/workflows/python.yml | 39 ++++++++++++++++----------- .github/workflows/test.yaml | 6 ++++- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/.github/scripts/test_ci_aggregates.py b/.github/scripts/test_ci_aggregates.py index 2d7375fd..c6eb9500 100644 --- a/.github/scripts/test_ci_aggregates.py +++ b/.github/scripts/test_ci_aggregates.py @@ -28,6 +28,13 @@ def test_matrix_verdicts_and_dependencies(self): "python_build", "PYTHON_SELECTED", ), + ( + "python.yml", + "faiss-comparison", + "python-build", + "python_build", + "PYTHON_SELECTED", + ), ("docs.yml", "docs-result", "build", "docs", "DOCS_SELECTED"), ): workflow = (WORKFLOWS / filename).read_text() diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index d65bd7b7..4d6775e4 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -78,6 +78,14 @@ jobs: 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 + - name: Install FAISS comparison dependency + if: matrix.python-version == '3.11' + run: python -m pip install faiss-cpu + - name: Test comparison script + if: matrix.python-version == '3.11' + env: + OMP_NUM_THREADS: "2" + run: python -m pytest tests/python/test_compare_with_faiss.py --tb=short -q python-result: name: Python validation @@ -123,22 +131,21 @@ jobs: 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') }} + # Keep the required check name; FAISS runs against the 3.11 source installation. name: FAISS comparison script + needs: [changes, python-build] + if: ${{ always() }} runs-on: ubuntu-latest - timeout-minutes: 20 - env: - OMP_NUM_THREADS: "2" + timeout-minutes: 5 steps: - - uses: actions/checkout@v7 - - uses: actions/setup-python@v7 - with: - python-version: "3.11" - cache: pip - - name: Verify runner CMake - run: cmake --version - - 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 + - name: Require selected comparison tests + env: + CHANGES_RESULT: ${{ needs.changes.result }} + PYTHON_SELECTED: ${{ needs.changes.outputs.python_build }} + MATRIX_RESULT: ${{ needs.python-build.result }} + run: | + if [ "$CHANGES_RESULT" = success ] && [ "$PYTHON_SELECTED" = false ]; then + test "$MATRIX_RESULT" = skipped || test "$MATRIX_RESULT" = success + else + test "$MATRIX_RESULT" = success + fi diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 072ab591..b5d23145 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -200,7 +200,9 @@ jobs: run: | sudo apt-get install -y ccache echo "CCACHE_DIR=$RUNNER_TEMP/ccache" >> "$GITHUB_ENV" + # OpenMP OFF runs the full core/FastScan suites in the integrations below. - name: Configure portable build + if: matrix.openmp == 'ON' run: >- cmake -S . -B build-clang -G Ninja -DCMAKE_BUILD_TYPE=Release @@ -210,6 +212,7 @@ jobs: -DRABITQ_USE_OPENMP=${{ matrix.openmp }} -DRABITQ_BUILD_INDEX=${{ matrix.openmp }} - name: Build and test + if: matrix.openmp == 'ON' run: | cmake --build build-clang --parallel 2 ctest --test-dir build-clang --output-on-failure --no-tests=error @@ -306,7 +309,8 @@ jobs: env: ASAN_OPTIONS: detect_leaks=1 UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 - run: ctest --test-dir build-sanitizers --output-on-failure + # Each case is a separate process; bound concurrent sanitizer memory use. + run: ctest --test-dir build-sanitizers --output-on-failure --no-tests=error --parallel 2 - name: Test installed sanitizer usage requirements env: ASAN_OPTIONS: detect_leaks=1 From 73a73f2d0c224017b20c94fcb6eb52f6237ce375 Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Fri, 23 Oct 2026 20:19:19 +0800 Subject: [PATCH 2/2] ci: split Windows component consumer checks --- .github/scripts/test_ci_workflow_contracts.py | 46 +++++++++++++++++++ .github/workflows/test.yaml | 18 +++++++- tests/core_consumer/run.cmake | 19 +++++++- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/.github/scripts/test_ci_workflow_contracts.py b/.github/scripts/test_ci_workflow_contracts.py index 5223163b..e68dc0ce 100644 --- a/.github/scripts/test_ci_workflow_contracts.py +++ b/.github/scripts/test_ci_workflow_contracts.py @@ -176,6 +176,52 @@ def expand(match): group(template, 0, "b", filename), ) + def test_windows_consumers_cover_each_component_and_openmp_mode(self): + workflow = (ROOT / ".github/workflows/test.yaml").read_text() + block = workflow.split(" core-consumer:\n", 1)[1].split( + " core-consumer-result:\n", 1 + )[0] + variants = re.findall( + r'- os: windows-2025-vs2026\n\s+openmp: "(ON|OFF)"' + r"\n\s+component: (core|fastscan)", + block, + ) + self.assertCountEqual( + variants, + [ + (mode, component) + for mode in ("ON", "OFF") + for component in ("core", "fastscan") + ], + ) + windows = block.split( + " - name: Test external core-only integrations (Windows)\n", 1 + )[1].split(" - name: Report compiler cache statistics", 1)[0] + self.assertIn("-DRABITQ_COMPONENT=${{ matrix.component }}", windows) + + def test_component_partition_rejects_unknown_or_empty_selection(self): + with tempfile.TemporaryDirectory() as directory: + for component in ("unknown", ""): + with self.subTest(component=component): + result = subprocess.run( + [ + "cmake", + f"-DRABITQ_SOURCE_DIR={ROOT}", + f"-DWORK_DIR={directory}", + f"-DRABITQ_COMPONENT={component}", + "-P", + str(ROOT / "tests/core_consumer/run.cmake"), + ], + capture_output=True, + text=True, + check=False, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn( + "RABITQ_COMPONENT must be core or fastscan", result.stderr + ) + self.assertFalse(list(Path(directory).iterdir())) + def test_documentation_summary_is_available_for_non_doc_prs(self): text = (ROOT / ".github/workflows/docs.yml").read_text() triggers = text.split("concurrency:", 1)[0] diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b5d23145..72b9834c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -382,14 +382,27 @@ jobs: run: ctest --test-dir build-consumer --output-on-failure core-consumer: - name: Core and FastScan consumers (${{ matrix.os }}, OpenMP ${{ matrix.openmp }}) + name: Core and FastScan consumers (${{ matrix.os }}, OpenMP ${{ matrix.openmp }}${{ matrix.component && format(', {0}', matrix.component) || '' }}) needs: changes if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.cpp == 'true') }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, ubuntu-24.04-arm, macos-15, windows-2025-vs2026] + os: [ubuntu-latest, ubuntu-24.04-arm, macos-15] openmp: ["ON", "OFF"] + include: + - os: windows-2025-vs2026 + openmp: "ON" + component: core + - os: windows-2025-vs2026 + openmp: "OFF" + component: core + - os: windows-2025-vs2026 + openmp: "ON" + component: fastscan + - os: windows-2025-vs2026 + openmp: "OFF" + component: fastscan runs-on: ${{ matrix.os }} timeout-minutes: 45 steps: @@ -433,6 +446,7 @@ jobs: run: >- cmake "-DRABITQ_SOURCE_DIR=$PWD" "-DWORK_DIR=$env:RUNNER_TEMP/rabitq-core" + -DRABITQ_COMPONENT=${{ matrix.component }} -DRABITQ_USE_OPENMP=${{ matrix.openmp }} -P tests/core_consumer/run.cmake - name: Report compiler cache statistics diff --git a/tests/core_consumer/run.cmake b/tests/core_consumer/run.cmake index 15b73a28..0d428954 100644 --- a/tests/core_consumer/run.cmake +++ b/tests/core_consumer/run.cmake @@ -1,4 +1,5 @@ # Run with -DRABITQ_SOURCE_DIR= -DWORK_DIR= -P run.cmake. +# Optional -DRABITQ_COMPONENT=core|fastscan selects one independent partition. cmake_minimum_required(VERSION 3.20) if(NOT RABITQ_SOURCE_DIR OR NOT WORK_DIR) message(FATAL_ERROR "RABITQ_SOURCE_DIR and WORK_DIR are required") @@ -26,7 +27,15 @@ endif() if(DEFINED OpenMP_ROOT) list(APPEND configure_options "-DOpenMP_ROOT=${OpenMP_ROOT}") endif() -foreach(component IN ITEMS core fastscan) +# Windows CI runs the two independent component checks on separate runners. +set(components core fastscan) +if(DEFINED RABITQ_COMPONENT) + if(NOT RABITQ_COMPONENT IN_LIST components) + message(FATAL_ERROR "RABITQ_COMPONENT must be core or fastscan") + endif() + set(components "${RABITQ_COMPONENT}") +endif() +foreach(component IN LISTS components) set(scan OFF) if(component STREQUAL "fastscan") set(scan ON) @@ -87,6 +96,12 @@ foreach(component IN ITEMS core fastscan) endforeach() endforeach() +# The core partition owns the core-only rejection checks and core benchmark. +if(NOT "core" IN_LIST components) + message(STATUS "FastScan external consumers passed") + return() +endif() + # Configuration must fail early for combinations that cannot provide full functionality. execute_process(COMMAND "${CMAKE_COMMAND}" -S "${RABITQ_SOURCE_DIR}" -B "${WORK_DIR}/invalid-python" -DRABITQ_BUILD_INDEX=OFF @@ -138,4 +153,4 @@ run("${CMAKE_COMMAND}" -S "${RABITQ_SOURCE_DIR}/benchmarks/core" run("${CMAKE_COMMAND}" --build "${WORK_DIR}/benchmark" --config Release --parallel 2) run("${CMAKE_CTEST_COMMAND}" --test-dir "${WORK_DIR}/benchmark" -C Release --output-on-failure) -message(STATUS "Core and FastScan external consumers and core benchmark passed") +message(STATUS "External consumers and core benchmark passed for: ${components}")