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

nordisk/nornir

.nornir/vector-cpu-simd.md

G2 — optimize the CPU vector path (SIMD + int8 VNNI)

Status: ✅ kernels + bench + tests landed (feat/g-series), benchmarked on the dev box. VNNI is structural-only there (Zen 2 has no AVX-512). See the 2026-07-02 update below — the int8 path is now an exact rerank, the approximate IVF is gone, and the kernels moved to znippy_zoomies::vann.

Goal

Shrink the GPU↔CPU embedding/search gap. The vector index (src/vector/mod.rs) is an exact-flat ANN: every query is scored against every stored vector by cosine (a dot product over L2-normalized f32). The score loop is the hot path. We attack it with wider SIMD and a denser numeric format, runtime-detected so one binary runs everywhere.

Layers (each independently dispatched)

layer kernel when
baseline dot_scalar always sound; the correctness oracle
f32 SIMD dot_avx512 (16-wide FMA) → dot_avx2 (8-wide FMA) runtime is_x86_feature_detected!
int8 (new) dot_i8_vnni (vpdpbusd, 64 int8 MAC/instr) → dot_i8_scalar vnni_available()
multicore scoped-thread split → per-thread top-k merge corpus > 2·1024 rows

The f32 SIMD + multicore layers pre-existed; G2 added the int8/VNNI layer, the batched per-query setup, and the bench.

int8 quantization (why it's cheap and accurate)

Stored vectors are L2-normalized, so every component is in [-1, 1]. Quantizing by Q = 127 round-to-nearest (clamped to [-127,127]) maps them to i8 with no scale to track per vector. The cosine of two quantized vectors ÷ Q² recovers the f32 cosine to ≲4e-2 absolute — and crucially the ranking is preserved (the test asserts identical top-1 and bounded score error). Two payoffs:

  1. 4× less memory traffic. The score loop is memory-bound on a big corpus; i8 rows are a quarter the bytes of f32, and a cache line covers 4× the components (cache-blocking falls out for free).
  2. VNNI density. AVX-512 vpdpbusd does 64 int8 multiply-accumulates into int32 per instruction vs 16 f32 FMA lanes. vpdpbusd is unsigned×signed, so we keep the row i8 and bias the query to u8 (q+128), correcting with Σ q·r = Σ (q+128)·r − 128·Σ r (the row's integer sum Σ r is precomputed once at quantization time).

Benchmark — nornir vector bench / bench_kernels

Pure-Rust timer (no criterion dep). It builds a deterministic n×dim corpus, scores one query through every kernel, and measures the int8 error against the scalar f32 reference — so the bench doubles as a correctness gate.

Dev box: AMD Threadripper PRO 3975WX (Zen 2 — AVX2+FMA, NO AVX-512/VNNI), 768-dim, n=200k, 30 iters:

kernel throughput speedup vs scalar max_err
scalar 1.5 Mdps 1.00× —
simd (avx2+fma) 6.7 Mdps 4.48× 9.5e-7
int8 (scalar) 5.7 Mdps 3.79× 4.2e-2

Stable across corpus sizes (20k cache-warm → 1M memory-bound: 4.4–4.5× SIMD, 3.8× int8).

GPU↔CPU ratio shrinking

A CUDA/ROCm GPU embeds/searches on the order of ~100× a scalar CPU. The G2 CPU path turns that scalar baseline into 4.48× (AVX2) — closing the gap to ~22×. On AVX-512 silicon dot_avx512 is 16-wide (another ~2× over AVX2) and vpdpbusd would push the int8 path past the AVX2 f32 number, shrinking the ratio further toward single digits. That VNNI step is structural-only here — the dev CPU lacks AVX-512, so the code compiles and is unit-tested via the scalar-i8 fallback, but the VNNI instruction can't be timed on this box.

What is benchmarked vs structural-only

Correctness (inject-assert law)

2026-07-02 — exact-only, exact int8 rerank, and where we stand vs academia

Two approximate paths removed. CozoDB HNSW and the Ragnar-style int8 IVF (ragnars_emb) are both gone. As of 2026-07-02 nornir's vector search was exact-only: the flat VectorIndex oracle plus a provably-lossless int8 prefilter → f32 exact rerank. (Update: a vendored, rayon-free HNSW ANN in znippy_zoomies::vann later landed as a scale-gated live fast path — built only at ≥ HNSW_MIN_ROWS (4096) and disable-able via NORNIR_VECTOR_HNSW, with the flat scan kept as the exact oracle. That is the vendored znippy HNSW, not the removed external CozoDB engine; see vector.md.) The int8 stage no longer merely "preserves ranking to ≲4e-2" — it keeps a candidate band (int8_score ≥ τ − 2E, E = quantization error bound) that provably contains the true top-k, then the f32 rerank returns byte-identical results to the flat oracle (test wired_i8_search_matches_exact_f32). The kernels now live in znippy_zoomies::vann (VectorIndex, score_i8_batch, quantize_i8), fork-join parallel across physical cores via gatling_forkjoin; nornir re-exports them.

Embedding vs search are two different speedups — do not conflate them:

Where we stand vs the academic state of the art (exact CPU kNN)

A web sweep (2026-07-02) positioned us against the published exact-search CPU literature. CWI Amsterdam is effectively the only institution publishing exact CPU kNN numbers, so they are the peer to beat:

Verdict: on the kernel we are top-notch — parity-or-better with every published hand-SIMD baseline (FAISS/USearch/SimSIMD), and our int8 prefilter → lossless f32 rerank is the same architecture as FAISS IndexRefine / ScaNN reorder / Elastic-Lucene, but with a stronger guarantee (provably exact, not approximate). int8 cuts bytes 4× exactly where the bandwidth bound bites.

Two levers left (neither started; they stack; NEITHER is Ragnar)

Both are exact — unlike the removed IVF, they never skip a vector.

  1. int8 VNNI accumulators 4 → 8–12 (the one that helps us). VPDPBUSD has 5-cycle latency and dual-issues on server silicon (Cascade Lake / Sapphire Rapids / Emerald Rapids). 4 accumulators saturate a single-VNNI-port client (~80%) but only ~40% of a dual-port server. Bumping to 8–12 (Little's law: latency ÷ recip-throughput) saturates both ports. Bit-exact (integer add is associative). Biggest effect in the cache-resident regime — nornir's common case (tens of thousands of chunks). Helps every single-query search. Source: uops.info VPDPBUSD.
  2. Batch queries into a blocked GEMM (situational). Same math, byte-identical result — it reads the corpus once per batch instead of once per query, flipping bandwidth-bound → compute-bound. Documented 25–64× on CPU (Stanford 40×, McCormick 25×, droyed 64–86×). Only helps when many queries are issued at once — nornir search is usually one query, so this is lower priority than (1). Composes with int8 (int8 GEMM via VNNI/AMX). It is not the Ragnar IVF: IVF was approximate (skips cells, recall < 100%); GEMM batching scans every vector and is exact.

Files