epzero 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epzero-0.5.0/CHANGELOG.md +129 -0
- {epzero-0.3.0 → epzero-0.5.0}/CMakeLists.txt +52 -23
- epzero-0.5.0/PKG-INFO +242 -0
- {epzero-0.3.0 → epzero-0.5.0}/bench/bench_recall.cpp +34 -3
- epzero-0.5.0/bench/regression.json +58 -0
- epzero-0.5.0/fuzz/fuzz_metadata.cpp +23 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/make_seeds.cpp +29 -2
- epzero-0.5.0/include/epzero/auto.h +109 -0
- {epzero-0.3.0 → epzero-0.5.0}/include/epzero/database.h +1 -0
- {epzero-0.3.0 → epzero-0.5.0}/include/epzero/distance.h +11 -0
- {epzero-0.3.0 → epzero-0.5.0}/include/epzero/hnsw.h +112 -1
- epzero-0.5.0/include/epzero/metadata.h +92 -0
- {epzero-0.3.0 → epzero-0.5.0}/include/epzero/text.h +13 -3
- {epzero-0.3.0 → epzero-0.5.0}/pyproject.toml +10 -4
- epzero-0.5.0/python/README.md +206 -0
- epzero-0.5.0/python/epzero/__init__.py +45 -0
- epzero-0.5.0/python/epzero/auto.py +149 -0
- epzero-0.5.0/python/epzero/haystack.py +283 -0
- epzero-0.5.0/python/epzero/ir.py +257 -0
- {epzero-0.3.0 → epzero-0.5.0}/python/epzero/langchain.py +69 -8
- epzero-0.5.0/python/epzero/llamaindex.py +243 -0
- epzero-0.5.0/python/epzero_py.cpp +1106 -0
- epzero-0.5.0/python/tests/test_auto.py +238 -0
- {epzero-0.3.0 → epzero-0.5.0}/python/tests/test_epzero.py +78 -2
- epzero-0.5.0/python/tests/test_haystack.py +74 -0
- epzero-0.5.0/python/tests/test_ir.py +128 -0
- {epzero-0.3.0 → epzero-0.5.0}/python/tests/test_langchain.py +17 -0
- epzero-0.5.0/python/tests/test_llamaindex.py +91 -0
- epzero-0.5.0/python/tests/test_metadata.py +67 -0
- epzero-0.5.0/src/auto.cpp +430 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/brute_force.cpp +6 -1
- {epzero-0.3.0 → epzero-0.5.0}/src/database.cpp +103 -31
- {epzero-0.3.0 → epzero-0.5.0}/src/distance.cpp +64 -21
- {epzero-0.3.0 → epzero-0.5.0}/src/distance_avx2.cpp +24 -1
- {epzero-0.3.0 → epzero-0.5.0}/src/distance_avxvnni.cpp +24 -1
- epzero-0.5.0/src/distance_f16c.cpp +72 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/distance_neon.cpp +36 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/distance_scalar.cpp +16 -0
- epzero-0.5.0/src/half.h +60 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/hnsw.cpp +568 -65
- {epzero-0.3.0 → epzero-0.5.0}/src/kernels.h +7 -0
- epzero-0.5.0/src/metadata.cpp +295 -0
- epzero-0.5.0/src/pca.h +206 -0
- epzero-0.5.0/src/platform.h +154 -0
- epzero-0.5.0/src/porter.h +233 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/recall.cpp +8 -2
- {epzero-0.3.0 → epzero-0.5.0}/src/text.cpp +36 -8
- epzero-0.5.0/tests/auto_test.cpp +345 -0
- epzero-0.5.0/tests/data/v4-from-0.4.0.epzero +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/tests/database_test.cpp +33 -2
- epzero-0.5.0/tests/half_test.cpp +157 -0
- {epzero-0.3.0 → epzero-0.5.0}/tests/hnsw_test.cpp +201 -2
- epzero-0.5.0/tests/metadata_test.cpp +116 -0
- {epzero-0.3.0 → epzero-0.5.0}/tests/text_test.cpp +32 -0
- {epzero-0.3.0 → epzero-0.5.0}/wasm/epzero_wasm.cpp +52 -2
- epzero-0.3.0/PKG-INFO +0 -114
- epzero-0.3.0/python/README.md +0 -85
- epzero-0.3.0/python/epzero/__init__.py +0 -36
- epzero-0.3.0/python/epzero_py.cpp +0 -581
- {epzero-0.3.0 → epzero-0.5.0}/bench/bench_adaptive.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/bench/bench_churn.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/bench/bench_distance.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/bench/bench_filter.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/bench/bench_guard.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/common.h +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/fuzz_graph.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/fuzz_snapshot.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/fuzz_text.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/fuzz_wal.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/regressions/graph-empty-with-entry-point +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/regressions/graph-entry-level-overflow +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/regressions/graph-upper-link-to-base-node +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/regressions/snapshot-empty-with-entry-point +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/fuzz/regressions/snapshot-huge-capacity +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/include/epzero/brute_force.h +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/include/epzero/dataset.h +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/include/epzero/parallel.h +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/include/epzero/recall.h +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/server/node_main.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/dataset.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/distance_avx512.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/distance_avx512vnni.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/distance_wasm.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/rabitq.h +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/src/visited_pool.h +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/tests/dataset_test.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/tests/distance_test.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/tests/recall_test.cpp +0 -0
- {epzero-0.3.0 → epzero-0.5.0}/tools/export_graph.cpp +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
An index that stays accurate as its data changes, fixed in place; metadata and keyword search in the
|
|
6
|
+
engine; LlamaIndex and Haystack beside LangChain; retrieval evaluation; and Windows. Files written by
|
|
7
|
+
0.4 load unchanged.
|
|
8
|
+
|
|
9
|
+
**Staying accurate after the build.**
|
|
10
|
+
|
|
11
|
+
- `index.recalibrate(queries)` (C++: `epzero::recalibrate`): re-measures the beam width for the
|
|
12
|
+
recall target on real queries and saves it in the profile, without rebuilding.
|
|
13
|
+
- `index.health()` (C++: `check_health`): live and deleted vectors, growth since calibration, and how
|
|
14
|
+
much of the data the principal directions still capture, with advice in plain words: compact,
|
|
15
|
+
recalibrate, or rebuild.
|
|
16
|
+
- `RecallGuard(index)` takes its target and starting beam width from the profile.
|
|
17
|
+
- `index.check_queries(queries)` (C++: `check_queries`) says whether queries look like the data: text
|
|
18
|
+
queries against image vectors sit 2.2-2.3× farther from their nearest neighbours than rows of the data,
|
|
19
|
+
against 0.99-1.09× for queries like the data, and recall calibrated on the data then falls to 0.74-0.80
|
|
20
|
+
for a 0.95 target. Its advice: `recalibrate(queries)`, which restored 0.945-0.958 on VIBE's
|
|
21
|
+
text-to-image sets.
|
|
22
|
+
- `index.requantize()` (C++: `HnswIndex::requantize`, then `refine_links`): for vectors added since the
|
|
23
|
+
build that lie outside the ranges the 8-bit codes were fitted on. It refits the ranges on the live
|
|
24
|
+
vectors, re-encodes them and reconnects the graph, in about half a rebuild's time; after a 4 sd drift
|
|
25
|
+
it brought the beam width for recall 0.95 from 200 back to a rebuild's 12. `health()` reports
|
|
26
|
+
`code_error` and advises it above 0.01.
|
|
27
|
+
- `index.get_vector(label)` returns a stored vector.
|
|
28
|
+
|
|
29
|
+
**Information retrieval.** `epzero.ir`: a `Collection` of documents with string ids, retrieved by vector,
|
|
30
|
+
by keyword (BM25) or both (reciprocal rank fusion), growing and shrinking after the build; TREC run files
|
|
31
|
+
in and out; relevance judgements in TREC or BEIR form; nDCG@k, MAP, recall, precision and MRR, matching
|
|
32
|
+
trec_eval (tested against it); and a BEIR dataset loader.
|
|
33
|
+
|
|
34
|
+
**Stemming.** `TextIndex` reduces English words to their Porter stems, so "connections" matches
|
|
35
|
+
"connected" (`stemmer="none"` keeps words as written). It is the reference implementation's algorithm,
|
|
36
|
+
the one Lucene and Anserini use, and agrees with NLTK's on all 39,011 words of SciFact's and NFCorpus's
|
|
37
|
+
vocabularies. nDCG@10 for BM25 went from 0.660 to 0.688 on SciFact and from 0.305 to 0.321 on NFCorpus
|
|
38
|
+
(BEIR's Anserini baselines: 0.665 and 0.325), and for hybrid search with all-MiniLM-L6-v2 from 0.682 to
|
|
39
|
+
0.713 and from 0.344 to 0.354. Stemmed text indexes are saved as text format 2; files saved before load
|
|
40
|
+
unstemmed, as they were written.
|
|
41
|
+
|
|
42
|
+
**Fixes.** `hybrid_search` searched with a fixed beam width of 64 and the default re-rank, ignoring the
|
|
43
|
+
index's calibrated profile; it now uses the profile unless `ef` is given. Calibration could settle on a
|
|
44
|
+
beam width below k, which only k-sized searches had measured.
|
|
45
|
+
|
|
46
|
+
**File format 5.** Indexes with a profile or 16-bit vectors are written as version 5; 0.4's version 4
|
|
47
|
+
files are still read (a file written by 0.4.0 is part of the test suite). See `docs/file-format.md`.
|
|
48
|
+
|
|
49
|
+
**Indexes grow.** `add()` no longer fails when an index is full: it grows the capacity (at least
|
|
50
|
+
doubling), copying the nodes and keeping the graph; `index.reserve(n)` and `index.capacity` do it ahead.
|
|
51
|
+
The LangChain, LlamaIndex and Haystack stores, which rebuilt their whole graph each time they filled, now
|
|
52
|
+
grow in place: loading 40,000 documents in batches of 100 went from 6.0 s to 3.4 s. Searches on other
|
|
53
|
+
threads wait for a growth, which moves the index's memory; with a RecallGuard attached, growing must be
|
|
54
|
+
done beforehand with `reserve()`.
|
|
55
|
+
|
|
56
|
+
**Metadata in the engine.** `epzero.Metadata` (C++: `MetadataIndex`) keeps str, int, float and bool fields
|
|
57
|
+
per label, with an inverted index per field and ordered numeric values, and `select(where)` returns the
|
|
58
|
+
labels that pass ({field: value}, {field: [values]}, or `$eq`, `$ne`, `$in`, `$nin`, `$gt`, `$gte`, `$lt`,
|
|
59
|
+
`$lte`, `$exists`) for a filtered search. The LangChain store now runs dict filters there instead of
|
|
60
|
+
checking every document in Python. Saved in its own file format, fuzzed in CI.
|
|
61
|
+
|
|
62
|
+
**Memory.** `half_vectors=True` keeps the full vectors as 16-bit floats: about half an index's memory,
|
|
63
|
+
at some speed (up to 17% on GIST1M) and recall (0.003-0.004 on OpenAI embeddings), so never chosen
|
|
64
|
+
automatically. Full-precision search is 4-9% faster, as vector addresses are resolved once per query.
|
|
65
|
+
|
|
66
|
+
**LlamaIndex.** `epzero.llamaindex.EpzeroVectorStore` (`pip install "epzero[llamaindex]"`): nodes with
|
|
67
|
+
their text and metadata, filters in the engine, `delete_ref_doc`, and persistence.
|
|
68
|
+
|
|
69
|
+
**Haystack.** `epzero.haystack.EpzeroDocumentStore` and `EpzeroEmbeddingRetriever`
|
|
70
|
+
(`pip install "epzero[haystack]"`): Haystack filters in the engine where the answer is provably the same,
|
|
71
|
+
and Haystack's document-store test suite passes.
|
|
72
|
+
|
|
73
|
+
**Windows.** The engine builds with MSVC and its tests run on Windows in CI; wheels for Windows (x64).
|
|
74
|
+
|
|
75
|
+
**Easier to read.** `Index`, `Metadata`, `TextIndex` and `Database` print what they hold (size, metric,
|
|
76
|
+
compression, calibrated beam width), and shape and name errors say what was passed.
|
|
77
|
+
|
|
78
|
+
## 0.4.0
|
|
79
|
+
|
|
80
|
+
**Automatic configuration.** `epzero.build(vectors, metric=...)` (C++: `epzero::build_auto` in
|
|
81
|
+
`epzero/auto.h`) configures an index from its data and the machine it is built on:
|
|
82
|
+
|
|
83
|
+
- From the data: 8-bit codes, whether to walk the graph on principal directions (at 256 dimensions or
|
|
84
|
+
more, when few directions hold most of the variance), the build passes, the re-rank, and the beam
|
|
85
|
+
width (`ef`) that reaches a recall target, calibrated on rows held out of the graph or on queries
|
|
86
|
+
you pass (`queries=`).
|
|
87
|
+
- From the machine: the prefetch setting, timed on this CPU, and every core.
|
|
88
|
+
- Measured on an Intel Xeon Gold 6226 and an AMD Threadripper PRO 9985WX: as fast as hand-tuned
|
|
89
|
+
settings (within 1-5%), recall 0.949-0.957 on real queries for a 0.95 target, and 1.3-4.1× Glass's
|
|
90
|
+
queries per second on GIST1M, OpenAI DBpedia and DEEP10M.
|
|
91
|
+
- Every setting can be fixed instead (`epzero.build(vectors, nav_dim=0, M=32)`, or `epzero.plan()`,
|
|
92
|
+
edit, build). `index.plan` shows each setting and why it was chosen.
|
|
93
|
+
- The choices are saved inside the index (`IndexProfile`, file format v4), and `search()` uses them
|
|
94
|
+
when called without settings. A loaded index re-applies its prefetch setting only on the same kind
|
|
95
|
+
of CPU (`epzero.machine()` names it).
|
|
96
|
+
|
|
97
|
+
**Faster search and builds.**
|
|
98
|
+
|
|
99
|
+
- `nav_dim`: walk the graph on 8-bit codes of principal directions, re-ranking with the full vectors.
|
|
100
|
+
With the other changes here, 2.8-4.2× Glass's queries per second on GIST1M and 1.6-2.7× on OpenAI
|
|
101
|
+
DBpedia embeddings at equal recall, on Intel and AMD servers.
|
|
102
|
+
- `refine_links()`: a second build pass in which every node searches the finished graph and
|
|
103
|
+
reconnects; `fill_free_links()` gives free link slots to two-hop neighbours.
|
|
104
|
+
- `fast_build`: build on the 8-bit codes.
|
|
105
|
+
- `rerank_z`: stop the re-rank early once the remaining candidates can't reach the top k.
|
|
106
|
+
- Queries are projected onto principal directions on the integer kernel; the AVX2 and AVX-VNNI integer
|
|
107
|
+
kernels finish with vector loads instead of a scalar tail.
|
|
108
|
+
- The prefetch tuner can fetch a record's every cache line; on a Xeon Gold 6226 that was worth up to
|
|
109
|
+
1.7× on some datasets.
|
|
110
|
+
- PCA runs as blocked matrix products (choosing directions 5× faster on GIST1M), training runs on
|
|
111
|
+
every core, and index memory is zeroed on every core, which spreads it across NUMA nodes.
|
|
112
|
+
|
|
113
|
+
**Fixes.**
|
|
114
|
+
|
|
115
|
+
- `Database`: a write that could not be applied (the database full, or not yet trained) was written to
|
|
116
|
+
the log first, after which every open failed replaying it. Writes are now checked before they are
|
|
117
|
+
logged, and replacing a vector in a full database frees its slot before compacting.
|
|
118
|
+
- `RecallGuard` searched, and measured its reference, with the default re-rank depth; it now uses the
|
|
119
|
+
index's own settings and re-ranks the reference's whole beam. `explain()` and `trace()` likewise.
|
|
120
|
+
- The LangChain store copied every stored vector on each add (quadratic in the store's size).
|
|
121
|
+
- `pip install epzero` from source failed on machines with gRPC installed: it tried to build the
|
|
122
|
+
cluster node, whose protocol files aren't in the package.
|
|
123
|
+
|
|
124
|
+
**Compatibility.** Indexes without a profile are still written in the format older versions read
|
|
125
|
+
(v2, or v3 with principal directions); automatically configured ones are v4, which 0.3 cannot load.
|
|
126
|
+
`search()`'s `k`, `ef`, `rerank` and `rerank_z` now default to the index's profile, which for an index
|
|
127
|
+
built by hand is the old defaults (10, 64, 2 × k, off).
|
|
128
|
+
|
|
129
|
+
Earlier releases are described by their git tags (`v0.2.0`, `v0.3.0`).
|
|
@@ -48,9 +48,11 @@ endif()
|
|
|
48
48
|
find_package(Threads REQUIRED)
|
|
49
49
|
|
|
50
50
|
add_library(epzero_engine
|
|
51
|
+
src/auto.cpp
|
|
51
52
|
src/distance.cpp
|
|
52
53
|
src/distance_scalar.cpp
|
|
53
54
|
src/hnsw.cpp
|
|
55
|
+
src/metadata.cpp
|
|
54
56
|
src/recall.cpp
|
|
55
57
|
src/database.cpp
|
|
56
58
|
src/text.cpp
|
|
@@ -59,20 +61,38 @@ add_library(epzero_engine
|
|
|
59
61
|
)
|
|
60
62
|
target_include_directories(epzero_engine PUBLIC include PRIVATE src)
|
|
61
63
|
target_link_libraries(epzero_engine PUBLIC Threads::Threads)
|
|
62
|
-
|
|
64
|
+
if(MSVC)
|
|
65
|
+
target_compile_options(epzero_engine PRIVATE /W3 /utf-8 /permissive- /Zc:__cplusplus)
|
|
66
|
+
target_compile_definitions(epzero_engine PRIVATE _CRT_SECURE_NO_WARNINGS NOMINMAX)
|
|
67
|
+
else()
|
|
68
|
+
target_compile_options(epzero_engine PRIVATE -Wall -Wextra -Wpedantic)
|
|
69
|
+
endif()
|
|
63
70
|
set_target_properties(epzero_engine PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
|
64
71
|
|
|
65
72
|
# SIMD kernels get ISA flags per file; the rest of the library stays baseline so the binary
|
|
66
73
|
# runs on any CPU of the architecture and picks the best kernel at runtime.
|
|
67
74
|
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
|
|
68
|
-
target_sources(epzero_engine PRIVATE src/distance_avx2.cpp src/distance_avx512.cpp)
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
target_sources(epzero_engine PRIVATE src/distance_avx2.cpp src/distance_avx512.cpp src/distance_f16c.cpp)
|
|
76
|
+
if(MSVC) # MSVC allows any intrinsic; /arch only lets the compiler use the ISA on its own
|
|
77
|
+
set_source_files_properties(src/distance_f16c.cpp src/distance_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
|
|
78
|
+
set_source_files_properties(src/distance_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512")
|
|
79
|
+
else()
|
|
80
|
+
set_source_files_properties(src/distance_f16c.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
|
81
|
+
set_source_files_properties(src/distance_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mpopcnt")
|
|
82
|
+
set_source_files_properties(src/distance_avx512.cpp PROPERTIES COMPILE_OPTIONS "-mavx512f")
|
|
83
|
+
endif()
|
|
71
84
|
# VNNI integer kernels, where the toolchain can build them: the compiler must know the
|
|
72
85
|
# instructions (GCC 11+, Clang 12+ for AVX-VNNI) and so must the assembler it hands them to
|
|
73
86
|
# (binutils 2.36+ for AVX-VNNI; RHEL 9's 2.35 doesn't), so each check compiles and links a use.
|
|
74
87
|
include(CheckCXXSourceCompiles)
|
|
75
|
-
|
|
88
|
+
if(MSVC)
|
|
89
|
+
set(EPZERO_AVXVNNI_FLAGS "/arch:AVX2")
|
|
90
|
+
set(EPZERO_AVX512VNNI_FLAGS "/arch:AVX512")
|
|
91
|
+
else()
|
|
92
|
+
set(EPZERO_AVXVNNI_FLAGS "-mavx2 -mavxvnni")
|
|
93
|
+
set(EPZERO_AVX512VNNI_FLAGS "-mavx512f -mavx512bw -mavx512vnni")
|
|
94
|
+
endif()
|
|
95
|
+
set(CMAKE_REQUIRED_FLAGS "${EPZERO_AVXVNNI_FLAGS}")
|
|
76
96
|
check_cxx_source_compiles([[
|
|
77
97
|
#include <immintrin.h>
|
|
78
98
|
int main(int argc, char**) {
|
|
@@ -80,7 +100,7 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
|
|
|
80
100
|
a = _mm256_dpbusd_avx_epi32(a, a, a);
|
|
81
101
|
return _mm256_extract_epi32(a, 0);
|
|
82
102
|
}]] EPZERO_TOOLCHAIN_AVXVNNI)
|
|
83
|
-
set(CMAKE_REQUIRED_FLAGS "
|
|
103
|
+
set(CMAKE_REQUIRED_FLAGS "${EPZERO_AVX512VNNI_FLAGS}")
|
|
84
104
|
check_cxx_source_compiles([[
|
|
85
105
|
#include <immintrin.h>
|
|
86
106
|
int main(int argc, char**) {
|
|
@@ -91,12 +111,14 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
|
|
|
91
111
|
unset(CMAKE_REQUIRED_FLAGS)
|
|
92
112
|
if(EPZERO_TOOLCHAIN_AVXVNNI)
|
|
93
113
|
target_sources(epzero_engine PRIVATE src/distance_avxvnni.cpp)
|
|
94
|
-
|
|
114
|
+
string(REPLACE " " ";" flags "${EPZERO_AVXVNNI_FLAGS}")
|
|
115
|
+
set_source_files_properties(src/distance_avxvnni.cpp PROPERTIES COMPILE_OPTIONS "${flags}")
|
|
95
116
|
set_property(SOURCE src/distance.cpp APPEND PROPERTY COMPILE_DEFINITIONS EPZERO_HAVE_AVXVNNI)
|
|
96
117
|
endif()
|
|
97
118
|
if(EPZERO_TOOLCHAIN_AVX512VNNI)
|
|
98
119
|
target_sources(epzero_engine PRIVATE src/distance_avx512vnni.cpp)
|
|
99
|
-
|
|
120
|
+
string(REPLACE " " ";" flags "${EPZERO_AVX512VNNI_FLAGS}")
|
|
121
|
+
set_source_files_properties(src/distance_avx512vnni.cpp PROPERTIES COMPILE_OPTIONS "${flags}")
|
|
100
122
|
set_property(SOURCE src/distance.cpp APPEND PROPERTY COMPILE_DEFINITIONS EPZERO_HAVE_AVX512VNNI)
|
|
101
123
|
endif()
|
|
102
124
|
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
|
|
@@ -118,8 +140,9 @@ if(EPZERO_BUILD_TESTS)
|
|
|
118
140
|
FetchContent_MakeAvailable(googletest)
|
|
119
141
|
|
|
120
142
|
enable_testing()
|
|
121
|
-
add_executable(epzero_tests tests/distance_test.cpp tests/hnsw_test.cpp tests/recall_test.cpp tests/dataset_test.cpp tests/database_test.cpp tests/text_test.cpp)
|
|
143
|
+
add_executable(epzero_tests tests/auto_test.cpp tests/distance_test.cpp tests/half_test.cpp tests/hnsw_test.cpp tests/metadata_test.cpp tests/recall_test.cpp tests/dataset_test.cpp tests/database_test.cpp tests/text_test.cpp)
|
|
122
144
|
target_link_libraries(epzero_tests PRIVATE epzero_engine GTest::gtest_main)
|
|
145
|
+
target_compile_definitions(epzero_tests PRIVATE EPZERO_TEST_DATA="${CMAKE_CURRENT_SOURCE_DIR}/tests/data")
|
|
123
146
|
if(EMSCRIPTEN)
|
|
124
147
|
# Runs under Node (CMAKE_CROSSCOMPILING_EMULATOR); workers are pre-spawned for the
|
|
125
148
|
# concurrency tests and the real filesystem backs the save/load tests.
|
|
@@ -145,7 +168,7 @@ if(EPZERO_BUILD_BENCH)
|
|
|
145
168
|
endif()
|
|
146
169
|
|
|
147
170
|
if(EPZERO_BUILD_FUZZ)
|
|
148
|
-
foreach(target snapshot graph wal text)
|
|
171
|
+
foreach(target snapshot graph wal text metadata)
|
|
149
172
|
add_executable(fuzz_${target} fuzz/fuzz_${target}.cpp)
|
|
150
173
|
target_link_libraries(fuzz_${target} PRIVATE epzero_engine)
|
|
151
174
|
target_link_options(fuzz_${target} PRIVATE -fsanitize=fuzzer)
|
|
@@ -192,20 +215,26 @@ if(EPZERO_BUILD_NODE AND NOT EMSCRIPTEN)
|
|
|
192
215
|
endif()
|
|
193
216
|
|
|
194
217
|
if(EMSCRIPTEN)
|
|
195
|
-
# ES
|
|
196
|
-
|
|
197
|
-
|
|
218
|
+
# ES modules for the browser demo: `import createEpzero from "./epzero.js"`. epzero-lab.js adds the
|
|
219
|
+
# automatic configuration (build_auto) for the benchmark lab, which is 36 KB more compressed: kept out
|
|
220
|
+
# of the module every other page loads first.
|
|
221
|
+
foreach(web_target epzero_web epzero_lab)
|
|
222
|
+
add_executable(${web_target} wasm/epzero_wasm.cpp)
|
|
223
|
+
target_link_libraries(${web_target} PRIVATE epzero_engine)
|
|
224
|
+
target_link_options(${web_target} PRIVATE
|
|
225
|
+
-lembind -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXPORT_NAME=createEpzero -sENVIRONMENT=web,worker -sSTACK_SIZE=1MB
|
|
226
|
+
# No eval / new Function in the generated bindings, so the site's Content Security Policy can
|
|
227
|
+
# forbid it (only WebAssembly compilation is allowed).
|
|
228
|
+
-sDYNAMIC_EXECUTION=0)
|
|
229
|
+
if(EPZERO_WASM_THREADS)
|
|
230
|
+
# Pre-spawn the workers: the module's own thread blocks while a build runs, so it cannot
|
|
231
|
+
# create new ones on demand.
|
|
232
|
+
target_link_options(${web_target} PRIVATE "-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency,16)")
|
|
233
|
+
endif()
|
|
234
|
+
endforeach()
|
|
198
235
|
set_target_properties(epzero_web PROPERTIES OUTPUT_NAME epzero SUFFIX .js)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
# No eval / new Function in the generated bindings, so the site's Content Security Policy can
|
|
202
|
-
# forbid it (only WebAssembly compilation is allowed).
|
|
203
|
-
-sDYNAMIC_EXECUTION=0)
|
|
204
|
-
if(EPZERO_WASM_THREADS)
|
|
205
|
-
# Pre-spawn the workers: the module's own thread blocks while a build runs, so it cannot
|
|
206
|
-
# create new ones on demand.
|
|
207
|
-
target_link_options(epzero_web PRIVATE "-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency,16)")
|
|
208
|
-
endif()
|
|
236
|
+
set_target_properties(epzero_lab PROPERTIES OUTPUT_NAME epzero-lab SUFFIX .js)
|
|
237
|
+
target_compile_definitions(epzero_lab PRIVATE EPZERO_WASM_LAB_AUTO=1)
|
|
209
238
|
endif()
|
|
210
239
|
|
|
211
240
|
if(EPZERO_BUILD_PYTHON)
|
epzero-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: epzero
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: A fast HNSW vector database in C++20 that configures itself: SIMD search, filtered search, recall monitoring, crash-safe storage
|
|
5
|
+
Keywords: vector database,hnsw,ann,nearest neighbor,embeddings,rag,langchain
|
|
6
|
+
Author: Lajat
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: C++
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Project-URL: Homepage, https://epzero.dev/
|
|
19
|
+
Project-URL: Source, https://github.com/Lazeeez/Epzero
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Provides-Extra: langchain
|
|
23
|
+
Requires-Dist: langchain-core>=0.3; extra == "langchain"
|
|
24
|
+
Provides-Extra: llamaindex
|
|
25
|
+
Requires-Dist: llama-index-core>=0.12; extra == "llamaindex"
|
|
26
|
+
Provides-Extra: haystack
|
|
27
|
+
Requires-Dist: haystack-ai>=2.10; extra == "haystack"
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest; extra == "test"
|
|
30
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
31
|
+
Requires-Dist: langchain-core>=0.3; extra == "test"
|
|
32
|
+
Requires-Dist: langchain-tests>=0.3; extra == "test"
|
|
33
|
+
Requires-Dist: llama-index-core>=0.12; extra == "test"
|
|
34
|
+
Requires-Dist: haystack-ai>=2.10; extra == "test"
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# epzero
|
|
38
|
+
|
|
39
|
+
An HNSW vector database written from scratch in C++20, with Python bindings.
|
|
40
|
+
|
|
41
|
+
- **Configures itself:** `epzero.build(vectors)` picks compression, the graph build and the search
|
|
42
|
+
effort for your data and recall target, times memory prefetching on your CPU, saves the choices
|
|
43
|
+
inside the index and tells you why it made each one. Any setting can be overridden.
|
|
44
|
+
- **Fast search:** hand-written SIMD kernels (AVX2, AVX-512, VNNI, NEON) chosen at runtime, 8-bit
|
|
45
|
+
codes, an optional walk on principal directions, and lock-free reads. On dedicated Intel Xeon Gold
|
|
46
|
+
6226 and AMD EPYC 7513 servers, one thread, at equal recall: 1.4-4.2× the queries per second of
|
|
47
|
+
Glass, the fastest library we compared, on GIST1M, OpenAI DBpedia and DEEP10M, and 1.0-1.3× on
|
|
48
|
+
GloVe-100 (with each dataset's best settings; see the project's scoreboard for every loss).
|
|
49
|
+
- **Filtered search that picks its own strategy:** an exact scan, a filtered graph walk, or a
|
|
50
|
+
two-hop walk (the idea behind ACORN), chosen per query by a cost model.
|
|
51
|
+
- **Recall you can see:** `RecallGuard` measures recall on a sample of live queries with a
|
|
52
|
+
confidence interval and moves the search effort to hold a target; `explain()` says why any given
|
|
53
|
+
item was not returned. With `per_query=True` each query also stops as soon as its answer settles,
|
|
54
|
+
so easy queries cost less and hard ones search further.
|
|
55
|
+
- **Hybrid search:** `TextIndex` (BM25 keyword search, with English stemming) and `hybrid_search()`,
|
|
56
|
+
which merges it with vector search by reciprocal rank fusion.
|
|
57
|
+
- **Retrieval evaluation:** `epzero.ir` keeps documents under string ids, runs dense, keyword and
|
|
58
|
+
hybrid retrieval, writes TREC run files and scores them against relevance judgements exactly as
|
|
59
|
+
trec_eval does. On BEIR with all-MiniLM-L6-v2, hybrid reaches nDCG@10 0.713 on SciFact and 0.354 on
|
|
60
|
+
NFCorpus, above either half alone.
|
|
61
|
+
- **Durable:** `Database` adds a write-ahead log with crash recovery and checkpoints.
|
|
62
|
+
- **8-bit quantization** with exact re-ranking, stopped early per query once the rest can't reach
|
|
63
|
+
the top k.
|
|
64
|
+
- **Stays accurate as data changes:** `health()` says when an index needs recalibrating,
|
|
65
|
+
requantizing, compacting or rebuilding, and each fix runs in place.
|
|
66
|
+
- **LangChain, LlamaIndex and Haystack** stores, each passing its framework's own test suite.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install epzero # the engine
|
|
70
|
+
pip install "epzero[langchain]" # plus the LangChain vector store
|
|
71
|
+
pip install "epzero[llamaindex]" # plus the LlamaIndex vector store
|
|
72
|
+
pip install "epzero[haystack]" # plus the Haystack document store and retriever
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quick start: let it configure itself
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import numpy as np
|
|
79
|
+
import epzero
|
|
80
|
+
|
|
81
|
+
vectors = np.random.rand(100_000, 768).astype(np.float32)
|
|
82
|
+
index = epzero.build(vectors, metric="cosine") # chooses every setting (below)
|
|
83
|
+
labels, distances = index.search(vectors[:5], k=10) # at the effort calibrated for recall 0.95
|
|
84
|
+
print(index.plan) # what it chose, and why
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`build` decides from the data: 8-bit compression, whether to walk on principal directions (high
|
|
88
|
+
dimensions with a concentrated spectrum), the build passes, the re-rank, and the beam width that
|
|
89
|
+
reaches your recall target, measured on rows held out of the graph. From the machine: the SIMD
|
|
90
|
+
kernel, the prefetch setting (timed on this CPU) and every core. Change anything:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
index = epzero.build(vectors, metric="cosine", recall=0.99) # a different target
|
|
94
|
+
index = epzero.build(vectors, metric="cosine", queries=my_queries) # calibrate on real queries
|
|
95
|
+
index = epzero.build(vectors, metric="cosine", nav_dim=0, refine=False, M=32) # override choices
|
|
96
|
+
plan = epzero.plan(vectors, metric="cosine") # look before building, edit, then pass it in
|
|
97
|
+
index = epzero.build(vectors, plan=plan.replace(ef_construction=400))
|
|
98
|
+
labels, distances = index.search(queries, k=50, ef=200) # per-call overrides
|
|
99
|
+
index.save("my.epz") # the settings and reasons are saved inside it
|
|
100
|
+
index = epzero.Index.load("my.epz") # search() uses them again
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`build` returns an ordinary `epzero.Index`: filters, deletes, `explain()` and `RecallGuard` all use
|
|
104
|
+
its settings. A saved index re-applies its prefetch setting only on the same kind of CPU (call
|
|
105
|
+
`tune_prefetch()` on another). The decisions are made in the C++ engine (`epzero/auto.h`), so C++
|
|
106
|
+
users get the same `build_auto()`.
|
|
107
|
+
|
|
108
|
+
If your queries differ from your data (text queries against image vectors, say), pass a few
|
|
109
|
+
hundred of them as `queries=`: recall calibrated on data rows is optimistic for those.
|
|
110
|
+
|
|
111
|
+
**Metadata.** Fields per label, filtered in the engine:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
meta = epzero.Metadata()
|
|
115
|
+
meta.set_many(labels, [{"source": "faq", "year": 2024}, ...])
|
|
116
|
+
allowed = meta.select({"source": ["faq", "docs"], "year": {"$gte": 2022}})
|
|
117
|
+
labels, distances = index.search(queries, k=10, filter=index.filter(allowed))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**After the build.** An index keeps changing; its calibration doesn't, by itself:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
print(index.health()) # growth since calibration, deleted share, code error, fit of the directions, advice
|
|
124
|
+
print(index.check_queries(queries)) # do these queries look like the data? (text vs images: no)
|
|
125
|
+
index.recalibrate(queries) # re-measure the beam width on real queries; saved in the profile
|
|
126
|
+
index.requantize() # vectors drifted outside the 8-bit codes' ranges: refit and reconnect
|
|
127
|
+
guard = epzero.RecallGuard(index, sample_rate=0.01) # or measure live recall and adjust as you go
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
When later vectors drift outside the ranges the 8-bit codes were fitted on, they are clipped and
|
|
131
|
+
the graph links chosen for them suffer: in a synthetic test with a 4 sd drift, the beam width needed
|
|
132
|
+
for recall 0.95 went from 12 to 200. `requantize()` refits the codes and reconnects the graph in about half a rebuild's
|
|
133
|
+
time, back to 12.
|
|
134
|
+
|
|
135
|
+
## Configured by hand
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
vectors = np.random.rand(100_000, 128).astype(np.float32)
|
|
139
|
+
index = epzero.Index(dim=128, metric="l2", max_elements=100_000, M=16, ef_construction=200)
|
|
140
|
+
index.add(vectors) # labels 0..n-1 (or pass labels=); grows as needed
|
|
141
|
+
|
|
142
|
+
labels, distances = index.search(vectors[:5], k=10, ef=64)
|
|
143
|
+
|
|
144
|
+
# Filtered search: compile the allowed labels once, reuse across queries.
|
|
145
|
+
allowed = index.filter(np.arange(0, 100_000, 50))
|
|
146
|
+
labels, distances = index.search(vectors[:5], k=10, filter=allowed)
|
|
147
|
+
|
|
148
|
+
# Hold recall at 0.95 as the workload drifts, checking 1% of queries; per_query=True lets each
|
|
149
|
+
# query stop once its answer settles (cheaper for the same recall).
|
|
150
|
+
guard = epzero.RecallGuard(index, target=0.95, sample_rate=0.01, reference_ef=1024, per_query=True)
|
|
151
|
+
labels, distances = guard.search(vectors[:5], k=10)
|
|
152
|
+
print(guard.report())
|
|
153
|
+
|
|
154
|
+
# Optional: measure the best memory-prefetch setting for this machine and data (not saved).
|
|
155
|
+
index.tune_prefetch(vectors[:500])
|
|
156
|
+
|
|
157
|
+
index.save("vectors.epzero")
|
|
158
|
+
index = epzero.Index.load("vectors.epzero")
|
|
159
|
+
|
|
160
|
+
# Hybrid: keyword search on the same labels, merged with the vector search.
|
|
161
|
+
text = epzero.TextIndex()
|
|
162
|
+
text.add(None, documents) # labels 0..n-1, matching the index
|
|
163
|
+
labels, scores = epzero.hybrid_search(index, text, query_vector, "error E-1042 at checkout", k=10)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## LangChain
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from langchain_openai import OpenAIEmbeddings
|
|
170
|
+
from epzero.langchain import EpzeroVectorStore
|
|
171
|
+
|
|
172
|
+
store = EpzeroVectorStore.from_texts(texts, OpenAIEmbeddings(), metadatas=metadatas)
|
|
173
|
+
store.similarity_search("how do I reset my password?", k=4, filter={"source": "faq"})
|
|
174
|
+
store.max_marginal_relevance_search("pricing", k=4, fetch_k=20)
|
|
175
|
+
store.hybrid_search("error E-1042 at checkout", k=4) # keywords + vectors
|
|
176
|
+
retriever = store.as_retriever(search_kwargs={"k": 8})
|
|
177
|
+
|
|
178
|
+
store.save_local("./store")
|
|
179
|
+
store = EpzeroVectorStore.load_local("./store", OpenAIEmbeddings())
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Metadata filters are a dict of required values (a list means "any of"), or a callable on the
|
|
183
|
+
metadata. They are compiled into an Epzero filter, so the engine chooses how to search the allowed
|
|
184
|
+
documents instead of over-fetching and discarding. The store passes LangChain's standard
|
|
185
|
+
vector-store test suite.
|
|
186
|
+
|
|
187
|
+
## LlamaIndex
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from llama_index.core import StorageContext, VectorStoreIndex
|
|
191
|
+
from epzero.llamaindex import EpzeroVectorStore
|
|
192
|
+
|
|
193
|
+
store = EpzeroVectorStore(metric="cosine")
|
|
194
|
+
index = VectorStoreIndex.from_documents(documents, storage_context=StorageContext.from_defaults(vector_store=store))
|
|
195
|
+
index.as_query_engine().query("How do I reset my password?")
|
|
196
|
+
store.persist("./epzero-store") # EpzeroVectorStore.from_persist_path("./epzero-store")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Metadata filters (`==`, `!=`, `>`, `>=`, `<`, `<=`, `in`, `nin`, combined with AND or OR) run in the
|
|
200
|
+
engine, as do `doc_ids` and `node_ids`; `delete_ref_doc` removes a document's nodes.
|
|
201
|
+
|
|
202
|
+
## Haystack
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from haystack import Pipeline
|
|
206
|
+
from epzero.haystack import EpzeroDocumentStore, EpzeroEmbeddingRetriever
|
|
207
|
+
|
|
208
|
+
store = EpzeroDocumentStore(metric="cosine")
|
|
209
|
+
store.write_documents(documents) # Documents with embeddings
|
|
210
|
+
pipeline = Pipeline()
|
|
211
|
+
pipeline.add_component("retriever", EpzeroEmbeddingRetriever(document_store=store, top_k=5))
|
|
212
|
+
pipeline.run({"retriever": {"query_embedding": embedding,
|
|
213
|
+
"filters": {"field": "meta.lang", "operator": "==", "value": "en"}}})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Filters use Haystack's syntax and run in the engine wherever that gives exactly Haystack's answer;
|
|
217
|
+
the rest (ranges on dates, `None`) go to Haystack's reference implementation. The store passes
|
|
218
|
+
Haystack's document-store test suite.
|
|
219
|
+
|
|
220
|
+
## Retrieval evaluation
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from epzero import ir
|
|
224
|
+
|
|
225
|
+
corpus, queries, qrels = ir.load_beir("scifact") # a BEIR dataset folder
|
|
226
|
+
collection = ir.Collection.build(list(corpus), doc_vectors, texts=list(corpus.values()), metric="cosine")
|
|
227
|
+
run = collection.retrieve(list(queries), vectors=query_vectors, texts=list(queries.values()),
|
|
228
|
+
k=100, mode="hybrid") # or "dense", "keyword"
|
|
229
|
+
ir.write_run(run, "run.txt", tag="my-system") # TREC format, for CLEF or TREC
|
|
230
|
+
print(ir.evaluate(run, qrels)) # nDCG@10, R@100, MAP, MRR@10
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`evaluate` matches trec_eval (tested against it, ties included) and also takes `per_query=True` and
|
|
234
|
+
measures like `P@5` or `nDCG@20`. Documents can be added, replaced and removed by id, and a collection
|
|
235
|
+
saves to a folder.
|
|
236
|
+
|
|
237
|
+
## Platforms
|
|
238
|
+
|
|
239
|
+
Wheels for Linux (x86-64 and ARM64, glibc 2.28+), macOS (Apple silicon and Intel) and, from 0.5,
|
|
240
|
+
Windows (x64), Python 3.10 to 3.13. Elsewhere, `pip install epzero` builds from source (CMake 3.20+, a C++20 compiler).
|
|
241
|
+
|
|
242
|
+
Live demo, benchmarks and design notes: https://epzero.dev/ · MIT license.
|
|
@@ -40,6 +40,16 @@ struct Options {
|
|
|
40
40
|
std::size_t repeat = 1; // passes over the query set, so many-thread runs last long enough to time
|
|
41
41
|
Quantization quantization = Quantization::None;
|
|
42
42
|
bool keep_vectors = true;
|
|
43
|
+
std::size_t nav_dim = 0;
|
|
44
|
+
bool fill_links = false;
|
|
45
|
+
bool fast_build = false;
|
|
46
|
+
bool half = false;
|
|
47
|
+
bool fill_after = false;
|
|
48
|
+
std::size_t refine = 0;
|
|
49
|
+
bool refine_exact = false;
|
|
50
|
+
std::uint64_t seed = 42;
|
|
51
|
+
float rerank_z = 0.0f;
|
|
52
|
+
std::size_t refine_ef = 0;
|
|
43
53
|
std::size_t rerank = 0;
|
|
44
54
|
std::size_t prefetch_depth = 0, prefetch_lines = 0; // 0: the index's default
|
|
45
55
|
std::vector<std::size_t> ef = {10, 20, 40, 80, 160, 320};
|
|
@@ -86,6 +96,16 @@ Options parse(int argc, char** argv) {
|
|
|
86
96
|
}
|
|
87
97
|
else if (arg == "--no-keep") o.keep_vectors = false;
|
|
88
98
|
else if (arg == "--rerank") o.rerank = std::stoul(value());
|
|
99
|
+
else if (arg == "--nav-dim") o.nav_dim = std::stoul(value());
|
|
100
|
+
else if (arg == "--fill-links") o.fill_links = true;
|
|
101
|
+
else if (arg == "--fast-build") o.fast_build = true;
|
|
102
|
+
else if (arg == "--half") o.half = true;
|
|
103
|
+
else if (arg == "--fill-after") o.fill_after = true;
|
|
104
|
+
else if (arg == "--refine") o.refine = std::stoul(value());
|
|
105
|
+
else if (arg == "--refine-exact") o.refine_exact = true;
|
|
106
|
+
else if (arg == "--seed") o.seed = std::stoull(value());
|
|
107
|
+
else if (arg == "--rerank-z") o.rerank_z = std::stof(value());
|
|
108
|
+
else if (arg == "--refine-ef") o.refine_ef = std::stoul(value());
|
|
89
109
|
else if (arg == "--prefetch") { // depth,lines
|
|
90
110
|
const auto v = parse_list(value());
|
|
91
111
|
if (v.size() != 2) throw std::invalid_argument("--prefetch takes depth,lines");
|
|
@@ -171,8 +191,9 @@ int main(int argc, char** argv) try {
|
|
|
171
191
|
std::printf("load %s in %.2fs\n", opt.load.c_str(), seconds_since(t0));
|
|
172
192
|
} else {
|
|
173
193
|
HnswParams params{.dim = dim, .metric = opt.metric, .max_elements = base.n, .M = opt.M,
|
|
174
|
-
.ef_construction = opt.ef_construction, .quantization = opt.quantization,
|
|
175
|
-
.keep_vectors = opt.keep_vectors
|
|
194
|
+
.ef_construction = opt.ef_construction, .seed = opt.seed, .quantization = opt.quantization,
|
|
195
|
+
.keep_vectors = opt.keep_vectors, .nav_dim = opt.nav_dim, .fill_links = opt.fill_links,
|
|
196
|
+
.fast_build = opt.fast_build, .half_vectors = opt.half};
|
|
176
197
|
loaded = std::make_unique<HnswIndex>(params);
|
|
177
198
|
t0 = Clock::now();
|
|
178
199
|
loaded->train(base.data.data(), std::min<std::size_t>(base.n, 100'000)); // no-op unquantized
|
|
@@ -184,6 +205,16 @@ int main(int argc, char** argv) try {
|
|
|
184
205
|
});
|
|
185
206
|
}
|
|
186
207
|
for (auto& b : builders) b.join();
|
|
208
|
+
for (std::size_t pass = 0; pass < opt.refine; ++pass) {
|
|
209
|
+
const double refine_at = seconds_since(t0);
|
|
210
|
+
loaded->refine_links(opt.threads, opt.refine_ef, opt.refine_exact);
|
|
211
|
+
std::printf("refined pass %zu in %.2fs\n", pass + 1, seconds_since(t0) - refine_at);
|
|
212
|
+
}
|
|
213
|
+
if (opt.fill_after) {
|
|
214
|
+
const double links_at = seconds_since(t0);
|
|
215
|
+
const std::size_t added = loaded->fill_free_links(opt.threads);
|
|
216
|
+
std::printf("filled %zu links in %.2fs\n", added, seconds_since(t0) - links_at);
|
|
217
|
+
}
|
|
187
218
|
build_s = seconds_since(t0);
|
|
188
219
|
std::printf("build %.2fs, %.0f inserts/s (%zu threads, M=%zu, efc=%zu)\n", build_s, base.n / build_s,
|
|
189
220
|
opt.threads, opt.M, opt.ef_construction);
|
|
@@ -230,7 +261,7 @@ int main(int argc, char** argv) try {
|
|
|
230
261
|
const std::size_t q = i % queries.n;
|
|
231
262
|
SearchStats stats;
|
|
232
263
|
const auto start = Clock::now();
|
|
233
|
-
auto found = index.search(queries.row(q), opt.k, {.ef = ef, .rerank = opt.rerank, .stats = &stats});
|
|
264
|
+
auto found = index.search(queries.row(q), opt.k, {.ef = ef, .rerank = opt.rerank, .rerank_z = opt.rerank_z, .stats = &stats});
|
|
234
265
|
latencies[i] = std::chrono::duration<double, std::micro>(Clock::now() - start).count();
|
|
235
266
|
distances_per_query[i] = stats.distance_computations;
|
|
236
267
|
full_per_query[i] = stats.full_distance_computations;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"float-l2": {
|
|
3
|
+
"4": {
|
|
4
|
+
"recall": 0.9068,
|
|
5
|
+
"distances": 209.99
|
|
6
|
+
},
|
|
7
|
+
"8": {
|
|
8
|
+
"recall": 0.9068,
|
|
9
|
+
"distances": 209.99
|
|
10
|
+
},
|
|
11
|
+
"16": {
|
|
12
|
+
"recall": 0.9664,
|
|
13
|
+
"distances": 247.212
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"float-cosine": {
|
|
17
|
+
"4": {
|
|
18
|
+
"recall": 0.9042,
|
|
19
|
+
"distances": 222.262
|
|
20
|
+
},
|
|
21
|
+
"8": {
|
|
22
|
+
"recall": 0.9042,
|
|
23
|
+
"distances": 222.262
|
|
24
|
+
},
|
|
25
|
+
"16": {
|
|
26
|
+
"recall": 0.967,
|
|
27
|
+
"distances": 263.986
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"sq8-second-pass": {
|
|
31
|
+
"4": {
|
|
32
|
+
"recall": 0.9132,
|
|
33
|
+
"distances": 234.102
|
|
34
|
+
},
|
|
35
|
+
"8": {
|
|
36
|
+
"recall": 0.9132,
|
|
37
|
+
"distances": 234.102
|
|
38
|
+
},
|
|
39
|
+
"16": {
|
|
40
|
+
"recall": 0.981,
|
|
41
|
+
"distances": 275.388
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"sq8-directions": {
|
|
45
|
+
"32": {
|
|
46
|
+
"recall": 0.4988,
|
|
47
|
+
"distances": 323.592
|
|
48
|
+
},
|
|
49
|
+
"64": {
|
|
50
|
+
"recall": 0.7316,
|
|
51
|
+
"distances": 390.994
|
|
52
|
+
},
|
|
53
|
+
"128": {
|
|
54
|
+
"recall": 0.9518,
|
|
55
|
+
"distances": 508.41
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Fuzzes MetadataIndex::load: arbitrary bytes as a metadata file must either load into an index that is
|
|
2
|
+
// safe to query, change and save, or throw. Never crash, read out of bounds or hit undefined behavior.
|
|
3
|
+
#include <exception>
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
#include "common.h"
|
|
7
|
+
#include "epzero/metadata.h"
|
|
8
|
+
|
|
9
|
+
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) {
|
|
10
|
+
static const auto path = epzero::fuzz::scratch("metadata") / "input.meta";
|
|
11
|
+
epzero::fuzz::write_bytes(path, data, size);
|
|
12
|
+
try {
|
|
13
|
+
epzero::MetadataIndex index = epzero::MetadataIndex::load(path.string());
|
|
14
|
+
(void)index.select({{.field = "source", .any_of = {std::string("faq")}}, {.field = "year", .min = 2000.0}});
|
|
15
|
+
(void)index.select({{.field = "year", .max = 1e9, .negate = true}});
|
|
16
|
+
index.set(1, {{"source", std::string("new")}});
|
|
17
|
+
index.remove(1);
|
|
18
|
+
index.save(path.string());
|
|
19
|
+
} catch (const std::exception&) {
|
|
20
|
+
// Rejected cleanly: the expected outcome for corrupt input.
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|