Table of Contents
numactl Binding
Binding pins a process to specific NUMA nodes using --cpunodebind and --membind. Together, they guarantee locality: all threads run on one node, all memory allocates from the same node.
numactl --cpunodebind=0 --membind=0 ./program # pin to node 0 numactl --cpunodebind=0-1 --membind=0-1 ./program # allow nodes 0 and 1
--cpunodebind=0 restricts the process to CPUs on node 0. The OS scheduler cannot move threads to other nodes. --membind=0 restricts memory allocation to node 0's DIMM. Any malloc or memory fault-in allocates from node 0.
Single-node binding: Most common for HPC. Bind a process to one node to maximize locality:
numactl --cpunodebind=0 --membind=0 ./app
This is the strongest form of locality—every thread and every page are on the same node. Cross-socket accesses should be rare (only library code or OS operations).
Multi-node binding: If a workload needs more memory than one node has:
numactl --cpunodebind=0-1 --membind=0-1 ./app # allow 2 nodes
This allows threads on nodes 0 or 1, and memory can allocate from either. Locality is weaker (threads on node 0 may access memory on node 1), but you don't run out of memory.
Behavior when memory exhausted: With --membind=0, if node 0 runs out of memory, the allocation fails (even if other nodes have free memory). For workloads of unknown size, use --preferred instead; see numactl Memory Policies.
Pinning with node lists:
numactl --cpunodebind=0,2 --membind=0,2 ./app # nodes 0 and 2 (skip 1) numactl --cpunodebind=0-3 --membind=0-3 ./app # nodes 0,1,2,3 (range)
Syntax: single node 0, list 0,2, or range 0-3. Mixed syntax: 0-2,4 means nodes 0,1,2,4.
Starting already-running processes: Binding must happen at process launch—before the first memory allocation—to be effective. You can't re-bind a running process and expect its existing memory to move:
numactl --cpunodebind=0 --membind=0 ./app # correct: bind at start numactl --cpunodebind=0 ./already_running_app # wrong: app already allocated memory
The second command does nothing (app's memory stays where it was).
Interacting with fork/exec: Binding is inherited by child processes spawned via fork() and exec(). The child inherits the parent's NUMA policy.
