Every output whose length only the call knows, such as matches, rows, products, monoliths and cursor or stream batches, lands in an Arena you pass, and the verb answers a view over it without copying. Arena::new_in(&executor) places it where that executor computes, decided once, and Arena::new() makes host pages for the calling thread.
Each call rewinds its arena, so the previous call’s views die and a steady loop allocates once.
To keep a result past the next call, or to read one while making the next, pass that call another arena.
An arena serves one call at a time, so each deferred call needs its own.
Every call, blocking or deferred, borrows the arena as &mut Arena and its result borrows it in turn, so the borrow checker refuses a stale view at compile time and keeps a call’s inputs out of the arena it rewinds.
An arena from a VRAM executor holds device memory, which host accessors refuse with Status::Device.
It grows, fills and frees on that executor’s first stream, so it serves only calls on that executor, any other refused with Status::Device, and holds the executor until it drops.
let mut arena = Arena::new_in(&executor)?; // Arena::new() is host pages for the calling threadfor batch in &batches { let matches = index.on(&executor).search(batch, 10, &mut arena)?; // rewinds: the last batch's matches are gone total += matches.query(0)?.distances.iter().sum::<f64>();}let (mut found, mut fetched) = (Arena::new()?, Arena::new()?); // two live results, two arenasfor &key in index.search(&query, 10, &mut found)?.query(0)?.keys { rerank(key, index.get::<f32>(key, &mut fetched)?.view()?); // rewinds fetched, never found}let (mut executors, mut arenas) = (Vec::new(), Vec::new()); // one executor and one arena per submitted callfor _ in &shards { executors.push(Executor::for_ram(&[], None, 8)?);}for executor in &executors { arenas.push(Arena::new_in(executor)?);}let mut calls = Vec::new();for ((shard, executor), arena) in shards.iter().zip(&executors).zip(&mut arenas) { calls.push(shard.deferred(executor).search(queries.clone().expect("room for a copy"), 10, arena)?);}for call in calls { consume(call.await?); // or: call.wait()?}
The library never frees an arena under a call: dropping one waits for the calls writing into it, and a call naming an arena that serves another call finishes with Status::Contended and rewinds nothing, so even a leaked Deferred leaves it safe.
An arena’s describe() says where it lives, and on which device.
Fixed-size reports, such as stats, descriptions and required, stay plain values, and so does state you carry between calls, like the attention carry.
rows borrows a packed slab; a dataset whose rows are padded to a wider pitch passes MatrixView::from_raw_parts instead, which takes the byte stride directly.
The scalar type of the input is independent of the stored quantization, so the f32 slab of the Quickstart lands in an i8 index with nothing converted beforehand.
The scalar types are NumKong’s, so nothing needs re-wrapping to move between the two crates, f16 and bf16 included, emulated where the CPU has no native support.
Rust type
Scalar
f64, f32
F64, F32
bf16, f16
BF16, F16
e5m2, e4m3, e3m2, e2m3
E5M2, E4M3, E3M2, E2M3
i8, i4x2
I8, I4x2
u8, u4x2, u1x8
U8, U4x2, U1x8
Initializing an index with a packed quantization and inserting floating-point vectors quantizes on ingest; the u1x8 type also lets you pass packed bit-vectors directly.
Each configurable verb’s config converts into its Dense*Config struct, whose other fields are named and default to what the C interface derives.
So the bare value is the whole call, as in search(&queries, 10, &mut arena), insert(&keys, &vectors) or save(path), and ..DenseSearchConfig::from(10) reaches every knob.
query pairs one query’s keys with its distances, nearest first and trimmed to the cells the call wrote, and iter walks every query in order.
for query in matches.iter()? { for (key, distance) in query.keys.iter().zip(query.distances) { println!("{key} at {distance}");}}
The breadth of the candidates rides the call rather than the handle, as expansion_factor on every config that walks the graph, so a recall sweep is a loop over ef values against one immutable index, with no mutation and no synchronization between points. traversal: Traversal::Scan scores every entry instead of walking, on RAM and NVMe handles, which gives that sweep its exact ground truth; a VRAM handle refuses it with Status::Unsupported. DenseTraversalStats reports the members visited, the distances computed and the starvation restarts, a graph-connectivity signal rather than a cost, overwritten by every call it is passed to.
API
StructDenseInsertConfigThe rows of an insertion and the knobs of its walk; see DenseVerbs::insert.
Walks the graph by default; Traversal::Scan scores every entry exactly, on RAM and NVMe
handles, and a VRAM handle refuses it with Status::Unsupported.
Admits only the keys it approves; the rest still route the walk, so recall does not
collapse at low selectivity. A VRAM handle refuses one with Status::Device.
An executor places it where it computes, host pages or its GPU's memory, decided once, and
nothing is allocated until a call writes into it. Every call rewinds and refills it,
invalidating the previous call's outputs, so a steady loop allocates once.
Every verb, blocking or deferred, borrows it as &mut Arena and answers outputs that borrow it in turn.
A Residency::Vram arena holds device memory, so the host accessors of its outputs refuse
with Status::Device. It grows, fills and frees on the first stream of the executor that made
it, so it serves only calls on that executor, any other refused with Status::Device, and
holds the executor until it drops.
Dropping it waits for the calls still writing into it.
Both pointers must address live structures laid out as ArrowSchema and ArrowArray and
following Arrow's C data interface, whose host buffers stay valid and unwritten for as long as
self stays borrowed.
into_partsHands the column over past the arena's borrow, for a consumer that takes the structures by value, refused with Status::Device over device memory, which must be released before its executor drops.
to_arrowExports every query's matches as one Arrow LargeListView of Struct<key, distance> in place, refused with Status::Contended while another call fills the arena.
Sub-byte kinds pack several dimensions per byte, as their xN suffixes spell. The library
reserves the numbering gaps for kinds not yet implemented, so this enum is open.
Values
F64
1
IEEE-754 binary64: ±1.80e308, epsilon 2.22e-16, subnormals to 4.94e-324, with infinities and NaNs.
F32
2
IEEE-754 binary32: ±3.40e38, epsilon 1.19e-7, subnormals to 1.40e-45, with infinities and NaNs.
BF16
3
Truncated f32 - 8-bit exponent, 7-bit mantissa: f32's range at ±3.39e38 and epsilon 7.81e-3, with
infinities and NaNs.
F16
4
IEEE-754 binary16: ±65504, epsilon 9.77e-4, subnormals to 5.96e-8, with infinities and NaNs.
E5M2
5
FP8 with a 5-bit exponent: ±57344, epsilon 0.25, with infinities and NaNs - f16's dynamic range carried
on two mantissa bits.
E4M3
6
FP8 with a 4-bit exponent: ±448, epsilon 0.125, no infinities, and two NaN codes at 0x7F and 0xFF.
E3M2
7
FP6 with a 3-bit exponent, byte-aligned with two padding bits: ±28, no infinities or NaNs, so every one
of its 64 codes is a number.
E2M3
8
FP6 with a 2-bit exponent, byte-aligned with two padding bits: ±7.5, no infinities or NaNs, and 72% of
its codes outside [-1, 1].
I8
10
8-bit signed integer spanning -128 to 127, every code a number.
I4x2
11
Two signed nibbles per byte spanning -8 to 7, even dimensions in the low nibble and odd ones in the high.
I32
12
32-bit signed integer spanning ±2.15e9, every code a number.
U8
20
8-bit unsigned integer spanning 0 to 255, every code a number.
U4x2
21
Two unsigned nibbles per byte spanning 0 to 15, even dimensions in the low nibble and odd ones in the high.
U1x8
22
Eight single-bit dimensions per byte - the form the Hamming & Jaccard metrics score, where only the
set-bit count of a XOR or AND carries meaning.
Slabs whose rows are padded to a wider pitch - memory-mapped datasets, cache-line-aligned
arenas - go through MatrixView::from_raw_parts instead, which takes the byte stride.
Rust-only.
keys_from_arrowViews the keys of one Arrow fixed_size_binary(16) column in place, tagged arrow.uuid or not, refused as ArrowRows::new refuses.