Recall cliff (up to 65pp) on sparse data at weak-block dims (dim = 8·odd or 16·odd)
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
Source: RyanCodrai/turbovec