Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
32 changes: 15 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -34,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

Expand Down Expand Up @@ -356,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

Expand All @@ -375,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

Expand Down
6 changes: 4 additions & 2 deletions tests/test_hnsw_graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <atomic>
static std::atomic<std::size_t> 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
Expand Down
3 changes: 2 additions & 1 deletion tests/test_integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <atomic>
static std::atomic<std::size_t> 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.
Expand Down
Loading