Home Multikernel Private Cloud Multikernel Sandbox Multikernel LiveUpdate ARM Platform RISC-V Platform OEM & Embedded SaaS & Database Clouds Technology FAQ Getting Started Blog About 中文 GitHub Schedule a Demo

Scaling Anonymous Page Faults on a 128-Core ARM NUMA Server

August 21, 2026 by Cong Wang, Founder and CEO

benchmark linux-kernel performance scalability arm numa

Modern ARM servers are shipping with core counts well beyond what was common a few years ago. At this scale, the core count alone says little about application throughput. NUMA topology, memory-management behavior, and kernel synchronization determine whether additional cores become additional capacity.

This post examines a 128-core ARM server with four NUMA nodes in a single physical socket, using the page_fault1_processes test from will-it-scale to measure how anonymous page fault throughput scales from one worker to all 128 cores. The central finding is that the workload scales poorly from core to core within a NUMA node, but the resulting per-node capacity scales almost linearly as additional nodes are added. Our previous will-it-scale post characterized the scaling wall on a 48-core x86 system and measured a multikernel split against it. This post is a single-kernel study on a different architecture with a considerably larger NUMA node, and it establishes where that wall stands on ARM.

Hardware Topology

The test system has 128 cores in one physical socket, organized as four NUMA nodes of 32 cores and approximately 64 GB of memory each:

NUMA node 0: CPUs   0-31
NUMA node 1: CPUs  32-63
NUMA node 2: CPUs  64-95
NUMA node 3: CPUs 96-127

Although the system contains only one socket, the processor exposes four NUMA domains. This topology is increasingly common in high-core-count parts, where a single package contains multiple compute dies, memory controllers, and interconnect segments. The NUMA distance matrix confirms a genuine internal hierarchy:

       0   1   2   3
0:    10  11  11  12
1:    11  10  12  11
2:    11  12  10  11
3:    12  11  11  10

A distance of 10 is local memory. Remote nodes report 11 or 12, so the processor distinguishes more than one tier of remote locality within the socket. These are relative topology costs rather than measured latencies, but they establish that the system is not a uniform 128-core machine.

Benchmark Method

page_fault1_processes has each worker map anonymous memory and touch it page by page. Every iteration passes through the fault handler, the page allocator, page zeroing, page table updates, memcg and per-node accounting, and LRU insertion. In processes mode, workers share nothing in userspace: each has its own address space and its own mappings. Any failure to scale is therefore inside the kernel.

Each run lasted 30 seconds. CPU placement was controlled with taskset, and the larger masks were chosen to align exactly with NUMA boundaries:

taskset -c 0-7    ./page_fault1_processes -s 30 -t 8
taskset -c 0-31   ./page_fault1_processes -s 30 -t 32    # node 0
taskset -c 0-63   ./page_fault1_processes -s 30 -t 64    # nodes 0-1
taskset -c 0-95   ./page_fault1_processes -s 30 -t 96    # nodes 0-2
taskset -c 0-127  ./page_fault1_processes -s 30 -t 128   # nodes 0-3

This design separates two questions: how the workload scales as cores are added within one NUMA node, and how aggregate throughput scales as additional nodes are brought online. The answers differ substantially.

Scaling Within One NUMA Node

The first experiment restricted execution to node 0, with 32 cores available:

Workers Throughput Speedup vs 1 Efficiency
1 0.91 M ops/s 1.00x 100%
2 1.74 M ops/s 1.91x 96%
4 3.15 M ops/s 3.46x 87%
8 4.64 M ops/s 5.10x 64%
16 4.58 M ops/s 5.03x 31%
24 4.53 M ops/s 4.98x 21%
32 4.46 M ops/s 4.90x 15%
One NUMA node, 32 cores: the ceiling is at eight workers page_fault1_processes, taskset to CPUs 0-31 · one worker per core, 30 second runs 1 M 2 M 3 M 4 M 5 M 6 M 1 2 4 8 16 24 32 workers (one per core) perfect scaling would reach 29 M at 32 workers 0.91 1.74 3.15 4.64 4.58 4.53 4.46 24 more cores, zero more throughput

Scaling from one to eight workers is reasonable: throughput rises from 0.91 million to 4.64 million operations per second, with efficiency declining but the curve still climbing. Beyond eight workers the behavior changes. Sixteen workers deliver 4.58 million, 24 deliver 4.53 million, and 32 deliver 4.46 million. The additional 24 cores contribute no throughput, and the curve declines slightly. With the node fully occupied, the workload reaches about 15% of ideal linear per-core scaling.

For this workload, a 32-core NUMA node does not behave like 32 independently scalable cores. Anonymous page fault throughput reaches a ceiling at approximately eight workers.

Scaling Across NUMA Nodes

The second experiment widened the CPU mask one node at a time:

Workers NUMA nodes Throughput vs 32 workers
32 1 4.46 M ops/s 1.00x
64 2 8.95 M ops/s 2.00x
96 3 13.49 M ops/s 3.02x
128 4 17.99 M ops/s 4.03x
Four NUMA nodes: throughput scales by the node, not by the core page_fault1_processes, taskset widened one NUMA node at a time · shaded bands are NUMA nodes 0 to 3 node 0 node 1 node 2 node 3 5 M 10 M 15 M 20 M 1 32 64 96 128 workers (one per core) 8.95 M 15% of ideal 13.49 M 15% of ideal 17.99 M 15% of ideal 4.46 M 15% of ideal +4.5 M per node

Relative to the saturated single-node result, two nodes deliver 2.00x, three deliver 3.02x, and four deliver 4.03x. Aggregate throughput increases almost exactly with the number of NUMA nodes, each contributing approximately 4.5 million operations per second largely independently of the others.

This should not be read as linear 128-core scaling. The unit that scales is the NUMA node, and each node has already lost most of its per-core efficiency before it joins the aggregate. Against the 0.91 million single-worker baseline:

Workers Ideal (0.91 M x N) Actual Efficiency
32 ~29 M 4.46 M 15%
64 ~58 M 8.95 M 15%
96 ~87 M 13.49 M 15%
128 ~116 M 17.99 M 15%

Throughput scales almost linearly from 32 to 128 workers while overall efficiency remains at roughly 15% throughout. There is no contradiction: the system adds approximately 4.5 million operations per second for every 32 cores. Measured per NUMA domain, that is strong scaling. Measured per core, it is not.

Below the Throughput Numbers

To characterize the saturation point, perf stat was used to compare eight workers against 32 on node 0:

  8 workers 32 workers
CPUs utilized 8.0 31.9
Page faults completed 165.4 M 162.9 M
System CPU time 200.0 s 1146.5 s
Wall time 36.0 s 36.1 s
IPC 1.8 1.7
Same work, 5.7x the CPU: eight workers versus 32 on one node perf stat, 30 second run on NUMA node 0 · bars normalized to the 8-worker run 8 workers 165 M 32 workers 163 M page faults completed 8 workers 200 s 32 workers 1147 s system CPU time 8 workers 1.2 µs 32 workers 7.0 µs system CPU time per fault

Four times as many CPUs complete essentially the same number of page faults while consuming 5.7x the system CPU time. Per completed fault, system time rises from approximately 1.2 microseconds to 7.0 microseconds. The additional cores are fully utilized but contribute no useful work; they execute kernel code that exists only because of the added concurrency. IPC is nearly unchanged, which argues against a simple memory-bandwidth stall: the cores retire instructions at a normal rate, but the instructions are synchronization and accounting rather than fault handling.

A kernel-only perf profile of the 32-worker case identifies where that time goes:

Where 32 workers spend their time in the kernel perf record, kernel only, 32 workers on NUMA node 0 · top five symbols by samples _raw_spin_unlock_irqrestore 22.89% __pi_clear_page 17.11% mod_node_state 5.34% _raw_spin_lock 4.96% mod_memcg_lruvec_state 4.89% spinlock MM accounting useful work (zeroing)

Further down the profile are the remaining memory-management functions: lruvec_stat_mod_folio, folio_remove_rmap_ptes, count_memcg_events, handle_mm_fault, do_page_fault, __alloc_frozen_pages_noprof, and get_page_from_freelist.

Lock profiles require some care. A sample attributed to _raw_spin_unlock_irqrestore reflects where the CPU was when the sampling interrupt was delivered, which on ARM includes the point where interrupts are re-enabled after a critical section, so part of that 23% is time deferred from inside the lock rather than time spent waiting for it. Inlining and attribution affect the rest. Even so, the shape is clear: over a quarter of kernel samples land on spinlock primitives, roughly a tenth on memcg and per-node statistics, and the one unambiguously useful symbol, __pi_clear_page, accounts for 17%.

The evidence points to a kernel-side scalability bottleneck in the memory-management path rather than exhaustion of CPU capacity. Identifying the specific shared structure would require further profiling; the usual candidates are the allocator’s zone lock, the LRU lock, and the per-node and per-memcg counters updated on every fault.

Interpreting the Results

What the hardware is, versus what the kernel sees one socket, four NUMA domains, four independent ceilings one socket · 128 cores NUMA node 0 CPUs 0-31 · 64 GB ceiling ~4.5 M/s reached at 8 of 32 cores NUMA node 1 CPUs 32-63 · 64 GB ceiling ~4.5 M/s reached at 8 of 32 cores NUMA node 2 CPUs 64-95 · 64 GB ceiling ~4.5 M/s reached at 8 of 32 cores NUMA node 3 CPUs 96-127 · 64 GB ceiling ~4.5 M/s reached at 8 of 32 cores ~18 M page faults/s from 128 cores: 15% of what 128 × one core would be

Physically, the system is one socket with 128 cores. For workloads that exercise kernel memory management, allocation, or shared kernel state, a more useful description is four domains of 32 cores, within each of which the kernel makes effective use of roughly eight. The second description predicts the benchmark results; the first does not.

The experiment also shows why measuring only the endpoints is insufficient. One worker at 0.91 million and 128 workers at 17.99 million is a 20x speedup, and the obvious conclusion is simply that the workload scales poorly. The intermediate points reveal the structure:

Range Behavior
1 to 8 workers core-level scaling
8 to 32 workers node saturation; CPU time up 5.7x, throughput flat
32 to 64 workers second NUMA node adds ~4.5 M
64 to 96 workers third node adds ~4.5 M
96 to 128 workers fourth node adds ~4.5 M

The NUMA boundaries explain why throughput resumes growing after the first node has saturated. Whatever is contended is largely per node, so each additional node brings a fresh instance of it. That is a useful clue in itself: the contention has a locality, and the locality is the NUMA domain.

Conclusion

The statement “the server scales” is incomplete without a unit. On this system, scaling at the core level stops at roughly eight workers per NUMA node. At the node level, each additional node contributes approximately 4.5 million operations per second, almost perfectly linearly. At the system level, 128 cores reach 18 million operations per second, about 15% of the single-worker rate multiplied out. All three statements describe the same run.

For performance engineering on high-core-count hardware, the practical lesson is to evaluate scalability against the NUMA topology rather than the total core count. A worker sweep should cross every node boundary, because those boundaries are where the curve changes shape. A single 128-core measurement reports how fast the machine is; measurements at 8, 32, 64, 96, and 128 workers report where that throughput comes from and where it is lost.

For multikernel, this is the relevant baseline. The x86 study showed that when a kernel’s shared state is the limit, partitioning the cores between kernels partitions the limit with them. On this system the hardware already defines four partitions, and the kernel’s contention respects them closely enough to scale by node. One kernel per NUMA node is the natural configuration to test on this machine, and it is the next experiment. We have not run it yet and make no claim about it here. What this post establishes is the number any such configuration must improve on: approximately 4.5 million operations per second per node, with eight of 32 cores doing useful work.

Reproducing the Measurement

The method does not depend on this hardware. Build will-it-scale, read the NUMA map from lscpu or numactl -H, and sweep page_fault1_processes with taskset masks that fill one node first and then add nodes whole. Run perf stat at the knee and at the full node width, and perf record on the saturated case. Absolute numbers will vary with core count and kernel version; the two-regime shape of the curve follows from where the kernel’s shared state lives and should not.

Multikernel is open source. If you are evaluating high-core-count servers and would like to discuss these results, contact us at contact@multikernel.io.