numa-shim 0.2.0

NUMA detection and reservation in Rust — zero third-party C/C++ dependencies (no libnuma, no hwloc); direct system-libc/Win32 FFI only. Linux mbind(2) via raw syscall(2), Windows VirtualAllocExNuma, sysfs cpumap reader. Zero crate deps by default; the public API is fully forbid(unsafe_code)-friendly.
Documentation

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/cpumapdetection 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) via syscall(number, ...) — the number is baked in as a constant (SYS_MBIND = 237 on x86_64, 235 on aarch64). No libnuma symbol needed; syscall(2) is always present in glibc and musl.
  • Linux node detection: reads /sys/devices/system/node/nodeN/cpumap via POSIX open/read/close with no heap allocation (stack buffer only).
  • Windows: Win32 APIs from kernel32.dll — always linked, no extra import lib.

Usage

[dependencies]
numa-shim = "0.2"

# Optional: enables reserve_preferred_on_node() which wraps aligned-vmem
# numa-shim = { version = "0.2", features = ["vmem-integration"] }
use numa_shim::current_node;

// Detect the current thread's NUMA node.
match current_node() {
    Some(node) => println!("on NUMA node {node}"),
    None       => println!("NUMA unavailable"),
}

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:

[dependencies]
numa-shim = { version = "0.2", features = ["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).
aligned-vmem = "0.2"
use numa_shim::{current_node, NodeId, reserve_preferred_on_node};
use aligned_vmem::{page_size, PAGE};

// 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() {
    Some(node) => {
        // current_node() remaps the sentinel to None, so `node` here is
        // never NO_NODE and NodeId::new cannot fail. The reservation itself
        // can still fail (e.g. `UnsupportedArchitecture` on a real Linux
        // target outside x86_64/aarch64) — `.ok().or_else(...)` falls back
        // to a plain reservation rather than panicking on that.
        reserve_preferred_on_node(ps * 16, PAGE.max(ps), NodeId::new(node).expect("never NO_NODE"))
            .ok()
            .or_else(|| aligned_vmem::reserve_aligned(ps * 16, PAGE.max(ps)))
            .expect("OOM")
    }
    None => {
        // No NUMA preference — plain aligned reservation.
        aligned_vmem::reserve_aligned(ps * 16, PAGE.max(ps)).expect("OOM")
    }
};
drop(r);

// Best-effort fallback with more detailed error handling:
let r = match current_node() {
    Some(node) => {
        // As above: `node` is never the sentinel here.
        match reserve_preferred_on_node(
            ps * 16,
            PAGE.max(ps),
            NodeId::new(node).expect("never NO_NODE"),
        ) {
            Ok(r) => r,
            Err(e) => {
                eprintln!(
                    "NUMA preference on node {} failed ({}); using an unbound reservation",
                    node, e
                );
                aligned_vmem::reserve_aligned(ps * 16, PAGE.max(ps)).expect("OOM")
            }
        }
    }
    None => {
        aligned_vmem::reserve_aligned(ps * 16, PAGE.max(ps)).expect("OOM")
    }
};
drop(r);

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