An executor owns host threads or device streams; an index owns entries.
Keeping them apart is what lets one pool serve several indexes, and what makes the cost of spawning visible at the line that spawns.
One factory per C constructor, each taking only what its hardware has: ForVram has no thread count because a single host thread drives one stream per device.
using Executor threads = Executor.ForRam(threads: 8); // Zero derives 7/8 of the coresusing Executor rings = Executor.ForNvme(threads: 4); // One submission ring apieceusing Executor streams = Executor.ForVram(scratchLimit: 1 << 30); // One stream per deviceusing Executor shared = Executor.OnPool(forkUnionPool); // Borrows another library's threadsthreads.Drain(Mood.Spinning); // Waits out the holding call, then keeps spinning
Each factory also names where its shards go, which is how a dual-socket or multi-GPU box is reached.
using Executor domains = Executor.ForRam(16, memoryDomains: [0, 1]); // One shard per NUMA nodeusing Executor devices = Executor.ForVram([0, 1], cudaStreams); // Borrowed streams, one per deviceusing Executor striped = Executor.ForNvme(paths: ["/mnt/nvme0", "/mnt/nvme1"]);
One call holds an executor at a time, while handles stay freely concurrent, so a caller wanting genuine concurrency gives each thread its own executor.
The receiver chooses where a verb runs, and the verb stays the same.
Called on the index, a verb runs on the caller’s own thread, single-threaded on RAM and on the handle’s device and default stream on VRAM.
On index.On(executor) it blocks on that executor, refused while another call holds it.
On index.Deferred(executor) it submits there and answers a Deferred<TResult> at once.
Matches local = index.Search(queries, wanted: 10, arena); // The caller's threadMatches pooled = index.On(pool).Search(queries, wanted: 10, arena); // Blocking, on the poolMatches awaited = await index.Deferred(pool).Search(queries, 10, arena); // Or: .Wait() in place of await
A Deferred<TResult> owns its outcome and the pins on every input it lends the library until the call is final, and borrows its index and arena exactly as a blocking call does, so Wait() answers the same view a blocking call would, over the same arena. Status reads Status.Deferred until then, Wait() blocks and answers the result or raises the failure, and await runs that wait on the thread pool.
Verbs that produce nothing answer a plain Deferred, as Task stands beside Task<TResult>.
Waiting again answers the same result or raises the same failure.
Disposing a call nobody waited for waits for it, discards what it produced, and lets its arena serve the next call.
One left unfinished and unreachable keeps its outputs and pins alive until a collection finds it final, rather than freeing memory the library still writes.
Disposing an index or an arena that a deferred call names waits for that call first.
One call holds an executor at a time, so a second call finding it held finishes at once with Status.Contended, and nothing queues.
A deferred call on a caller-exclusive pool runs on the pool’s thread 0, so polling alone finishes it, and its progress handler and predicate run on the pool’s threads; a single thread or a VRAM executor runs it at submission.
Calls depending on each other are ordered by waiting on the first before submitting the second.
API
ClassDeferredA submitted call, which owns its outcome and the pins on its inputs until the call is final, and borrows its index and arena as a blocking call does; the verbs that produce nothing answer this, the rest a Deferred<TResult>.3
Status polls without blocking, Wait blocks until the call is final and raises its failure, and await runs that wait on the thread pool. Disposing one nobody waited for waits for it, then discards what it produced and lets its arena serve the next call.
Every wait after the first answers the same result or raises the same failure, and Status keeps reading the final status. One left unfinished and unreachable is released by the first collection that finds it final, rather than freeing memory the library still reads and writes.
WaitBlocks until the call is final, then raises its failure. Calls usearch_outcome_wait.
An executor owns host threads or device streams; an index owns entries. Keeping them apart is what lets one pool serve several indexes, and what makes the cost of spawning visible at the line that spawns rather than hidden in a constructor.
One factory per C constructor, each taking only what its own hardware has: host workers and memory domains for RAM, a borrowed ForkUnion pool for OnPool, one submission ring apiece per device path for NVMe, and device ordinals with optional borrowed streams for VRAM, where a single host thread drives one stream per device and there is no thread count to name. The kind must match the residency of every handle the executor drives, and a mismatch fails the call with Status.Device.
A verb runs on an executor when its receiver names one, as index.On(pool) and index.Deferred(pool) do, and on the caller's own thread when called on the index itself: single-threaded on RAM, on the handle's device and default stream on VRAM. Calls on one executor run in submission order, while handles stay freely concurrent - it is the executor that orders, never the index.
using Executor pool = Executor.ForRam(threads: 8);
using DenseIndex index = DenseIndex.InitRam(DenseOptions.Default with { Dimensions = 3 });
index.On(pool).Insert(keys, RowsView.RowsOf<float>(vector, 3));
ForRamSpawns host workers for RAM-resident indexes, interleaved across the compute domains local to the named memory domains - the very IDs the indexes were initialized with.
The ring count; zero derives from those devices' queue capabilities.
paths
Devices or directories to place shards on; empty leaves each shard where its manifest points.
deferredLimit
The deferred calls tracked at once; zero takes a default.
cpuClass
The class whose waiters and atomics the threads use; null derives the CPU's own.
Returns
The spawned executor.
ForVramOpens one submission stream on every named device, or borrows the caller's, all of them driven by the one host thread that calls the verbs - which is why this factory takes no thread count.
A verb is written once as such a struct, and the receiver alone decides whether it blocks or defers.
SubmitFixes what the call lends, pinning it past the C verb only when deferred, and leases its handles for the C verb alone, then calls it with its outputs written to outputs .