epzero 0.3.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.3.0/CMakeLists.txt +219 -0
- epzero-0.3.0/PKG-INFO +114 -0
- epzero-0.3.0/bench/bench_adaptive.cpp +171 -0
- epzero-0.3.0/bench/bench_churn.cpp +297 -0
- epzero-0.3.0/bench/bench_distance.cpp +58 -0
- epzero-0.3.0/bench/bench_filter.cpp +236 -0
- epzero-0.3.0/bench/bench_guard.cpp +305 -0
- epzero-0.3.0/bench/bench_recall.cpp +285 -0
- epzero-0.3.0/fuzz/common.h +41 -0
- epzero-0.3.0/fuzz/fuzz_graph.cpp +22 -0
- epzero-0.3.0/fuzz/fuzz_snapshot.cpp +19 -0
- epzero-0.3.0/fuzz/fuzz_text.cpp +21 -0
- epzero-0.3.0/fuzz/fuzz_wal.cpp +28 -0
- epzero-0.3.0/fuzz/make_seeds.cpp +57 -0
- epzero-0.3.0/fuzz/regressions/graph-empty-with-entry-point +0 -0
- epzero-0.3.0/fuzz/regressions/graph-entry-level-overflow +0 -0
- epzero-0.3.0/fuzz/regressions/graph-upper-link-to-base-node +0 -0
- epzero-0.3.0/fuzz/regressions/snapshot-empty-with-entry-point +0 -0
- epzero-0.3.0/fuzz/regressions/snapshot-huge-capacity +0 -0
- epzero-0.3.0/include/epzero/brute_force.h +28 -0
- epzero-0.3.0/include/epzero/database.h +134 -0
- epzero-0.3.0/include/epzero/dataset.h +34 -0
- epzero-0.3.0/include/epzero/distance.h +65 -0
- epzero-0.3.0/include/epzero/hnsw.h +376 -0
- epzero-0.3.0/include/epzero/parallel.h +47 -0
- epzero-0.3.0/include/epzero/recall.h +162 -0
- epzero-0.3.0/include/epzero/text.h +105 -0
- epzero-0.3.0/pyproject.toml +57 -0
- epzero-0.3.0/python/README.md +85 -0
- epzero-0.3.0/python/epzero/__init__.py +36 -0
- epzero-0.3.0/python/epzero/langchain.py +346 -0
- epzero-0.3.0/python/epzero_py.cpp +581 -0
- epzero-0.3.0/python/tests/test_epzero.py +251 -0
- epzero-0.3.0/python/tests/test_langchain.py +114 -0
- epzero-0.3.0/server/node_main.cpp +435 -0
- epzero-0.3.0/src/brute_force.cpp +156 -0
- epzero-0.3.0/src/database.cpp +451 -0
- epzero-0.3.0/src/dataset.cpp +88 -0
- epzero-0.3.0/src/distance.cpp +208 -0
- epzero-0.3.0/src/distance_avx2.cpp +124 -0
- epzero-0.3.0/src/distance_avx512.cpp +83 -0
- epzero-0.3.0/src/distance_avx512vnni.cpp +34 -0
- epzero-0.3.0/src/distance_avxvnni.cpp +37 -0
- epzero-0.3.0/src/distance_neon.cpp +82 -0
- epzero-0.3.0/src/distance_scalar.cpp +53 -0
- epzero-0.3.0/src/distance_wasm.cpp +97 -0
- epzero-0.3.0/src/hnsw.cpp +1763 -0
- epzero-0.3.0/src/kernels.h +46 -0
- epzero-0.3.0/src/rabitq.h +162 -0
- epzero-0.3.0/src/recall.cpp +192 -0
- epzero-0.3.0/src/text.cpp +303 -0
- epzero-0.3.0/src/visited_pool.h +87 -0
- epzero-0.3.0/tests/database_test.cpp +303 -0
- epzero-0.3.0/tests/dataset_test.cpp +74 -0
- epzero-0.3.0/tests/distance_test.cpp +112 -0
- epzero-0.3.0/tests/hnsw_test.cpp +733 -0
- epzero-0.3.0/tests/recall_test.cpp +163 -0
- epzero-0.3.0/tests/text_test.cpp +143 -0
- epzero-0.3.0/tools/export_graph.cpp +74 -0
- epzero-0.3.0/wasm/epzero_wasm.cpp +690 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.20)
|
|
2
|
+
project(epzero VERSION 0.1.0 LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
7
|
+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
|
8
|
+
|
|
9
|
+
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
|
10
|
+
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
|
11
|
+
endif()
|
|
12
|
+
|
|
13
|
+
option(EPZERO_BUILD_TESTS "Build unit tests" ON)
|
|
14
|
+
option(EPZERO_BUILD_BENCH "Build benchmarks" ON)
|
|
15
|
+
option(EPZERO_BUILD_PYTHON "Build the Python module (normally via pip install ./engine)" OFF)
|
|
16
|
+
set(EPZERO_SANITIZE "" CACHE STRING "Sanitizer to enable: address, thread, or undefined")
|
|
17
|
+
|
|
18
|
+
if(EPZERO_SANITIZE)
|
|
19
|
+
add_compile_options(-fsanitize=${EPZERO_SANITIZE} -fno-omit-frame-pointer -g)
|
|
20
|
+
add_link_options(-fsanitize=${EPZERO_SANITIZE})
|
|
21
|
+
endif()
|
|
22
|
+
|
|
23
|
+
# Fuzzing (Clang + libFuzzer): every parser of untrusted bytes, under AddressSanitizer and UBSan.
|
|
24
|
+
# The engine is instrumented for coverage; only the fuzz targets link libFuzzer's main().
|
|
25
|
+
option(EPZERO_BUILD_FUZZ "Build the libFuzzer targets (Clang only)" OFF)
|
|
26
|
+
if(EPZERO_BUILD_FUZZ)
|
|
27
|
+
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
|
28
|
+
message(FATAL_ERROR "EPZERO_BUILD_FUZZ needs Clang (libFuzzer)")
|
|
29
|
+
endif()
|
|
30
|
+
add_compile_options(-fsanitize=fuzzer-no-link,address,undefined -fno-sanitize-recover=undefined
|
|
31
|
+
-fno-omit-frame-pointer -g)
|
|
32
|
+
add_link_options(-fsanitize=address,undefined)
|
|
33
|
+
endif()
|
|
34
|
+
|
|
35
|
+
# WebAssembly: fixed-width SIMD is supported by every browser the demo targets, so the whole
|
|
36
|
+
# build uses it (there is no runtime feature detection in WASM). Threads map to Web Workers
|
|
37
|
+
# sharing one SharedArrayBuffer, which requires a cross-origin isolated page.
|
|
38
|
+
option(EPZERO_WASM_THREADS "WebAssembly build uses threads (needs a cross-origin isolated page)" ON)
|
|
39
|
+
if(EMSCRIPTEN)
|
|
40
|
+
add_compile_options(-msimd128 -fwasm-exceptions)
|
|
41
|
+
add_link_options(-fwasm-exceptions -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=4GB)
|
|
42
|
+
if(EPZERO_WASM_THREADS)
|
|
43
|
+
add_compile_options(-pthread)
|
|
44
|
+
add_link_options(-pthread -Wno-pthreads-mem-growth)
|
|
45
|
+
endif()
|
|
46
|
+
endif()
|
|
47
|
+
|
|
48
|
+
find_package(Threads REQUIRED)
|
|
49
|
+
|
|
50
|
+
add_library(epzero_engine
|
|
51
|
+
src/distance.cpp
|
|
52
|
+
src/distance_scalar.cpp
|
|
53
|
+
src/hnsw.cpp
|
|
54
|
+
src/recall.cpp
|
|
55
|
+
src/database.cpp
|
|
56
|
+
src/text.cpp
|
|
57
|
+
src/brute_force.cpp
|
|
58
|
+
src/dataset.cpp
|
|
59
|
+
)
|
|
60
|
+
target_include_directories(epzero_engine PUBLIC include PRIVATE src)
|
|
61
|
+
target_link_libraries(epzero_engine PUBLIC Threads::Threads)
|
|
62
|
+
target_compile_options(epzero_engine PRIVATE -Wall -Wextra -Wpedantic)
|
|
63
|
+
set_target_properties(epzero_engine PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
|
64
|
+
|
|
65
|
+
# SIMD kernels get ISA flags per file; the rest of the library stays baseline so the binary
|
|
66
|
+
# runs on any CPU of the architecture and picks the best kernel at runtime.
|
|
67
|
+
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
|
|
68
|
+
target_sources(epzero_engine PRIVATE src/distance_avx2.cpp src/distance_avx512.cpp)
|
|
69
|
+
set_source_files_properties(src/distance_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mpopcnt")
|
|
70
|
+
set_source_files_properties(src/distance_avx512.cpp PROPERTIES COMPILE_OPTIONS "-mavx512f")
|
|
71
|
+
# VNNI integer kernels, where the toolchain can build them: the compiler must know the
|
|
72
|
+
# instructions (GCC 11+, Clang 12+ for AVX-VNNI) and so must the assembler it hands them to
|
|
73
|
+
# (binutils 2.36+ for AVX-VNNI; RHEL 9's 2.35 doesn't), so each check compiles and links a use.
|
|
74
|
+
include(CheckCXXSourceCompiles)
|
|
75
|
+
set(CMAKE_REQUIRED_FLAGS "-mavx2 -mavxvnni")
|
|
76
|
+
check_cxx_source_compiles([[
|
|
77
|
+
#include <immintrin.h>
|
|
78
|
+
int main(int argc, char**) {
|
|
79
|
+
__m256i a = _mm256_set1_epi8(static_cast<char>(argc));
|
|
80
|
+
a = _mm256_dpbusd_avx_epi32(a, a, a);
|
|
81
|
+
return _mm256_extract_epi32(a, 0);
|
|
82
|
+
}]] EPZERO_TOOLCHAIN_AVXVNNI)
|
|
83
|
+
set(CMAKE_REQUIRED_FLAGS "-mavx512f -mavx512bw -mavx512vnni")
|
|
84
|
+
check_cxx_source_compiles([[
|
|
85
|
+
#include <immintrin.h>
|
|
86
|
+
int main(int argc, char**) {
|
|
87
|
+
__m512i a = _mm512_set1_epi8(static_cast<char>(argc));
|
|
88
|
+
a = _mm512_dpbusd_epi32(a, a, a);
|
|
89
|
+
return _mm512_reduce_add_epi32(a);
|
|
90
|
+
}]] EPZERO_TOOLCHAIN_AVX512VNNI)
|
|
91
|
+
unset(CMAKE_REQUIRED_FLAGS)
|
|
92
|
+
if(EPZERO_TOOLCHAIN_AVXVNNI)
|
|
93
|
+
target_sources(epzero_engine PRIVATE src/distance_avxvnni.cpp)
|
|
94
|
+
set_source_files_properties(src/distance_avxvnni.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mavxvnni")
|
|
95
|
+
set_property(SOURCE src/distance.cpp APPEND PROPERTY COMPILE_DEFINITIONS EPZERO_HAVE_AVXVNNI)
|
|
96
|
+
endif()
|
|
97
|
+
if(EPZERO_TOOLCHAIN_AVX512VNNI)
|
|
98
|
+
target_sources(epzero_engine PRIVATE src/distance_avx512vnni.cpp)
|
|
99
|
+
set_source_files_properties(src/distance_avx512vnni.cpp PROPERTIES COMPILE_OPTIONS "-mavx512f;-mavx512bw;-mavx512vnni")
|
|
100
|
+
set_property(SOURCE src/distance.cpp APPEND PROPERTY COMPILE_DEFINITIONS EPZERO_HAVE_AVX512VNNI)
|
|
101
|
+
endif()
|
|
102
|
+
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
|
|
103
|
+
target_sources(epzero_engine PRIVATE src/distance_neon.cpp)
|
|
104
|
+
elseif(EMSCRIPTEN)
|
|
105
|
+
target_sources(epzero_engine PRIVATE src/distance_wasm.cpp)
|
|
106
|
+
endif()
|
|
107
|
+
|
|
108
|
+
if(EMSCRIPTEN AND NOT EPZERO_WASM_THREADS)
|
|
109
|
+
set(EPZERO_BUILD_TESTS OFF) # the concurrency tests need threads
|
|
110
|
+
endif()
|
|
111
|
+
|
|
112
|
+
if(EPZERO_BUILD_TESTS)
|
|
113
|
+
include(FetchContent)
|
|
114
|
+
FetchContent_Declare(googletest
|
|
115
|
+
URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz
|
|
116
|
+
DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
|
|
117
|
+
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
|
|
118
|
+
FetchContent_MakeAvailable(googletest)
|
|
119
|
+
|
|
120
|
+
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)
|
|
122
|
+
target_link_libraries(epzero_tests PRIVATE epzero_engine GTest::gtest_main)
|
|
123
|
+
if(EMSCRIPTEN)
|
|
124
|
+
# Runs under Node (CMAKE_CROSSCOMPILING_EMULATOR); workers are pre-spawned for the
|
|
125
|
+
# concurrency tests and the real filesystem backs the save/load tests.
|
|
126
|
+
target_link_options(epzero_tests PRIVATE -sPTHREAD_POOL_SIZE=16 -sNODERAWFS=1 -sENVIRONMENT=node)
|
|
127
|
+
endif()
|
|
128
|
+
include(GoogleTest)
|
|
129
|
+
gtest_discover_tests(epzero_tests DISCOVERY_TIMEOUT 60)
|
|
130
|
+
endif()
|
|
131
|
+
|
|
132
|
+
if(EPZERO_BUILD_BENCH)
|
|
133
|
+
add_executable(bench_distance bench/bench_distance.cpp)
|
|
134
|
+
target_link_libraries(bench_distance PRIVATE epzero_engine)
|
|
135
|
+
add_executable(bench_recall bench/bench_recall.cpp)
|
|
136
|
+
target_link_libraries(bench_recall PRIVATE epzero_engine)
|
|
137
|
+
add_executable(bench_filter bench/bench_filter.cpp)
|
|
138
|
+
target_link_libraries(bench_filter PRIVATE epzero_engine)
|
|
139
|
+
add_executable(bench_churn bench/bench_churn.cpp)
|
|
140
|
+
target_link_libraries(bench_churn PRIVATE epzero_engine)
|
|
141
|
+
add_executable(bench_guard bench/bench_guard.cpp)
|
|
142
|
+
target_link_libraries(bench_guard PRIVATE epzero_engine)
|
|
143
|
+
add_executable(bench_adaptive bench/bench_adaptive.cpp)
|
|
144
|
+
target_link_libraries(bench_adaptive PRIVATE epzero_engine)
|
|
145
|
+
endif()
|
|
146
|
+
|
|
147
|
+
if(EPZERO_BUILD_FUZZ)
|
|
148
|
+
foreach(target snapshot graph wal text)
|
|
149
|
+
add_executable(fuzz_${target} fuzz/fuzz_${target}.cpp)
|
|
150
|
+
target_link_libraries(fuzz_${target} PRIVATE epzero_engine)
|
|
151
|
+
target_link_options(fuzz_${target} PRIVATE -fsanitize=fuzzer)
|
|
152
|
+
endforeach()
|
|
153
|
+
add_executable(fuzz_seeds fuzz/make_seeds.cpp)
|
|
154
|
+
target_link_libraries(fuzz_seeds PRIVATE epzero_engine)
|
|
155
|
+
endif()
|
|
156
|
+
|
|
157
|
+
# Writes the web demo's prebuilt graph (see tools/export_graph.cpp and web/scripts/build-graph.sh).
|
|
158
|
+
if(NOT EMSCRIPTEN)
|
|
159
|
+
add_executable(epzero_export_graph tools/export_graph.cpp)
|
|
160
|
+
set_target_properties(epzero_export_graph PROPERTIES OUTPUT_NAME epzero-export-graph)
|
|
161
|
+
target_link_libraries(epzero_export_graph PRIVATE epzero_engine)
|
|
162
|
+
endif()
|
|
163
|
+
|
|
164
|
+
option(EPZERO_BUILD_NODE "Build the cluster data node (needs gRPC C++ and protobuf)" ON)
|
|
165
|
+
if(EPZERO_BUILD_NODE AND NOT EMSCRIPTEN)
|
|
166
|
+
find_package(PkgConfig)
|
|
167
|
+
if(PkgConfig_FOUND)
|
|
168
|
+
pkg_check_modules(GRPCPP IMPORTED_TARGET grpc++)
|
|
169
|
+
pkg_check_modules(PROTOBUF IMPORTED_TARGET protobuf)
|
|
170
|
+
endif()
|
|
171
|
+
find_program(PROTOC protoc)
|
|
172
|
+
find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin)
|
|
173
|
+
if(GRPCPP_FOUND AND PROTOBUF_FOUND AND PROTOC AND GRPC_CPP_PLUGIN)
|
|
174
|
+
set(PROTO_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../proto)
|
|
175
|
+
set(PROTO_OUT ${CMAKE_CURRENT_BINARY_DIR}/proto)
|
|
176
|
+
file(MAKE_DIRECTORY ${PROTO_OUT})
|
|
177
|
+
set(PROTO_SRCS ${PROTO_OUT}/epzero/v1/epzero.pb.cc ${PROTO_OUT}/epzero/v1/epzero.grpc.pb.cc)
|
|
178
|
+
add_custom_command(
|
|
179
|
+
OUTPUT ${PROTO_SRCS}
|
|
180
|
+
COMMAND ${PROTOC} -I ${PROTO_ROOT} --cpp_out=${PROTO_OUT} --grpc_out=${PROTO_OUT}
|
|
181
|
+
--plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN} epzero/v1/epzero.proto
|
|
182
|
+
DEPENDS ${PROTO_ROOT}/epzero/v1/epzero.proto
|
|
183
|
+
COMMENT "Generating gRPC code for epzero.proto")
|
|
184
|
+
add_executable(epzero_node server/node_main.cpp ${PROTO_SRCS})
|
|
185
|
+
set_target_properties(epzero_node PROPERTIES OUTPUT_NAME epzero-node)
|
|
186
|
+
target_include_directories(epzero_node PRIVATE ${PROTO_OUT})
|
|
187
|
+
target_link_libraries(epzero_node PRIVATE epzero_engine PkgConfig::GRPCPP PkgConfig::PROTOBUF)
|
|
188
|
+
install(TARGETS epzero_node RUNTIME DESTINATION bin)
|
|
189
|
+
else()
|
|
190
|
+
message(STATUS "gRPC C++ not found: skipping epzero-node")
|
|
191
|
+
endif()
|
|
192
|
+
endif()
|
|
193
|
+
|
|
194
|
+
if(EMSCRIPTEN)
|
|
195
|
+
# ES module for the browser demo: `import createEpzero from "./epzero.js"`.
|
|
196
|
+
add_executable(epzero_web wasm/epzero_wasm.cpp)
|
|
197
|
+
target_link_libraries(epzero_web PRIVATE epzero_engine)
|
|
198
|
+
set_target_properties(epzero_web PROPERTIES OUTPUT_NAME epzero SUFFIX .js)
|
|
199
|
+
target_link_options(epzero_web PRIVATE
|
|
200
|
+
-lembind -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXPORT_NAME=createEpzero -sENVIRONMENT=web,worker -sSTACK_SIZE=1MB
|
|
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()
|
|
209
|
+
endif()
|
|
210
|
+
|
|
211
|
+
if(EPZERO_BUILD_PYTHON)
|
|
212
|
+
find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)
|
|
213
|
+
find_package(pybind11 CONFIG REQUIRED)
|
|
214
|
+
pybind11_add_module(epzero_python python/epzero_py.cpp)
|
|
215
|
+
# The compiled core of the Python package epzero (engine/python/epzero), installed inside it.
|
|
216
|
+
set_target_properties(epzero_python PROPERTIES OUTPUT_NAME _core)
|
|
217
|
+
target_link_libraries(epzero_python PRIVATE epzero_engine)
|
|
218
|
+
install(TARGETS epzero_python DESTINATION epzero)
|
|
219
|
+
endif()
|
epzero-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: epzero
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: An HNSW vector database built from scratch in C++20: 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: Topic :: Database
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Project-URL: Homepage, https://epzero.dev/
|
|
18
|
+
Project-URL: Source, https://github.com/Lazeeez/Epzero
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: numpy
|
|
21
|
+
Provides-Extra: langchain
|
|
22
|
+
Requires-Dist: langchain-core>=0.3; extra == "langchain"
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
26
|
+
Requires-Dist: langchain-core>=0.3; extra == "test"
|
|
27
|
+
Requires-Dist: langchain-tests>=0.3; extra == "test"
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# epzero
|
|
31
|
+
|
|
32
|
+
An HNSW vector database written from scratch in C++20, with Python bindings.
|
|
33
|
+
|
|
34
|
+
- **Fast search:** hand-written SIMD kernels (AVX2, AVX-512, NEON), chosen at runtime, and
|
|
35
|
+
lock-free reads. On SIFT1M it serves 1.13× hnswlib's and 1.17× FAISS's queries per second at
|
|
36
|
+
equal recall, on the same machine.
|
|
37
|
+
- **Filtered search that picks its own strategy:** an exact scan, a filtered graph walk, or a
|
|
38
|
+
two-hop walk (the idea behind ACORN), chosen per query by a cost model.
|
|
39
|
+
- **Recall you can see:** `RecallGuard` measures recall on a sample of live queries with a
|
|
40
|
+
confidence interval and moves the search effort to hold a target; `explain()` says why any given
|
|
41
|
+
item was not returned. With `per_query=True` each query also stops as soon as its answer settles,
|
|
42
|
+
so easy queries cost less and hard ones search further.
|
|
43
|
+
- **Hybrid search:** `TextIndex` (BM25 keyword search) and `hybrid_search()`, which merges it
|
|
44
|
+
with vector search by reciprocal rank fusion.
|
|
45
|
+
- **Durable:** `Database` adds a write-ahead log with crash recovery and checkpoints.
|
|
46
|
+
- **8-bit quantization** with exact re-ranking.
|
|
47
|
+
- **A LangChain vector store** in `epzero.langchain`.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install epzero # the engine
|
|
51
|
+
pip install "epzero[langchain]" # plus the LangChain vector store
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import numpy as np
|
|
58
|
+
import epzero
|
|
59
|
+
|
|
60
|
+
vectors = np.random.rand(100_000, 128).astype(np.float32)
|
|
61
|
+
index = epzero.Index(dim=128, metric="l2", max_elements=100_000, M=16, ef_construction=200)
|
|
62
|
+
index.add(vectors) # labels 0..n-1 (or pass labels=)
|
|
63
|
+
|
|
64
|
+
labels, distances = index.search(vectors[:5], k=10, ef=64)
|
|
65
|
+
|
|
66
|
+
# Filtered search: compile the allowed labels once, reuse across queries.
|
|
67
|
+
allowed = index.filter(np.arange(0, 100_000, 50))
|
|
68
|
+
labels, distances = index.search(vectors[:5], k=10, filter=allowed)
|
|
69
|
+
|
|
70
|
+
# Hold recall at 0.95 as the workload drifts, checking 1% of queries; per_query=True lets each
|
|
71
|
+
# query stop once its answer settles (cheaper for the same recall).
|
|
72
|
+
guard = epzero.RecallGuard(index, target=0.95, sample_rate=0.01, reference_ef=1024, per_query=True)
|
|
73
|
+
labels, distances = guard.search(vectors[:5], k=10)
|
|
74
|
+
print(guard.report())
|
|
75
|
+
|
|
76
|
+
# Optional: measure the best memory-prefetch setting for this machine and data (not saved).
|
|
77
|
+
index.tune_prefetch(vectors[:500])
|
|
78
|
+
|
|
79
|
+
index.save("vectors.epzero")
|
|
80
|
+
index = epzero.Index.load("vectors.epzero")
|
|
81
|
+
|
|
82
|
+
# Hybrid: keyword search on the same labels, merged with the vector search.
|
|
83
|
+
text = epzero.TextIndex()
|
|
84
|
+
text.add(None, documents) # labels 0..n-1, matching the index
|
|
85
|
+
labels, scores = epzero.hybrid_search(index, text, query_vector, "error E-1042 at checkout", k=10)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## LangChain
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from langchain_openai import OpenAIEmbeddings
|
|
92
|
+
from epzero.langchain import EpzeroVectorStore
|
|
93
|
+
|
|
94
|
+
store = EpzeroVectorStore.from_texts(texts, OpenAIEmbeddings(), metadatas=metadatas)
|
|
95
|
+
store.similarity_search("how do I reset my password?", k=4, filter={"source": "faq"})
|
|
96
|
+
store.max_marginal_relevance_search("pricing", k=4, fetch_k=20)
|
|
97
|
+
store.hybrid_search("error E-1042 at checkout", k=4) # keywords + vectors
|
|
98
|
+
retriever = store.as_retriever(search_kwargs={"k": 8})
|
|
99
|
+
|
|
100
|
+
store.save_local("./store")
|
|
101
|
+
store = EpzeroVectorStore.load_local("./store", OpenAIEmbeddings())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Metadata filters are a dict of required values (a list means "any of"), or a callable on the
|
|
105
|
+
metadata. They are compiled into an Epzero filter, so the engine chooses how to search the allowed
|
|
106
|
+
documents instead of over-fetching and discarding. The store passes LangChain's standard
|
|
107
|
+
vector-store test suite.
|
|
108
|
+
|
|
109
|
+
## Platforms
|
|
110
|
+
|
|
111
|
+
Wheels for Linux (x86-64 and ARM64, glibc 2.28+) and macOS (Apple silicon and Intel), Python 3.10
|
|
112
|
+
to 3.13. Elsewhere, `pip install epzero` builds from source (CMake 3.20+, a C++20 compiler).
|
|
113
|
+
|
|
114
|
+
Live demo, benchmarks and design notes: https://epzero.dev/ · MIT license.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Per-query effort: a fixed beam width for every query against patience, which stops each query
|
|
2
|
+
// once its best k have stopped changing (SearchOptions::patience, with ef as a cap).
|
|
3
|
+
//
|
|
4
|
+
// A fixed ef spends the same effort on every query: too much on easy ones, too little on the
|
|
5
|
+
// hardest. For each setting this reports mean recall@k and mean distance computations (the cost
|
|
6
|
+
// curve), and the tail: the share of queries answered badly (recall@k below 0.8 and 0.5), the
|
|
7
|
+
// 99th-percentile cost, and single-thread throughput.
|
|
8
|
+
//
|
|
9
|
+
// bench_adaptive --base sift_base.fvecs --query sift_query.fvecs --truth sift_groundtruth.ivecs \
|
|
10
|
+
// --index sift.idx --json adaptive.json
|
|
11
|
+
// (--metric cosine for angular datasets such as GloVe; ground truth as ivecs, top-k first)
|
|
12
|
+
|
|
13
|
+
#include <algorithm>
|
|
14
|
+
#include <chrono>
|
|
15
|
+
#include <cstdio>
|
|
16
|
+
#include <filesystem>
|
|
17
|
+
#include <fstream>
|
|
18
|
+
#include <stdexcept>
|
|
19
|
+
#include <string>
|
|
20
|
+
#include <thread>
|
|
21
|
+
#include <vector>
|
|
22
|
+
|
|
23
|
+
#include "epzero/dataset.h"
|
|
24
|
+
#include "epzero/hnsw.h"
|
|
25
|
+
#include "epzero/parallel.h"
|
|
26
|
+
|
|
27
|
+
using namespace epzero;
|
|
28
|
+
|
|
29
|
+
namespace {
|
|
30
|
+
|
|
31
|
+
struct Options {
|
|
32
|
+
std::string base, query, truth, index, json, name = "dataset";
|
|
33
|
+
Metric metric = Metric::L2;
|
|
34
|
+
std::size_t k = 10, M = 16, ef_construction = 200, threads = 0, queries = 0, rounds = 3;
|
|
35
|
+
std::vector<std::size_t> efs = {10, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024};
|
|
36
|
+
std::vector<std::size_t> patiences = {2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128};
|
|
37
|
+
std::vector<std::size_t> caps = {256, 1024};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
std::vector<std::size_t> list(const std::string& s) {
|
|
41
|
+
std::vector<std::size_t> out;
|
|
42
|
+
std::size_t start = 0;
|
|
43
|
+
while (start < s.size()) {
|
|
44
|
+
const std::size_t comma = s.find(',', start);
|
|
45
|
+
out.push_back(std::stoul(s.substr(start, comma - start)));
|
|
46
|
+
if (comma == std::string::npos) break;
|
|
47
|
+
start = comma + 1;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Options parse(int argc, char** argv) {
|
|
53
|
+
Options o;
|
|
54
|
+
for (int i = 1; i < argc; ++i) {
|
|
55
|
+
const std::string arg = argv[i];
|
|
56
|
+
auto value = [&]() -> std::string {
|
|
57
|
+
if (i + 1 >= argc) throw std::invalid_argument(arg + " needs a value");
|
|
58
|
+
return argv[++i];
|
|
59
|
+
};
|
|
60
|
+
if (arg == "--base") o.base = value();
|
|
61
|
+
else if (arg == "--query") o.query = value();
|
|
62
|
+
else if (arg == "--truth") o.truth = value();
|
|
63
|
+
else if (arg == "--index") o.index = value();
|
|
64
|
+
else if (arg == "--json") o.json = value();
|
|
65
|
+
else if (arg == "--name") o.name = value();
|
|
66
|
+
else if (arg == "--metric") o.metric = value() == "cosine" ? Metric::Cosine : Metric::L2;
|
|
67
|
+
else if (arg == "--k") o.k = std::stoul(value());
|
|
68
|
+
else if (arg == "--M") o.M = std::stoul(value());
|
|
69
|
+
else if (arg == "--efc") o.ef_construction = std::stoul(value());
|
|
70
|
+
else if (arg == "--threads") o.threads = std::stoul(value());
|
|
71
|
+
else if (arg == "--queries") o.queries = std::stoul(value());
|
|
72
|
+
else if (arg == "--rounds") o.rounds = std::stoul(value());
|
|
73
|
+
else if (arg == "--efs") o.efs = list(value());
|
|
74
|
+
else if (arg == "--patience") o.patiences = list(value());
|
|
75
|
+
else if (arg == "--caps") o.caps = list(value());
|
|
76
|
+
else throw std::invalid_argument("unknown argument " + arg);
|
|
77
|
+
}
|
|
78
|
+
if (o.base.empty() || o.query.empty() || o.truth.empty()) throw std::invalid_argument("pass --base, --query and --truth");
|
|
79
|
+
if (o.threads == 0) o.threads = std::max(1u, std::thread::hardware_concurrency());
|
|
80
|
+
return o;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
struct Row {
|
|
84
|
+
std::string policy; // "fixed" or "patience"
|
|
85
|
+
std::size_t ef, patience;
|
|
86
|
+
double recall, distances, p99_distances, below_08, below_05, qps;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
} // namespace
|
|
90
|
+
|
|
91
|
+
int main(int argc, char** argv) try {
|
|
92
|
+
const Options opt = parse(argc, argv);
|
|
93
|
+
const Dataset queries = read_fvecs(opt.query, opt.queries);
|
|
94
|
+
const auto truth = read_ivecs(opt.truth, queries.n);
|
|
95
|
+
const std::size_t nq = queries.n, k = opt.k;
|
|
96
|
+
|
|
97
|
+
std::unique_ptr<HnswIndex> index;
|
|
98
|
+
if (!opt.index.empty() && std::filesystem::exists(opt.index)) {
|
|
99
|
+
index = HnswIndex::load(opt.index);
|
|
100
|
+
std::printf("loaded %s (%zu vectors)\n", opt.index.c_str(), index->size());
|
|
101
|
+
} else {
|
|
102
|
+
const Dataset base = read_fvecs(opt.base);
|
|
103
|
+
index = std::make_unique<HnswIndex>(HnswParams{.dim = base.dim, .metric = opt.metric, .max_elements = base.n,
|
|
104
|
+
.M = opt.M, .ef_construction = opt.ef_construction});
|
|
105
|
+
const auto start = std::chrono::steady_clock::now();
|
|
106
|
+
parallel_for(0, base.n, opt.threads, [&](std::size_t i) { index->add(i, base.row(i)); });
|
|
107
|
+
std::printf("built %zu x %zu in %.1f s\n", base.n, base.dim,
|
|
108
|
+
std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count());
|
|
109
|
+
if (!opt.index.empty()) index->save(opt.index);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// One setting: per-query recall and cost, then throughput as the best of `rounds` timed passes.
|
|
113
|
+
const auto run = [&](const std::string& policy, std::size_t ef, std::size_t patience) {
|
|
114
|
+
std::vector<double> recall(nq), cost(nq);
|
|
115
|
+
for (std::size_t q = 0; q < nq; ++q) {
|
|
116
|
+
SearchStats stats;
|
|
117
|
+
const auto got = index->search(queries.row(q), k, {.ef = ef, .patience = patience, .stats = &stats});
|
|
118
|
+
std::size_t hits = 0;
|
|
119
|
+
for (std::size_t t = 0; t < k && t < truth[q].size(); ++t) {
|
|
120
|
+
hits += std::any_of(got.begin(), got.end(), [&](const SearchResult& r) { return r.label == truth[q][t]; });
|
|
121
|
+
}
|
|
122
|
+
recall[q] = static_cast<double>(hits) / static_cast<double>(k);
|
|
123
|
+
cost[q] = static_cast<double>(stats.distance_computations);
|
|
124
|
+
}
|
|
125
|
+
double best = 0;
|
|
126
|
+
for (std::size_t r = 0; r < opt.rounds; ++r) {
|
|
127
|
+
const auto start = std::chrono::steady_clock::now();
|
|
128
|
+
for (std::size_t q = 0; q < nq; ++q) index->search(queries.row(q), k, {.ef = ef, .patience = patience});
|
|
129
|
+
best = std::max(best, static_cast<double>(nq) / std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count());
|
|
130
|
+
}
|
|
131
|
+
Row row{policy, ef, patience, 0, 0, 0, 0, 0, best};
|
|
132
|
+
for (std::size_t q = 0; q < nq; ++q) {
|
|
133
|
+
row.recall += recall[q] / static_cast<double>(nq);
|
|
134
|
+
row.distances += cost[q] / static_cast<double>(nq);
|
|
135
|
+
row.below_08 += (recall[q] < 0.8) / static_cast<double>(nq);
|
|
136
|
+
row.below_05 += (recall[q] < 0.5) / static_cast<double>(nq);
|
|
137
|
+
}
|
|
138
|
+
std::sort(cost.begin(), cost.end());
|
|
139
|
+
row.p99_distances = cost[std::min(nq - 1, nq * 99 / 100)];
|
|
140
|
+
std::printf("%-8s ef %5zu patience %4zu recall %.4f dist %7.0f p99 dist %7.0f <0.8 %5.2f%% <0.5 %5.2f%% qps %8.0f\n",
|
|
141
|
+
policy.c_str(), ef, patience, row.recall, row.distances, row.p99_distances, 100 * row.below_08,
|
|
142
|
+
100 * row.below_05, row.qps);
|
|
143
|
+
return row;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
std::vector<Row> rows;
|
|
147
|
+
for (const auto ef : opt.efs) rows.push_back(run("fixed", ef, 0));
|
|
148
|
+
for (const auto cap : opt.caps) {
|
|
149
|
+
for (const auto p : opt.patiences) rows.push_back(run("patience", cap, p));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!opt.json.empty()) {
|
|
153
|
+
std::ofstream out(opt.json);
|
|
154
|
+
out << "{\n \"dataset\": \"" << opt.name << "\", \"n\": " << index->size() << ", \"dim\": " << index->params().dim
|
|
155
|
+
<< ", \"queries\": " << nq << ", \"k\": " << k << ", \"M\": " << index->params().M
|
|
156
|
+
<< ", \"ef_construction\": " << index->params().ef_construction << ",\n \"rows\": [\n";
|
|
157
|
+
for (std::size_t i = 0; i < rows.size(); ++i) {
|
|
158
|
+
const Row& r = rows[i];
|
|
159
|
+
out << " {\"policy\": \"" << r.policy << "\", \"ef\": " << r.ef << ", \"patience\": " << r.patience
|
|
160
|
+
<< ", \"recall\": " << r.recall << ", \"distances\": " << r.distances << ", \"p99_distances\": " << r.p99_distances
|
|
161
|
+
<< ", \"below_0.8\": " << r.below_08 << ", \"below_0.5\": " << r.below_05 << ", \"qps\": " << r.qps << "}"
|
|
162
|
+
<< (i + 1 < rows.size() ? "," : "") << "\n";
|
|
163
|
+
}
|
|
164
|
+
out << " ]\n}\n";
|
|
165
|
+
std::printf("wrote %s\n", opt.json.c_str());
|
|
166
|
+
}
|
|
167
|
+
return 0;
|
|
168
|
+
} catch (const std::exception& e) {
|
|
169
|
+
std::fprintf(stderr, "error: %s\n", e.what());
|
|
170
|
+
return 1;
|
|
171
|
+
}
|