Skip to content

test: make graph tests independent of graph connectivity - #228

Merged
gouyt13 merged 1 commit into
mainfrom
test/216-graph-test-connectivity
Nov 12, 2026
Merged

gouyt13 merged 1 commit into
mainfrom
test/216-graph-test-connectivity

Conversation

@gouyt13

@gouyt13 gouyt13 commented Nov 12, 2026

Copy link
Copy Markdown
Member

Closes #216

HNSW-RaBitQ, SymphonyQG and the hnswlib graph behind IVF-RaBitQ's HNSW routing do not guarantee that every point is reachable. A multi-threaded or unseeded build can leave a point with no incoming edge, so tests that require every point to be found fail now and then in CI. #227 fixed two Python cases. This PR finishes the audit of the graph tests in tests/unit/rabitqlib/index/ and tests/python/.

Changes

Two approaches, depending on what a test checks.

  • Exact checks → fixed seed and one thread.
    • test_large_dimensions.py: SymphonyQG is seeded and builds with one thread. HNSW already used one thread and the default seed.
    • test_symqg.py: the shared built_symqg fixture uses the 0.8 form with seed=42 and one thread. The rebuild case passes seed=42 to the 0.7 keyword form.
    • test_portable_indexes.py: the SymphonyQG case uses the 0.8 form with seed=42 and one thread.
    • test_hnsw.py: three cases pass num_threads=1 to add, where the Python default is every CPU:
      • the dense-labels case compares the first ten points' results before and after add;
      • the resize case requires those points afterwards;
      • the remove case compares graphs grown separately from two loaded copies.
  • Parallel construction → keep the threads, check self-recall of every point.
    • test_examples.py: the example scripts build graphs with two threads, and SymphonyQG also has no fixed seed. HNSW and SymphonyQG now require a self-recall of at least 95% over all 128 points. IVF keeps its exact check.
    • test_ivf_initializers.py: the parallel HNSW-routing build over 1,024 centroids requires a self-recall of at least 99% over every point.

Tests that stay exact

Two tests listed in #216 keep their exact checks, although they build with several threads:

  • HnswAddTest.ParallelAddKeepsLabelsAndTheGraph;
  • the Python test_parallel_build_preserves_self_retrieval.

Both set M to the point count. The base layer therefore holds 2M ≥ N − 1 links, so mutually_connect never prunes a reverse link, and every point stays reachable under any thread interleaving. Only their comments change, to say this. The same holds for the dense-graph add test, which keeps the default thread count, and for hnswlib routing over 32 or 33 centroids (kM = 16, so maxM0_ = 32).

An aggregate check would have weakened these tests. An experimental build that drops 5 of 512 reverse links passed a 99% self-recall check, and failed the exact one.

Relation to #214

test_hnsw.py, test_symqg.py, test_portable_indexes.py and test_ivf_initializers.py are still on the #214 list. This PR changes only the cases above. In test_symqg.py and test_portable_indexes.py, the SymphonyQG fixture and the portable case already use the 0.8 form, so that they can be seeded.

Checks

All on Linux x86-64, Xeon Gold 6418H (96 CPUs), GCC 14.3, Release, RABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF; the Python package was built from this branch.

  • Full suites: C++ 499/499; Python 1146 passed, 2 skipped. check-format.sh and check-python.sh pass.

  • Loops of the changed and restored tests, all with 0 failures:

    Test Runs
    HnswAddTest.ParallelAddKeepsLabelsAndTheGraph 1,000 on 96 CPUs, 1,000 restricted to 4 CPUs
    changed test_hnsw.py cases, test_symqg.py, test_portable_indexes.py, the parallel routing case, test_large_dimensions.py 1,008
    the two dense HNSW Python cases 200, restricted to 4 CPUs
    the HNSW and SymphonyQG example cases 208
    test_large_dimensions.py 1,000 more
  • Still fails when broken: a search returning wrong IDs fails every changed check. The exact checks also catch a lost reverse link, as described above. Apart from that experiment, the Python changes were not mutation-tested.

  • Review: an independent review found that a first version had weakened the two tests above, and had changed routing tests that cannot fail. Those changes were reverted.

Not covered

HNSW-RaBitQ, SymphonyQG and hnswlib-based IVF routing do not guarantee
that every point is reachable. Tests that required every point after a
multi-threaded or unseeded build failed now and then in CI.

Tests that check exact results now build with a fixed seed and one
thread: the large-dimension round trip, the SymphonyQG fixture,
rebuild and portable cases, and the HNSW add, resize and remove cases
whose searches compare graphs. Tests about parallel construction keep
their threads and check the self-recall of every point instead: the
example scripts, and IVF HNSW routing over 1,024 centroids.

Tests whose graphs keep every reverse link stay exact. With M equal to
the point count, the base layer holds 2M >= N - 1 links and never
prunes one, so every point stays reachable under any thread
interleaving. Their comments now say so.
@github-actions github-actions Bot added the release:maintenance Release category managed from the PR title label Nov 12, 2026
@gouyt13
gouyt13 merged commit 1e8dce8 into main Nov 12, 2026
40 checks passed
@gouyt13
gouyt13 deleted the test/216-graph-test-connectivity branch November 12, 2026 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:maintenance Release category managed from the PR title

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make graph-index tests independent of graph connectivity

1 participant