Swift · Executors

Executors

import USearch 3.0.0-rc.1 · 4c3e5023

An index says where its payload lives; an Executor says who walks it. One executor serves any number of handles, so several categories of index share one machine’s cores or devices instead of each claiming a private set. Its kind is fixed by the factory that spawned it and must match every handle it drives; a mismatch reports device.

let hostThreads = try Executor.forRam(memoryDomains: [0, 1], threads: 0)
let submissionRings = try Executor.forNvme(paths: ["/mnt/nvme0"], threads: 8)
let deviceStreams = try Executor.forVram(devices: [0, 1], streams: nil, scratchLimit: 1 << 30)

Each factory takes every argument its C constructor has, and only those. forVram can borrow your CUDA streams, one per device, or cap per-device scratch. A thread count of zero derives from the hardware: 7/8 of the cores those memory domains carry, since full occupancy starves the ordered publication path, and for NVMe the devices’ queue capabilities. forVram takes no thread count at all: one host thread drives one stream per device, so there is no count to name.

One call holds an executor at a time, and index handles stay freely shareable. Executor.onPool(memoryDomains:cpuClass:pool:) borrows an existing ForkUnion pool instead of spawning threads, and drain(mood:) waits until no call holds the executor and picks whether its threads park or spin once idle.

§ Receivers & Deferred Calls

The receiver chooses where a verb runs and how it completes, and the verb stays the same. Called on the index, a verb runs on the calling thread; 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 at once. DenseIndex, DenseIndexOn and DenseIndexDeferredOn take each verb with the same arguments, and a blocking call takes no executor or completion, with a default config deriving every knob.

let here = try index.search(queries, wanted: 10, arena: arena)                  // the calling thread
let blocking = try index.on(executor).search(queries, wanted: 10, arena: arena) // blocking, on the executor
let call = try index.deferred(executor).search(queries, wanted: 10, arena: arena) // Deferred<Matches>
let matches = try await call.value  // or: try call.wait()

A Deferred owns its outcome and its outputs, and keeps alive every object whose memory the call reads, until the call is final. It borrows the index, the executor and the arena as a blocking call does, since releasing any of them waits for the calls naming it, and its waited result is the same view a blocking call answers. A blocking call lends its inputs in place and frees its outcome, slots and callbacks on return; a deferred one copies only the keys and paths it borrows. status reads deferred until then, through the outcome word with acquire ordering, and wait() blocks and answers the result or throws the failure, the same every time it is asked. value waits on a Dispatch thread and then reads the result in the caller’s isolation, so an async caller suspends rather than blocks, and the cooperative pool keeps its threads. Releasing an unfinished one waits for it first.

let call = try index.deferred(executor).search(queries, wanted: 10, arena: arena)
if call.status == .deferred {
    // other work
}
let matches = try await call.value  // or: try call.wait()

A failure this binding catches before the call crosses throws at submission, and one the library catches arrives through status and wait. One call holds an executor at a time, so a second call finding it held finishes at once with 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 waiting on the first before submitting the second. Rows built over a Tensor stay alive with the call, while memory borrowed through a MatrixView or a softmax carry must outlive it. The constructors that the C interface lets fan out take an executor in their config and run on the calling thread without one.

API

ClassDeferredA call submitted to an executor, which finishes there while the caller does other work.3
Types.swift:724 ↗
final class Deferred<Value>

It owns the call's outcome and outputs, and keeps alive every object whose memory the call reads, until the call is final. The handles it names, the arena among them, it borrows as a blocking call does. Releasing an unfinished one waits for it first.

Conforms to

Sendable
statusdeferred until the call is final, then success or its failure, read without blocking.
Types.swift:739 ↗
var status: Status { get }

Binding-only: reads the status word wait would, with its acquire ordering.

waitBlocks until the call is final, then answers its result or throws its failure, the same every time it is asked.
Types.swift:748 ↗
func wait() throws(USearchError) -> Value
valueWaits for the call on a Dispatch thread, so an async caller suspends rather than blocks, and the cooperative pool keeps every thread; the result is read back in the caller's isolation.
Types.swift:758 ↗
nonisolated(nonsending) var value: Value { get async throws(USearchError) }

Binding-only: runs wait.

StructMoodWhat an executor does with its threads or streams once it goes idle.4
Types.swift:926 ↗
struct Mood

Conforms to

Hashable, Sendable, CustomStringConvertible, Equatable
parkedIdle threads and streams sleep; the next call pays a wake-up.
Types.swift:931 ↗
static let parked: Mood
spinningIdle threads and streams stay hot, trading power for the next call's latency.
Types.swift:933 ↗
static let spinning: Mood
descriptionThe mood rendered as the library spells it.
Types.swift:936 ↗
var description: String { get }
initReads a spelling back, and nil for one no mood carries.
Types.swift:939 ↗
init?(name: String)
ClassExecutorThe host threads or device streams a fanned-out verb runs on, and the traversal scratch each of them walks with.6
Types.swift:958 ↗
final class Executor

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 factory that spawned it and must match the residency of every handle it drives; a mismatch reports device.

A verb called on the index itself runs on the calling thread - single-threaded on RAM, on the handle's device and default stream on VRAM. Calls on one executor run in submission order, and index handles stay freely shareable.

Conforms to

Sendable
residencyWhere it computes, which the factory that spawned it named.
Types.swift:963 ↗
let residency: Residency
forRamSpawns host threads for RAM-resident indexes, interleaved across the compute domains local to memoryDomains - the very list the indexes were initialized with.
Types.swift:978 ↗
static func forRam(
    memoryDomains: [Int32] = [],
    cpuClass: CPUClass? = nil,
    threads: Int = 0,
    deferredLimit: Int = 0
) throws(USearchError) -> Executor

Zero threads takes 7/8 of the cores those domains carry, since full occupancy starves the ordered publication path. Every factory's deferredLimit sizes the table of deferred calls it tracks at once, and zero takes a default. A cpuClass narrows the threads' waiters and atomics to that class's; nil derives the CPU's own.

onPoolCreates a RAM executor over a borrowed ForkUnion fu_pool_t, so several libraries in one process share one set of threads while each keeps its own scratch.
Types.swift:1008 ↗
static func onPool(
    memoryDomains: [Int32] = [],
    cpuClass: CPUClass? = nil,
    pool: UnsafeMutableRawPointer,
    deferredLimit: Int = 0
) throws(USearchError) -> Executor

The pool must outlive the executor, and keeps its lender's mood.

forNvmeSpawns host threads for NVMe-resident indexes, one submission ring apiece, across the compute domains local to the devices paths names.
Types.swift:1039 ↗
static func forNvme(
    paths: [String] = [],
    cpuClass: CPUClass? = nil,
    threads: Int = 0,
    deferredLimit: Int = 0
) throws(USearchError) -> Executor

Zero threads derives from those devices' queue capabilities.

forVramOpens one submission stream on every named device, all of them driven by the one host thread that calls the verbs - which is why this factory takes no thread count.
Types.swift:1067 ↗
static func forVram(
    devices: [Int32] = [],
    gpuClass: GPUClass? = nil,
    streams: [UnsafeMutableRawPointer?]? = nil,
    scratchLimit: Int = 0,
    deferredLimit: Int = 0
) throws(USearchError) -> Executor

streams borrows the caller's CUDA streams, one per device in devices order, and nil creates the executor's own. A zero scratchLimit lets per-device scratch grow as geometry asks. A gpuClass the devices do not run is refused with unsupported.

drainWaits for every call submitted to this executor, then adopts mood whenever it next goes idle; a failure is the executor's own, such as a lost device, never one of a drained call.
Types.swift:1105 ↗
func drain(mood: Mood = .parked) throws(USearchError)
StructDenseIndexOnAn index's verbs on an executor, each blocking until final after every call submitted there before it; see DenseIndex.on(_:) and the index's own verbs for what each one does.20
DenseIndex.swift:1379 ↗
struct DenseIndexOn

A blocking call borrows the arena it is given, and its result reads it until the arena's next call.

Conforms to

Sendable
*× 2Reserves capacity for a total number of entries, the current size included.
DenseIndex.swift:1393 ↗
func reserve(_ capacity: Int) throws(USearchError)
*× 2Erases all entries, keeping the reserved capacity and the frozen geometry.
DenseIndex.swift:1398 ↗
func clear() throws(USearchError)
*× 2Reports the frozen metadata and the current counters in one call.
DenseIndex.swift:1401 ↗
func describe() throws(USearchError) -> DenseIndexDescription
*× 2The number of entries stored under a key; zero means the index holds none.
DenseIndex.swift:1406 ↗
func count(key: Key) throws(USearchError) -> Int
containsBinding-only: calls count.
DenseIndex.swift:1411 ↗
func contains(key: Key) throws(USearchError) -> Bool
*× 2Retrieves every vector stored under a key into arena, one row per entry sharing the key.
DenseIndex.swift:1414 ↗
func get<Value>(
    key: Key,
    as value: Value.Type,
    arena: Arena
) throws(USearchError) -> DenseVectors<Value> where Value : ScalarValue
*× 2Inserts rows of vectors under their keys, all or nothing.
DenseIndex.swift:1423 ↗
func insert(
    keys: [Key],
    vectors: RowsView,
    config: DenseInsertConfig = DenseInsertConfig()
) throws(USearchError)
*× 2Re-selects the neighbors of every stored entry against the finished graph, healing islands.
DenseIndex.swift:1433 ↗
@discardableResult func refine(
    config: DenseRefineConfig = DenseRefineConfig()
) throws(USearchError) -> DenseRefineReport
*× 2Audits how much of the graph a search can reach from its landmarks.
DenseIndex.swift:1438 ↗
func health() throws(USearchError) -> DenseReachability
*× 2Scores multi-row queries against multi-vector documents by late interaction, into arena.
DenseIndex.swift:1454 ↗
func maxsim(
    _ queries: RowsView,
    rowsPerQuery: Int,
    wanted: Int,
    arena: Arena,
    config: DenseMaxsimConfig = DenseMaxsimConfig()
) throws(USearchError) -> DenseProducts
*× 2The approximate sparse product of rows against the indexed vectors, written in arena.
DenseIndex.swift:1467 ↗
func multiply(
    _ left: RowsView,
    arena: Arena,
    config: DenseMultiplyConfig = DenseMultiplyConfig()
) throws(USearchError) -> DenseProducts
*× 2Softmax attention of query heads over every indexed key their walks score, into arena.
DenseIndex.swift:1476 ↗
func attend<Value>(
    _ queries: RowsView,
    lookup: ValueLookup,
    as value: Value.Type,
    arena: Arena,
    config: DenseAttendConfig = DenseAttendConfig()
) throws(USearchError) -> DenseVectors<Value> where Value : ScalarValue
*× 2Absorbs every donor into this index, keeping their graphs and consuming the donors.
DenseIndex.swift:1490 ↗
@discardableResult func absorb(
    _ donors: [DenseIndex],
    config: DenseAbsorbConfig = DenseAbsorbConfig()
) throws(USearchError) -> DenseRefineReport
*× 2Splits this index into empty receivers, graph-local parts keeping their edges.
DenseIndex.swift:1499 ↗
@discardableResult func split(
    into receivers: [DenseIndex],
    config: DenseSplitConfig = DenseSplitConfig()
) throws(USearchError) -> DenseRefineReport
*× 2Draws up to wanted keys around config.focus, or across the whole index, into arena.
DenseIndex.swift:1507 ↗
func sample(
    wanted: Int,
    arena: Arena,
    config: DenseSampleConfig = DenseSampleConfig()
) throws(USearchError) -> DenseDraws
*× 2Writes the index under a path, in the layout its extension picks.
DenseIndex.swift:1516 ↗
func save(path: String, config: DenseSaveConfig = DenseSaveConfig()) throws(USearchError)
save*× 2Writes the index into arena as one monolith, the bytes a .safegraphs path holds.
DenseIndex.swift:1521 ↗
func saveBuffer(
    arena: Arena,
    config: DenseSaveBufferConfig = DenseSaveBufferConfig()
) throws(USearchError) -> Monolith
cursorOpens a cursor whose every pull blocks on this executor.
DenseIndex.swift:1529 ↗
func cursor(starts: [Key] = []) throws(USearchError) -> DenseCursor
searchStreamOpens a stream whose every pull blocks on this executor.
DenseIndex.swift:1534 ↗
func searchStream(
    _ query: RowsView,
    config: DenseSearchStreamInitConfig = DenseSearchStreamInitConfig()
) throws(USearchError) -> DenseSearchStream
StructDenseIndexDeferredOnAn index's verbs submitted to an executor, each answering a Deferred at once; see DenseIndex.deferred(_:) and the index's own verbs for what each one does.
DenseIndex.swift:1549 ↗
struct DenseIndexDeferredOn

A failure this binding catches before the call crosses throws at submission. Every object whose memory a call reads, like rows built over a Tensor, stays alive until it is final, and memory borrowed from elsewhere must outlive it too. The index, the executor and the arena are borrowed as a blocking call borrows them, since releasing any of them waits for the calls naming it.

Conforms to

Sendable