numa aware memory allocation
**NUMA-Aware Memory Allocation** is **the practice of placing memory pages on the NUMA (Non-Uniform Memory Access) node closest to the processor that will most frequently access them, minimizing memory latency and maximizing bandwidth for parallel applications** — on modern multi-socket servers, ignoring NUMA topology can cause 2-3× performance degradation due to remote memory access penalties.
**NUMA Architecture Fundamentals:**
- **Memory Locality**: each processor socket has directly attached memory (local DRAM) — accessing local memory takes 80-100 ns, while accessing memory on another socket (remote) takes 130-200 ns, a 1.5-2× latency penalty
- **Bandwidth Asymmetry**: local memory bandwidth per socket is typically 100-200 GB/s (DDR5), while the inter-socket interconnect (UPI, Infinity Fabric) provides 50-100 GB/s — remote bandwidth is 50-70% of local
- **NUMA Node**: a processor socket and its local memory form a NUMA node — a dual-socket server has 2 NUMA nodes, a quad-socket has 4, and AMD EPYC processors expose multiple NUMA nodes per socket (NPS4 mode creates 4 nodes per socket)
- **Topology Discovery**: numactl --hardware displays the system's NUMA topology — shows node distances, memory sizes, and CPU-to-node mappings
**Linux NUMA Memory Policies:**
- **First-Touch**: the default policy — memory pages are allocated on the NUMA node of the processor that first writes to them — effective when initialization and computation happen on the same threads
- **Interleave**: pages are distributed round-robin across specified NUMA nodes — provides uniform average latency and balances memory bandwidth across nodes — ideal for shared data structures accessed by all threads
- **Bind**: restricts allocation to specified NUMA nodes — ensures data stays local even if threads migrate — used with process pinning to guarantee locality
- **Preferred**: attempts allocation on the specified node but falls back to others if memory is exhausted — softer constraint than bind, prevents out-of-memory failures on overcommitted nodes
**Programming APIs:**
- **numactl Command**: numactl --membind=0 --cpunodebind=0 ./program — pins both threads and memory to node 0 — simplest approach requiring no code changes
- **libnuma (numa_alloc_onnode)**: programmatic NUMA allocation — numa_alloc_onnode(size, node) allocates size bytes on the specified NUMA node, enabling fine-grained per-object placement
- **mbind System Call**: sets NUMA policy for specific memory ranges — MPOL_BIND, MPOL_INTERLEAVE, MPOL_PREFERRED flags with a node mask specifying allowed nodes
- **mmap with NUMA**: combine mmap(MAP_ANONYMOUS) with mbind to create NUMA-aware memory regions — enables custom allocators with per-page NUMA control
**Parallel Programming Patterns:**
- **Parallel First-Touch Initialization**: initialize arrays in a parallel loop with the same thread-to-data mapping as the computation — each thread touches its portion first, placing pages on the correct NUMA node — dramatically improves performance compared to serial initialization
- **Socket-Aware Thread Binding**: pin OpenMP threads to specific cores with OMP_PLACES=cores and OMP_PROC_BIND=close — ensures threads and their data remain on the same NUMA node throughout execution
- **Per-Node Data Structures**: allocate separate copies of shared data structures on each NUMA node — threads access their node-local copy, periodic synchronization merges results
- **NUMA-Aware Memory Pools**: custom allocators maintain per-node free lists — thread-local allocation draws from the local node's pool, eliminating cross-node allocation overhead
**Common Pitfalls:**
- **Serial Initialization**: initializing a large array in the main thread places all pages on node 0 (first-touch) — subsequent parallel access from node 1 threads incurs remote latency for every access
- **Thread Migration**: if the OS migrates a thread to a different NUMA node, its previously local memory becomes remote — use taskset, pthread_setaffinity_np, or cgroup cpusets to prevent migration
- **Memory Balancing**: Linux's automatic NUMA balancing (AutoNUMA) migrates pages to reduce remote accesses — can help but also adds overhead from page scanning and migration, sometimes hurting performance
- **Transparent Huge Pages (THP)**: 2MB huge pages reduce TLB misses but make NUMA migration more expensive — a single misplaced 2MB page wastes more bandwidth than a misplaced 4KB page
**Diagnosis and Monitoring:**
- **numastat**: displays per-node memory allocation statistics — numa_miss and numa_foreign counters reveal cross-node allocation failures
- **perf stat**: hardware performance counters track local vs. remote memory accesses — high remote access ratios indicate NUMA placement problems
- **Intel VTune**: NUMA analysis view correlates memory access latency with thread placement — identifies specific data structures causing remote access bottlenecks
**NUMA-aware programming transforms memory access from a random-latency operation into a predictable low-latency one — for memory-bandwidth-bound applications (which includes most HPC and data analytics workloads), proper NUMA placement is the single largest performance optimization after basic parallelization.**