Skip to content

Commit 0097247

Browse files
authored
QVAC-20631 feat[api]: add TurboVec CPU vector indexing (#3786)
* feat[api]: add TurboVec CPU IdMapIndex integration Expose synchronous CJS and ESM APIs for f32, q4, q8, and TurboVec q2/q4 vector indexes, including exact and filtered search, IVF, persistence, mmap, delta logging, and deterministic disposal. Add the native ggml-vector-index bindings, Fabric target compatibility, and a temporary pinned vcpkg overlay for the stacked TurboVec Fabric changes. * feat[api]: align IdMapIndex with latest TurboVec CPU contract Update the Fabric pin, enforce the 1,024-dimension limit, map revised remove and durability errors, and document v4 delta persistence semantics. Add cross-platform integration coverage and an isolated TurboVec CPU benchmark harness. * feat: update TurboVec fabric and add RAG integration coverage * fix[api]: harden TurboVec lifecycle and benchmark compatibility * fix[api]: preserve IdMapIndex export identity and CommonJS interop * fix: align vector index C++ types and identifiers with lint rules * fix: add Android Vulkan-Hpp include path for fabric overlay * fix: add Android SPIRV headers to fabric overlay * fix: bump qvac-fabric to 10069.2.0 across consumers * fix: enable Fabric vector index for embed builds * fix[api]: reclaim dropped filters and invalidate them on partial mutation failure - prepareFilter() now holds filters via WeakRef with a FinalizationRegistry, so a dropped-but-undisposed filter is reclaimed by GC instead of leaking native memory for the index's lifetime - addWithIds() and compact() now invalidate filters in a finally block, matching addLogged/removeLogged/compactDelta, so a partial native failure can't leave stale filters marked valid - drop the now-unused VCPKG_OVERLAY_PORTS entry from CMakeLists.txt now that qvac-fabric is consumed from the official vcpkg registry * fix: make embed id map mobile tests resolve package exports
1 parent 2a2ffe9 commit 0097247

52 files changed

Lines changed: 4868 additions & 32 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/embed-llamacpp/.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ benchmarks
1414
index.js
1515
addon.js
1616
addonLogging.js
17+
idMapIndex.js
18+
test/types/consumer-cjs.test.js
1719
*.d.ts

packages/embed-llamacpp/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [0.35.0] - 2026-08-20
4+
5+
### Changed
6+
7+
- `qvac-fabric` dependency bumped `10069.1.1` -> `10069.2.0` (TurboVec CPU
8+
support from the fabric runtime; no API change for this package).
9+
310
## [0.34.0] - 2026-08-18
411

512
### Changed

packages/embed-llamacpp/CMakeLists.txt

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ find_path(QVAC_LIB_INFERENCE_ADDON_CPP_INCLUDE_DIRS "inference-addon-cpp/JsInter
4444
# IMPORTED interface. Make OpenSSL discoverable before find_package(llama)
4545
# so the target chain resolves on local builds.
4646
find_package(OpenSSL)
47+
find_package(ggml CONFIG REQUIRED)
4748
find_package(llama CONFIG REQUIRED)
4849

4950
if(WIN32)
@@ -68,24 +69,61 @@ target_sources(
6869
${embed-llamacpp}
6970
PRIVATE
7071
${PROJECT_SOURCE_DIR}/addon/src/js-interface/binding.cpp
72+
${PROJECT_SOURCE_DIR}/addon/src/js-interface/vector-index-binding.cpp
7173
${PROJECT_SOURCE_DIR}/addon/src/model-interface/utils.cpp
7274
${PROJECT_SOURCE_DIR}/addon/src/model-interface/AsyncWeightsLoader.cpp
7375
${PROJECT_SOURCE_DIR}/addon/src/model-interface/BackendSelection.cpp
7476
${PROJECT_SOURCE_DIR}/addon/src/model-interface/logging.cpp
7577
${PROJECT_SOURCE_DIR}/addon/src/model-interface/LlamaLazyInitializeBackend.cpp
7678
${PROJECT_SOURCE_DIR}/addon/src/model-interface/ModelMetadata.cpp
7779
${PROJECT_SOURCE_DIR}/addon/src/model-interface/BertModel.cpp
80+
${PROJECT_SOURCE_DIR}/addon/src/model-interface/VectorIndex.cpp
7881
)
7982
target_include_directories(
8083
${embed-llamacpp}
8184
PRIVATE
8285
${QVAC_LIB_INFERENCE_ADDON_CPP_INCLUDE_DIRS}
8386
${PROJECT_SOURCE_DIR}/addon/src
8487
)
88+
89+
if(TARGET llama AND NOT TARGET llama::llama)
90+
add_library(llama::llama ALIAS llama)
91+
endif()
92+
if(NOT TARGET llama::common)
93+
if(TARGET llama::llama-common)
94+
add_library(llama::common ALIAS llama::llama-common)
95+
elseif(TARGET llama-common)
96+
add_library(llama::common ALIAS llama-common)
97+
else()
98+
find_library(LLAMA_COMMON_LIB
99+
NAMES llama-common
100+
REQUIRED
101+
HINTS ${LLAMA_LIB_DIR}
102+
NO_CMAKE_FIND_ROOT_PATH)
103+
add_library(llama-common UNKNOWN IMPORTED)
104+
set_target_properties(llama-common
105+
PROPERTIES
106+
IMPORTED_LOCATION "${LLAMA_COMMON_LIB}"
107+
INTERFACE_INCLUDE_DIRECTORIES "${LLAMA_INCLUDE_DIR}"
108+
INTERFACE_LINK_LIBRARIES "llama::llama")
109+
add_library(llama::common ALIAS llama-common)
110+
endif()
111+
endif()
112+
113+
if(TARGET ggml::ggml-vector-index)
114+
set(QVAC_VECTOR_INDEX_TARGET ggml::ggml-vector-index)
115+
elseif(TARGET ggml::vector-index)
116+
set(QVAC_VECTOR_INDEX_TARGET ggml::vector-index)
117+
elseif(TARGET ggml-vector-index)
118+
set(QVAC_VECTOR_INDEX_TARGET ggml-vector-index)
119+
else()
120+
message(FATAL_ERROR "qvac-fabric did not export a ggml vector-index target")
121+
endif()
122+
85123
target_link_libraries(
86124
${embed-llamacpp}
87125
PRIVATE
88-
llama::llama llama::llama-common
126+
llama::llama llama::common ${QVAC_VECTOR_INDEX_TARGET}
89127
)
90128

91129
target_compile_definitions(${embed-llamacpp} PUBLIC JS_LOGGER)
@@ -103,4 +141,4 @@ if(BUILD_TESTING)
103141
enable_testing()
104142

105143
add_subdirectory(test/unit)
106-
endif()
144+
endif()

packages/embed-llamacpp/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This native C++ addon, built using the `Bare` Runtime, simplifies running text e
1616
- [6. Generate embeddings for input sequence](#6-generate-embeddings-for-input-sequence)
1717
- [7. Release Resources](#7-release-resources)
1818
- [API behavior by state](#api-behavior-by-state)
19+
- [IdMapIndex vector database](#idmapindex-vector-database)
1920
- [Quickstart Example](#quickstart-example)
2021
- [Other Examples](#other-examples)
2122
- [Benchmarking](#benchmarking)
@@ -226,6 +227,50 @@ A second `run()` while a job is active is serialized by `exclusiveRunQueue` —
226227

227228
**Cancellation API:** Prefer cancelling from the model: `await model.cancel()`. This cancels the current job and the Promise resolves when the job has actually stopped (future-based in C++). You can also call `await response.cancel()` on the value returned by `run()`; it is equivalent and targets the same job. Both are no-op when idle.
228229

230+
## IdMapIndex vector database
231+
232+
`IdMapIndex` is a synchronous CPU vector index with stable unsigned 64-bit IDs. Vector-only consumers should use the subpath export so importing the index does not load the embedding-model runtime:
233+
234+
```javascript
235+
const IdMapIndex = require('@qvac/embed-llamacpp/idMapIndex')
236+
237+
const index = new IdMapIndex({ dim: 768, storage: 'turbovec-q4' })
238+
try {
239+
index.addWithIds(vectors, new BigUint64Array([1n, 2n]))
240+
index.prepare()
241+
const { ids, scores } = index.search(query, 2)
242+
} finally {
243+
index.dispose()
244+
}
245+
```
246+
247+
`vectors` contains row-major `Float32Array` data with one `dim`-sized row per ID. Queries use the same layout and may contain multiple rows. Search uses dot-product similarity; L2-normalize indexed vectors and queries first when cosine similarity is required. Results are ordered by descending score and then ascending ID.
248+
249+
### Storage modes
250+
251+
| Storage | Effective bits | Snapshot | Mmap | Delta log |
252+
|---------|---------------:|----------|------|-----------|
253+
| `f32` | 32 | v2 | Yes | Yes |
254+
| `q8` | 8 | v2 | Yes | Yes |
255+
| `q4` | 4 | v2 | Yes | Yes |
256+
| `turbovec-q4` | 4 | v3 | No | No |
257+
| `turbovec-q2` | 2 | v3 | No | No |
258+
259+
The default storage is `q8`. `bitWidth: 2` selects `turbovec-q2`, while `bitWidth: 4` selects generic `q4`; use `storage: 'turbovec-q4'` explicitly for TurboVec Q4.
260+
261+
TurboVec requires a 64-bit target and dimensions divisible by 8 and no greater than 1,024. It provides approximate rotated/quantized dot-product search. TurboVec snapshots support `write()` and `load()`, but reject mmap loading, delta-log loading, logged mutations, and delta compaction.
262+
263+
### Operations and lifecycle
264+
265+
- `addWithIds()`, `remove()`, `contains()`, and `compact()` mutate or inspect the index.
266+
- `search()`, `searchFiltered()`, and `prepareFilter()` perform full-scan retrieval.
267+
- `buildIvf()` enables approximate `searchIvf()` retrieval. IVF state is in-memory only and must be rebuilt after a mutation or snapshot load.
268+
- `write()` and `load()` persist all storage modes. Generic `f32`, `q8`, and `q4` additionally support `loadMmap()`, `loadWithDelta()`, `addLogged()`, `removeLogged()`, and `compactDelta()`.
269+
- Mutations invalidate prepared filters and IVF state.
270+
- Call `dispose()` on indexes and prepared filters when finished. Disposal is idempotent.
271+
272+
See [RAG with TurboVec](./examples/ragWithTurboVec.js) for an end-to-end embedding and retrieval example.
273+
229274
## Quickstart Example
230275

231276
Clone the repository and navigate to it:
@@ -247,6 +292,12 @@ npm run quickstart
247292

248293
- [Batch Inference](./examples/batchInference.js) – Demonstrates running multiple prompts at once using batch inference.
249294
- [Native Logging](./examples/nativelog.js) – Demonstrates C++ addon logging integration.
295+
- [RAG with TurboVec](./examples/ragWithTurboVec.js) – Embeds document chunks, retrieves relevant context with `IdMapIndex`, and prepares it for an LLM.
296+
297+
Run the TurboVec RAG retrieval example on a 64-bit desktop:
298+
```bash
299+
bare examples/ragWithTurboVec.js
300+
```
250301

251302
## Benchmarking
252303

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#pragma once
2+
//
3+
// Maps fabric vector-index C error codes to JS-throwable messages. Mirrors
4+
// the layout of `BertErrors.hpp` but for the ANN index path. Kept in its own
5+
// header so the binding can include it without dragging in any BertModel /
6+
// LlamaLazyInitializeBackend symbols (lifecycle isolation requirement of
7+
// the POC).
8+
9+
#include <cstdint>
10+
11+
#include <ggml-vector-index.h>
12+
13+
namespace qvac_lib_infer_llamacpp_embed::vector_index_errors {
14+
15+
constexpr const char* ADDON_ID = "IdMapIndex";
16+
17+
enum class VecIndexError : std::int32_t {
18+
Ok = GGML_VEC_INDEX_OK,
19+
InvalidArgument = GGML_VEC_INDEX_E_INVALID_ARG,
20+
Duplicate = GGML_VEC_INDEX_E_DUPLICATE,
21+
NotFound = GGML_VEC_INDEX_E_NOT_FOUND,
22+
Io = GGML_VEC_INDEX_E_IO,
23+
BadMagic = GGML_VEC_INDEX_E_BAD_MAGIC,
24+
BadVersion = GGML_VEC_INDEX_E_BAD_VERSION,
25+
OutOfMemory = GGML_VEC_INDEX_E_OOM,
26+
PartialCompact = GGML_VEC_INDEX_E_PARTIAL_COMPACT,
27+
NotDurable = GGML_VEC_INDEX_E_NOT_DURABLE,
28+
Internal = GGML_VEC_INDEX_E_INTERNAL,
29+
};
30+
31+
constexpr const char* toString(VecIndexError code) noexcept {
32+
switch (code) {
33+
case VecIndexError::Ok:
34+
return "OK";
35+
case VecIndexError::InvalidArgument:
36+
return "InvalidArgument";
37+
case VecIndexError::Duplicate:
38+
return "DuplicateId";
39+
case VecIndexError::NotFound:
40+
return "NotFound";
41+
case VecIndexError::Io:
42+
return "IOError";
43+
case VecIndexError::BadMagic:
44+
return "BadMagic";
45+
case VecIndexError::BadVersion:
46+
return "BadVersion";
47+
case VecIndexError::OutOfMemory:
48+
return "OutOfMemory";
49+
case VecIndexError::PartialCompact:
50+
return "PartialCompact";
51+
case VecIndexError::NotDurable:
52+
return "NotDurable";
53+
case VecIndexError::Internal:
54+
return "InternalError";
55+
}
56+
return "UnknownError";
57+
}
58+
59+
} // namespace qvac_lib_infer_llamacpp_embed::vector_index_errors

packages/embed-llamacpp/addon/src/js-interface/binding.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
#include "../addon/AddonJs.hpp"
44

5+
namespace qvac_lib_inference_addon_embed::vector_index {
6+
bool registerBindings(js_env_t* env, js_value_t* exports);
7+
}
8+
59
js_value_t*
610
qvacLibInferLlamacppEmbedExports(js_env_t* env, js_value_t* exports) {
711

@@ -28,7 +32,12 @@ qvacLibInferLlamacppEmbedExports(js_env_t* env, js_value_t* exports) {
2832
V("setLogger", qvac_lib_inference_addon_cpp::JsInterface::setLogger)
2933
V("releaseLogger", qvac_lib_inference_addon_cpp::JsInterface::releaseLogger)
3034
#undef V
31-
// NOLINTEND(cppcoreguidelines-macro-usage)
35+
// NOLINTEND(cppcoreguidelines-macro-usage)
36+
37+
if (!qvac_lib_inference_addon_embed::vector_index::registerBindings(
38+
env, exports)) {
39+
return nullptr;
40+
}
3241

3342
return exports;
3443
}

0 commit comments

Comments
 (0)