Table of Contents

numactl

What is numactl?

numactl is a command-line tool for controlling NUMA (Non-Uniform Memory Access) policy: which CPU cores a process runs on and which memory node it allocates from. On a multi-socket server, memory attached to a different CPU socket than the one running a thread is slower to access than local memory, sometimes by a factor of two or more, and numactl is the tool for making sure a program's threads and its memory end up on the same node.

The name describes the hardware reality it manages. A modern multi-socket machine is not one uniform pool of RAM; each CPU socket has its own directly-attached memory controller and DIMMs. A core can still reach memory attached to another socket, but it has to cross the inter-socket interconnect to do it, which costs extra latency and consumes shared interconnect bandwidth. The OS scheduler and memory allocator try to keep a process's threads and pages co-located automatically, but for HPC workloads where every percentage point of memory bandwidth matters, that default heuristic often isn't good enough.

numactl lets you override the default with an explicit policy: pin a process to specific cores, force its memory to come from a specific node, or interleave allocations evenly across all nodes when a workload's access pattern is genuinely uniform rather than node-local.

Install

On Debian and Ubuntu:

sudo apt install numactl

On Fedora and RHEL:

sudo dnf install numactl

Practice

numactl --hardware shows the NUMA topology of the current machine, how many nodes exist, how much memory each has, and the relative distance (latency cost) between nodes:

numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 64000 MB
node 1 cpus: 8 9 10 11 12 13 14 15
node 1 size: 64000 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

The distance table shows that accessing node 1's memory from a core on node 0 costs roughly twice (21 vs 10) what a local access costs, a concrete number for what “NUMA effects” actually mean.

To pin a process to node 0's cores and force its memory to come from node 0:

numactl --cpunodebind=0 --membind=0 ./my_program

Concepts

cpunodebind and membind

--cpunodebind restricts which node's CPUs the process can be scheduled on; --membind restricts which node's memory the process can allocate from. Used together, they guarantee the strongest form of locality: every thread runs on node N and every allocation comes from node N's memory.

numactl --cpunodebind=0 --membind=0 ./app

If the workload actually needs more memory than a single node has, --membind on a single node will cause allocation failures once that node's memory is exhausted, even if other nodes have free capacity; --preferred is the softer alternative for that case.

preferred vs interleave

--preferred=N requests allocation from node N but falls back to other nodes if N is full, rather than failing outright. --interleave=all (or a specific node list) spreads pages round-robin across nodes, which is the right choice for a large shared data structure accessed roughly equally by threads on every node, rather than one thread's private working set.

numactl --interleave=all ./app        # spread memory evenly across all nodes
numactl --preferred=0 ./app            # prefer node 0, allow fallback

Physcpubind for fine-grained pinning

--physcpubind pins to specific logical CPUs rather than a whole node, useful when combined with a thread-affinity scheme (like OpenMP's OMP_PLACES/OMP_PROC_BIND) to control exactly which core each thread lands on:

numactl --physcpubind=0-3 --membind=0 ./app

Diagnosing NUMA effects

numastat -p <pid> shows, for a running process, how many of its pages actually live on each node, which is the way to confirm whether a NUMA binding policy is having the intended effect rather than assuming it from the command line alone:

numastat -p $(pgrep my_program)

A process whose memory is scattered evenly across nodes despite an intended --membind=0 policy usually indicates the binding was applied after the process had already allocated memory elsewhere; binding has to happen at process launch, before the first allocation, to be effective.