An executor owns the threads or device streams that run calls, the traversal scratch they walk with, and the pinned bounce buffer staging passes through.
One executor serves any number of indexes, of any quantization and metric, since its scratch is sized from geometry, grown to the widest asked of it, and kept between calls.
A VRAM executor’s scratch_limit caps each stream’s run, which every call’s walk scratch and outputs carve from, and a call needing more reports usearch_status_out_of_memory_k.
One call holds an executor at a time, while a handle admits any number of concurrent callers: the executor serializes, never the index.
int32_t const sockets[2] = {0, 1};usearch_executor_for_ram(&(usearch_executor_for_ram_t) {.executor = &executor, .outcome = &outcome, .ram = {sockets, 2}, // NUMA nodes whose cores it spawns on .threads = 0}); // zero takes 7/8 of those cores
Threads interleave across the named domains, so a half-size executor takes half of every socket rather than all of one, and the default stops at 7/8 because full occupancy starves ordered publication. usearch_executor_for_vram opens one stream per device, or borrows the caller’s, and usearch_executor_for_nvme spawns one thread per submission ring.
An application with its own ForkUnion pool lends it with usearch_executor_on_pool instead, though the engine refuses borrowed pools with usearch_status_unsupported_k for now.
An executor’s kind must match the residency of every handle it drives, and a mismatch reports usearch_status_device_k.
A NULL executor is accepted by every verb and runs the call on the caller’s own thread, final when it returns whatever its completion, which suits size lookups and other short calls.
The exception is a VRAM verb that takes an arena: that arena must be a GPU arena on the index’s first device, and it serves only calls on the executor that made it.
A second call on a held executor finishes usearch_status_contended_k, so callers wanting real parallelism give each thread its own executor. usearch_executor_drain waits until no call holds the executor, then sets its threads’ mood until the next call: usearch_mood_parked_k, the default, lets idle threads sleep in the kernel, usearch_mood_spinning_k keeps them hot.
A blocking verb called from inside a progress handler or predicate of a call on the same executor reports usearch_status_contended_k, and so does a drain on the thread running that call’s body; a drain from any other of its threads would wait on itself. usearch_executor_free waits out the call holding the executor before it releases the threads; host arenas the executor made stay valid until freed, and GPU arenas it made must be freed before it.
Every call reports through its outcome and names its completion in the same struct, so no verb has a second, asynchronous twin. usearch_completion_blocking_k, the default, returns with the final status stored and every output readable. usearch_completion_deferred_k returns once submitted, as an io_uring entry does, and the call finishes on its executor.
A thread pool, a GPU stream or a remote server can serve the same verbs without changing a call site.
Submission copies the argument struct, but everything it points to stays borrowed until the outcome is final.
One call holds an executor at a time, so calls overlap by running on different executors, each with its own outcome and arena:
The status reads usearch_status_deferred_k until the call is final, even for a call refused at submission.
Then the library stores the message and the final status with release ordering, and touches nothing the struct names after that.
The caller reads it with acquire ordering: usearch_outcome_status polls and usearch_outcome_wait blocks on an atomic wait, both returning the status.
Those two are the only safe readers of a deferred outcome, since the status word is atomic only by their contract.
Never wait on a call’s own outcome from inside its progress handler or predicate, since the call finishes only after they return.
A call ends in one order: its arena and the counts of the handles it names, then the pool, then the executor, then the message and the status.
So a caller that reads success may resubmit on the same executor or free a handle at once.
A call submitted while another holds the executor finishes at once with usearch_status_contended_k, and nothing queues, so submitting never allocates.
Every *_free waits until each executor call naming its handle has ended, so a handle may be dropped at any moment, though never from a callback of a call naming it.
Waits, drains and frees only wait: none of them runs another thread’s call.
A caller-inclusive pool, a single thread or a WebAssembly build needs its caller to make progress, so a deferred call on it runs at submission and is final when the verb returns.
A caller-exclusive pool, which usearch_executor_for_ram and usearch_executor_for_nvme spawn whenever they spawn more than one OS thread, delegates a deferred call to its own thread 0.
That thread runs the call in the caller’s place and forks the other threads as the caller would, so the call finishes while the caller does something else, one call per executor at a time.
Progress handlers and predicates of a delegated call therefore run on the pool’s threads.
VRAM executors run every verb at submission, since each verb reads its device results back on the host before it ends.
Together these let one manager thread in int main() overlap a RAM, an NVMe and a VRAM executor: it submits a deferred call to each and polls their outcomes.
API
Types
usearch_completion_tWhether a call returns with its final status, or once submitted to its executor.
Return with the final status stored and every output readable; the default.
usearch_completion_deferred_k
1
Return once submitted, the status deferred until final; the caller waits or polls.
usearch_executor_tEverything a call needs to run but an index does not own: the host threads or device streams that advance the work, the traversal scratch each of them walks with, and the pinned bounce buffer staging passes through.
One executor serves any number of index handles, so separate categories of indexes share one machine's cores or devices instead of each claiming a private set. Its kind is fixed by the constructor that produced it and must match the residency of every handle it drives; a mismatch reports usearch_status_device_k.
Scratch is carved one slice per thread or stream, sized from geometry rather than from the scalar type, which is what lets a single executor drive indexes of different quantizations and metrics. It grows to the widest geometry asked of it, is rewound per call, and is kept between calls, so a steady workload allocates once.
Calls on one executor start in submission order, and handles stay freely concurrent - it is the executor that orders, never the index. A blocking call runs alone: after every earlier call finishes, and before any later one starts. An executor without threads of its own - caller-inclusive, single-thread, or WebAssembly - runs a deferred call at submission or holds it until its wait, its executor's next drain or blocking call, or the free of a handle it names.
Every executor tracks its deferred calls in a fixed table of deferred_limit slots, so submitting never allocates. A deferred submission that finds the table full finishes usearch_status_contended_k, and blocking calls take no slot.
Every verb accepts a NULL executor and then runs on the caller's thread, blocking whatever its completion says: RAM work runs single-threaded, VRAM work runs on the handle's device and default stream and synchronizes before returning, and NVMe reads are synchronous. Its outputs land in any arena of the handle's residency.
The library stages only to change layout on the side that computes, or to make pageable memory DMA-able, and never routes device bytes through the host:
Case
Action
Device inputs on the executor's device
read in place
Device inputs needing padding or a cast
a device-to-device copy or a cast kernel
Pinned or managed host inputs
a direct asynchronous upload
Pageable host inputs
the pinned bounce buffer, then an upload
Outputs in an arena on the executor's device
the kernel writes them directly
usearch_mood_tWhat an executor does with its threads or streams once it goes idle.
usearch_outcome_statusReads the status of outcome without waiting, with the acquire ordering its final store pairs with, so a final status also publishes every output of the call.
The one way to poll: the status word is atomic only by this contract, so a caller reading the field directly needs an atomic load its language may not offer on foreign memory.
Parameters
outcome
in
An outcome passed to a call.
Returns
The final status once the call is final.
usearch_executor_for_ramSpawns host threads for RAM-resident indexes, interleaved across the compute domains local to the named memory domains, or borrows an existing ForkUnion pool.
Passing the very placement the indexes were built with is what makes compute and memory domains agree, leaving no second place to name hardware and no way for the two to disagree.
Interleaving is why a half-size executor takes half of every socket rather than all of one, and the default stops at 7/8 of the cores because full occupancy starves the ordered publication path. A borrowed pool keeps its own threads and its lender's mood, so one machine runs one pool however many libraries schedule onto it.
The memory domains of the indexes it will drive; copied, not borrowed.
threadssize_t
Threads to spawn; zero takes 7/8 of the cores those domains carry.
deferred_limitsize_t
Deferred calls it tracks at once, sizing its deferred table; zero takes a default.
usearch_executor_on_poolCreates a RAM executor over a borrowed ForkUnion pool, so several libraries in one process share one set of threads while each keeps its own scratch.
usearch_executor_for_vramOpens one submission stream on every distinct device vram names, or borrows the caller's, all of them driven by whichever host thread submits.
One stream per device is the whole design: every shard on a device queues on its stream in order, a single host thread keeps every device fed, and adding host threads would buy nothing, since the work has already left for the accelerator by the time one of them would return. A GPU executor never changes process-wide device flags.
The devices of the indexes it will drive; copied, not borrowed.
streamsvoid *const *
The optional borrowed CUDA streams, one per device in vram order; NULL creates its own.
scratch_limitsize_t
The optional ceiling on each stream's run, which every call's scratch and outputs carve from, past which a call reports usearch_status_out_of_memory_k; zero lets the run grow as the geometry asks.
deferred_limitsize_t
Deferred calls it tracks at once, sizing its deferred table; zero takes a default.
usearch_executor_for_nvmeSpawns host threads for NVMe-resident indexes, one submission ring apiece, interleaved across the compute domains local to the devices the paths name.
One ring per thread is what keeps submission lock-free: a thread owns its ring outright, so an external-memory walk never coordinates with a sibling to issue a read. A ring reports its reads only to the thread that owns it, so deferred calls finish on those threads.
The I/O engine every ring submits through, opened exactly or refused with the reason its setup gave; usearch_nvme_class_unknown_k tries io_uring, then AIO, then blocking reads.
usearch_status_config_mismatch_k Kernel paths mixed with transport identifiers, SPDK pinned over kernel paths or a kernel engine over transport identifiers, or more threads than an SPDK controller granted I/O queue pairs.
usearch_status_unsupported_k Transport identifiers in a build without SPDK, or a pinned engine this build lacks.
usearch_executor_drainWaits for every call submitted to the executor before the drain began, then sets the mood it adopts whenever it next goes idle.
The drain reads the submission ticket on entry and waits up to it, so calls submitted while it waits can't keep it waiting forever. Every call reports its own failure through its own outcome, so a drain reports only what befell the executor itself.
Parked means futex waits on RAM, blocking-sync events on VRAM, and ring waits on NVMe; spinning polls each of them instead. A borrowed pool keeps its lender's mood.
usearch_executor_freeDrains every call, releases the scratch and the streams it owns, and frees the executor; NULL is safe, and it cannot fail, so takes no outcome.
Waits until every deferred call naming the handle is final. Waits until every call running on the executor, blocking or deferred, is final. Never call it from inside a progress handler, a predicate, or a host function the executor runs, since it would drain itself. Host arenas it made stay valid, each keeping its memory until freed; GPU arenas keep the rule usearch_arena_t notes.