#309·turbovec

Recall cliff (up to 65pp) on sparse data at weak-block dims (dim = 8·odd or 16·odd)

Author: RyanCodraiCreated Jul 29, 2026Updated Jul 31, 2026
Labelsneeds-human-decision

Found by bug-hunt wave 11 (recall-quality lens). Severity: high for sparse/bag-of-words workloads; dense embeddings unaffected.

turbovec/src/rotation.rs:92: block_size(dim) is the largest power-of-two divisor of dim, and the Walsh-Hadamard mixes only within B-sized blocks (2 rounds, rotation.rs:30-48). When B is 8 or 16, the transform cannot Gaussianize inputs that actually need mixing — and recall falls off a cliff between adjacent dims.

Recipe: n=20,000, 10-nonzero random sparse (N(0,1) values), L2-normalized, 300 held-out queries, seeds 0 and 1. Metric matches benchmarks/suite/recall_*.py (control: real openai-1536 n=100k 2-bit → R@1 0.886 vs published 0.891; GloVe d=200 4-bit → 0.840 vs 0.8498).

dim B bits R@1 (s0 / s1) R10@10
1024 1024 2 0.883 / 0.850 0.919 / 0.915
1016 8 2 0.453 / 0.427 0.557 / 0.547
1032 8 2 0.393 / 0.410 0.533 / 0.553
1040 16 2 0.780 / 0.760 0.839 / 0.833
1088 64 2 0.883 / 0.860 0.923 / 0.918
2048 2048 2 0.913 / 0.917 0.942 / 0.944
2056 8 2 0.270 / 0.263 0.453 / 0.449

Monotone recovery in B, reproducible across seeds, and the gap widens with dim (−47pp at 1032, −65pp at 2056). Present at 4-bit too (d=2048 R@1 0.990 vs d=2056 0.707). Also reproduced on a tf-idf-like recipe (nz=20, |N(0,1)|): d=1024 0.847 vs d=1032 0.483 at 2-bit.

Scope: dense real embeddings are fine — openai-1536 truncated to 1528/1032/520/264 (all B=8) and nytimes-256→248 all matched their power-of-2 neighbours within ±1.5pp. This bites sparse / bag-of-words / SPLADE-style / hashed-feature vectors — exactly what the rotation exists for.

Undocumented: rotation.rs internally acknowledges "weak-block dims (B = 8)", but no user-facing doc (README, docs/api.md) warns that dim choice affects recall. Someone picking dim 1000, 1032, 2056 or 3000 with sparse features silently loses half their recall; padding to 1024/1088 fixes it. At minimum this needs a doc warning; better would be a constructor warning or more mixing rounds when B is small.

Generated with Claude Code