Skip to content

numa: per-layer pin arenas — fix the PIN_GB=all VMA explosion (#419)#429

Merged
JustVugg merged 2 commits into
JustVugg:devfrom
ZacharyZcR:fix/numa-vma-419
Jul 20, 2026
Merged

numa: per-layer pin arenas — fix the PIN_GB=all VMA explosion (#419)#429
JustVugg merged 2 commits into
JustVugg:devfrom
ZacharyZcR:fix/numa-vma-419

Conversation

@ZacharyZcR

Copy link
Copy Markdown
Contributor

Fixes #419. Supersedes the mechanism-level parts of #425 (and keeps its two good ideas, done so they work).

Root cause (measured, not guessed)

OOM slab on the 4 TB box is VMA count exhaustion, not memory: every per-slab mbind carries its own memory policy, so bound regions cannot merge with neighbours — measured ~2 VMAs per slab, with or without MPOL_MF_MOVE (experiment). A PIN_GB=all load is 19,456 experts × (slab + fslab):

19,456 × 2 allocations × ~2 VMAs ≈ 78k  >  vm.max_map_count = 65,530 (default)

posix_memalignmmapENOMEM at the limit, with terabytes free — and the failure lands mid-load, exactly as reported. It also explains #425's own numastat (362 GB on node 0): that patch's probe disables NUMA outright, so it loads by not interleaving at all.

The fix

One arena per layer instead of one policy per slab. Experts of a layer share a tensor shape, so a layer's pins pack at a fixed stride into two arenas (weights + scales): 2 mbinds and a handful of VMAs per layer instead of ~500. Whole-model: ~150 policy VMAs instead of ~78,000.

  • Slices are pre-attached to the pin slots before the load loop; slab_cap covers expert_load's realloc check, so its alloc branch never fires — expert_load itself is untouched.
  • aslab marks arena ownership: expert_host_release detaches instead of freeing (a REPIN gpu-swap promotion must not free() an interior pointer), expert_host_ensure re-attaches the slice before reloading. GPU-prefix slots stay on individual allocs — their host backing really is released after upload, exactly as today.
  • Kept from fix: gracefully handle mbind EPERM and remove unnecessary MPOL_MF_MOVE #425, fixed so they work: flags=0 (every bind lands before the pread that first-touches the pages — nothing to migrate), and the EPERM probe — now on a page-aligned page (mbind rejects unaligned addresses with EINVAL) checking errno==EPERM specifically, so a constrained container degrades with a message and an EINVAL can never masquerade as a missing capability (the fix: gracefully handle mbind EPERM and remove unnecessary MPOL_MF_MOVE #425 probe disabled NUMA unconditionally on every machine — see the review).
  • Scope: arena path activates only when interleave is actually on (g_numa_nodes>=2, Linux, non-mmap). Default builds stay byte-identical.

Test plan

  • make check — 77/77, zero warnings (Linux CPU)
  • MinGW cross syntax check (arena code compiled out off-Linux; new ESlot fields inert)
  • 2-socket A/B (in progress, will post numbers here): acceptance = numastat balanced across both nodes, wc -l /proc/<pid>/maps flat during a PIN_GB=all load, and the GPU seems not helpful at all (0.03 TPS) - (4xA100/80G + 4TB RAM Test) #82 decode gain retained
  • @snailwei — this should load on your 4×A100/4TB box with stock vm.max_map_count and actually interleave; a numastat -p after load would be the perfect confirmation

Marked draft until the A/B numbers are in.

@ZacharyZcR

Copy link
Copy Markdown
Contributor Author

2-socket A/B — all acceptance criteria met

Machine: 2× Xeon Silver 4510 · 251 GB · 6× RTX 5090 · GLM-5.2 int4. Both arms built from identical trees (md5-verified), COLI_NUMA=1 PIN=<stats> PIN_GB=all RAM_GB=auto TEMP=0 DRAFT=0, /proc/<pid>/maps sampled every 2–3 s through load + generation.

1. CPU-only, PIN_GB=all (the #419 configuration)

stock dev this PR
peak VMA count 33,492 — climbs linearly at ~250/s for the whole pin phase 1,673 — flat
pins loaded 10,888 (206.0 GB, 112 s) 10,965 (207.4 GB, 108 s)
decode 0.20 tok/s · hit 63.8% 0.20 tok/s · hit 64.5%
numastat (node0 / node1, Private) 79.1 / 123.0 GB 80.5 / 124.4 GB

The stock curve is the #419 crash in slow motion: ~3.1 VMAs per pinned expert, linear in pin count. This box pins 10.9k experts (251 GB); scale that slope to the 4 TB box's 19,456 and it crosses the default vm.max_map_count=65530 mid-load — while the arena build stays three orders of magnitude below it, independent of pin count. numastat distributions are identical between arms: the interleave behaves exactly as stock (which also confirms MPOL_MF_MOVE was doing nothing on fresh slabs).

2. Full-residency GPU stack (6× 5090, CUDA_EXPERT_GB=auto, 96-token greedy)

stock dev this PR
peak VMA count 28,958 11,771
decode 1.31 tok/s · hit 94.4% 1.33 tok/s · hit 94.4%
generated text byte-identical to stock (diff shows only timing lines)

The first fix revision still peaked ~29k here: the VRAM-ranked prefix slabs (host staging, read once for upload, freed right after) were individually bound. Second commit skips binding those — transient staging earns no policy. The remaining 11.7k is the LRU ecache (its cap auto-raises when CUDA_RELEASE_HOST frees host RAM), which serves decode reads and stays correctly bound — bounded by cache cap, not model size.

Un-drafting. @snailwei your 4×A100/4TB box is the definitive test: stock vm.max_map_count, COLI_NUMA=1 PIN_GB=all, and a numastat -p after load — it should both survive and interleave this time.

@JustVugg

Copy link
Copy Markdown
Owner

Heads-up @ZacharyZcR — I was about to merge this (measured VMA-exhaustion root cause + 2-socket A/B is convincing, and it supersedes #425 cleanly), but merging #422 and #438 just landed in colibri.c ahead of it and it now shows CONFLICTING. Could you rebase onto current dev? It should be a quick one — the conflict is nearby churn in the same file, not your NUMA logic. Ready to merge the moment it's green again. Thanks for the careful root-cause work on this one.

@ZacharyZcR

Copy link
Copy Markdown
Contributor Author

Follow-up finding: the arenas also fix a ±21% bandwidth lottery

While chasing decode noise on the 6×5090 host we found that effective CPU-expert bandwidth (P0-EXEC, identical 10,588-row workload, same env) swung 45.2–68.5 GB/s across runs (±21%) on per-slab mbind — every process start re-rolls ~20k tiny policy placements, and the interleave lands differently each time. That swing is ±0.7 tok/s on this box: larger than most micro-optimizations we measured this week.

With this PR's per-layer arenas stacked on the same tree, four consecutive runs measured 62.6 / 65.7 / 66.4 / 66.6 GB/s (±3%) — and the mean sits at the top of the historical range. Two deterministic binds per layer instead of twenty thousand lottery tickets: the placement lands in the good configuration every time.

So beyond the #419 crash fix, the arenas buy run-to-run performance determinism on NUMA hosts: decode settled at 7.85–7.94 tok/s across runs where the per-slab build wandered 6.5–8.0. (Methodology note that fell out of this: on multi-socket boxes, any A/B verdict should report the P0-EXEC GB/s alongside tok/s as a machine-state fingerprint.)

…gg#419)

Root cause of JustVugg#419's 'OOM slab': every per-slab mbind carries its own
memory policy, so bound regions cannot merge — measured ~2 VMAs per slab,
with or without MPOL_MF_MOVE. A PIN_GB=all load (19,456 experts x
slab+fslab) creates ~78k VMAs and crosses the default
vm.max_map_count=65530: posix_memalign dies with terabytes free.

The fix binds the pinned hot-store as ONE arena per layer. Experts of a
layer share a tensor shape, so a layer's pins pack at a fixed stride into
two arenas (weights + scales): 2 mbinds and a handful of VMAs per layer
instead of ~500. Slices are pre-attached to the slots before the load
loop — slab_cap covers expert_load's realloc check, so its alloc branch
never fires and expert_load itself is untouched. aslab marks arena
ownership: expert_host_release detaches instead of freeing (a REPIN
gpu-swap promotion must not free() an interior pointer), and
expert_host_ensure re-attaches the slice before reloading.

Per-slab mbind remains for the bounded allocations (dense qalloc, LRU
ecache, GPU-tier staging), now without MPOL_MF_MOVE: every bind lands
before the pread that first-touches the pages, so there is nothing to
migrate.

numa_init also gains a capability probe done right: one page-aligned
page (mbind rejects unaligned addresses with EINVAL), disabling only on
errno==EPERM — so a constrained container degrades with a message
instead of crashing later, and an EINVAL can never masquerade as a
missing capability.

The arena path activates only when interleave is actually on
(g_numa_nodes>=2, Linux, non-mmap): default builds stay byte-identical.
The VRAM-ranked prefix's host slabs are upload staging — read once and
freed right after — so binding them buys nothing and cost ~2 transient
VMAs each: measured PEAK maps 28,958 on the six-GPU host even with the
pin arenas in place. With the skip: 11,771, all of it the bounded LRU
ecache, which serves decode reads and stays correctly bound.
@ZacharyZcR

Copy link
Copy Markdown
Contributor Author

Rebased onto current dev — the conflicts were the new expert_load demand-flag signature from #422/#438 meeting the arena's ensure/pin-load call sites; resolved keeping both (arena re-attach + demand=0 semantics). make check 83/83, zero warnings. Ready when you are.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants