C# · Executors

Executors

using Cloud.Unum.USearch; 3.0.0-rc.1 · 4c3e5023

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 cores
using Executor rings = Executor.ForNvme(threads: 4);              // One submission ring apiece
using Executor streams = Executor.ForVram(scratchLimit: 1 << 30); // One stream per device
using Executor shared = Executor.OnPool(forkUnionPool);           // Borrows another library's threads

threads.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 node
using Executor devices = Executor.ForVram([0, 1], cudaStreams);        // Borrowed streams, one per device
using 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.

§ Receivers & Deferred Calls

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 thread
Matches pooled = index.On(pool).Search(queries, wanted: 10, arena);       // Blocking, on the pool
Matches 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>.

using Deferred<Matches> call = index.Deferred(pool).Search(queries, wanted: 10, arena);
if (call.Status == Status.Deferred)
{
    // other work
}
Matches matches = await call;                                       // Or: call.Wait()

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
Types.cs:1313 ↗
class Deferred

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.
Types.cs:1335 ↗
void Wait();
GetAwaiterWaits on the thread pool, so await suspends the caller rather than blocking it.
Types.cs:1344 ↗
TaskAwaiter GetAwaiter();

Binding-only.

Returns

The awaiter of that wait.

DisposeWaits for an unfinished call and discards what it produced; a call already waited for is left as it is.
Types.cs:1348 ↗
void Dispose();

Calls usearch_outcome_wait.

ClassExecutorThe workers every fanned-out verb runs on, shared across indexes.6
Types.cs:1002 ↗
class Executor

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.
Types.cs:1031 ↗
Executor ForRam(
    ulong threads = 0,
    ReadOnlySpan<int> memoryDomains = default,
    ulong deferredLimit = 0,
    CPUClass? cpuClass = null);

Calls usearch_executor_for_ram.

Parameters

threadsThe worker count; zero takes 7/8 of the cores those domains carry, since full occupancy starves the ordered publication path.
memoryDomainsNUMA node IDs to place shards on; empty derives every node the process may allocate on.
deferredLimitThe deferred calls tracked at once; zero takes a default.
cpuClassThe class whose waiters and atomics the threads use; null derives the CPU's own.

Returns

The spawned executor.

OnPoolWraps a ForkUnion pool another library already runs, so one process keeps one set of threads while each library keeps its own scratch.
Types.cs:1068 ↗
Executor OnPool(
    IntPtr pool,
    ReadOnlySpan<int> memoryDomains = default,
    ulong deferredLimit = 0,
    CPUClass? cpuClass = null);

Calls usearch_executor_on_pool.

Parameters

poolThe borrowed fu_pool_t, which must outlive the executor and keeps its lender's mood.
memoryDomainsNUMA node IDs of the indexes it will drive; empty derives.
deferredLimitThe deferred calls tracked at once; zero takes a default.
cpuClassThe class whose waiters and atomics the threads use; null derives the CPU's own.

Returns

The executor over that pool.

ForNvmeSpawns host workers for NVMe-resident indexes, one submission ring apiece, across the compute domains local to the devices the paths name.
Types.cs:1105 ↗
Executor ForNvme(
    ulong threads = 0,
    ReadOnlySpan<string> paths = default,
    ulong deferredLimit = 0,
    CPUClass? cpuClass = null);

Calls usearch_executor_for_nvme.

Parameters

threadsThe ring count; zero derives from those devices' queue capabilities.
pathsDevices or directories to place shards on; empty leaves each shard where its manifest points.
deferredLimitThe deferred calls tracked at once; zero takes a default.
cpuClassThe 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.
Types.cs:1149 ↗
Executor ForVram(
    ReadOnlySpan<int> devices = default,
    ReadOnlySpan<IntPtr> streams = default,
    ulong scratchLimit = 0,
    ulong deferredLimit = 0,
    GPUClass? gpuClass = null);

Calls usearch_executor_for_vram.

Parameters

devicesCUDA device ordinals to place shards on; empty derives one free device.
streamsBorrowed CUDA streams, one per device in that order; empty opens its own.
scratchLimitThe ceiling on scratch bytes per device; zero lets scratch grow as geometry asks.
deferredLimitThe deferred calls tracked at once; zero takes a default.
gpuClassA class the devices must run, refused with Status.Unsupported otherwise; null takes the devices' own.

Returns

The opened executor.

DrainWaits for every call submitted to this executor, then sets the mood it adopts whenever it next goes idle.
Types.cs:1188 ↗
void Drain(Mood mood = Mood.Parked);

Calls usearch_executor_drain.

Parameters

moodWhat the executor does with its workers once idle; the last drain wins.
DisposeDrains every thread or stream, releases the scratch it owns, and frees the executor.
Types.cs:1201 ↗
void Dispose();

Freeing cannot fail, and a freed executor must not be used. Calls usearch_executor_free.

InterfaceICallOne verb's call: what it lends the library, and what it builds once final.3
Types.cs:1463 ↗
interface ICall

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 .
Types.cs:1468 ↗
void Submit(CallOutputs *outputs, Completion completion);

Throws only before the C verb is called.

ReleaseSettles what the call handed over - donors it consumed - then unpins what it lent, once the call is final.
Types.cs:1472 ↗
void Release(CallOutputs *outputs);
FinishRaises the failure of a final call, or builds what it produced.
Types.cs:1475 ↗
void Finish(CallOutputs *outputs);