Zig · Executors

Executors

const usearch = @import("usearch");const Mood = @import("usearch").Mood;const Executor = @import("usearch").Executor;const Deferred = @import("usearch").Deferred; 3.0.0-rc.1 · 4c3e5023

One executor serves any number of indexes, and its kind must match the residency their handles were built for. There is a constructor per C one, each taking a config of exactly what its C structure takes:

var sockets = try usearch.Executor.forRam(.{ .memory_domains = &.{ 0, 1 }, .threads = 0 });
var shared = try usearch.Executor.onPool(fork_union_pool, .{ .memory_domains = &.{0} }); // borrows a `fu_pool_t`
var gpus = try usearch.Executor.forVram(.{ .devices = &.{ 0, 1 }, .streams = null, .scratch_limit = 0 });
var drives = try usearch.Executor.forNvme(.{ .paths = &.{.of("/mnt/nvme0")}, .threads = 0 });

Zero threads takes 7/8 of the cores, since full occupancy starves the ordered publication path; forVram takes no thread count at all, driving one stream per device from the calling thread, either its own or one of the CUDA streams you lend. drain(mood) waits until no call holds the executor, then lets the threads go .parked or stay .spinning once idle.

§ Receivers & Deferred Calls

The receiver chooses where a verb runs and how it completes, and the verb stays the same. The index runs it on the calling thread, index.on(executor) blocks on an executor, refused while another call holds it, and index.deferred(executor, &call) submits it there into a usearch.Deferred you hold:

const here = try index.search(queries, 10, &arena, .{});               // the calling thread
const there = try index.on(executor).search(queries, 10, &other, .{}); // blocking, on the executor
var call: usearch.Deferred(usearch.DenseSearch) = undefined;
try index.deferred(executor, &call).search(queries, 10, &spare, .{});  // submitted, into `call`
defer call.deinit();

DenseIndex.On and DenseIndex.DeferredOn(Call) offer every verb the index does, with the same arguments, the *Arena included, because one generic writes each verb once for both; a deferred receiver compiles only the verb its storage holds. Constructors address no index yet, so no receiver offers them; the ones the C interface lets fan out take an executor in their config, and run on the calling thread without one.

A Deferred owns what the library reports into until its call is final, the outcome and any fixed-size result, and borrows the rest, as a blocking call does. The handles it names, such as the index, the executor, the arena, a cursor or a stream, need nothing more: freeing one waits for every deferred call naming it. The plain memory it reads, such as the rows, the keys, the path, the carry and the handlers’ contexts, stays yours, and must outlive the call.

var call: usearch.Deferred(usearch.DenseGet) = undefined;
try index.deferred(executor, &call).get(key, .f32, &arena);
defer call.deinit();
if (call.status() == .deferred) {
    // other work
}
const vectors = try call.wait(); // the view a blocking `get` answers, valid until the next call given `arena`

The storage is yours to place, so submitting allocates nothing: a stack slot, a field, or an array of them for a batch of submitted calls. The library reports into the storage, so a submitted Deferred stays put until deinit; a heap slot comes from allocator.create and is destroyed after deinit. Zig has no async keyword left to lean on, so a deferred call is waited on and nothing else: status reads the outcome with acquire ordering and never blocks, and wait blocks in the library until the call is final and answers its result or error, the same one however often it is asked. deinit waits the same way, and the slot can then hold another call. A Deferred has one owner: wait and deinit never run on two threads at once, while status reads from any. message answers the static detail of the final status, empty until the call is final and on success. One call holds an executor at a time, so a second call finding it held finishes at once with Error.Contended, and nothing queues. A deferred call on a caller-exclusive pool runs on the pool’s thread 0, so polling status alone finishes it, and its progress handler and predicate run on the pool’s threads; a single thread, WebAssembly or a VRAM executor runs it at submission. Calls depending on each other are ordered by waiting on the first before submitting the second. A shape only the binding can check, like a key run longer than its rows, is refused before anything is submitted, so the submitting call itself fails and the slot holds nothing to deinit.

API

Types

ExecutorForRamConfigOptional settings of Executor.forRam, mirroring usearch_executor_for_ram_t.
pub const ExecutorForRamConfig = struct

Fields

memory_domains[]const i32The memory domains of the indexes it will drive; empty derives every node the process may use.
threadsusizeThreads to spawn; zero takes 7/8 of the cores those domains carry.
deferred_limitusizeDeferred calls it tracks at once, sizing its deferred table; zero takes a default.
diagnostics?*OutcomeWhere the call's final outcome, its static message included, is copied; null copies nothing.
ExecutorOnPoolConfigOptional settings of Executor.onPool, mirroring usearch_executor_on_pool_t.
pub const ExecutorOnPoolConfig = struct

Fields

memory_domains[]const i32The memory domains of the indexes it will drive; empty derives every node the process may use.
deferred_limitusizeDeferred calls it tracks at once, sizing its deferred table; zero takes a default.
diagnostics?*OutcomeWhere the call's final outcome, its static message included, is copied; null copies nothing.
ExecutorForVramConfigOptional settings of Executor.forVram, mirroring usearch_executor_for_vram_t.
pub const ExecutorForVramConfig = struct

Fields

devices[]const i32The devices of the indexes it will drive; empty derives one free device.
streams?[]const ?*anyopaqueBorrowed CUDA streams, one per device in devices order; null creates its own.
scratch_limitusizeThe ceiling on scratch bytes per device; zero lets scratch grow as geometry asks.
deferred_limitusizeDeferred calls it tracks at once, sizing its deferred table; zero takes a default.
diagnostics?*OutcomeWhere the call's final outcome, its static message included, is copied; null copies nothing.
ExecutorForNvmeConfigOptional settings of Executor.forNvme, mirroring usearch_executor_for_nvme_t.
pub const ExecutorForNvmeConfig = struct

Fields

paths[]const BytesViewThe devices of the indexes it will drive; empty leaves every shard where its manifest points.
threadsusizeThreads to spawn, one submission ring apiece; zero derives from the devices' queue capabilities.
deferred_limitusizeDeferred calls it tracks at once, sizing its deferred table; zero takes a default.
diagnostics?*OutcomeWhere the call's final outcome, its static message included, is copied; null copies nothing.
EnumMoodWhat an executor does with its threads or streams once it goes idle.2
types.zig:748 ↗
pub const Mood = enum(c_int)

Mirrors usearch_mood_t.

Values

parked1Park them; the next call pays a wake-up.
spinning2Keep them spinning, trading power for the next call's latency.
nameThe canonical spelling, as .parked spells "parked".
types.zig:755 ↗
pub fn name(self: Mood) [:0]const u8

Calls usearch_mood_name.

fromNameReads a spelling back, or null when no mood spells it.
types.zig:760 ↗
pub fn fromName(text: []const u8) ?Mood

Calls usearch_mood_from_name.

StructExecutorThe host threads or device streams that advance a call, and the traversal scratch each of them walks with - everything a verb needs to run but an index does not own.6
types.zig:869 ↗
pub const Executor = struct

One executor serves any number of index handles, and its kind is fixed by the constructor that produced it: it must match the residency of every handle it drives, or the call reports Error.Device. A verb called on the index itself takes no executor and runs on the calling thread.

Calls on one executor run in submission order; handles stay freely concurrent - it is the executor that orders, never the index. An executor without threads of its own holds a deferred call until its wait, its next drain or blocking call, or the free of a handle it names.

Fields

handle?*anyopaqueThe opaque usearch_executor_t the ABI hands back.
residencyResidencyThe residency of the constructor that produced it, which places the arenas it makes.
forRamSpawns host threads for RAM-resident indexes across the domains local to memory_domains, which should be the very slice the indexes were placed with.
types.zig:880 ↗
pub fn forRam(config: ExecutorForRamConfig) Error!Executor

Zero threads takes 7/8 of the cores those domains carry, because full occupancy starves the ordered publication path. Calls usearch_executor_for_ram.

onPoolCreates a RAM executor over a borrowed ForkUnion fu_pool_t, so several libraries in one process share one set of threads; the pool must outlive the executor, and keeps its lender's mood.
types.zig:897 ↗
pub fn onPool(
    pool: *anyopaque,
    config: ExecutorOnPoolConfig,
) Error!Executor

Calls usearch_executor_on_pool.

forNvmeSpawns host threads for NVMe-resident indexes, one submission ring apiece, across the domains local to the devices paths names.
types.zig:913 ↗
pub fn forNvme(config: ExecutorForNvmeConfig) Error!Executor

Calls usearch_executor_for_nvme.

forVramOpens one submission stream on every named device, or borrows streams, one CUDA stream per device in devices order, all driven by whichever host thread submits.
types.zig:930 ↗
pub fn forVram(config: ExecutorForVramConfig) Error!Executor

Calls usearch_executor_for_vram.

drainWaits for every call submitted before the drain began, then sets the mood the executor adopts whenever it next goes idle.
types.zig:957 ↗
pub fn drain(self: Executor, mood: Mood) Error!void

Reports only what befell the executor itself. Calls usearch_executor_drain.

deinitDrains every call, releases the scratch and the streams it owns, and frees the executor, which is invalid afterwards; the host arenas it made keep their memory, while its GPU arenas must be freed first.
types.zig:967 ↗
pub fn deinit(self: *Executor) void

Calls usearch_executor_free.

StructDeferredA submitted call, held in storage the caller places - on the stack, in a struct, or from an allocator - which owns its outcome until the call is final, while the arena, the handles and the slices it was handed stay borrowed until then.5
dense.zig:1483 ↗
pub fn Deferred(comptime Call: type) type

status polls without blocking, wait blocks until the call is final and answers its result or error - the same one however often it is asked - and deinit waits the same way. A submitted Deferred stays put until deinit, and has one owner: wait and deinit never run on two threads at once, while status reads from any.

Fields

callCall
answer?(Error!Call.Result)
submitCopies call into self, which stays put until final, and submits it.
dense.zig:1491 ↗
pub fn submit(
    self: *Self,
    call: Call,
    index: ?*anyopaque,
    executor: ?*anyopaque,
) void

Binding-only.

status.deferred until the call is final, then its final status.
dense.zig:1497 ↗
pub fn status(self: *Self) Status
messageThe static detail of the final status; empty until the call is final, and on success.
dense.zig:1502 ↗
pub fn message(self: *Self) [:0]const u8
waitBlocks until the call is final, then answers its result, the same views a blocking call answers over the arena it borrowed.
dense.zig:1508 ↗
pub fn wait(self: *Self) Error!Call.Result

One thread waits at a time.

deinitBlocks until the call is final; the storage is reusable afterwards, and the result's views stay as valid as the arena keeps them.
dense.zig:1517 ↗
pub fn deinit(self: *Self) void

Calls usearch_outcome_wait.