Files
clawhdf5/BENCHMARKS.md
osobh b2dce41532
CI / test (push) Failing after 3s
bench: world-model sample loading — clawhdf5 reads h5py files 7x faster
than h5py (5e)

stable-worldmodel (arXiv 2605.21800, LeCun/Balestriero) supports HDF5 as
one of three native formats and measures generic HDF5 at 1,416-1,474
samples/s for per-frame sample loading. This measures clawhdf5 against
that shape, hardware-controlled: clawhdf5 and h5py reading the SAME file
on the SAME machine.

worldmodel_sampling example: mmap an (N,H,W,C) uint8 observation dataset,
read each frame once per pass in shuffled (dataloader) order. The file is
written by h5py (benchmarks/gen_worldmodel_frames.py) — clawhdf5 parsing
an externally-produced HDF5 file is itself the interop result — and read
by both clawhdf5 and the h5py counterpart (benchmarks/bench_worldmodel_h5py.py,
opening exactly stable-worldmodel's HDF5Dataset: swmr + 256 MB cache).

Results (tank, Ryzen 7 7800X3D, 20000x64x64x3 = 246 MB, in page cache,
median of 3):

  clawhdf5 zero-copy view        593k samples/sec   8.1x
  clawhdf5 materialised copy     518k samples/sec   7.1x
  h5py (swmr, 256 MB cache)       73k samples/sec   1.0x

The materialised-copy row is the fair equal-work comparison (to_vec per
frame, matching h5py's numpy materialisation) and is still 7.1x faster;
that the copy costs almost nothing shows the gap is h5py's per-frame call
overhead, not data movement. Honest caveats in BENCHMARKS.md: absolute
numbers are NOT comparable to the paper's (different hardware, smaller
frames, no torch/transform), only the same-machine ratio is; this is an
in-page-cache measurement isolating read-path overhead, not disk
bandwidth.

Adds only an example, two benchmark scripts, and a BENCHMARKS.md section —
no library code. (Workspace clippy has pre-existing toolchain drift
unrelated to this change; tracked separately.)
2026-08-07 22:54:26 -07:00

990 lines
45 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ClawhDF5 Benchmark Results
> Pure Rust. Zero C dependencies. Single file. Fast enough to forget it's there.
**System:** Intel i7-12650H (10C/16T, 4.7 GHz boost) · 32 GB DDR5 · Linux 6.8.0
**Rust:** 1.96.0-nightly (2026-03-14) · `--release` profile
**Date:** 2026-07-01
> **Traceability note:** the "h5bench-Equivalent I/O Benchmarks" and both
> "Independent Validation: tank" sections below meet a dated,
> hardware-cited, reproducible standard (explicit date, machine spec, and a
> runnable command per result) — this now covers "LongMemEval Results",
> "SIMD & Parallelism", "Vector Search Latency", and "Comparison to MemX" via
> their tank re-runs. The remaining undated sections above (Hybrid Search,
> Knowledge Graph, Memory Consolidation, Temporal Index, Write Path, Decision
> Gate, Memory Strategy, Multi-Session Benchmark, Memory Footprint,
> Consolidation Efficiency, Ephemeral Tier) do not yet meet that bar — this is
> a known, tracked documentation gap, not a claim that those numbers are wrong.
>
> **Correctness note (2026-08-06).** Being dated and reproducible is necessary but
> not sufficient — a number can be perfectly reproducible and still measure the
> wrong thing. A methodology audit found two such cases and both have been
> retracted in place: the session-level LongMemEval figures (degenerate on the
> oracle variant) and the MemX retrieval comparison (mismatched granularity and
> corpus). Every cross-system comparison in this file now carries an explicit
> scoping caveat. Where a section states a scoring target, that declaration is the
> contract — read it before citing the number.
---
## Vector Search Latency
Brute-force cosine similarity over 384-dimensional embeddings (OpenAI text-embedding-3-small size).
| Scale | Flat Search | Pre-norm | IVF (nprobe=10) | IVF-PQ | RAIRS |
|-------|-------------|----------|-----------------|--------|-------|
| **1K** | 54 µs | 62 µs | — | — | — |
| **10K** | 753 µs | 706 µs | 27 µs | — | 159 µs |
| **100K** | 11.4 ms | — | 1.32 ms | 1.19 ms | — |
**Key insight:** At 10K records (typical agent memory), IVF search delivers **27 µs** — that's 26x faster than flat search. Even at 100K records, IVF-PQ keeps search under **1.2 ms**.
### Comparison to MemX (arxiv:2603.16171)
MemX claims end-to-end search under 90ms at 100K records (Rust + libSQL + FTS5).
> **Caveat — not like-for-like.** MemX's `<90 ms` is *end-to-end* search across their
> full pipeline (dense embeddings + FTS5 + four-factor re-ranking). The clawhdf5
> figures below are a *single component* — raw vector search latency, excluding
> embedding, keyword, fusion, and re-ranking stages. A component measured against a
> full pipeline will always look favourable; the "speedup" column overstates the real
> advantage by an unquantified margin and should be read as an order-of-magnitude
> indication only, not a benchmark result. Matching MemX's measurement boundary is
> tracked as follow-up work.
| Metric | MemX (claimed, end-to-end) | ClawhDF5 (component only) | Ratio |
|--------|----------------------------|---------------------------|-------|
| 100K flat search | <90 ms | 11.4 ms | ~8x |
| 100K IVF-PQ search | — | 1.19 ms | ~76x |
| Keyword search 10K | 1,100x improvement over unindexed | 583 µs (BM25) | Comparable |
---
## SIMD & Parallelism
384-dimensional cosine similarity at 10K scale.
| Strategy | Latency | vs Sequential |
|----------|---------|---------------|
| Sequential (scalar) | 1.07 ms | 1.0x |
| SIMD (auto-vectorized) | 545 µs | **2.0x** |
| Rayon (parallel) | 553 µs | **1.9x** |
| Adaptive (auto-select) | 564 µs | **1.9x** |
At 100K:
| Strategy | Latency |
|----------|---------|
| SIMD | 13.7 ms |
| Rayon parallel | 8.3 ms |
---
## Hybrid Search (Vector + BM25)
1K records, 384-dimensional embeddings with BM25 keyword index.
| Method | Latency | Notes |
|--------|---------|-------|
| Weighted fusion | 198 µs | Original min-max normalization |
| **RRF (k=60)** | **222 µs** | Reciprocal Rank Fusion — better quality, ~12% overhead |
| BM25-only 1K | 67 µs | Keyword search alone |
| Hybrid 10K | 2.04 ms | Full hybrid at 10K scale |
---
## Knowledge Graph
Graph traversal and entity operations.
| Operation | Scale | Latency |
|-----------|-------|---------|
| BFS traversal | 100 entities | 5.4 µs |
| BFS traversal | 1,000 entities | 24 µs |
| Spreading activation | 100 entities | 16.9 µs |
| Entity resolution (Levenshtein) | 100 entities | 64 µs |
| Alias resolution (short query) | 100 aliases | 10.4 µs |
| Alias resolution (long query) | 100 aliases | 11.6 µs |
**All graph operations complete in microseconds.** Spreading activation across 100 entities with 5 propagation steps finishes in 17 µs.
---
## Memory Consolidation
Hippocampal-inspired tiered memory management.
| Operation | Scale | Latency |
|-----------|-------|---------|
| Consolidation cycle | 100 records | 15 µs |
| Consolidation cycle | 1,000 records | 164 µs |
| Importance scoring | 100 records | 25 µs |
A full consolidation pass over 1,000 memories (eviction + promotion across Working → Episodic → Semantic) completes in **164 µs**. This can run on every memory write without perceptible latency.
---
## Temporal Index
Sorted timestamp index with binary search.
| Operation | Scale | Latency |
|-----------|-------|---------|
| Range query | 10K timestamps | **716 ns** |
| Batch insert | 10K timestamps | 4.69 ms |
Sub-microsecond temporal queries. "What happened between 3pm and 5pm?" over 10K records: **716 nanoseconds.**
---
## Write Path
HDF5 persistence with optional Write-Ahead Log.
| Operation | Latency | Notes |
|-----------|---------|-------|
| Single save (no WAL) | 61 µs | Direct HDF5 write (owned-Vec IO path) |
| Single save (with WAL) | 18 µs | WAL group-commit append; HDF5 write batched at flush |
| Batch 100 | 723 µs | 7.2 µs per record |
| Batch 1,000 | 6.17 ms | 6.2 µs per record |
| WAL save (1K existing) | 539 µs | Incremental append |
| WAL flush 100 entries | 787 µs | Merge WAL → HDF5 |
| Session tick 1K | 5.76 ms | Full session maintenance |
| Session tick 10K | 89.8 ms | Background operation |
---
## Decision Gate
Trivial/non-trivial classification for memory write filtering.
| Check | Latency |
|-------|---------|
| Trivial skip ("ok", "yes") | 61 ns |
| Short phrase skip | 86 ns |
| Non-trivial pass | 705 ns |
| Ratio check | 488 ns |
**Sub-microsecond filtering.** The gate decides whether to save a memory in under 1 µs.
---
## Memory Strategy
End-to-end strategy evaluation including embedding operations.
| Strategy | Condition | Latency |
|----------|-----------|---------|
| SaveEveryExchange (substantive) | Saves | 923 ns |
| SaveEveryExchange (trivial) | Skips | 67 ns |
| SaveOnSemanticShift (empty store) | Saves | 941 ns |
---
## Summary
| Capability | Typical Latency | Scale |
|------------|----------------|-------|
| **Full memory search** | <1 ms | 10K records |
| **Hybrid vector+keyword** | <200 µs | 1K records |
| **Knowledge graph query** | <25 µs | 1K entities |
| **Temporal range query** | <1 µs | 10K timestamps |
| **Memory write** | <20 µs | Per record (WAL group-commit append) |
| **Consolidation cycle** | <165 µs | 1K records |
| **Importance gate** | <1 µs | Per record |
**The entire memory pipeline — search, retrieve, re-rank, filter — runs in single-digit milliseconds at agent-typical scales. Fast enough that memory becomes invisible infrastructure.**
---
_Latency benchmarks generated with Criterion.rs (50-100 samples per benchmark). Results may vary by hardware._
---
## LongMemEval Results
> **Scoring target declaration.** Per [arXiv 2605.24060](https://arxiv.org/abs/2605.24060),
> which found that changing scoring target alone alters nDCG on 8394% of queries and
> can reverse system rankings, this section states its measurement contract explicitly:
>
> - **Dataset variant:** both are now reported below — the full `longmemeval_s`
> haystack (**the headline number**) and `longmemeval_oracle` (evidence sessions
> only, a substantially easier corpus, kept for continuity). The harness does not
> trust the filename: it measures evidence-session density from the data and
> labels the run from that, so a mislabelled input cannot yield a mislabelled
> result. Measured density is 4.0% on `longmemeval_s` and 100.0% on the oracle.
> - **Metric:** *retrieval recall.* A "hit" means the gold-labelled memory appeared in
> the top-k. **No answer is generated and none is scored** — the dataset's `answer`
> field is deserialized and never read. This is **not** the official LongMemEval
> leaderboard metric, which is end-to-end QA accuracy (retrieve → generate → LLM
> judge). Retrieval recall reported as QA accuracy typically overstates by 2030 points.
> - **Granularity:** turn-level = the returned memory's source turn had `has_answer == true`.
> - **k = 10**, n = 500.
> - **Retrieval mode:** all three are reported below. Historically the bench passed
> zero-vector embeddings with `vector_weight=0.0`, so the HNSW/vector stage was
> inert and every published number was BM25 alone. Real `all-MiniLM-L6-v2`
> embeddings are now available via `--features embeddings --embeddings <dir>`,
> and BM25-only / vector-only / hybrid are each measured separately.
**Mode:** BM25-only retrieval — zero embeddings, `vector_weight=0.0`, `keyword_weight=1.0`
> **Run:** `cargo run --release --bin longmemeval_bench -- benchmarks/longmemeval/longmemeval_s_cleaned.json`
> (~70 s for all 500 questions on the tank reference machine). Omit the path for the
> oracle variant; add `--limit N` for an evenly-strided subsample.
### Full haystack — `longmemeval_s`, n=500 (the number to cite)
47.7 sessions and 493.5 turns per question; 4.0% of haystack sessions are evidence
sessions, so retrieval has to actually discriminate.
| Metric | Turn-level | Session-level |
|--------|-----------|---------------|
| Hit@1 | 53.8% | 86.2% |
| Hit@5 | **75.0%** | **93.6%** |
| Hit@10 | 81.6% | 96.6% |
| MRR | 0.6320 | 0.8948 |
Session-level is reported here because on this corpus it is meaningful — unlike on
the oracle variant, where it was degenerate and was retracted (below). At 4.0%
evidence density a session-level hit reflects discrimination rather than corpus
shape.
Per-type, session-level: `single-session-assistant` 100.0% Hit@1 (n=56),
`knowledge-update` 96.2% (n=78), `single-session-user` 94.3% (n=70),
`multi-session` 84.2% (n=133), `temporal-reasoning` 84.2% (n=133), and
`single-session-preference` 33.3% (n=30) — the one category where BM25 clearly
struggles, since a preference question's evidence rarely shares vocabulary with
the question.
### Retrieval mode ablation — full haystack, n=500
Real 384-d `all-MiniLM-L6-v2` embeddings, 190,015 unique texts encoded once on an
RTX 5060 Ti (~13 min; the same work on the 8-core CPU was still unfinished after
30 minutes, so the GPU path is not a convenience here). Turn-level:
| Mode | Hit@1 | Hit@5 | Hit@10 | MRR |
|------|-------|-------|--------|-----|
| BM25 only (`0.0`/`1.0`) | **53.8%** | 75.0% | 81.6% | **0.6320** |
| Vector only (`1.0`/`0.0`) | 36.0% | 71.8% | 81.6% | 0.5027 |
| Hybrid (`0.7`/`0.3`) | 44.4% | **79.2%** | **86.0%** | 0.5868 |
Session-level:
| Mode | Hit@1 | Hit@5 | Hit@10 | MRR |
|------|-------|-------|--------|-----|
| BM25 only | 86.2% | 93.6% | 96.6% | 0.8948 |
| Vector only | 85.4% | 94.2% | 96.6% | 0.8901 |
| Hybrid | **88.2%** | **95.8%** | **97.8%** | **0.9158** |
### Weight sweep — full haystack, n=500
`0.7/0.3` was a documented default, never a searched one. Sweeping
`vector_weight` from 0.0 to 1.0 (`--sweep`, reusing the one-time embedding
table) shows it is not merely suboptimal but **strictly dominated**:
| vector / keyword | Hit@1 | Hit@5 | Hit@10 | MRR | session Hit@5 |
|---|---|---|---|---|---|
| 0.0 / 1.0 (BM25) | **53.8%** | 75.0% | 81.6% | 0.6320 | 93.6% |
| 0.1 / 0.9 | 53.2% | 77.4% | 83.8% | 0.6374 | 95.0% |
| 0.2 / 0.8 | 53.6% | 78.2% | 85.6% | 0.6440 | 95.4% |
| 0.3 / 0.7 | 53.2% | 78.8% | 87.2% | **0.6463** | 96.0% |
| **0.4 / 0.6** | 51.6% | **81.4%** | 87.8% | 0.6429 | 96.8% |
| 0.5 / 0.5 | 48.2% | **81.4%** | **88.2%** | 0.6234 | **97.4%** |
| 0.6 / 0.4 | 46.6% | 79.8% | 87.4% | 0.6069 | 96.6% |
| 0.7 / 0.3 *(old default)* | 44.4% | 79.2% | 86.0% | 0.5868 | 95.8% |
| 0.8 / 0.2 | 40.6% | 76.2% | 85.4% | 0.5571 | 95.2% |
| 0.9 / 0.1 | 37.8% | 73.4% | 84.6% | 0.5289 | 94.2% |
| 1.0 / 0.0 (vector) | 36.0% | 71.8% | 81.6% | 0.5027 | 94.2% |
**`0.4/0.6` beats `0.7/0.3` on every metric at both granularities** — Hit@1
+7.2pp, Hit@5 +2.2, Hit@10 +1.8, MRR +0.056. There is no trade being made; the
old default was simply on the wrong side of the peak. **`0.4/0.6` is the
recommended setting**, with `0.3/0.7` preferable if rank-1 precision matters
most (it takes the best MRR in the sweep and gives up only 0.6pp of Hit@1
against pure BM25).
**Correction.** An earlier revision of this section, measuring only `0.7/0.3`,
concluded that fusion "buys deeper recall and pays for it at rank 1" and advised
callers taking a single top hit to prefer BM25. That was an artifact of the
badly-chosen weight, not a property of fusion. At `0.3/0.7` hybrid *beats* BM25
on MRR (0.6463 vs 0.6320) and on Hit@5 (78.8% vs 75.0%) while costing 0.6pp of
Hit@1. The advice below is corrected accordingly.
**Hybrid wins, once the weights are right.** At the old `0.7/0.3` the picture
looked like a trade: best at Hit@5 and Hit@10, worse than BM25 at Hit@1 and MRR.
The sweep above shows that was the weight, not fusion. At `0.4/0.6` hybrid leads
Hit@5 and Hit@10 outright; at `0.3/0.7` it also leads MRR and is within 0.6pp of
BM25 at Hit@1. Both dominate `0.7/0.3`.
The rows below are kept at the three original settings because they are what the
mode ablation measured — read them as "the shape of each stage in isolation",
and take the operating point from the sweep.
The same pattern shows up independently in omni-cortex's four-signal RRF ablation,
where adding BM25 to a dense retriever raised nDCG@5 while lowering Hit@1 and MRR.
Two different codebases, two different fusion schemes, same direction.
Vector-only being *worse* than BM25 at every turn-level cutoff except Hit@10 is
worth stating plainly rather than hiding: LongMemEval questions share substantial
vocabulary with their evidence turns, which is close to the best case for lexical
matching, and MiniLM at 384 dimensions is a small embedding model.
> **Run:** `cargo run --release --bin longmemeval_bench --features embeddings -- \
> benchmarks/longmemeval/longmemeval_s_cleaned.json --embeddings weights/all-minilm-l6-v2`
> For the GPU path use `--features embeddings-cuda`. That requires `nvcc` on
> `PATH` at *build* time — cudarc's build script shells out to it. The toolkit
> installs to `/usr/local/cuda/bin`, which many distributions do not export;
> check with `nvcc --version` and, if it is missing, add it somewhere every
> shell reads (for zsh that is `~/.zshenv`, not `~/.zshrc`, since build tooling
> runs non-interactively). The device is selected at runtime with a CPU
> fallback, so a machine without CUDA still produces correct numbers — just far
> more slowly, and the bench says so on startup.
>
> Weights: `huggingface.co/sentence-transformers/all-MiniLM-L6-v2` — place
> `model.safetensors` and `tokenizer.json` in the `--embeddings` directory.
### Oracle variant — `longmemeval_oracle`, n=500 (easier corpus, kept for continuity)
| Metric | ClawhDF5 (BM25-only, oracle variant) |
|--------|--------------------------------------|
| Hit@1 | 52.6% |
| Hit@5 | **84.4%** |
| Hit@10 | 90.4% |
| MRR | 0.6597 |
Turn-level. The 9.4-point gap between this and the full haystack's 75.0% is the
price of the harder corpus, and is the reason oracle-only numbers should not be
presented as LongMemEval results. Session-level figures on this variant are
degenerate — see below.
With real embeddings the same oracle corpus gives BM25-only 84.2% / vector-only
80.4% / hybrid **85.2%** Hit@5 turn-level — hybrid ahead at Hit@5 and Hit@10 and
behind at Hit@1, matching the full-haystack pattern above. (BM25-only reads 84.2%
here against 84.4% with zero embedding vectors: one question of 500 changes rank,
with MRR identical at 0.6597. On the full haystack the two agree exactly.)
### Retracted: session-level recall and the MemX comparison
Earlier revisions of this file reported session-level Hit@1/5/10 of **100.0%** with
MRR **1.0000**, uniform across all six question types, and claimed clawhdf5
"outperforms MemX at turn-level retrieval (84.4% vs 51.6%)". **Both are withdrawn.**
**The session-level numbers are a degenerate artifact.** On the `longmemeval_oracle`
variant, the ingested haystack for a question consists essentially only of that
question's evidence sessions. Every returned document therefore belongs to an answer
session, so session-level hit rate is ≈1.0 at rank 0 *by construction* — which is
exactly why the result was a uniform 100.0% across every question type. It measured
the shape of the corpus, not the retriever.
**The MemX comparison was not like-for-like on two independent axes.** MemX
([arxiv:2603.16171](https://arxiv.org/abs/2603.16171)) reports Hit@5 = 51.6% /
MRR = 0.380 at **fact-level granularity over 220,349 fact-level records drawn from
19,195 sessions**, and explicitly notes that fact-level "doubl[es] session-level
performance." Our 84.4% is **turn-level, on the oracle subset**. Different retrieval
granularity, and a corpus smaller by orders of magnitude. A higher number on an
easier corpus at a different granularity is not an outperformance claim, and it
should not have been presented as one.
The full-haystack half of that gap is now closed: the section above reports
`longmemeval_s` over all 500 questions. The **granularity** mismatch remains — MemX
measures fact-level, we measure turn-level and session-level — so no cross-system
claim is made here even now. Matching granularity would require fact-level
extraction over the haystack, which this harness does not do.
### Search Latency (LongMemEval, n=500 queries)
| Metric | Latency |
|--------|---------|
| avg | 1,004 µs |
| p50 | 1,017 µs |
| p95 | 2,031 µs |
| p99 | 2,912 µs |
Sub-millisecond median search across variable-length chat histories.
---
## Multi-Session Benchmark (MemoryArena)
**Dataset:** Deterministic synthetic conversations — 50 sessions × ~20 turns = 999 turns
**Topics:** Personal info, food preferences, music, travel, work/schedule, hobbies
**Queries:** 35 questions across 4 types
> **Run:** `cargo run --release --bin memory_arena`
### Results by Query Type
| Query Type | N | Hit@1 | Hit@5 | Hit@10 | MRR | Avg Latency |
|------------|---|-------|-------|--------|-----|-------------|
| single-session | 25 | 40.0% | 92.0% | 100.0% | 0.5788 | 7,853 µs |
| multi-session | 5 | 40.0% | 60.0% | 80.0% | 0.5333 | 7,887 µs |
| temporal | 3 | 33.3% | 66.7% | 66.7% | 0.5000 | 7,899 µs |
| knowledge-update | 2 | 0.0% | 50.0% | 50.0% | 0.2500 | 7,899 µs |
| **OVERALL** | **35** | **37.1%** | **82.9%** | **91.4%** | **0.5468** | **7,870 µs** |
**Key findings:**
- Hit@10 of 91.4% across all query types with BM25-only (no embeddings)
- Single-session recall strongest at 100% Hit@10
- Knowledge-update hardest (requires temporal disambiguation) — would improve significantly with vector similarity
- Latency dominated by BM25 index build over 999 turns (~7.9 ms)
---
## Memory Footprint
HDF5 file size at various record counts — 384-dimensional embeddings, 200-char text.
> **Run:** `cargo run --release --bin footprint_bench`
### Uncompressed (no WAL)
| Records | File Size | Raw Data | Bytes/Record | Throughput |
|---------|-----------|----------|--------------|------------|
| 100 | 176.4 KB | 169.5 KB | 1.8 KB | 100,000 rec/s |
| 1K | 1.7 MB | 1.7 MB | 1.8 KB | 109,643 rec/s |
| 10K | 17.0 MB | 16.6 MB | 1.7 KB | 118,100 rec/s |
| 50K | 85.0 MB | 82.8 MB | 1.7 KB | 110,723 rec/s |
| 100K | 169.8 MB | 165.6 MB | 1.7 KB | 111,422 rec/s |
**1.7 KB per record** — HDF5 overhead is near-zero. Ingestion throughput exceeds **100K records/sec**.
### With Gzip Compression (level 6)
| Records | Compressed | Ratio | Bytes/Record |
|---------|------------|-------|--------------|
| 100 | 31.5 KB | 5.37x | 323 B |
| 1K | 277.1 KB | 6.12x | 283 B |
| 10K | 2.7 MB | 6.17x | 281 B |
| 50K | 13.4 MB | 6.17x | 281 B |
| 100K | 26.9 MB | 6.15x | 282 B |
**6.2x compression ratio** — 100K agent memories in 27 MB compressed.
### Text Length Comparison (10K records, no compression)
| Text Length | File Size | Bytes/Record | Throughput |
|-------------|-----------|--------------|------------|
| short (50 chars) | 15.6 MB | 1.6 KB | 177,925 rec/s |
| medium (200 chars) | 17.0 MB | 1.7 KB | 172,152 rec/s |
| long (1000 chars) | 24.6 MB | 2.5 KB | 157,807 rec/s |
### WAL Overhead (1K records)
| Mode | File Size | Ingest Time | Overhead |
|------|-----------|-------------|----------|
| No WAL | 1.7 MB | 5.7 ms | — |
| With WAL | 1.7 MB + 9 B WAL | 5.3 ms | ±8% (negligible) |
---
## Consolidation Efficiency
Hippocampal-inspired memory consolidation improves both retrieval quality and search speed.
> **Run:** `cargo run --release --bin consolidation_efficiency`
### Retrieval Quality Before vs. After Consolidation
**Setup:** 1,000 records (10 signal + 990 noise), working_capacity=100
| Metric | Before | After | Delta |
|--------|--------|-------|-------|
| Records in store | 1,000 | 100 | 90% |
| Hit@1 | 100.0% | 100.0% | — |
| Hit@5 | 100.0% | 100.0% | — |
| Hit@10 | 100.0% | 100.0% | — |
| MRR | 1.0000 | 1.0000 | — |
| Search latency | 2,752 µs | 312 µs | **8.8x faster** |
Signal records survive consolidation because they are accessed 15+ times, giving them high decay scores. 900 noise records evicted, search speeds up 8.8x, and **zero quality loss** — perfect recall maintained.
### Consolidation Cycle Time
| Records | Cycle Time | Evictions | Promotions |
|---------|-----------|-----------|------------|
| 100 | 21 µs | 100 | 0 |
| 1K | 345 µs | 1,000 | 0 |
| 10K | 17.3 ms | 10,000 | 0 |
---
## Ephemeral Tier (Redis Comparison)
In-memory key-value store with TTL, capacity eviction, and embedding search.
No network hop, no serialization — direct HashMap operations.
> **Run:** `cargo run --release --bin ephemeral_perf`
### Latency Comparison
| Operation | clawhdf5 Ephemeral | Redis (single-node)¹ | Speedup |
|-----------|-------------------|---------------------|---------|
| SET | **356 ns/op** | ~25,000 ns/op | **70x** |
| GET (hit) | **179 ns/op** | ~25,000 ns/op | **140x** |
| GET (miss) | **62 ns/op** | ~25,000 ns/op | **403x** |
| DELETE | **124 ns/op** | ~25,000 ns/op | **202x** |
| SET+embedding | **268 ns/op** | N/A | — |
> ¹ Redis latency includes network round-trip (loopback). clawhdf5 ephemeral is in-process — no network.
### Throughput
| Operation | ops/sec |
|-----------|---------|
| SET | 2,810,649 |
| GET | 5,584,684 |
| DELETE | 8,093,731 |
| SET+EMB (384d) | 3,725,877 |
### Embedding Search (ephemeral tier)
| Scale | Latency |
|-------|---------|
| 10K entries @ 384d | 2.9 ms/query |
---
## World-Model Sample Loading (vs h5py / stable-worldmodel shape)
Reproduces the access pattern of `stable-worldmodel`'s HDF5 dataloader
([arXiv 2605.21800](https://arxiv.org/abs/2605.21800), LeCun/Balestriero
group), which supports HDF5 as one of three native formats and measures
generic HDF5 at **1,416-1,474 samples/s** (vs Lance 4,815) for per-frame
sample loading. This benchmark measures **clawhdf5 vs h5py on the same
machine and the same file**, so the comparison is hardware-controlled.
**Absolute numbers are not comparable to the paper's** - different hardware
(AMD Ryzen 7 7800X3D, local NVMe, warm page cache), smaller frames, and no
torch-tensor / transform step. Only the clawhdf5-vs-h5py ratio *here* is a
controlled result. The workload is the dataloader shape: a `(N, H, W, C)`
uint8 observation dataset (20,000 x 64x64x3 = 246 MB), each frame read once
per pass in a fixed shuffled (random-access) order, 10 passes.
Both read a **file written by h5py** - clawhdf5 parsing an
externally-produced HDF5 file is itself the interop result. h5py opens SWMR
with a 256 MB chunk cache, exactly `stable-worldmodel`'s `HDF5Dataset`; it
materialises each frame as a numpy array (`d[i]`) and sums it. clawhdf5
mmaps once, takes a zero-copy `&[u8]` over the contiguous dataset, and
indexes frame `i` as a subslice.
| Reader | samples/sec (median of 3) | vs h5py |
|--------|---------------------------|---------|
| **clawhdf5** (zero-copy view) | **593,000** | **8.1x** |
| **clawhdf5** (materialised copy per frame) | **518,000** | **7.1x** |
| h5py (swmr, 256 MB cache) | 73,000 | 1.0x |
The **materialised-copy row is the fair, equal-work comparison** - it
`to_vec()`s every frame so clawhdf5 pays the same per-frame allocation h5py
does, and it is still **7.1x faster**. That the copy costs almost nothing
(518k vs 593k) shows the h5py gap is **per-frame call overhead** (Python +
library dispatch), not data movement. This is an in-page-cache measurement:
it isolates the read-path overhead both libraries add on top of the OS,
which is the thing that differs - not disk bandwidth, which is shared.
Reproduce (`benchmarks/`):
```bash
python benchmarks/gen_worldmodel_frames.py /tmp/wm_frames.h5 20000
cargo run --release -p clawhdf5-bench --example worldmodel_sampling -- /tmp/wm_frames.h5 10
cargo run --release -p clawhdf5-bench --example worldmodel_sampling -- /tmp/wm_frames.h5 10 --copy
python benchmarks/bench_worldmodel_h5py.py /tmp/wm_frames.h5 10
```
Measured 2026-08-07 on tank (Ryzen 7 7800X3D, 246 MB dataset in page cache).
## Cross-Platform Notes
> **Run:** `./benchmarks/cross_platform.sh [--full] [--output results.json]`
### Measured Platforms
| Platform | CPU | 10K IVF Search | Notes |
|----------|-----|----------------|-------|
| Linux x86_64 | Intel i7-12650H (10C, 4.7 GHz) | 27 µs | Primary CI target |
| macOS aarch64 | Apple M3 Max (14C) | ~18 µs | ~33% faster via NEON SIMD |
### Reproducibility
```bash
rustup override set nightly
# Latency benchmarks (Criterion)
cargo bench -p clawhdf5-agent
# Full benchmark suite
cargo run --release --bin longmemeval_bench
cargo run --release --bin memory_arena
cargo run --release --bin footprint_bench
cargo run --release --bin consolidation_efficiency
cargo run --release --bin ephemeral_perf
```
---
## h5bench-Equivalent I/O Benchmarks
Criterion harness mirroring h5bench serial workloads. clawhdf5 benchmarks dated 2026-07-01;
libhdf5 1.14.6 head-to-head comparison dated 2026-06-30 (same hardware, same Criterion harness).
```bash
cargo bench -p clawhdf5-bench # clawhdf5-only
cargo bench -p clawhdf5-bench --features libhdf5-compare # head-to-head
```
### Sequential Read Throughput
Both read a 1-D contiguous f32 dataset. clawhdf5 parses from `Vec<u8>` (zero-copy);
libhdf5 reads from a temp file including `open` + `read` + `close` overhead.
| Workload | n=1K | n=10K | n=100K |
|----------|------|-------|--------|
| **clawhdf5** f32 | 634 ns / **5.9 GiB/s** | 2.44 µs / **15.3 GiB/s** | 24.5 µs / **15.2 GiB/s** |
| libhdf5 f32 | 45.2 µs / 85 MiB/s | 47.8 µs / 799 MiB/s | 73.9 µs / 5.0 GiB/s |
| **Speedup** | **71×** | **20×** | **3.0×** |
| clawhdf5 f64 | 743 ns / **10.0 GiB/s** | 4.17 µs / **17.8 GiB/s** | 43.3 µs / **17.2 GiB/s** |
| clawhdf5 from_disk (f64, OS I/O) | — | 10.1 µs / **7.4 GiB/s** | 77.6 µs / **9.6 GiB/s** |
| clawhdf5 hyperslab (f64, 10% slice) | — | 4.09 µs / **1.8 GiB/s** | 50.1 µs / **1.5 GiB/s** |
libhdf5 f64 comparison excluded — clawhdf5's datatype encoding differs from libhdf5's (known
gap), making cross-format reads unreliable for comparison.
### Chunked Read Throughput
| Matrix size | Latency | Throughput |
|-------------|---------|-----------|
| 64×64 f32 | 6.39 µs | **2.4 GiB/s** |
| 256×256 f32 | 41.7 µs | **5.9 GiB/s** |
| 512×512 f32 | 176 µs | **5.5 GiB/s** |
### Sequential Write Throughput
Both write to disk. At 100K elements both converge on the OS `write()` syscall ceiling.
| Workload | n=1K | n=10K | n=100K |
|----------|------|-------|--------|
| **clawhdf5** f32 | 9.44 µs / **404 MiB/s** | 25 µs / **1.49 GiB/s** | 228 µs / **1.63 GiB/s** |
| libhdf5 f32 | 77.9 µs / 49 MiB/s | 87.8 µs / 435 MiB/s | 214 µs / 1.74 GiB/s |
| **Speedup** | **8.2×** | **3.5×** | **≈ tie** |
| clawhdf5 f64 embeddings | 6.50 µs (n=128) | 8.67 µs (n=512) / **450 MiB/s** | 10.27 µs (n=1K) / **761 MiB/s** |
### Chunked Write: Codec Comparison (with auto-shuffle)
Auto-shuffle is applied before all compression codecs by default — AoS→SoA byte transpose,
implements byte-grouping pre-filter per arXiv:2506.18062. Shuffle dramatically improves
throughput for float/int data by creating long runs of similar bytes.
| Matrix size | Zstd-3 + shuffle | Deflate-6 + shuffle | Speedup |
|-------------|-----------------|---------------------|---------|
| 32×32 f32 | 48 µs / **81 MiB/s** | 39 µs / **100 MiB/s** | Deflate 1.23× faster (small chunk) |
| 128×128 f32 | **148 µs / 422 MiB/s** | 153 µs / **407 MiB/s** | Parity |
| 512×512 f32 | **1.34 ms / 748 MiB/s** | 1.39 ms / **719 MiB/s** | Zstd 1.04× faster |
Impact of auto-shuffle vs no-shuffle baseline:
| Matrix size | Zstd-3 speedup | Deflate-6 speedup |
|-------------|----------------|-------------------|
| 32×32 | +19% | +38% |
| 128×128 | +25% | **+204%** |
| 512×512 | +25% | **+157%** |
Both codecs perform at parity at large sizes (~720750 MiB/s). Use `.with_zstd(3)` or
`.with_deflate(6)` for write-heavy workloads. Use `.without_shuffle()` only for byte arrays
or data that doesn't benefit from AoS→SoA transposition.
### Chunked Write vs libhdf5 (deflate-6)
clawhdf5 compresses all chunks in memory and issues a single `write()`. libhdf5 flushes each
chunk individually via its Virtual File Layer (one `pwrite()` per chunk).
| Matrix | clawhdf5 deflate-6 + shuffle | libhdf5 deflate-6 | Speedup |
|--------|------------------------------|-------------------|---------|
| 32×32 f32 | 39 µs / 100 MiB/s | 172 µs / 23 MiB/s | **4.4×** |
| 128×128 f32 | 153 µs / 407 MiB/s | 3,150 µs / 20 MiB/s | **20.6×** |
| 512×512 f32 | 1,390 µs / 719 MiB/s | 53,300 µs / 19 MiB/s | **38.4×** |
The 32×32 speedup (4.4×) is lower than the 512×512 speedup (38.4×) because shuffle adds
overhead that dominates at 4 KB chunks. libhdf5 was benchmarked without shuffle. The speedup
compounds with matrix size because libhdf5's per-chunk VFL overhead is proportional to chunk
count while clawhdf5's single-pass cost is constant.
### Codec Comparison: Pcodec vs Zstd-3
Pcodec (arXiv:2502.06112) is a pure-Rust lossless numerical codec with 3094% better compression
ratio than Zstd for f32/f64 columns. Both sides benchmarked **without** auto-shuffle here (shuffle
degrades Pcodec which handles byte organization internally; Zstd-3 without shuffle numbers shown
for an apples-to-apples comparison).
| Matrix size | Pcodec | Zstd-3 (no shuffle) | Winner |
|-------------|--------|---------------------|--------|
| 32×32 f32 | 95 µs / **41 MiB/s** | 57 µs / **68 MiB/s** | Zstd-3 (1.66×) |
| 128×128 f32 | 528 µs / **118 MiB/s** | 179 µs / **349 MiB/s** | Zstd-3 (2.95×) |
| 512×512 f32 | 1.69 ms / **591 MiB/s** | 1.64 ms / **610 MiB/s** | Parity (3% diff) |
Pcodec's fixed per-chunk distributional analysis overhead (~400 µs) dominates at 32×32 (4 KB).
At 512×512 (1 MB) the speeds converge. **Pcodec's advantage is compression ratio, not encode
speed** — less data on disk means faster reads and lower storage cost. Enable with
`.with_pcodec()` for write-once/read-many workloads (embedding archives, scientific datasets).
### Metadata Throughput
clawhdf5 accumulates all metadata in memory and serializes in one pass. libhdf5 acquires a
global file mutex and flushes to disk on every attribute write or group creation.
**Attributes and datasets** (k = attribute or dataset count):
| Workload | k=4 | k=16 | k=64 | k=128 |
|----------|-----|------|------|-------|
| **clawhdf5** attrs_write (i64) | 8.05 µs / 494 Kop/s | 17.2 µs / 932 Kop/s | 49.2 µs / 1.30 Mop/s | 87.3 µs / 1.47 Mop/s |
| libhdf5 attrs_write | 100 µs / 40 Kop/s | 170 µs / 94 Kop/s | 472 µs / 136 Kop/s | 929 µs / 138 Kop/s |
| **Speedup** | **12.4×** | **9.9×** | **9.6×** | **10.6×** |
| clawhdf5 attrs_read | 1.06 µs / 3.78 Mop/s | 3.64 µs / 4.39 Mop/s | 15.7 µs / 4.08 Mop/s | 31.3 µs / 4.09 Mop/s |
| clawhdf5 string_attrs (write+read) | 5.17 µs / 774 Kop/s | 16.5 µs / 967 Kop/s | 33.6 µs / 951 Kop/s | — |
| clawhdf5 multi_dataset_write | 10.1 µs / 397 Kop/s | 31.5 µs / 508 Kop/s | 104 µs / 614 Kop/s | — |
**Groups** (k = group count):
| Workload | k=4 | k=16 | k=32 | k=64 |
|----------|-----|------|------|------|
| **clawhdf5** groups_create | 12.1 µs / 330 Kop/s | 33.7 µs / 475 Kop/s | 66.7 µs / 480 Kop/s | 121 µs / 529 Kop/s |
| libhdf5 groups_create | 140 µs / 28 Kop/s | 433 µs / 37 Kop/s | 690 µs / 46 Kop/s | 1,340 µs / 48 Kop/s |
| **Speedup** | **11.6×** | **12.8×** | **9.5×** | **11.1×** |
| clawhdf5 groups_traverse | 664 ns / 6.0 Mop/s | 3.55 µs / 4.5 Mop/s | 4.87 µs / 6.6 Mop/s | 10.6 µs / 6.0 Mop/s |
---
## vs libhdf5 Summary
| Workload | clawhdf5 | libhdf5 | Speedup |
|----------|----------|---------|---------|
| Sequential read, 1K f32 | 634 ns | 45.2 µs | **71×** |
| Sequential read, 100K f32 | 24.5 µs · 15.2 GiB/s | 73.9 µs · 5.0 GiB/s | **3.0×** |
| Sequential write, 100K f32 | 228 µs · 1.63 GiB/s | 214 µs · 1.74 GiB/s | **≈ tie** |
| Chunked write deflate-6, 512×512 | 1,390 µs · 719 MiB/s | 53,300 µs · 19 MiB/s | **38.4×** |
| Attribute write, 128 attrs | 87.3 µs · 1.47 Mop/s | 929 µs · 138 Kop/s | **10.6×** |
| Group create, 64 groups | 121 µs · 529 Kop/s | 1,340 µs · 48 Kop/s | **11.1×** |
### Why the Gaps
**Metadata (1013×):** libhdf5 was designed for MPI parallel filesystems where every metadata
write must be immediately visible to other processes. It acquires a global file mutex and
flushes to disk per operation. clawhdf5 builds the entire file in memory and writes it in one
shot — no locking, no flushing, no C heap allocation per message.
**Chunked compressed write (438×):** libhdf5 writes each chunk individually through its VFL
(Virtual File Layer), one `pwrite()` per chunk. clawhdf5 compresses all chunks in memory (Rayon
parallel when > 2 chunks), lays them out contiguously, and issues a single `write()`. The
speedup compounds with matrix size: libhdf5's per-chunk overhead is proportional to chunk count
while clawhdf5's architectural cost is constant.
**Small reads (2071×):** libhdf5's per-open overhead (chunk cache init, SWMR lock, metadata
read) dominates at sub-millisecond payloads. clawhdf5 has no global state — `File::from_bytes()`
starts parsing immediately.
**Large contiguous writes (≈ tie at 100K):** Both are bottlenecked by the OS `write()` syscall
to the page cache. There is no algorithmic headroom above ~1.7 GiB/s on this hardware.
### Caveats
- libhdf5 f64 read comparison excluded — clawhdf5's f32 datatype encoding differs from libhdf5's (known compatibility gap). f64 results are clawhdf5-only.
- Serial benchmarks. clawhdf5 uses Rayon for chunk compression when > 2 chunks; that parallelism is already reflected in the chunked write numbers.
- clawhdf5 reads from `Vec<u8>` (zero-copy from mmap in production); libhdf5 reads from a temp file. This gives clawhdf5 a structural read advantage that reflects realistic API usage.
---
## Independent Validation: tank (Ryzen 7 7800X3D), 2026-08-03
The `vs libhdf5 Summary` numbers above were re-run on a second, independently
administered machine (`tank`: AMD Ryzen 7 7800X3D, 8C/16T, Ubuntu 26.04, libhdf5
1.14.6 via `apt`) to confirm they reproduce off the original i7-12650H box, and to
add benchmark coverage for two claims that a documentation review found were not
traceable to any dated benchmark run (see git history around 2026-08-03 for context).
This section documents both.
### Reproduction of the vs-libhdf5 Summary table
| Workload | clawhdf5 (tank) | libhdf5 (tank) | Speedup (tank) | Speedup (i7-12650H, above) |
|----------|-----------------|-----------------|----------------|------------------------------|
| Sequential read, 1K f32 | 553 ns | 44.2 µs | **79.9×** | 71× |
| Sequential read, 100K f32 | 23.3 µs | 63.6 µs | **2.7×** | 3.0× |
| Sequential write, 100K f32 | 210 µs | 189 µs | **≈ tie** (clawhdf5 ~11% behind) | ≈ tie (clawhdf5 ~7% behind) |
| Chunked write deflate-6, 512×512 | 1.44 ms | 65.0 ms | **45.3×** | 38.4× |
| Attribute write, 128 attrs | 85.2 µs | 877 µs | **10.3×** | 10.6× |
| Group create, 64 groups | 130 µs | 1.37 ms | **10.6×** | 11.1× |
Five of six rows land within ~15% of the original i7-12650H figures — consistent
with normal cross-machine variance, not a methodology artifact. The chunked-write
row moved further (38.4× → 45.3×, +18%): tank's libhdf5 per-chunk write cost scales
worse relative to its own sequential-write throughput than on the i7, likely IPC/
memory-subsystem dependent. Both figures are real and dated; we report both rather
than picking one.
### New coverage: replacing the retracted "metadata parse / 308×" and "zero-copy mmap / 313 ns" claims
An earlier README revision cited `19 ns` vs `2,080 µs` (labeled, incorrectly, `308×`)
for "metadata parse," and `313 ns` for "zero-copy mmap" — neither figure traced to
any benchmark in this file. Both have been retracted from the README. In their
place, two new Criterion benchmarks were added
(`crates/clawhdf5-bench/benches/h5bench_meta.rs`,
`crates/clawhdf5-bench/benches/h5bench_read.rs`) and run on tank:
**`metadata_open_from_disk`** — opens a small file from disk (`std::fs::read` /
`hdf5::File::open`) and resolves one attribute. Both sides pay real OS I/O, unlike
the retracted claim.
| Operation | clawhdf5 | libhdf5 | Speedup |
|-----------|----------|---------|---------|
| Open file + read 1 attribute | 4.01 µs | 39.3 µs | **9.8×** |
**`metadata_parse_in_memory`** (clawhdf5-only) — times `File::from_bytes()` alone,
given bytes already resident in memory, i.e. header-parse cost with disk I/O
excluded. There is no fair libhdf5-side equivalent (its API has no "parse from an
in-memory buffer, skip the OS open" path), so this is reported standalone rather
than as a speedup multiple — this is the honest version of what the old `19 ns`
number was trying to claim.
| Operation | clawhdf5 (in-memory, no I/O) |
|-----------|------------------------------|
| Parse superblock + resolve 1 attribute | 549 ns |
**`read_zerocopy_mmap`** — opens via `MmapFile` and reads an f64 dataset through
`read_f64_zerocopy()`, summing every element to force the mapped pages to actually
fault in (returning only a slice length, as an earlier draft of this benchmark did,
would repeat the exact "measures nothing" mistake being fixed here).
| n (f64 elements) | clawhdf5 mmap (zerocopy, page-fault-forced) | clawhdf5 (`Vec<u8>` copy) | libhdf5 (disk open + copy) |
|-------------------|----------------------------------------------|----------------------------|------------------------------|
| 1,000 | 7.86 µs | 4.50 µs | 44.2 µs |
| 10,000 | 19.0 µs | 9.53 µs | 47.1 µs |
| 100,000 | 112 µs | 72.0 µs | 81.2 µs |
Honest result: at these sizes, forcing full materialization through the mmap path
is **not** faster than the plain `Vec<u8>` copy path — `mmap()`/page-fault overhead
per call outweighs the copy it avoids. This contradicts the retracted `313 ns`
claim outright and is a genuinely useful finding: `MmapFile`'s real advantage is
avoiding the allocation/copy for large files or sparse access patterns (lower peak
RSS, share pages across processes), not raw single-shot read latency at these
sizes. No README claim is made from this row; it's recorded here for the record
and to keep future readers from reintroducing the old number.
**Reproduce:**
```bash
cargo bench -p clawhdf5-bench --features libhdf5-compare --bench h5bench_meta -- metadata_open_from_disk
cargo bench -p clawhdf5-bench --features libhdf5-compare --bench h5bench_meta -- metadata_parse_in_memory
cargo bench -p clawhdf5-bench --features libhdf5-compare --bench h5bench_read -- read_zerocopy_mmap
```
## Independent Validation: tank — LongMemEval & Vector Search (Ryzen 7 7800X3D), 2026-08-05
Re-running the "LongMemEval Results" and "SIMD & Parallelism" sections above on
tank (AMD Ryzen 7 7800X3D, 8C/16T, Ubuntu 26.04, same machine as the
vs-libhdf5 validation above) to give both sections the dated, hardware-cited,
reproducible citation the top-of-file traceability note flags them as
missing.
### LongMemEval Results (reproduction)
```bash
cd benchmarks/longmemeval
wget https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_oracle.json
cargo run --release --bin longmemeval_bench
```
Recall numbers are deterministic (pure BM25 retrieval over a fixed dataset) and
reproduce exactly. Scoring target as declared in the LongMemEval section above:
retrieval recall, turn-level, k=10, `longmemeval_oracle` variant, BM25-only.
| Metric | Turn-Level |
|--------|------------|
| Hit@1 | 52.6% |
| Hit@5 | **84.4%** |
| Hit@10 | 90.4% |
| MRR | 0.6597 |
Session-level figures are omitted here — they are degenerate on the oracle variant
and have been retracted; see "Retracted: session-level recall and the MemX
comparison" above.
Search latency (hardware-dependent, tank numbers):
| Metric | avg | p50 | p95 | p99 |
|--------|-----|-----|-----|-----|
| Latency | 2,431 µs | 2,105 µs | 7,250 µs | 12,018 µs |
Higher than the i7-12650H figures at the top of this file (avg 1,004 µs) despite
tank's faster single-core performance elsewhere in this document — BM25 search
latency here scales with per-question haystack size and this run's variance is
wider (p99 is ~5x the mean), suggesting this metric is more sensitive to
momentary scheduling/cache effects than the flat-array vector-search benchmarks.
Recorded as-is rather than smoothed.
### SIMD & Parallelism (reproduction, with a correction)
```bash
cargo bench -p clawhdf5-agent --bench bench -- "^(strategy_scalar_10k|strategy_simd_10k|strategy_rayon_10k|adaptive_search_10k|simd_cosine_100k|rayon_cosine_100k)$"
```
The original 10K table above compares named benchmarks (`vector_search`,
`rayon`, `strategy`) that, on inspection, don't all exercise the same
scalar-vs-SIMD-vs-parallel axis the table implies — several of the
`simd_cosine_10k`/`sequential_cosine_10k`-style benchmarks actually call the
same underlying function under different names. The `adaptive_benches` group's
`strategy_scalar_10k` / `strategy_simd_10k` / `strategy_rayon_10k` benchmarks
are the ones that genuinely hold the dataset fixed and vary only the
`SearchStrategy` enum, so they're the correct apples-to-apples comparison —
used here instead.
| Strategy | Latency (tank) | vs Sequential |
|----------|-----------------|----------------|
| Sequential (scalar) | 502 µs | 1.0x |
| SIMD (auto-vectorized) | 327 µs | **1.53x** |
| Rayon (parallel) | 323 µs | **1.55x** |
| Adaptive (auto-select) | 339 µs | **1.48x** |
Honest finding: the speedup from SIMD/parallelism over scalar is real but
smaller here (~1.5x) than the i7-12650H figures above (~2.0x). The Ryzen 7
7800X3D's large L3 cache (96MB 3D V-Cache) measurably narrows the gap versus a
naive scalar loop compared to the i7 — this is a genuine hardware-dependent
result, not a regression or measurement error, and is recorded rather than
reconciled away.
At 100K, no `strategy_*` benchmark exists in the current suite (`adaptive_benches`
only covers n=10,000), so this row uses the same `simd_cosine_100k`/
`rayon_cosine_100k` benchmarks as the original table — not a true scalar
baseline, so no "vs Sequential" multiple is reported for it:
| Strategy | Latency (tank) |
|----------|-----------------|
| SIMD | 6.60 ms |
| Rayon parallel | 4.73 ms |
### Vector Search Latency & Comparison to MemX (reproduction)
```bash
cargo bench -p clawhdf5-agent --bench bench -- "^(vector_search_1k|simd_cosine_10k|simd_cosine_100k|prenorm_search_10k|ivf_search_10k_nprobe10|ivf_search_100k_nprobe10|ivf_pq_search_100k|rairs_search_10k_nprobe10|bm25_search_10k)$"
```
| Scale | Flat Search | Pre-norm | IVF (nprobe=10) | IVF-PQ | RAIRS |
|-------|-------------|----------|-----------------|--------|-------|
| **1K** | 47.8 µs | — | — | — | — |
| **10K** | 501 µs | 322 µs | 24.8 µs | — | 109 µs |
| **100K** | 6.60 ms | — | 608 µs | 865 µs | — |
(The 1K Pre-norm cell from the original table has no corresponding benchmark
in the current suite — not re-verified, left blank rather than guessed.)
Same not-like-for-like caveat as the "Comparison to MemX" section at the top of this
file applies — MemX's figure is end-to-end, these are a single component. Ratios are
an order-of-magnitude indication, not a benchmark result.
| Metric | MemX (claimed, end-to-end) | ClawhDF5 (tank, component only) | Ratio |
|--------|----------------------------|----------------------------------|-------|
| 100K flat search | <90 ms | 6.60 ms | ~14x |
| 100K IVF-PQ search | — | 865 µs | ~104x |
| Keyword search 10K | 1,100x improvement over unindexed | 520 µs (BM25) | Comparable |
Every figure in this subsection is faster than the corresponding i7-12650H
number at the top of this file, consistent with the Ryzen 7 7800X3D's higher
single-core throughput and larger cache observed in the vs-libhdf5 validation
above.