Zig · Linear Algebra

Linear Algebra

const usearch = @import("usearch");const ValueLookup = @import("usearch").ValueLookup;const DenseProducts = @import("usearch").DenseProducts; 3.0.0-rc.1 · 4c3e5023

§ Sparse Multiplication

Three verbs, multiply, maxsim and attend, treat the index as a sparse operand rather than as a kNN store, each folding its arithmetic into the graph walk instead of materializing a match list. All three read logits rather than distances, so they refuse anything but an inner-product index with Error.ConfigMismatch.

multiply is a sparse matrix product: every left row is walked against the index, and how many pairs it scores is known only once the walk ends, so the products come back as a ragged DenseProducts: keys, scores and their Apache Arrow offsets, all in the arena.

const products = try index.on(executor).multiply(try .of(f32, left, 768), &arena, .{ .wanted = 64 });
for (0..products.queries) |row| {
    const pairs = try products.query(row);
    accumulate(row, pairs.keys, pairs.scores);
}

offsets, keys and scores answer the whole tape at once, for a consumer that takes Arrow buffers as they are.

§ Late Interaction

maxsim is late interaction: every query row takes its best match within a key, and those maxima sum to that key’s score. A document is one key holding its token rows, which is what a values_per_key above one exists for, and the outputs are sized over documents rather than rows. The keys land as a DenseProducts, one run per query, largest score first, and a zero wanted keeps every key a query scored.

const documents = try index.on(executor).maxsim(try .of(f32, tokens, 768), tokens_per_query, 10, &arena, .{});
for (0..documents.queries) |query| {
    const ranked = try documents.query(query);
    rerank(query, ranked.keys, ranked.scores);
}

§ Attention

attend is Flash Attention folded into the walk: every key it scores enters an online softmax, with no top-k cut, and the outputs come back as wide as the keys and divided by the partition. The index stores no values, so a ValueLookup resolves each scored key to its row, as wide as the keys and stored in the index’s quantization. Its trampoline checks the kind and width every batch states, so a missing row, or one of another kind or width, refuses the call with Error.ShapeMismatch. It runs on the executor’s threads, so it must tolerate concurrent calls. That trampoline is a host function, which serves RAM and NVMe indexes; a VRAM walk calls the lookup on the device, so a VRAM index takes a ValueLookup whose callback is device-callable, set directly. The head_count query heads of grouped-query attention share one walk per token, row t · head_count + h being head h of token t, and a carry of one DenseSoftmaxState per query row, lent rather than written in the arena, folds this branch into another over the same queries. The attended rows land in the arena as the queries’ own scalar type, or as the config’s outputs_scalar.

const Cache = struct {
    rows: []const usearch.Scalar.BF16,
    fn resolve(self: *const @This(), key: usearch.Key) ?[]const usearch.Scalar.BF16 {
        const entry: usize = @intCast(key.asInteger() orelse return null);
        return self.rows[entry * 128 ..][0..128];
}
};
const attended = try index.on(executor).attend(try .of(f32, queries, 128), .of(usearch.Scalar.BF16, &cache, Cache.resolve), &arena, .{
    .head_count = 8,
    .carry = carry,
});
const first_head = try attended.row(f32, 0);

API

Types

DenseSoftmaxStateThe online-softmax carry of one query row - Flash Attention's running maximum and partition.
pub const DenseSoftmaxState = extern struct

Mirrors usearch_dense_softmax_state_t.

The two are one concept: a maximum without its partition is a number nothing can rescale, so they share an element rather than travelling as two arrays a caller could supply half of.

Fields

maximum_scoref32The largest logit seen so far - the recurrence's m.
partitionf32The sum of exponentials taken against that maximum - the recurrence's l.
DenseMultiplyConfigOptional settings of DenseIndex.multiply.
pub const DenseMultiplyConfig = struct

Fields

wantedusizeBound on pairs kept per left row, the first ones its walk scored; zero keeps every pair.
expansion_factorusizeThe breadth of the candidates, which widens the emitted pattern; zero derives.
progressProgressCallback
progress_stepsusizeReports the call makes at most, the last at completion; zero derives to ten.
stats?*DenseTraversalStats
DenseAttendConfigOptional settings of DenseIndex.attend.
pub const DenseAttendConfig = struct

Fields

head_countusizeThe query heads sharing the index's key-value head, and with it one walk per token: row t · head_count + h is head h of token t. It must divide the rows; zero means one.
carry?[]DenseSoftmaxStateThe softmax state, one per query row, lent rather than written in the arena; null skips it.
outputs_scalarScalarThe scalar type the attended rows land as; .unknown takes the queries' own.
expansion_factorusizeThe breadth of the candidates; zero derives. Wider candidates walk further, folding more of the tail.
score_scalef32The logit scale, conventionally the inverse square root of the head dimensionality; zero derives it from the index's frozen geometry.
progressProgressCallback
progress_stepsusizeReports the call makes at most, the last at completion; zero derives to ten.
stats?*DenseTraversalStats
DenseMaxsimConfigOptional settings of DenseIndex.maxsim.
pub const DenseMaxsimConfig = struct

Fields

expansion_factorusizeThe breadth of the candidates; zero derives.
progressProgressCallback
progress_stepsusizeReports the call makes at most, the last at completion; zero derives to ten.
stats?*DenseTraversalStats
DenseMultiplyOne DenseIndex.multiply, as a Deferred holds it.
pub const DenseMultiply = struct

Fields

leftRowsView
configDenseMultiplyConfig
stampArena.StampThe call the borrowed arena serves, which every view over the outputs carries.
productsusearch_matches_tape_view_t
writtenusize
requiredusize
outcomeOutcome
DenseAttendOne DenseIndex.attend, as a Deferred holds it.
pub const DenseAttend = struct

Fields

queriesRowsView
value_lookupValueLookup
configDenseAttendConfig
stampArena.StampThe call the borrowed arena serves, which every view over the outputs carries.
outputsusearch_rows_view_t
outcomeOutcome
DenseMaxsimOne DenseIndex.maxsim, as a Deferred holds it.
pub const DenseMaxsim = struct

Fields

queriesRowsView
rows_per_queryusize
wantedusize
configDenseMaxsimConfig
stampArena.StampThe call the borrowed arena serves, which every view over the outputs carries.
matchesusearch_matches_tape_view_t
writtenusize
requiredusize
outcomeOutcome
StructValueLookupWhere attend reads value rows, as the C interface carries it: a usearch_value_lookup_t and the state forwarded into it.1
dense.zig:57 ↗
pub const ValueLookup = struct

Build one with ValueLookup.of.

Fields

callback?ValueLookupFunction
state?*anyopaque
ofWraps resolve in a trampoline generated per call site, which answers one key's value row of Element, or null where it has none.
dense.zig:72 ↗
pub fn of(
    comptime Element: type,
    context: anytype,
    comptime resolve: fn (@TypeOf(context), Key) ?[]const Element,
) ValueLookup

Pass {} for a lookup needing no context, and a single-item pointer otherwise.

A row must be as wide as the keys, stored in the index's quantization, and alive until the call is final. The trampoline checks the scalar and columns each batch states, so a null row, or one of another kind or width, refuses the call with Error.ShapeMismatch. It runs concurrently from the executor's threads.

The trampoline is a host function, so it serves RAM and NVMe handles only: a VRAM walk calls callback on the device, so a VRAM handle needs a device-callable one set directly.

StructDenseProductsThe ragged output of DenseIndex.multiply or DenseIndex.maxsim in an arena, laid out as Apache Arrow does: query i spans pairs offsets[i] up to offsets[i + 1] of keys and scores.5
dense.zig:2173 ↗
pub const DenseProducts = struct

Mirrors usearch_matches_tape_view_t.

It reads the arena, so every read is refused once that arena serves another call or is freed.

Fields

stampArena.Stamp
viewusearch_matches_tape_view_t
queriesusizeLeft-hand rows the call multiplied, or queries it scored.
writtenusizePairs written across every query.
requiredusizePairs scored across every left row, or keys across every query for a maxsim, before wanted cut any.
queryThe pairs of one left row in traversal order, or one query's keys largest first for a maxsim, refusing a position past queries with Error.InvalidArgument, a stale view with Error.Uninitialized, and device memory with Error.Device.
dense.zig:2187 ↗
pub fn query(
    self: DenseProducts,
    position: usize,
) Error!QueryMatches
offsetsThe queries + 1 offsets, refused as query refuses a stale view or device memory.
dense.zig:2196 ↗
pub fn offsets(self: DenseProducts) Error![]const usize
keysEvery pair's indexed key, end to end, refused as query refuses a stale view or device memory.
dense.zig:2202 ↗
pub fn keys(self: DenseProducts) Error![]const Key
scoresEvery pair's product, positionally paired with keys, refused as keys is.
dense.zig:2208 ↗
pub fn scores(self: DenseProducts) Error![]const Distance
residencyWhere the pairs live, which the host reads only as .ram or .nvme.
dense.zig:2214 ↗
pub fn residency(self: DenseProducts) Residency