Zig · Topology

Topology

const forkunion = @import("forkunion");const Topology = @import("forkunion").Topology;const ComputeDomain = @import("forkunion").ComputeDomain;const MemoryDomain = @import("forkunion").MemoryDomain;const MemoryDomainId = @import("forkunion").MemoryDomainId;const Fabric = @import("forkunion").Fabric; 4.0.0-rc.1 · d927445f

A machine is described along two independent axes. A compute domain is a bindable group of cores sharing a Quality-of-Service class and locality — it is what a pool spawns onto, and the index a worker callback receives. A memory domain is a bank of memory with its own capacity and access cost — it is what the allocator targets, may be cpuless like a CXL expander, and may serve several compute domains at once. The axes stay separate because they don’t line up: performance and efficiency cores often share one memory controller, and equally-fast cores are often split across cache clusters or sockets.

Each axis carries a level, a dense ordinal grouping domains of like performance. Levels can be fewer than domains, since several domains may share one, and the two axes count in opposite directions: compute levels grow with performance, memory levels grow with distance, placing HBM below DDR and CXL above. Two objects split the answers by provenance, not by axis: a Topology holds what the platform declares — domains, cores, QoS classes, volumes, on both axes — while a Fabric holds what ForkUnion observes: per-edge latencies, bandwidths, and distances, and the per-medium tiers derived from them. The pipeline is harvest all the way down: a Topology harvests the declared structure from the OS and stays immutable, a pool spawns on it, and a Fabric then harvests the observed performance from the silicon through that pool’s pinned workers, pointer-chasing and streaming every reachable edge. A memory level is a property of the medium, independent of the querying core: it keys on the best bandwidth any initiator sustains to the pool, ties split by the best latency, so a 3 TB/s HBM pool outranks DDR even at equal latency.

Compute axisMemory axis
Count domainscompute_domains_countmemory_domains_count
Count levelscompute_levels_countmemory_levels_count
Level of a domaincompute_level_inmemory_level_in
Faster meanshigherlower

Names are spelled here as in Rust, which Mojo matches; C prefixes them with fu_, and Zig spells them in camelCase. Rust, Zig, and Mojo all carry the domain coordinates as distinct types — ComputeDomain, MemoryDomain, and MemoryDomainId — while C passes plain indices. Domain counts and compute levels answer from the Topology; memory levels and the per-edge magnitudes answer from a harvested Fabric, which C prefixes with fu_fabric_.

Beyond the level ordinals, two magnitudes describe a compute domain, both best-effort. compute_capacity_in forwards the kernel’s own rating of a core, read from /sys/devices/system/cpu/cpuN/cpu_capacity, normalized so the fastest core on the machine reads 1024. Linux publishes that file on arm64 and on Intel hybrids, and nothing like it exists on Windows or Apple, which read 0. A kernel that rates every core alike rates nothing, so a tie also reads 0; Linux then splits the classes by their core PMUs, cpu_core and cpu_atom on Intel hybrids. compute_cache_bytes_per_core_in reports each logical core’s share of the deepest cache confined to a domain: one instance’s bytes over the cores sharing it. It is enumerated exactly rather than measured — from the per-core cache hierarchy in sysfs on Linux, CPUID leaves elsewhere on x86, sysctl on Apple, and the cache relationships on Windows. Domains of identical throughput can sit behind very differently sized caches, so neither value follows from the other. Treat both as hints, present only where the hardware volunteers them, and never as a number a program requires.

The memory axis carries its own magnitudes, all answered by the harvested Fabric. memory_distance gives the relative cost of reaching a memory domain from a compute domain — 10 for local, higher for remote, on ACPI SLIT’s scale but from measured ratios — while memory_bandwidth and memory_latency give the observed saturated read bandwidth and dependent-load latency per edge. Back on the Topology, volume_ram and volume_ram_in report installed RAM, total or per domain, while huge_pages_count_in and volume_huge_pages_in report the free huge pages a domain can still back an allocation with, as a count or in bytes, and huge_pages_count and volume_huge_pages total them. Like the compute magnitudes, these are best-effort: fabric queries read 0 before a harvest or on edges no pinned worker could reach.

Finally, local_memory_of bridges the axes, naming the memory domain a given compute domain should allocate from. Where no topology is harvested, every query degrades to a single compute domain and a single memory domain rather than failing, so these loops need no conditional compilation.

API

Types

ThreadInDomainOne thread, situated in one compute domain - the "where I am" every callback receives.
pub const ThreadInDomain = struct

Every dispatch hands its callback two things: the work, and this. A callback placing memory reads compute_domain to find the node it runs on; one indexing per-thread scratch reads thread.

Fields

threadusizeThe physical thread executing the work.
compute_domainComputeDomainThe compute domain the thread is pinned to - a same-QoS core cluster.
StructTopologyAn explicit, owned handle to this machine's discovered compute and memory topology.18
topology.zig:178 ↗
pub const Topology = struct

Build one with init, query it through the methods below, and hand it to pool constructors and memory-domain allocators so they know which machine they are placing work and pages onto.

Fields

handle*anyopaque
initDiscovers this machine's topology, returning an owned handle.
topology.zig:182 ↗
pub fn init() Error!Topology
deinitReleases the topology handle.
topology.zig:190 ↗
pub fn deinit(self: Topology) void
logicalCoresCountReturns the number of logical CPU cores available
topology.zig:195 ↗
pub fn logicalCoresCount(self: Topology) Error!usize
memoryDomainsCountReturns the number of memory domains available
topology.zig:202 ↗
pub fn memoryDomainsCount(self: Topology) Error!usize
memoryDomainIdAtIndexResolves a memory domain's dense index to the allocator's OS id; .none if out of range.
topology.zig:211 ↗
pub fn memoryDomainIdAtIndex(
    self: Topology,
    memory_domain: MemoryDomain,
) Error!MemoryDomainId

A real domain may itself carry .none where the OS names none, so an index this machine does not have is a reported refusal rather than that same value.

computeDomainsCountReturns the number of distinct thread compute_domains
topology.zig:218 ↗
pub fn computeDomainsCount(self: Topology) Error!usize
logicalCoresCountInReturns the number of logical cores backing a given compute domain (0 if out of range).
topology.zig:225 ↗
pub fn logicalCoresCountIn(
    self: Topology,
    compute_domain: ComputeDomain,
) Error!usize
computeLevelInReturns the performance level of a compute domain (higher = more performant).
topology.zig:232 ↗
pub fn computeLevelIn(
    self: Topology,
    compute_domain: ComputeDomain,
) Error!usize
localMemoryOfReturns the memory domain nearest a given compute domain, its local allocation target.
topology.zig:242 ↗
pub fn localMemoryOf(
    self: Topology,
    compute_domain: ComputeDomain,
) Error!MemoryDomain

Performance - tiers, latencies, bandwidths, distances - is not the topology's to declare: harvest a Fabric to measure it in-process.

computeLevelsCountReturns the number of distinct Quality-of-Service levels.
topology.zig:252 ↗
pub fn computeLevelsCount(self: Topology) Error!usize

May be smaller than computeDomainsCount, as several domains can share one level - equally-fast cores may still be split across cache clusters, or across NUMA nodes.

computeCapacityInReturns the relative throughput of one core in a compute domain (0 if unknown).
topology.zig:263 ↗
pub fn computeCapacityIn(
    self: Topology,
    compute_domain: ComputeDomain,
) Error!usize

A magnitude on the Linux cpu_capacity scale, where 1024 is the fastest core present. Weight work by this - computeLevelIn is a dense ordinal and must never be divided by. Platforms that rank cores without rating them report 0; weigh by core count instead.

computeCacheBytesPerCoreInReturns each core's share of the deepest cache confined to a compute domain (0 if unknown).
topology.zig:273 ↗
pub fn computeCacheBytesPerCoreIn(
    self: Topology,
    compute_domain: ComputeDomain,
) Error!usize

Sizes a cache-resident chunk, a different question from how many chunks a domain deserves - domains of equal throughput may back onto very differently sized caches.

volumeRamReturns the total RAM volume, in bytes, across all memory domains, regardless of page size.
topology.zig:280 ↗
pub fn volumeRam(self: Topology) Error!usize
volumeRamInReturns the RAM volume, in bytes, held by a given memory domain (0 if out of range).
topology.zig:287 ↗
pub fn volumeRamIn(
    self: Topology,
    memory_domain: MemoryDomain,
) Error!usize
volumeHugePagesReturns the total huge-page volume, in bytes, across all memory domains.
topology.zig:294 ↗
pub fn volumeHugePages(self: Topology) Error!usize
volumeHugePagesInReturns the huge-page volume, in bytes, held by a given memory domain (0 if out of range).
topology.zig:301 ↗
pub fn volumeHugePagesIn(
    self: Topology,
    memory_domain: MemoryDomain,
) Error!usize
hugePagesCountReturns the total number of free huge pages across all memory domains.
topology.zig:308 ↗
pub fn hugePagesCount(self: Topology) Error!usize
hugePagesCountInReturns the number of free huge pages in a given memory domain (0 if out of range).
topology.zig:315 ↗
pub fn hugePagesCountIn(
    self: Topology,
    memory_domain: MemoryDomain,
) Error!usize
EnumComputeDomainA dense compute-domain index, in 0..Topology.computeDomainsCount().2
types.zig:100 ↗
pub const ComputeDomain = enum(usize)

A distinct type from MemoryDomain because the two index different axes of the machine and the compiler is the only thing that can tell them apart at a call site.

atWraps a dense index, as produced by iterating 0..computeDomainsCount().
types.zig:104 ↗
pub fn at(dense_index: usize) ComputeDomain
*× 2The dense index, for arithmetic and for the FFI boundary.
types.zig:109 ↗
pub fn index(self: ComputeDomain) usize
EnumMemoryDomainA dense memory-domain index, in 0..Topology.memoryDomainsCount().1
types.zig:115 ↗
pub const MemoryDomain = enum(usize)
atWraps a dense index, as produced by iterating 0..memoryDomainsCount().
types.zig:119 ↗
pub fn at(dense_index: usize) MemoryDomain
EnumMemoryDomainIdThe sparse OS id the kernel labels a NUMA node with, which the allocators key off.2
types.zig:133 ↗
pub const MemoryDomainId = enum(i32)

Distinct from MemoryDomain: that one iterates, this one allocates. An AllocationResult carries only this id, so it can outlive the Topology that resolved it.

Values

none-1Names no domain - what an out-of-range lookup answers.
identifierThe raw OS id, for the FFI boundary.
types.zig:139 ↗
pub fn identifier(self: MemoryDomainId) i32
isValidWhether this id names a real domain rather than the none sentinel.
types.zig:144 ↗
pub fn isValid(self: MemoryDomainId) bool
StructFabricThe measured memory fabric - what this process observed, as opposed to the structure a Topology declares.8
scheduling.zig:473 ↗
pub const Fabric = struct

Two query families: edge queries (initiator, target) describe one interconnect link; medium queries (target) describe the memory pool itself, with no initiator.

Completes the harvest pipeline: a Topology is harvested first and stays immutable, a Pool spawns on it, and the fabric then harvests through that pool's pinned workers, snapshotting what it needs so the topology may be freed after. Before a harvest every query answers 0, and memoryLevelsCount answers 1.

Fields

handle*anyopaque
initCreates an empty, unharvested fabric.
scheduling.zig:477 ↗
pub fn init() Error!Fabric
deinitDestroys the fabric and frees its observations.
scheduling.zig:484 ↗
pub fn deinit(self: Fabric) void
harvestMeasures the memory fabric through the pool's pinned workers, replacing any previous harvest; the topo is only read.
scheduling.zig:495 ↗
pub fn harvest(
    self: Fabric,
    topo: Topology,
    pool: Pool,
) Error!void

Returns false on allocation failure, or for a pool whose workers are not pinned per domain: flat pools and those pinned to one compute domain have no fabric to walk; it is left empty, never half-written. Not thread-safe: it dispatches on the pool and rebuilds the fabric, so call it between task batches. Expect seconds of runtime on large fabrics.

memoryLatencyReturns the measured dependent-load latency, in nanoseconds, on an edge - the best recording; 0 before a harvest, for an edge no worker could reach, or an out-of-range index.
scheduling.zig:502 ↗
pub fn memoryLatency(
    self: Fabric,
    compute_domain: ComputeDomain,
    memory_domain: MemoryDomain,
) Error!usize
memoryBandwidthReturns the measured saturated read bandwidth (MB/s) on an edge, streamed by all the initiator domain's workers at once - the best recording; 0 if unreached or out of range.
scheduling.zig:510 ↗
pub fn memoryBandwidth(
    self: Fabric,
    compute_domain: ComputeDomain,
    memory_domain: MemoryDomain,
) Error!usize
memoryDistanceReturns the relative access distance on an edge (10 = local, per the SLIT convention): the measured latency ratio to the initiator's local domain, clamped so local carries the row's minimum; unwalked edges fall back to 10-local / 20-remote.
scheduling.zig:519 ↗
pub fn memoryDistance(
    self: Fabric,
    compute_domain: ComputeDomain,
    memory_domain: MemoryDomain,
) Error!usize
memoryLevelInReturns the derived speed class of a memory domain (lower = faster: HBM < DDR < CXL), keyed by the best bandwidth any initiator sustains to it, ties split by the best latency.
scheduling.zig:527 ↗
pub fn memoryLevelIn(
    self: Fabric,
    memory_domain: MemoryDomain,
) Error!usize
memoryLevelsCountReturns the number of distinct derived memory tiers, the memory-axis twin of Topology.computeLevelsCount; 1 on single-tier systems and before a harvest.
scheduling.zig:535 ↗
pub fn memoryLevelsCount(self: Fabric) Error!usize