bench(longmemeval): --float16, and float16 measured on real embeddings

`longmemeval_bench --float16` builds every per-question store with
MemoryConfig::float16, so the vector stage searches half-rounded
embeddings exactly as such a store holds them.

Full longmemeval_s (500 questions, ~494 turns each) with real
all-MiniLM-L6-v2 embeddings, f32 vs float16, on tank (CUDA): identical
at every Hit@k and MRR, turn and session level, in all eight modes —
bar RRF session MRR 0.9253 vs 0.9254 and one or two flips out of ~320
in which gold session ranks first. The f32 run reproduces the published
hybrid numbers exactly. The earlier float16 evidence was synthetic
clustered data only; this is the real-embedding check.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-24 19:21:14 -05:00
co-authored by Claude Opus 5.5
parent c470244a6f
commit dbaf3f505d
3 changed files with 40 additions and 1 deletions
+24
View File
@@ -226,6 +226,30 @@ The in-memory cache holds the half-rounded values, so the store searches the
same before and after a reopen; RAM use is unchanged (the cache is still same before and after a reopen; RAM use is unchanged (the cache is still
`f32`). What `float16` saves is disk, and the I/O that goes with it. `f32`). What `float16` saves is disk, and the I/O that goes with it.
**On real embeddings.** The table above is synthetic clustered data. The full
LongMemEval haystack (`longmemeval_s`, 500 questions, ~494 turns each) with
real all-MiniLM-L6-v2 embeddings, run once with `f32` stores and once with
`--float16`, measured 2026-09-24 on tank (embeddings on an RTX 5060 Ti):
```bash
cargo run --release -p clawhdf5-bench --bin longmemeval_bench --features embeddings-cuda -- \
benchmarks/longmemeval/longmemeval_s.json --embeddings weights/all-minilm-l6-v2 [--float16]
```
| mode | turn Hit@1 | turn Hit@5 | turn Hit@10 | turn MRR | session Hit@5 | session MRR |
|---|---:|---:|---:|---:|---:|---:|
| Hybrid 0.4 / 0.6, f32 | 51.6% | 81.4% | 87.8% | 0.6430 | 96.8% | 0.9347 |
| Hybrid 0.4 / 0.6, float16 | 51.6% | 81.4% | 87.8% | 0.6430 | 96.8% | 0.9347 |
| Vector only, f32 | 36.0% | 71.8% | 81.6% | 0.5031 | 94.2% | 0.8901 |
| Vector only, float16 | 36.0% | 71.8% | 81.6% | 0.5031 | 94.2% | 0.8901 |
All eight modes the harness runs (BM25, vector, hybrid, RRF, stemmed, and
both re-rank variants) were identical at every Hit@k and MRR, turn and session
level, except RRF's session MRR (0.9253 vs 0.9254) and one or two flips in
which of two gold sessions ranks first, out of ~320. Those flips show the
half-precision path was in effect; they do not change a single hit. The f32
run reproduces the published hybrid numbers exactly.
### Opening a store (`read_from_disk`) ### Opening a store (`read_from_disk`)
`HDF5Memory::open` memory-mapped the file, copied the whole mapping into a `HDF5Memory::open` memory-mapped the file, copied the whole mapping into a
+3 -1
View File
@@ -89,7 +89,9 @@
precision.** At 100K x 384 the file goes from 154.0 to 80.8 MiB (−48%), a precision.** At 100K x 384 the file goes from 154.0 to 80.8 MiB (−48%), a
checkpoint from 752 to 512 ms and open from 300 to 252 ms, with the same checkpoint from 752 to 512 ms and open from 300 to 252 ms, with the same
vector recall@10 against an exact scan (0.999 vs 0.994) and the same vector recall@10 against an exact scan (0.999 vs 0.994) and the same
`hybrid_search` latency; at 10K open is 3 ms slower. The cache rounds each `hybrid_search` latency; at 10K open is 3 ms slower. On the full
LongMemEval haystack with real MiniLM embeddings every retrieval metric is
identical to `f32` (`longmemeval_bench --float16`). The cache rounds each
embedding as it is saved, so memory and file agree bit for bit and a store embedding as it is saved, so memory and file agree bit for bit and a store
returns the same results before and after a reopen (tested). Out-of-range returns the same results before and after a reopen (tested). Out-of-range
values are refused with `MemoryError::InvalidEntry` rather than stored as values are refused with `MemoryError::InvalidEntry` rather than stored as
@@ -64,6 +64,11 @@ use tempfile::TempDir;
const EMBEDDING_DIM: usize = 384; const EMBEDDING_DIM: usize = 384;
/// `--float16`: build every per-question store with `MemoryConfig::float16`,
/// so embeddings are rounded to half precision as they are saved — exactly
/// what such a store searches over.
static FLOAT16: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// A mode's fusion, as one short string for the reports. /// A mode's fusion, as one short string for the reports.
fn describe(mode: Mode) -> String { fn describe(mode: Mode) -> String {
let fusion = match mode.fusion { let fusion = match mode.fusion {
@@ -431,6 +436,7 @@ fn evaluate_question(
let mut config = MemoryConfig::new(dir.path().join("lme.h5"), "lme-bench", EMBEDDING_DIM); let mut config = MemoryConfig::new(dir.path().join("lme.h5"), "lme-bench", EMBEDDING_DIM);
config.wal_enabled = false; config.wal_enabled = false;
config.compact_threshold = 0.0; config.compact_threshold = 0.0;
config.float16 = FLOAT16.load(std::sync::atomic::Ordering::Relaxed);
let mut memory = HDF5Memory::create(config).expect("failed to create HDF5Memory"); let mut memory = HDF5Memory::create(config).expect("failed to create HDF5Memory");
memory.set_token_filter(mode.tokens); memory.set_token_filter(mode.tokens);
@@ -940,6 +946,10 @@ fn main() {
limit = Some(v.parse().expect("--limit must be a positive integer")); limit = Some(v.parse().expect("--limit must be a positive integer"));
} }
"--sweep" => sweep = true, "--sweep" => sweep = true,
"--float16" => {
FLOAT16.store(true, std::sync::atomic::Ordering::Relaxed);
eprintln!("Stores use MemoryConfig::float16 (half-precision embeddings)");
}
"--rerank-sweep" => { "--rerank-sweep" => {
// Re-ranking needs the vector stage to have candidates worth // Re-ranking needs the vector stage to have candidates worth
// reordering, so this is an embeddings-only comparison. // reordering, so this is an embeddings-only comparison.
@@ -971,6 +981,9 @@ fn main() {
--rerank-sweep\n\ --rerank-sweep\n\
compare re-ranking off, metadata-only (the old\n\ compare re-ranking off, metadata-only (the old\n\
behaviour) and blended at several half-lives.\n\ behaviour) and blended at several half-lives.\n\
--float16\n\
build each store with MemoryConfig::float16, to\n\
compare retrieval on half-precision embeddings.\n\
--sweep instead of the three named modes, sweep vector_weight\n\ --sweep instead of the three named modes, sweep vector_weight\n\
from 0.0 to 1.0 in 0.1 steps. The 0.7/0.3 default was\n\ from 0.0 to 1.0 in 0.1 steps. The 0.7/0.3 default was\n\
never searched; this is what searches it." never searched; this is what searches it."