Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy-zoomies

.nornir/stree-reasoning.md

Static tree vs. static ANN — stree and vann are the same idea, twice

Class: reasoning · Functions: stree + vann. Why they share DNA, why they stay two structures, and why the generic STree<T> and the IVF layer were both rejected. The chosen shapes are in stree-design.md and vann-design.md.

⛔ SUPERSEDED (2026-07-02, same day). The vann::RagnarsEmbIndex IVF was removed hours after this note was written — nornir requires exact (100%-recall) search, and IVF is approximate (it skips nprobe cells and can miss the true nearest). vann today is the exact flat VectorIndex oracle + a provably-lossless int8/VNNI prefilter → f32 rerank (byte-identical to flat). stree32 is untouched and still shipping — it is an exact-key search tree, not a vector index. Everything below about IVF / nprobe / k-means / posting lists / "recall knob" describes deleted code and is retained only as design history. The one durable, still-true point is the shared batch-prefetch SIMD DNA: stree32's branchless count_lt and vann's vpdpbusd int8 dot both turn a latency-bound scan into a throughput-bound one via precomputed layout + prefetch. The int8 kernel lives on in the exact rerank path.

Original note (2026-07-02) for the two static, cache-aware search structures in znippy-zoomies: the exact-key S+tree (stree / stree32, Ragnar Groot Koerkamp's design) and the (now-removed) approximate-NN IVF vector index (vann::RagnarsEmbIndex). Built from the same Ragnar DNA.

TL;DR

stree32 (S+tree) vann (RagnarsEmb IVF)
Question "where is key q?" "which vectors are nearest q?"
Domain 1-D ordered keys (u32/i64) high-D vectors (768-D f32 → i8)
Answer exact index, or None approximate top-k (id, score)
Structure implicit S(+)-tree (pointer-free) k-means centroids + CSR posting lists
Traversal log_{B+1}(N) dependent hops 2 hops: scan centroids → scan nprobe postings
Inner loop branchless count_lt SIMD compare vpdpbusd int8 dot (VNNI)
Exactness 100% (recall = 1) tunable via nprobe (recall knob)
Speedup vs naive up to ~40× vs binary search ~60–1000× vs flat scan (data-dependent)
Static? yes — rebuilt only every ≥1024 commits yes — rebuilt on re-embed

They are two points on one design axis: precompute a memory layout that mirrors the traversal order, then let SIMD + prefetch turn a latency-bound scan into a throughput-bound one.


The similarities — one Ragnar DNA

Both structures win the same way, and it is not "a cleverer algorithm" — it is respecting the memory hierarchy. Three shared traits (the rail at the bottom of the diagram above):

1. Contiguous memory in traversal order (no pointer chasing)

Neither structure ever dereferences a pointer into cold memory on the hot path.

2. A branchless SIMD inner loop

Same shape: fold many elements per instruction, keep the branch predictor out of it. The specific intrinsic differs because the elements differ (ordered keys vs quantized vector lanes) — that difference is the whole reason they stay two concrete implementations rather than one generic (mirroring the deliberate STree64/STree32 split).

3. A software-pipelined batch that hides the miss (MLP)

This is the sharpest shared idea, and it has its own diagram:

A single query is latency-bound: every hop (down a tree level, or to the next posting run) is a cache miss the core would otherwise sit idle through. Ragnar's fix is to run P queries in flight: each query issues a prefetch for its next hop and then yields; another query's compute fills the gap. The wall clock collapses from "sum of the misses" to "the compute, with the misses overlapped."

Same driver, two payloads (the bottom rail of the pipeline diagram): a stree hop is prefetch child → count_lt → descend; a vann hop is prefetch posting rows → score_i8_batch → merge top-k.

Both also share the "static" contract: build is comparatively expensive (sorting keys / k-means), queries are cheap and read-only, and the structure is frozen between rebuilds — the reason the diagram dresses both in ice.


The differences — why they are still two structures

Ordered vs. unordered domain (the root difference)

stree exists because its keys are totally ordered: count_lt is meaningful, so the tree can route with exact arithmetic and land on the answer. Embedding vectors have no total order — "nearest in 768-D" is not a position in a sorted array — so vann cannot route; it must partition (k-means) and scan candidates. That is the line between an exact index and an approximate one.

Exact vs. approximate (and the knob)

Depth vs. breadth of traversal

Build cost & determinism

Quantization

vann additionally quantizes f32→i8 (the vectors are L2-normalized, so components live in [-1,1] and quantize to ~1e-2 cosine error) to quarter the bytes the scan streams. stree keys are exact integers — no quantization, no recall loss from it. int8 is a vann-only lever (and its recall ceiling on dense data is a documented seam, cured by an optional f32 re-rank of the candidate set).


Where they meet — the hybrid (why co-locating them pays off)

The two are not just neighbours by heritage; they compose in a retrieval stack. vann finds the fuzzy vector neighbours; an exact-key structure resolves the crisp parts:

That is the architectural payoff of putting the ANN engine in znippy-zoomies beside stree/gatling/psort: the exact-key tree, the fuzzy-vector index, the no-barrier batch driver (gatling), and the parallel sort (psort) that k-means and top-k both want are one toolbox, reusable by skade, nornir, and any other consumer without pulling a whole application repo.


Provenance & cross-repo notes