numa-shim
Rust NUMA detection and placement — zero third-party C/C++ dependencies.
The key differentiator: zero third-party C/C++ dependencies — no libnuma,
no hwloc, no libcuda. The crate calls the system libc / kernel32 directly
via FFI (sched_getcpu, raw syscall(2), VirtualAllocExNuma) — system
interfaces any Rust program already links to, not third-party C libraries.
| Platform | Node detection | Memory placement |
|---|---|---|
| Linux x86_64 / aarch64 | sched_getcpu + sysfs /sys/devices/system/node/nodeN/cpumap |
mbind(2) via raw syscall(2) — no libnuma, no hwloc |
| Linux, other architecture | sched_getcpu + sysfs /sys/devices/system/node/nodeN/cpumap — detection works the same as above |
not supported — Err(UnsupportedArchitecture) (the mbind-based reservation path is x86_64/aarch64-only; detection is architecture-independent) |
| Windows (64-bit) | GetCurrentProcessorNumberEx + GetNumaProcessorNodeEx |
VirtualAllocExNuma (via vmem-integration feature) |
| macOS | not available (no public NUMA API) | not supported — Err(UnsupportedPlatform) |
| miri | not available | not supported — Err(UnsupportedPlatform) |
Windows support is 64-bit only (x86_64-pc-windows-msvc and equivalent
64-bit targets). This is an explicit policy decision (task #1313, fifteenth
review finding F11,
docs/reviews/2026-08-24-170047-numa-shim-publication-audit-Sol-codex.md),
not a "might work, untested" gap: the crate's Windows FFI test layout has
always assumed a 64-bit pointer width (MEMORY_BASIC_INFORMATION's
PartitionId field exists in winnt.h only under #if defined(_WIN64)), CI
has only ever covered 64-bit windows-latest, and 32-bit Windows
(target_pointer_width = "32") is out of scope — no support, testing, or
compatibility is planned for it. The policy is compile-time enforced (task
#1321, sixteenth review P2): building this crate for a 32-bit Windows
target fails with a compile_error! naming this policy instead of
silently producing an unsupported build; 32-bit non-Windows targets are
unaffected.
Why yet another NUMA crate?
Most Rust NUMA crates link to libnuma or hwloc, pulling in heavy C
dependencies that complicate cross-compilation and static linking. numa-shim
calls the kernel directly:
- Linux:
mbind(2)viasyscall(number, ...)— the number is baked in as a constant (SYS_MBIND = 237on x86_64,235on aarch64). Nolibnumasymbol needed;syscall(2)is always present in glibc and musl. - Linux node detection: reads
/sys/devices/system/node/nodeN/cpumapvia POSIXopen/read/closewith no heap allocation (stack buffer only). - Windows: Win32 APIs from
kernel32.dll— always linked, no extra import lib.
Usage
[]
= "0.2"
# Optional: enables reserve_preferred_on_node() which wraps aligned-vmem
# numa-shim = { version = "0.2", features = ["vmem-integration"] }
use current_node;
// Detect the current thread's NUMA node.
match current_node
There is deliberately no "migrate an existing allocation" API. That is a
crate-scope decision, not an OS limitation: Linux mbind(2) can move
already-resident pages, but only via its MPOL_MF_MOVE/MPOL_MF_MOVE_ALL
flags — a privileged, partially-failing operation whose contract is
different, more complex, and riskier than reservation-time preference; this
crate does not take it on. With default flags (all this crate ever passes),
mbind(2) affects only future page faults — a preference installed after
an allocation's first touch would leave its resident pages exactly where
they are. A NUMA preference must therefore be requested at reservation
time, before the first touch — see
reserve_preferred_on_node below.
Feature flags
vmem-integration
Enables reserve_preferred_on_node, which reserves aligned anonymous virtual memory
with a NUMA preference using aligned-vmem:
[]
= { = "0.2", = ["vmem-integration"] }
# `reserve_preferred_on_node` returns an `aligned_vmem::Reservation`, but you
# do NOT need `aligned-vmem` as a direct dependency just to name that type:
# numa-shim re-exports it (`numa_shim::Reservation`). The direct dependency
# below is only for using `aligned-vmem`'s own items — `page_size()` / `PAGE`,
# or `reserve_aligned` for the best-effort fallback shown below — because a
# feature enabled on numa-shim does not put `aligned_vmem` in YOUR crate's
# extern prelude (it stays an optional transitive dep).
= "0.2"
use ;
use ;
// Reserve fresh memory with a NUMA preference installed BEFORE the first
// page fault — the only point where a preference can be in place before any
// page is touched (still SOFT: "installed," not "placement guaranteed").
// `NodeId::new` rejects only the `NO_NODE` sentinel (u32::MAX):
// an id the platform cannot address still constructs and surfaces as
// `Err(ReserveNumaError::InvalidNode)` (Linux nodemask limit) or
// `Err(ReserveNumaError::Os(..))` (Windows forwards any id to the OS).
// `None` from `current_node()` means undetermined topology -> no NUMA
// preference (task #1308; `Some(0)` only ever means genuinely-resolved node 0).
let ps = page_size;
let r = match current_node ;
drop;
// Best-effort fallback with more detailed error handling:
let r = match current_node ;
drop;
Without this feature, numa-shim has zero runtime dependencies.
mock (build-time cfg flag)
Test-only: replaces the real platform NUMA syscalls with a recording stub
(numa_shim::mock) so CI can assert the wrapping logic on any target,
including macOS and miri, where no real NUMA API exists. Enabled by the
build-time cfg flag numa_shim_mock via RUSTFLAGS="--cfg numa_shim_mock"
(task #1288, mirroring aligned-vmem's task #962), NOT a Cargo feature.
Resolution: This used to be a Cargo feature whose unification hazard was
documented as an open risk (task #726). Resolved 2026-08-23 (task #1288) by
converting it to the build-time --cfg numa_shim_mock flag. The cfg still
applies build-graph-wide once set — what changed is WHO can set it: only the
top-level build invoker via an explicit RUSTFLAGS/build-script choice, never a
transitive dependency through Cargo's additive feature-unification, and never
--all-features/docs.rs/cargo add by accident. Migration for 0.1.0
--features mock consumers: see the CHANGELOG's "Removed" section.
Public API
/// Sentinel: no NUMA node / unsupported platform (detection-side interop only; the reservation API takes `NodeId`, never the sentinel).
pub const NO_NODE: u32 = u32::MAX;
/// NUMA node of the calling thread, or None if undeterminable (no NUMA API,
/// OS failure, or — on Linux — topology could not resolve this CPU).
pub fn current_node() -> Option<u32>;
/// Outcome of a NUMA-node determination attempt for the calling thread:
/// Resolved(n) — CPU genuinely resolved to node n via the platform
/// topology; TopologyUnavailable (Linux only) — CPU index obtained but not in
/// any cached sysfs cpumap (unreadable topology, node >= 64, or no NUMA
/// sysfs at all), which current_node() maps to None; Unavailable — no NUMA
/// API on this platform or the OS API failed (current_node() returns None).
/// Fail-closed behavior: both non-Resolved variants map to None (task #1308).
#[non_exhaustive]
pub enum NodeResolution { Resolved(u32), TopologyUnavailable, Unavailable }
/// Additive alternative to current_node(): same resolution logic, but
/// distinguishes WHY detection failed (diagnostics) — both non-Resolved
/// outcomes map to None, not a way to recover a node-0 answer.
pub fn current_node_resolution() -> NodeResolution;
/// NUMA node identifier for the reservation/policy API.
/// `NodeId::new(u32) -> Option<NodeId>` rejects only the `NO_NODE`
/// sentinel; platform-specific node validity surfaces as a typed error
/// from the fallible reservation API.
pub struct NodeId(u32);
/// Failure cause of a NUMA-preferred reservation attempt.
#[non_exhaustive]
pub enum ReserveNumaError {
UnsupportedPlatform,
UnsupportedArchitecture,
InvalidArguments,
InvalidNode,
Os(std::io::Error),
}
/// Re-export of `aligned_vmem::Reservation` — the return type of
/// `reserve_preferred_on_node`, nameable as `numa_shim::Reservation` with NO
/// direct `aligned-vmem` dependency.
#[cfg(feature = "vmem-integration")]
pub use aligned_vmem::Reservation;
/// Reserve aligned anonymous memory with a preferred NUMA node, installing
/// the preference at reservation time — before the first page fault.
/// Linux: mbind(MPOL_PREFERRED) on the COMPLETE OS reservation span, return
/// value checked, reservation released on policy failure. Windows:
/// VirtualAllocExNuma. No silent fallback anywhere.
#[cfg(feature = "vmem-integration")]
pub fn reserve_preferred_on_node(
size: usize,
align: usize,
node: NodeId,
) -> Result<aligned_vmem::Reservation, ReserveNumaError>;
The three #[doc(hidden)] test-only modules — numa_shim::cpumap,
numa_shim::eintr, and numa_shim::linux — are not part of this surface: they are test oracles,
exempt from this crate's SemVer guarantees, and may change or be removed
in any release (including patch releases) without a deprecation period
(task #1289).
Linux syscall numbers
| Architecture | SYS_MBIND |
|---|---|
| x86_64 | 237 |
| aarch64 | 235 |
On other Linux architectures reserve_preferred_on_node returns
Err(ReserveNumaError::UnsupportedArchitecture) — no silent skip (the
syscall number is unknown; contributions welcome).
node >= 64 returns Err(ReserveNumaError::InvalidNode) (task #1306;
previously a silent skip): the Linux nodemask is a single u64, so only
node IDs 0..63 can be addressed, even though mbind(2) itself supports
node counts up to MAX_NUMNODES (commonly 1024 on real kernels).
MSRV
Rust 1.88
License
MIT OR Apache-2.0