pub const AllocationResult = struct Fields
memory_domain_id | MemoryDomainId | |
|---|---|---|
ptr | [*]u8 | |
allocated_bytes | usize | |
bytes_per_page | usize |
Placing memory on a chosen domain is implemented on Linux via the mbind syscall, on FreeBSD via domainset policies, and on Windows via VirtualAllocExNuma, and degrades to a single domain elsewhere.
The portable domain_allocator_t alias picks the right backend per platform - linux_numa_allocator_t, freebsd_numa_allocator_t, windows_numa_allocator_t, or a plain aligned fallback - each an STL-compatible allocator bound to one memory_domain_id_t.
Let’s say you are building a Big Data application — brute-forcing Vector Search with the NumKong library — on a 2 dual-socket CPU system, the kind that powers production USearch deployments. The first part of that program may be responsible for sharding the incoming stream of data between distinct memory regions. That part, in our simple example will be single-threaded:
#include <vector> // `std::vector`
#include <span> // `std::span`
#include <forkunion.hpp> // `linux_numa_allocator`, `machine_topology_t`, `distributed_pool_t`
#include <numkong/spatial.h> // `nk_angular_f32`, `nk_f64_t`
namespace fu = ashvardanian::forkunion;
using floats_alloc_t = fu::linux_numa_allocator<float>;
constexpr std::size_t dimensions = 768; /// Matches most BERT-like models
static std::vector<float, floats_alloc_t> first_half(floats_alloc_t(fu::memory_domain_id_t {0}));
static std::vector<float, floats_alloc_t> second_half(floats_alloc_t(fu::memory_domain_id_t {1}));
static fu::machine_topology_t machine_topology;
static fu::distributed_pool_t distributed_pool;
/// Dynamically shards incoming vectors across 2 nodes in a round-robin fashion.
void append(std::span<float, dimensions> vector) {
bool put_in_second = first_half.size() > second_half.size();
if (put_in_second) second_half.insert(second_half.end(), vector.begin(), vector.end());
else first_half.insert(first_half.end(), vector.begin(), vector.end());
}The concurrent part would involve spawning threads adjacent to every memory pool to find the best search_result_t.
The primary search function, in ideal world would look like this:
search_result_t.That is, however, overly complicated to implement. Such tree-like hierarchical reductions are optimal in a theoretical sense. Still, assuming the relative cost of spin-locking once at the end of a thread scope and the complexity of organizing the code, the more straightforward path is better. A minimal example would look like this:
/// On each compute domain we'll synchronize the threads
struct search_result_t {
nk_f64_t best_distance {std::numeric_limits<nk_f64_t>::max()};
std::size_t best_index {0};
};
inline search_result_t pick_best(search_result_t const& a, search_result_t const& b) noexcept {
return a.best_distance < b.best_distance ? a : b;
}
/// Uses all CPU threads to search for the closest vector to the @p query.
search_result_t search(std::span<float, dimensions> query) {
bool const need_to_spawn_threads = distributed_pool.threads_count() == 0;
if (need_to_spawn_threads) {
fu::status_t const harvested = machine_topology.harvest();
assert(fu::succeeded(harvested) && "Failed to harvest NUMA topology");
assert(machine_topology.memory_domains_count() == 2 && "Expected exactly 2 NUMA nodes");
fu::status_t const spawned = distributed_pool.spawn(machine_topology);
assert(fu::succeeded(spawned) && "Failed to spawn NUMA pools");
}
search_result_t result;
fu::spin_mutex_t result_update; // ? Lighter `std::mutex` alternative w/out futexes
std::size_t const first_vectors = first_half.size() / dimensions;
std::size_t const total_vectors = first_vectors + second_half.size() / dimensions;
auto slices = distributed_pool.for_slices(total_vectors,
[&](fu::tasks_range_t tasks, fu::thread_in_domain_t) noexcept {
search_result_t thread_local_result;
for (std::size_t const task : tasks) {
bool const in_second = task >= first_vectors;
auto const &shard = in_second ? second_half : first_half;
std::size_t const local_index = in_second ? task - first_vectors : task;
nk_f64_t distance;
nk_angular_f32(query.data(), shard.data() + local_index * dimensions, dimensions, &distance);
thread_local_result = pick_best(thread_local_result, {distance, task});
}
// ! Still synchronizing over a shared mutex for brevity.
std::lock_guard<fu::spin_mutex_t> lock(result_update);
result = pick_best(result, thread_local_result);
});
slices.join();
return result;
}In a dream world, we would call distributed_pool.for_n, but there is no clean way to make the scheduling processes aware of the data distribution in an arbitrary application, so that’s left to the user.
The for_slices helper pairs each run with a fu::thread_in_domain_t naming its compute domain, but slices follow the thread count rather than the shard boundary, so the callback above picks the shard per task.
For more flexibility around building higher-level low-latency systems, there are unsafe APIs expecting you to manually “join” the broadcasted calls: unsafe_for_threads returns a generation token, is_complete polls it without blocking, and unsafe_join blocks until that generation completes.
The manual two-vector sharding above is what the symmetric allocators automate: symmetric_memory_allocator_t - fu_allocate_symmetric in C - maps one virtual range striped a slice per memory domain, and Rust, Zig, and Mojo wrap it as ShardedArray<T>, one shard per domain, and ReplicatedArray<T>, a full copy per domain for read-mostly data.
To place and coordinate pools by hand, spawn_on - fu_pool_spawn_on in C - pins a pool to a single compute domain, while locate_thread_in and threads_count_in map a global thread index to its domain and count the workers living there.
AllocationResultResult of a memory-domain allocation; carries only the OS id, so it can outlive the topology.2pub const AllocationResult = struct memory_domain_id | MemoryDomainId | |
|---|---|---|
ptr | [*]u8 | |
allocated_bytes | usize | |
bytes_per_page | usize |
asSliceReturns the allocated memory as a slicepub fn asSlice(self: AllocationResult) []u8 freeFrees the allocationpub fn free(self: AllocationResult) void DomainAllocatorAllocator bound to a single memory domain, compatible with Zig's std.mem.Allocator interface.4pub const DomainAllocator = struct This type is the allocation API: build one for a memory domain's OS id - from
Topology.memoryDomainIdAtIndex - and it both hands out AllocationResults through
allocateAtLeast/allocate and backs a std.mem.Allocator through allocator.
memory_domain_id | MemoryDomainId |
|---|
initBinds to the memory domain named by @p memory_domain_id, which must name a real domain - ask isValid first, since an out-of-range lookup answers .none.pub fn init(memory_domain_id: MemoryDomainId) Self allocateAtLeastAllocates at least @p minimum_bytes on this domain at optimal page size, or null on failure.pub fn allocateAtLeast(
self: Self,
minimum_bytes: usize,
) ?AllocationResult allocateAllocates exactly @p bytes on this domain, or null on failure.pub fn allocate(self: Self, bytes: usize) ?[*]u8 allocatorpub fn allocator(self: *Self) Allocator ReplicatedArrayOne full length-len copy of a sequence per domain, so every thread reads a node-local replica.8pub fn ReplicatedArray(comptime T: type) type A thin owner of one symmetric mapping the allocator stripes across the nodes - replica d lives
at base + d * stride_bytes and holds len elements. Raw uninitialized storage: the caller
fills every replica and keeps them coherent, mirroring the C++ replicated_array. T must be
plain-old-data, since the container runs no constructors or destructors.
base_bytes | ?[*]u8 | |
|---|---|---|
stride_bytes | usize | |
domains | usize | |
total_bytes | usize | |
len | usize |
initAllocates one uninitialized length-n replica per memory domain.pub fn init(topology: Topology, n: usize) Error!Self *× 2Unmaps the whole striped mapping.pub fn deinit(self: *Self) void is*× 2Whether the container holds no elements.pub fn isEmpty(self: Self) bool baseThe typed base pointer of the first replica's storage.pub fn base(self: Self) [*]T memoryDomainsCountThe number of per-domain replicas.pub fn memoryDomainsCount(self: Self) usize strideBytesThe page-aligned byte distance between consecutive replicas.pub fn strideBytes(self: Self) usize onMemoryDomainThe whole replica living on memory_domain.pub fn onMemoryDomain(self: Self, memory_domain: MemoryDomain) []T atOne element of the replica on memory_domain.pub fn at(
self: Self,
memory_domain: MemoryDomain,
local_index: usize,
) *T ShardedArrayA sequence partitioned across memory domains as contiguous segments, each element stored once.9pub fn ShardedArray(comptime T: type) type Each domain owns a contiguous logical segment of segment elements, so element i lives at {i / segment, i % segment} - see locationOf - and a scan of a domain's shard is sequential in
memory. Backed by one symmetric mapping; the trailing shard may be short. T must be
plain-old-data, mirroring ReplicatedArray.
base_bytes | ?[*]u8 | |
|---|---|---|
stride_bytes | usize | |
domains | usize | |
total_bytes | usize | |
len | usize | |
segment | usize |
initAllocates uninitialized storage for n elements partitioned round-robin across domains.pub fn init(topology: Topology, n: usize) Error!Self baseThe typed base pointer of the first shard's storage.pub fn base(self: Self) [*]T memoryDomainsCountThe number of shards, one per memory domain.pub fn memoryDomainsCount(self: Self) usize strideBytesThe page-aligned byte distance between consecutive shards.pub fn strideBytes(self: Self) usize lengthOnMemoryDomainHow many elements the shard on memory_domain holds - a trailing shard may be shorter.pub fn lengthOnMemoryDomain(
self: Self,
memory_domain: MemoryDomain,
) usize locationOfThe memory domain and local index that store logical element logical_index.pub fn locationOf(
self: Self,
logical_index: usize,
) struct { memory_domain: MemoryDomain, local_index: usize } logicalIndexOfThe logical index at local_index on memory_domain - the inverse of locationOf.pub fn logicalIndexOf(
self: Self,
memory_domain: MemoryDomain,
local_index: usize,
) usize onMemoryDomainThe whole shard living on memory_domain.pub fn onMemoryDomain(self: Self, memory_domain: MemoryDomain) []T atThe single home of a logical element.pub fn at(
self: Self,
memory_domain: MemoryDomain,
local_index: usize,
) *T allocateAllocates exactly the requested bytes on a domain; id from Topology.memoryDomainIdAtIndex.pub fn allocate(
memory_domain_id: MemoryDomainId,
bytes: usize,
) ?[*]u8 allocateAtLeastAllocates on a memory domain with optimal page size; id from Topology.memoryDomainIdAtIndex.pub fn allocateAtLeast(
memory_domain_id: MemoryDomainId,
minimum_bytes: usize,
) ?AllocationResult