Repository navigation
foreach_range(...; schedule = :steal): prefer victims sharing a cache or NUMA node - #85
Merged
Merged
Conversation
For each worker, order the other workers in tiers: those sharing its last-level cache within its NUMA node, the rest of the node, then the remote nodes by distance. The topology comes from Linux sysfs: the sharing set of each CPU's highest-level unified or data cache, and the node CPU lists, which are ignored when they overlap (emulated NUMA). Workers only get an order when each is pinned to a CPU of its own; otherwise, and wherever a file cannot be read, the order stays cyclic. Groundwork for #83; nothing uses the orders yet.
With topology-derived victim orders, a thief in a steal region scans the victims sharing its last-level cache, then the rest of its NUMA node, and crosses to other nodes only after a pass over its node found nothing. After a remote steal it returns to its node, whose blocks may have been refilled. A thief still retires only after a full scan found nothing, and keeps the quarter steals, startup protection and protected-block retries. Proximity is that of the worker executing the scan, so a block rescued by the caller uses the caller's order (cyclic off the pool), while republished work still goes to the rescued block. The orders are computed in `Pool()` from the affinity masks it already reads (Linux, Julia ≥ 1.11), so pinning must happen before the pool starts, or be followed by `ThreadTeams.shutdown!()`. The static path is unchanged. First proposal of #83.
Collaborator
Author
|
The first Windows run was cancelled after hitting the 60-minute limit with no test output; a rerun passed in 8 minutes. The same kind of hang shows up on earlier runs without these changes (the |
Benchmark Results (Julia v1)Time benchmarks
|
This was referenced Oct 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the first proposal of #83: in
foreach_range(...; schedule = :steal), thieves prefer victims that share their last-level cache, then their NUMA node, and steal from other nodes only once a pass over their own node found nothing. Stacked on #84, whose shared rescue clock this builds on; review only the last two commits.Design
Pool(): the sharing set of each CPU's highest-level unified/data cache, the node CPU lists (ignored when they overlap, as with the emulated NUMA nodes of a Raspberry Pi 5), and node distances. It's only used when every worker's affinity mask is a single CPU of its own, whichPool()already reads (Julia ≥ 1.11). Otherwise, on other OSes, on 1.10, or when any file is missing, the order stays cyclic. We read sysfs instead of using SysInfo.jl/Hwloc.jl: SysInfo, which ThreadPinning uses, exposes no cache domains or node distances, and both load Hwloc (~50–250 ms) for every user; the sysfs reads take ~0.6 ms, and only for pinned pools.RangeState: LLC ∩ node → rest of the node → remote nodes by distance; within a tier, rotated after the worker (remote tiers by the thief's rank within its node, so a node's thieves don't all start on the same remote victim). Victims beyondpare skipped.PROTECTED_PASSESare unchanged.ThreadTeams.shutdown!()after pinning; documented in the docstring and caveats page. No revalidation, no public API. The static path is unchanged.Left out after measuring: publishing commands round-robin over nodes (proposal 3's cheap variant) gave no smaller start spread and was slower on desktops; the go-word protocol it would really need is a bigger change.
Results
Paired ratios, local-first / cyclic, measured in the same process with randomized interleaving (median over 3 processes), with #84 underneath; < 1 is faster. Forced
:stealforeach_range, threads pinned with ThreadPinning.2× Xeon Gold 6430 (L3 = socket = node), Julia 1.11:
:numa:cores:numa:cores(one node: topology unused)Indices moved across sockets by steals drop from 37–81% (cyclic) to 0–4% on axpy. KernelAbstractions' CPU back-end (JuliaGPU/KernelAbstractions.jl#906) forced to steal follows (0.80–0.88 at
:numa); its adaptive policy gets 0.72–0.74 on axpy 1e7 at 64 threads, because faster launches keep it under its 50 µs steal threshold.One node, two L3s (Ryzen 5950X / 9950X, 16 threads): axpy 1e5 0.85 / 0.95, axpy 1e6 1.02 / 0.95; stencils and an imbalanced triangular loop within ±2%. Stolen indices that stay within the CCD: 31% → 89% (5950X, axpy 1e5).
Hosts where topology is unused (96-core Ampere Altra with no shared cache in sysfs, macOS, unpinned): same code as cyclic, no change beyond noise.
Topology detection was checked against
lscpu/sysfs on a Raspberry Pi 5 (overlapping nodes rejected), Jetson Orin/Xavier/Nano, an Intel laptop, an M1 Mac (cyclic) and the hosts above. Building the orders takes 0.6–1.3 ms at pool start for pinned pools, ~0.03 ms otherwise.Tests: pure order construction on synthetic topologies (2 nodes × 2 LLCs with
:cores/:numalayouts, sub-NUMA clustering, overlapping nodes, missing caches,p < nworkers), the sysfs parser on fake trees, and steal behaviour with installed orders (a thief takes a same-node victim before a remote one, and returns locally after a remote steal); these fail with cyclic order. Full suite on Julia 1.12 (-t 16,1) and 1.10 (-t 8).