From b9a93f810fd385fdfaa8c37dac2ade16afa63ee8 Mon Sep 17 00:00:00 2001 From: Jonathan Kashi Date: Tue, 1 Sep 2026 17:23:41 -0400 Subject: [PATCH 1/2] CI: full validation matrix on every push, with sanitizers Six GitHub Actions jobs: Linux gcc, Linux clang, native arm64 (real ARM silicon via GitHub's free arm64 runners - the job also runs the clustered benchmark and uploads it, so ARM performance numbers are measured on hardware, not emulated), Windows MinGW-w64 (MSYS2/UCRT64), ASan+UBSan across all five suites, and ThreadSanitizer on the concurrency suite (with the mmap_rnd_bits=28 sysctl the TSan runtime needs on modern kernels). Debug and -DNDEBUG configurations everywhere. Local pre-flight: ASan+UBSan pass all five suites with zero findings. TSan is environmentally blocked under WSL2 (its injected interop mappings trip "unexpected memory mapping" at startup regardless of ASLR settings), so CI is the TSan executor of record. Test shims: the allocation counters in test_hnsw_graph.cpp and test_integration.cpp are now std::atomic - worker threads allocate their contexts during concurrent-build tests, and the plain size_t increment was a genuine (harness-side) data race TSan would have flagged. README gains the CI badge. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017ULiWeodALX2ZQiLKvTw25 --- .github/workflows/ci.yml | 103 +++++++++++++++++++++++++++++++++++++ README.md | 2 + tests/test_hnsw_graph.cpp | 6 ++- tests/test_integration.cpp | 3 +- 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..773f33a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,103 @@ +# EdgeVector CI: the full validation matrix on every push/PR. +# +# Six jobs: Linux gcc, Linux clang, native arm64 (real ARM silicon - also +# runs the clustered benchmark so ARM performance numbers are measured, not +# emulated), Windows MinGW, ASan+UBSan, and TSan on the concurrency suite. +# Every suite runs in both the assert-enabled and -DNDEBUG configurations. +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + linux-gcc: + name: Linux x86-64 (gcc) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Test (debug) + run: make -C tests run + - name: Test (release, -DNDEBUG) + run: make -C tests run-release + + linux-clang: + name: Linux x86-64 (clang) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Test (debug) + run: make -C tests run CXX=clang++ + - name: Test (release, -DNDEBUG) + run: make -C tests run-release CXX=clang++ + + linux-arm64: + name: Linux arm64 (native silicon) + runs-on: ubuntu-24.04-arm + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - name: Test (debug) + run: make -C tests run + - name: Test (release, -DNDEBUG) + run: make -C tests run-release + - name: Benchmark on real ARM silicon (clustered scenario) + run: | + make -C tests benchmark_100k + cd tests && ./benchmark_100k clustered | tee bench_arm64.md + - uses: actions/upload-artifact@v4 + with: + name: bench-arm64 + path: tests/bench_arm64.md + + windows-mingw: + name: Windows (MinGW-w64) + runs-on: windows-latest + timeout-minutes: 40 + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v4 + - uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: false + install: mingw-w64-ucrt-x86_64-gcc make + - name: Test (debug) + run: make -C tests run + - name: Test (release, -DNDEBUG) + run: make -C tests run-release + + asan-ubsan: + name: ASan + UBSan + runs-on: ubuntu-24.04 + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + - name: Test under AddressSanitizer + UndefinedBehaviorSanitizer + run: > + make -C tests run + CXXFLAGS="-std=c++17 -O1 -g -march=native + -fsanitize=address,undefined -fno-sanitize-recover=all + -Wall -Wextra -Werror" + + tsan: + name: ThreadSanitizer (concurrent build + queries) + runs-on: ubuntu-24.04 + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + - name: Lower ASLR entropy (TSan is incompatible with 32-bit mmap_rnd_bits) + run: sudo sysctl vm.mmap_rnd_bits=28 + - name: HNSW suite under ThreadSanitizer + run: | + cd tests + g++ -std=c++17 -O1 -g -march=native -fsanitize=thread \ + -Wall -Wextra -Werror -I../include \ + test_hnsw_graph.cpp -o test_hnsw_graph_tsan + ./test_hnsw_graph_tsan diff --git a/README.md b/README.md index 534bbce..5979902 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # EdgeVector +[![CI](https://github.com/JonathanKash/EdgeVector/actions/workflows/ci.yml/badge.svg)](https://github.com/JonathanKash/EdgeVector/actions/workflows/ci.yml) + A header-only C++17 vector search library for edge devices: **binary quantization + HNSW + memory-mapped storage** in three headers, ~2,000 lines, zero dependencies — with a two-stage retrieval pipeline that reaches diff --git a/tests/test_hnsw_graph.cpp b/tests/test_hnsw_graph.cpp index 6448101..0cd6829 100644 --- a/tests/test_hnsw_graph.cpp +++ b/tests/test_hnsw_graph.cpp @@ -29,9 +29,11 @@ // --------------------------------------------------------------------------- // Global allocation counter. Every path into the C++ free store goes through // these replacements, so a zero delta across search() calls is hard evidence -// of a zero-allocation query path, not a code-reading claim. +// of a zero-allocation query path, not a code-reading claim. Atomic because +// worker threads allocate (their contexts) during concurrent-build tests. // --------------------------------------------------------------------------- -static std::size_t g_new_calls = 0; +#include +static std::atomic g_new_calls{0}; // The malloc/free pairing below is the canonical way to replace the global // allocator; GCC 13+'s -Wmismatched-new-delete cannot see that these are the diff --git a/tests/test_integration.cpp b/tests/test_integration.cpp index e71d5b8..8c326ac 100644 --- a/tests/test_integration.cpp +++ b/tests/test_integration.cpp @@ -27,7 +27,8 @@ // Counting operator new (same shim as test_hnsw_graph.cpp): proves the search // path stays allocation-free when reading vectors straight out of the mapping. -static std::size_t g_new_calls = 0; +#include +static std::atomic g_new_calls{0}; // See test_hnsw_graph.cpp: GCC 13+'s -Wmismatched-new-delete misfires on the // canonical malloc/free allocator replacement; silenced for the shim only. From a8a992341850fe9a0128d5280111a8e5e771fd38 Mon Sep 17 00:00:00 2001 From: Jonathan Kashi Date: Tue, 1 Sep 2026 17:27:13 -0400 Subject: [PATCH 2/2] README: real-ARM silicon results from CI; retire the last ARM caveat The arm64 CI job ran the clustered benchmark on native ARM hardware: 0.995 float-exact recall@10 at 166 us/query (~6,000 QPS single thread), 4-thread 100k build in 9.5 s, reclaim 1.8 ms, graph load 0.018 s - with every recall figure bit-identical to x86-64. The "ARM performance pending real silicon" limitation is removed from the roadmap; CI now measures it on every push. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017ULiWeodALX2ZQiLKvTw25 --- README.md | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 5979902..f150922 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,11 @@ zero dependencies — with a two-stage retrieval pipeline that reaches - **Learned ITQ rotation** for anisotropic (real-embedding-shaped) data: +21 recall points at the same beam width, or the same accuracy at a fraction of the compute — while preserving cosine exactly -- **Validated on Linux (POSIX mmap), Windows (MinGW-w64), and AArch64** - (all suites pass under QEMU emulation with native arm64 g++; the Hamming - kernel compiles to NEON `cnt`), debug and `-DNDEBUG` release, always under - `-O3 -Wall -Wextra -Werror` +- **CI-validated on every push** across Linux gcc + clang, Windows + (MinGW-w64), and **native arm64 silicon** — where the benchmark runs on + real ARM hardware: 0.995 float-recall at 166 µs/query (~6,000 QPS, + 1 thread) — plus ASan/UBSan on all suites and **ThreadSanitizer on the + concurrent build**, debug and `-DNDEBUG`, always `-Wall -Wextra -Werror` ## The accuracy architecture @@ -358,12 +359,13 @@ hostile-file rejection, end-to-end recall gain), concurrent construction the full referential-integrity sweep and the recall gate), and the zero-allocation proof for all three search modes. -**AArch64:** `tests/arm64.Dockerfile` reproduces the ARM validation — all -five suites compiled by native arm64 g++ 13 at `-march=armv8-a -Werror` and -executed under QEMU user-mode emulation, with the Hamming kernel confirmed to -compile to NEON `cnt` (vector popcount) instructions. Emulated timings are -meaningless, so ARM *performance* claims wait for real silicon; correctness — -including bit-identical ITQ training results vs x86-64 — is validated. +**AArch64:** validated on **real arm64 silicon** in CI on every push (the +Hamming kernel compiles to NEON `cnt`), with the clustered benchmark run on +hardware and uploaded as an artifact. Measured there (4-core runner, single- +thread queries): 0.995 float-exact recall@10 at 166 µs/query, 4-thread build +of 100k vectors in 9.5 s, slot reclaim 1.8 ms, graph load 0.018 s — and every +recall figure bit-identical to x86-64. `tests/arm64.Dockerfile` reproduces +the ARM run locally under QEMU when no ARM hardware is at hand. ## Status and roadmap @@ -377,13 +379,7 @@ v0.8. Known limitations, in priority order: an mmap'ed block means writing and remapping a larger file 2. **Highly selective filters degrade** toward a scan of the reachable graph (true of every filtered-HNSW implementation; documented, not hidden) -3. **ARM validated for correctness, not yet for performance** — all suites - pass on aarch64 under QEMU emulation (including the 4-thread build and - query tests) with NEON popcount codegen confirmed, but latency/QPS - numbers on real ARM silicon are still pending; QEMU on an x86 host also - only partially exercises ARM's weaker memory model (the build's - correctness rests on mutex ordering, not x86 TSO, by design) -4. **Parallel builds are nondeterministic** in link structure (insertion +3. **Parallel builds are nondeterministic** in link structure (insertion order interleaves); use serial `insert()` or `insert_batch(..., 1)` when bit-reproducible graphs matter