Java · Executors

Executors

import cloud.unum.usearch.*; 3.0.0-rc.1 · 4c3e5023

An executor owns the host threads or device streams a fanned-out verb runs on; 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. Its residency must match the residency of every handle it drives. The index itself runs a call on the calling thread instead: single-threaded on RAM, on the handle’s device and default stream on VRAM.

There is one factory per C constructor, and each takes every argument its C twin takes.

Executor.forRam(new int[] {0, 1}, null, 16);                        // two NUMA nodes, sixteen workers
Executor.forRam(new int[0], CPUClass.INTEL_HASWELL, 0);             // waiters and atomics of one class
Executor.onPool(forkUnionPool, new int[0], null);                   // a thread pool the caller already runs
Executor.forVram(new int[] {0, 1, 2, 3}, null, new MemorySegment[0], 0); // four devices, own streams
Executor.forNvme(new String[] {"/mnt/a", "/mnt/b"}, null, 8);       // two drives, eight submission rings

forVram takes no thread count: one host thread keeps every device fed, so the width of that pool is the device list itself. It borrows one CUDA stream per device when given them, and caps scratch bytes per device at a non-zero scratchLimit. Naming no hardware derives it: an empty domain list spreads over every memory domain the process may allocate on, an empty device list opens one free device, and an empty path list leaves every shard where its manifest already points.

try (Executor pool = Executor.forRam(new int[0], null, 16)) {
    DenseIndex.On receiver = index.on(pool);
    receiver.insert(keys, vectors);
    Matches matches = receiver.search(queries, 10, arena);
    receiver.save("index.safegraphs");
    pool.drain(Mood.PARKED);
}

One call holds an executor at a time, and handles themselves stay freely concurrent. drain waits until no call holds the executor, then sets what it does once idle: Mood.PARKED lets its threads sleep, Mood.SPINNING keeps them hot for the next call’s latency.

§ Concurrency

A handle may be shared across threads. On a RAM handle, insertions and searches overlap freely: searches read only entries published behind the visibility watermark and never write to the graph, while insertions synchronize through per-node locks and publish in order. Serialization is the one exception, demanding a mutation-free window, though concurrent readers keep searching throughout.

Every call leases the index it names through the FFM scope its handle lives in, so closing an index while another thread’s blocking call still runs on it raises IllegalStateException rather than freeing the handle under that call; deferred calls are waited for by the close itself. A pull leases its cursor or stream in place of the index, and a call leases one scope only, so an executor, or the index under a pull, is closed once no other thread still calls through it. An index, executor, arena or deferred call the program drops without closing is released once it is unreachable, and never while a call naming it is unfinished.

§ Receivers & Deferred Calls

The receiver chooses how a call completes, and the verb stays the same. DenseIndex runs a verb on the caller’s thread, index.on(executor) answers an On whose verbs block on that executor, and index.deferred(executor) answers a DeferredOn whose verbs submit there and answer a Deferred. A Deferred owns its outcome, its argument structure and its inputs until the call is final; status polls without blocking, and join blocks, then answers or raises. It borrows the index, executor and arena it names exactly as a blocking call does, and join answers the same view a blocking call would, over that arena.

try (Arena arena = new Arena(pool);
        Deferred<Matches> call = index.deferred(pool).search(queries, 10, arena)) {
    if (call.status() == Status.DEFERRED) {
        // other work
}
    Matches matches = call.join();    // valid until the next call given `arena`
}

The first join frees the outcome and keeps what it answered or raised, so a later status or join repeats it; only a closed Deferred raises IllegalStateException. Closing an unfinished Deferred waits for the call before freeing what it lent, so a native slab lent to one must stay open until then. Dropping one never blocks the collector: what it lent stays reachable until the call is final, and only then is it freed. One call holds an executor at a time, so a second call finding it held finishes with Status.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 or a VRAM executor runs it at submission. Calls depending on each other are ordered by joining the first before submitting the second, since no call carries an ordering field.

API

Types

MoodWhat an executor does with its threads or streams once it goes idle.
Types.java:441 ↗
enum Mood

Fields

PARKEDIdle threads sleep; the next call pays a wake-up.
SPINNINGIdle threads stay hot, trading power for the next call's latency.
ExecutorThe workers every fanned-out verb runs on, shared across indexes: an executor owns host threads or device streams and their scratch, an index owns entries.
Types.java:650 ↗
class Executor

One factory per C constructor, each taking every argument its own kind takes: memory domains and a thread count for RAM, a borrowed ForkUnion pool, device ordinals and optional streams for VRAM, where a single host thread drives one stream per device, and device paths and a thread count for NVMe. Naming no hardware derives it, a zero deferred limit takes the default size of the deferred-call table, and the kind must match the residency of every handle it drives.

A receiver names the executor its verbs run on, and the index alone runs them on the caller's thread. Calls on one executor run in submission order; handles stay freely concurrent.


try (Executor pool = Executor.forRam(new int[] {0, 1}, null, 16, 0);
        DenseIndex index = DenseIndex.initRam(new DenseOptions().dimensions(256), new int[0])) {
    index.on(pool).insert(keys, vectors);
    pool.drain(Mood.PARKED);
}
DeferredA submitted call, which owns its outcome, argument structure and inputs, and borrows its handles and arena, until the call is final.
Types.java:1303 ↗
template <Result>
class Deferred

status() polls without blocking and join() blocks until the call is final, then answers what it produced or raises what it reported; the first join frees the outcome and every later one repeats its answer. Closing an unfinished one waits for the call before freeing what it lent, and a dropped one keeps what it lent until the call is final, when the collector frees it without ever waiting. It starts no thread: waiting off the caller's thread is the caller's own, as withContext(Dispatchers.IO) { call.join() } in a Kotlin coroutine or a virtual thread in Java.

Binding-only; joining and closing call usearch_outcome_wait.

ClassOnThe blocking verbs of a dense index, written once for the two receivers that block: the DenseIndex itself, which runs every call on the caller's thread, and the DenseIndex.On that DenseIndex#on makes, which runs it on an executor.20
DenseVerbs.java:64 ↗
class On

The DenseIndex.DeferredOn that DenseIndex#deferred makes offers the same verbs, each answering a Types.Deferred.

Every verb that takes knobs beyond its inputs has a bare form and one taking its Dense*Config, named after the C structure it fills. Every verb whose output scales with the call takes an Arena last, borrows it until it returns, and answers a view over it that the arena's next call invalidates.

Binding-only; every verb names the C symbol it calls.

reserveClaims capacity for a number of incoming vectors ahead of insertion, a hint, since insertions grow the index on demand.
DenseVerbs.java:87 ↗
void reserve(long capacity);

The capacity is the desired total, including the current size. Calls usearch_dense_reserve.

Parameters

capacitythe desired total number of entries, including the current size.
clearErases all entries, keeping the reserved capacity and the frozen geometry.
DenseVerbs.java:97 ↗
void clear();

No other call may overlap it. Calls usearch_dense_clear.

describeReports the frozen metadata and the current counters in one call.
DenseVerbs.java:102 ↗
DenseIndexDescription describe();

Calls usearch_dense_describe.

countHow many entries are stored under a key; zero means the index holds none.
DenseVerbs.java:111 ↗
long count(Key key);

Calls usearch_dense_count.

Parameters

keythe key to look up.
containsWhether the index holds at least one entry under a key.
DenseVerbs.java:120 ↗
boolean contains(Key key);

Binding-only sugar over count.

Parameters

keythe key to look up.
getRetrieves every vector stored under a key into arena, one row per entry sharing it, in insertion order, packed at the natural width of the kind asked for - one call, however many there turn out to be.
DenseVerbs.java:136 ↗
DenseVectors get(Key key, Scalar outputsScalar, Arena arena);

Calls usearch_dense_get.

Parameters

keythe key to look up.
outputsScalarthe scalar kind the rows are converted into.
arenathe arena the output lands in, borrowed until the call returns.

Returns

the rows read in place, none when the key holds nothing.

insertInserts a matrix of vectors under their keys, with every knob derived.
DenseVerbs.java:147 ↗
void insert(Key[] keys, RowsView vectors);
void insert(Key[] keys, RowsView vectors, DenseInsertConfig config);

Inserts a matrix of vectors under their keys - the native batch operation, where a single vector is a one-row matrix.

Overlaps safely with concurrent searches on the same handle.

The input scalar type is independent of the stored quantization; the library converts on ingest. A batch is all-or-nothing: a returning call published every entry, a failing one published none. An accelerator fault severe enough to poison the device context is the sole exception, reporting DEVICE and latching the index unusable rather than partially applied. Calls usearch_dense_insert.

Parameters

keysone key per vector row.
vectorsthe rows to insert, of any scalar kind the engine converts.
configthe insertion knobs.
refineRe-selects the neighbors of every stored entry, with every knob derived.
DenseVerbs.java:178 ↗
RefineReport refine();
RefineReport refine(DenseRefineConfig config);

Re-selects the neighbors of every stored entry against the finished graph, improving neighbors chosen when the graph around them was still incomplete.

Searches may run throughout on a RAM handle, since re-selection publishes one node at a time and never removes one; insertions may not. Re-selecting a settled node changes nothing, so a caller loops until the report's changed falls below whatever rate it considers settled. Calls usearch_dense_refine.

Parameters

configthe refinement knobs.
healthAudits how much of the graph a search reaches: forward reachability from as many landmarks as one search draws, swept level by level.
DenseVerbs.java:206 ↗
DenseReachability health();

Read-only, so concurrent insertions merely leave fresh entries unaudited. Calls usearch_dense_health.

maxsimLate-interaction scoring keeping up to wanted keys per query, zero keeping every key a query scored, with every other knob derived.
DenseVerbs.java:250 ↗
DenseProducts maxsim(
    RowsView queries,
    long rowsPerQuery,
    long wanted,
    Arena arena);
DenseProducts maxsim(
    RowsView queries,
    long rowsPerQuery,
    DenseMaxsimConfig config,
    Arena arena);

Late-interaction scoring of multi-row queries against multi-vector documents: 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, so the wanted count is the "k" over keys rather than over rows.

Every query carries the same number of rows, so a padded token batch applies as is, and the query count is the row count divided by that. Approximate in the same way a search is: a key can score below its exact value but never above it. Runs on the inner-product kinds alone. Each query's keys land in arena largest score first, as one ragged output like multiply's. Calls usearch_dense_maxsim.

Parameters

queriesthe query rows, as wide as the frozen geometry.
rowsPerQuerythe token rows every query carries.
wantedthe most keys kept per query, zero keeping every key it scored.
arenathe arena the output lands in, borrowed until the call returns.
configthe wanted count and the other scoring knobs.
multiplyApproximate sparse product keeping every pair, with every other knob derived.
DenseVerbs.java:287 ↗
DenseProducts multiply(RowsView left, Arena arena);
DenseProducts multiply(
    RowsView left,
    DenseMultiplyConfig config,
    Arena arena);

Approximate sparse product of a matrix against the indexed vectors - the linear-algebra sibling of search, reporting every pair the traversal scored rather than only the nearest.

Pairs arrive in traversal order, and the pairs the walk never reaches are the low and negative products. Runs on the inner-product kinds alone.

Only the call knows how many pairs it scores, so the keys, the products and their offsets land in arena as one ragged output. Calls usearch_dense_multiply.

Parameters

leftthe rows multiplied against the indexed vectors.
arenathe arena the output lands in, borrowed until the call returns.
configthe product knobs.
attendSoftmax attention with every knob derived.
DenseVerbs.java:321 ↗
DenseVectors attend(
    RowsView queries,
    ValueLookup valueLookup,
    Arena arena);
DenseVectors attend(
    RowsView queries,
    ValueLookup valueLookup,
    DenseAttendConfig config,
    Arena arena);

Softmax attention of every query row over every indexed key its walk scores, folded as Flash Attention folds key tiles, with no top-k cut, writing one row per query into arena already divided by the partition.

Runs on the inner-product kinds alone, since a softmax needs logits.

The index holds one layer's keys for one key-value head and no values, so the lookup resolves every scored key to its value row. Calls usearch_dense_attend.

Parameters

queriesthe query rows, as wide as the frozen geometry.
valueLookupresolves every scored key to its value row.
arenathe arena the output lands in, borrowed until the call returns.
configthe attention knobs.
absorbAbsorbs every donor into this index, one after another, keeping their graphs rather than re-inserting their vectors and linking each in across the appended part.
DenseVerbs.java:361 ↗
RefineReport absorb(DenseIndex... donors);

Every donor refuses every verb until the call is final; then each one absorbed stays closed, and one it did not absorb is a live index again, which the caller still owns, though its entries may already have moved here. Calls usearch_dense_absorb.

Parameters

donorsthe indexes absorbed, of this index's residency, scalar, metric and geometry.

Returns

what the closing refinement did to the graph.

splitSplits this index into the empty receivers, the inverse of absorb: the entries are partitioned along the graph, each part lands in one receiver with its edges kept, and a closing refinement re-links every receiver, leaving this index empty and live.
DenseVerbs.java:378 ↗
RefineReport split(DenseSplitConfig config, DenseIndex... receivers);

Calls usearch_dense_split.

Parameters

configthe partitioning knobs.
receiversat least two empty indexes matching this one, which take the parts.

Returns

what the closing refinements did, across the receivers.

sampleDraws keys around the focus, or across the whole index, each hanging from a shown or earlier draw and placed in three dimensions beside the shown ones - so a view grows one connected sample as it zooms.
DenseVerbs.java:394 ↗
Sample sample(long wanted, Arena arena, DenseSampleConfig config);

Writes up to wanted draws, and a larger count under the same seed draws a superset; only a RAM index samples yet. Calls usearch_dense_sample.

Parameters

wantedthe most draws written.
arenathe arena the output lands in, borrowed until the call returns.
configthe focus, seed and other sampling knobs.
saveWrites the index under a path, in the layout its extension picks - one .safegraphs monolith, or a .safegraphs.json manifest beside its shards.
DenseVerbs.java:407 ↗
void save(String path);

Demands a mutation-free window; concurrent readers keep searching throughout. Calls usearch_dense_save.

Parameters

paththe destination; its extension picks the layout.
saveBufferWrites the index as one monolith - every shard, entries in placement order - the same bytes save writes to a .safegraphs path, into arena.
DenseVerbs.java:419 ↗
Monolith saveBuffer(Arena arena);

Calls usearch_dense_save_buffer.

Parameters

arenathe arena the output lands in, borrowed until the call returns.
cursorOpens a walk over the current contents, pulled on this receiver's executor.
DenseVerbs.java:432 ↗
DenseCursor cursor(Key... starts);

Each shard's publication watermark is read as the walk enters it, so an entry once passed is never revisited. Without starts it yields every entry as stored; with them, the start keys' own entries first in order, then the entries those link to breadth-first, each once. Calls usearch_dense_cursor_init.

Parameters

startsthe keys whose entries the walk starts from, or none to yield every entry.
searchStreamOpens a resumable search for one query row, with the breadth of the candidates derived.
DenseVerbs.java:447 ↗
DenseSearchStream searchStream(RowsView query);
DenseSearchStream searchStream(RowsView query, long expansionFactor);

Opens a resumable search for one query row, pulled on this receiver's executor: best-first traversal is naturally pausable, so callers that post-filter aggressively pull until satisfied instead of guessing the wanted count upfront.

Calls usearch_dense_search_stream_init.

Parameters

queryexactly one query row.
expansionFactorthe breadth of the candidates, zero to derive it.