feat(agent): build the vector index in parallel by default

`parallel` joins the agent's default features, so the HNSW bulk build uses the
thread pool: cold index build at 10K records 1152 -> ~380 ms in a same-moment
A/B (the graph is identical either way). Nothing else on the measured paths
changes — ingest, checkpoint, open and steady-state query times are the same
with the feature on or off. Adds rayon to the default dependency set; opt out
with `--no-default-features --features float16,hnsw`.

Harness: `--e2e-only` runs the end-to-end section without the index
benchmarks. Note for anyone comparing numbers: this machine's absolute timings
drifted ~1.5x over a long session, so only same-moment A/B runs are
comparable.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
osobh
2026-09-19 13:52:23 -07:00
co-authored by Claude Fable 5.1
parent 8803d0754b
commit f507803ec1
4 changed files with 12 additions and 4 deletions
+3 -1
View File
@@ -9,7 +9,9 @@
With the `parallel` feature, planning and pruning run on a thread pool (10K: With the `parallel` feature, planning and pruning run on a thread pool (10K:
388 ms, 100K: ~21 s -> 5.9 s on 16 cores). The graph is deterministic and 388 ms, 100K: ~21 s -> 5.9 s on 16 cores). The graph is deterministic and
identical with or without the feature. `clawhdf5-agent`'s `parallel` feature identical with or without the feature. `clawhdf5-agent`'s `parallel` feature
now enables it for the agent's index. enables it for the agent's index and is now **on by default** (adds `rayon`
to the default dependency set; build with `--no-default-features --features
float16,hnsw` to opt out).
- `clawhdf5-ann`: `HnswIndex::search` returned fewer than `k` results — often - `clawhdf5-ann`: `HnswIndex::search` returned fewer than `k` results — often
none — when the records nearest the query had been deleted: it collected `ef` none — when the records nearest the query had been deleted: it collected `ef`
candidates, *then* dropped the deleted ones, *then* took `k`. Deleted nodes candidates, *then* dropped the deleted ones, *then* took `k`. Deleted nodes
+2
View File
@@ -33,6 +33,8 @@ Cargo workspace with 16 crates under `crates/` (plus `libaec-sys`, an internal F
the approximate `clawhdf5-ann` index for the vector stage (the index mirrors the approximate `clawhdf5-ann` index for the vector stage (the index mirrors
the cache and self-heals on drift). Build the agent with the cache and self-heals on drift). Build the agent with
`--no-default-features --features float16` to force the exact linear cosine scan. `--no-default-features --features float16` to force the exact linear cosine scan.
The agent's `parallel` feature (also default) builds the index on a thread
pool; the graph is identical with or without it.
The index uses the HNSW paper's diversity heuristic for neighbour selection The index uses the HNSW paper's diversity heuristic for neighbour selection
(plain closest-M capped recall on clustered data: 0.31 recall@10 at 100K). Its (plain closest-M capped recall on clustered data: 0.31 recall@10 at 100K). Its
graph is saved to `<store>.h5.ann` at each checkpoint and reloaded by `open()` graph is saved to `<store>.h5.ann` at each checkpoint and reloaded by `open()`
+1 -1
View File
@@ -45,7 +45,7 @@ name = "memory_bench"
harness = false harness = false
[features] [features]
default = ["float16", "hnsw"] default = ["float16", "hnsw", "parallel"]
float16 = ["half"] float16 = ["half"]
# Rayon-parallel brute-force search strategies, and a parallel bulk build of # Rayon-parallel brute-force search strategies, and a parallel bulk build of
# the HNSW index (same graph, several times faster on a multi-core machine). # the HNSW index (same graph, several times faster on a multi-core machine).
@@ -485,8 +485,12 @@ fn main() {
let mut json = Vec::new(); let mut json = Vec::new();
println!("## Search harness"); println!("## Search harness");
for &n in sizes { // `--e2e-only` skips the index benchmarks, so the end-to-end section runs
bench_ann(n, &mut json); // in a process that has not already spun up a thread pool.
if !args.iter().any(|a| a == "--e2e-only") {
for &n in sizes {
bench_ann(n, &mut json);
}
} }
if ann_only { if ann_only {