feat(hugepages): a declared pool ceiling caps the grow-only write - #399
Merged
Conversation
…e-counting The grow-only runtime write (_ensure_hugepages) computes current + required - avail, where required already includes any declared hugepages_reserve_extra_mb (the co-resident stack's headroom) but avail gives no credit for pages that same stack already holds — only free pages and pages the RigForge-managed miner itself holds. The stack's reservation therefore lands in the write twice: once via required's declared headroom, once again because avail doesn't cancel it back out. Re-derived independently (not from the write-up that first reported it) against an 8 GiB reduced-tier appliance box: the write comes out to 6118 pages (~12 GiB) instead of the honest 3782 (rigforge#398, pithead#1103). hugepages_reserve_extra_mb can't fix this — no declared value cancels a double count in the arithmetic that combines it. A new config key, hugepages_pool_ceiling_mb (default 0, inert), caps the WRITE itself instead: when declared, vm.nr_hugepages is never grown past the ceiling regardless of what required/avail compute. Absent, the write is byte-for-byte unchanged (proved by a regression test against the same 8 GB fixture). Closes #398
Security review on fix/398-hugepages-ceiling found that ceiling_pages was computed as ceil(HUGEPAGES_POOL_CEILING_MB / 2) — the same rounding rule EXTRA_2MB_PAGES uses for headroom, which is right for a value being ADDED but wrong for a cap: an odd declared ceiling (5121 MB) rounded up to 2561 pages (5122 MB), one page past the declared ceiling, contradicting the "never grown past the ceiling" contract. All three existing fixtures used even MB values so the suite couldn't see it. Floor instead (bash integer division already truncates toward zero for positive operands, so dropping the "+ 1" is the whole fix). Added an odd-MB fixture (5121 -> 2560 pages) that kills the ceil mutation: restoring "+ 1" flips it to write vm.nr_hugepages=2561 and drops the "already at its declared ceiling" WARN, confirmed by hand before this commit. Updated the two in-code comments and the CHANGELOG entry to state the floor behavior.
VijitSingh97
added a commit
that referenced
this pull request
Aug 23, 2026
Restart-free control-apply fast path (#397/#381) and the HugePages pool-ceiling contract that closes the co-resident double-count (#399/#398, pithead#1103). Recovers the topology guard, e2e-dashboard leg, and msr-test PATH fix that had landed on main only (back-merge restoring the main-ancestor invariant). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U189N8GtbdUVtm8UtVNcDw
Merged
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.
fix(hugepages): a pool ceiling stops the co-resident write from double-counting
Closes #398
Branch:
fix/398-hugepages-ceilingSHA:
b01b187a611346a35243e9c252ca231f81da8c8dRoot cause (re-derived from the code, not copied from #398 forward)
rigforge.sh:1494-1503_ensure_hugepages, the grow-only runtime write:required(util/proposed-grub.sh:100-101, the fallback branch — the one that actually runs onthe appliance, since it never gets 1G pages) =
1168*NUMA_NODES + THREADS + 50 + EXTRA_2MB_PAGES, whereEXTRA_2MB_PAGESis the declaredhugepages_reserve_extra_mb(theco-resident stack's headroom) converted to 2MB pages.
avail(rigforge.sh:1487-1492) =HugePages_Free + _miner_held_hugepages. Pages a differentconsumer (a co-hosted Pithead stack's p2pool/monerod) already holds are neither free nor
RigForge-miner-held, so they never appear in
avail.Write both out symbolically (
M= miner's own base need,E= declared headroom,S= pages thestack actually holds,
H= pages the RigForge miner holds — 0 on first run — andcurrent = HugePages_Free + S + H, true whenever nothing else touches the pool):Eexists to declare "how much to leave for the stack," i.e.E ≈ S. Sonew_pool ≈ M + 2S: thestack's reservation is counted once inside
required(asE) and a second time via thecurrent − availcancellation, becauseavailgives zero credit for pages a different consumer holds.8 GB worked example (
NUMA_NODES=1,THREADS=4,RESERVE_EXTRA_MB=5120,current=2560pages, stack holding ~2336 →
HugePages_Free=224, miner not yet running):EXTRA_2MB_PAGES = (5120+1)/2 = 2560(util/proposed-grub.sh:87)required = 1168 + 4 + 50 + 2560 = 3782pages (util/proposed-grub.sh:101) — not the 3870both Grow-only HugePages write double-counts a co-resident stack's declared reservation #398 and pithead#1103/#1306 quote; that number conflates
proposed-grub.sh's two 2MBformula branches (
128+threads+10from the 1G-active branch,1168*numa+...+50from thefallback) into one call that never actually happens. The fallback formula is the one this box
runs.
avail = 224 + 0 = 224(rigforge.sh:1487-1492)new_pool = 2560 + 3782 - 224 = 6118pages →sudo sysctl -w vm.nr_hugepages=6118→6118 * 2 MiB ≈ 11.95 GiBrequested on a box reportingMemTotal: 8133520 kB(≈7.76 GiB).The conclusion (the pool blows past the box's RAM) matches #398/pithead#1103/#1306; the
intermediate page count doesn't — 6118, not 6206, because the real fallback formula's terms are
+50and no separate128+10on top of1168, not both formulas' terms summed. Confirmed byrunning the actual code against the fixture (this PR's new tests), not by hand-checking the prior
write-up.
The honest total (the stack's reservation counted once) is
M + E = required = 3782pages(≈7.39 GiB) — no value of
hugepages_reserve_extra_mbgets there, because the double-counted termis
S(the stack's actual held pages), whichEcannot cancel no matter what it's set to.The fix (#398's Option 3, from pithead#1103)
A new config key,
hugepages_pool_ceiling_mb(default0= no ceiling), caps the writeinstead of feeding more headroom into the requirement:
rigforge.sh:574-590(parse_config) — parses and validates the key the same way ashugepages_reserve_extra_mb(whole MB, 0-65536).rigforge.sh:686(_warn_unknown_config_keys'sknownlist) — added, so a typo warns instead ofsilently no-op'ing.
rigforge.sh:1509-1535(_ensure_hugepages) — whenHUGEPAGES_POOL_CEILING_MB > 0and thecomputed target exceeds the ceiling: write the ceiling instead (still grow-only relative to
current) if the ceiling is abovecurrent; otherwise leave the pool untouched and warn (aceiling never shrinks someone else's reservation — it only stops RigForge from growing past
it).
ceiling_pages = HUGEPAGES_POOL_CEILING_MB / 2, floored — a cap must round toward lessmemory, never more (see "Security review round-trip" below; this is the fixed form).
config.reference.json— new key +_docsentry.CHANGELOG.md—[Unreleased] / Fixedentry.Conservative activation: with the key absent or
0(every rig today),_ensure_hugepages'sarithmetic and its
sysctl -wcall are byte-for-byte what they were before this PR — proved by aregression test using the same 8 GB fixture as the double-count reproduction, not a synthetic
one.
Tests run (
bash tests/run.sh)New unit coverage (
tests/run.sh, both echoed as their own sections):== unit: parse_config hugepages_pool_ceiling_mb (#398) ==— parses, defaults to0, rejectsnegative and oversized values (mirrors the existing
hugepages_reserve_extra_mbtests).== unit: hugepages pool ceiling bounds the grow-only write (#398) ==—_ensure_hugepagesexercised directly (not through
tune_kernel/proposed-grub.sh, which are already coveredelsewhere) against the 8 GB fixture:
vm.nr_hugepages=6118) — the regression pin.currentbut below the uncapped target (6400 MB) → write capped at 3200 pages,6118absent, capping logged.current(5120 MB, the real reduced-tier number) → no write at all, aWARN naming the ceiling.
no write, not
vm.nr_hugepages=2561— the fixture that killed the rounding bug caught insecurity review (below).
vm.nr_hugepages=200.Mutation kill, stated explicitly and verified by hand: reverted
_ensure_hugepagesback to itspre-fix body (
sudo sysctl -w vm.nr_hugepages=$((current + required - avail)), no ceiling clamp)and re-ran the suite. Exactly the 5 assertions that depend on the ceiling clamp went red (the other
6118-fixture assertion and the no-ceiling regression test stayed green, as expected):
Then restored the fix and confirmed the suite is green again.
Security review round-trip
An earlier version of this branch (SHA
d45891481fe5ce27e509b6be4ad00797b653e24f) computedceiling_pages = (HUGEPAGES_POOL_CEILING_MB + 1) / 2— the same round-up ruleEXTRA_2MB_PAGESuses elsewhere for headroom being added, which is correct there but wrong fora cap. Security review reproduced it: an odd declared ceiling (5121 MB) rounded up to 2561 pages
(5122 MB) — one page past the declared ceiling, contradicting "never grown past the ceiling."
All three original fixtures used even MB values, so the suite couldn't see it.
Fixed by flooring instead (
ceiling_pages = HUGEPAGES_POOL_CEILING_MB / 2— bash integer divisionalready truncates toward zero for positive operands, so dropping the
+ 1is the whole fix), plus:pool is already at-or-past the floored ceiling). Mutation kill, verified by hand: restoring
the
+ 1flips the code to the CAP branch instead of the already-met branch and writesvm.nr_hugepages=2561(5122 MB) — both new assertions go red.rigforge.sh:585-587(parse_config) andrigforge.sh:1525-1528(
_ensure_hugepages) now state the floor behavior explicitly, and the CHANGELOG entry says "anodd declared MB value floors to the 2MB page below rather than rounding up past it."
config.reference.json's_docsentry forhugepages_pool_ceiling_mbstates the same.Re-ran the ceiling test blocks and
make lintafter the floor fix — both clean (below).Full suite, RUN:
bash tests/run.sh→ 1818 passed, 1 failed (2 more passing than the priorSHA — the new odd-ceiling fixture). The 1 failure is
msr-apply: missing wrmsr warns, never fails the unit (#140)— the known pre-existing hostwrmsrflake, unrelated to this change (present identically before this branch's first commit, on
origin/develop).make lint, RUN: shellcheck (--severity=warning) + shfmt (-i 4 -d) over the full script andtest set — clean, exit 0.
make lint-md, RUN: markdownlint over all docs incl.CHANGELOG.md— 0 errors.What was NOT run
per the operator's hard rules, no bench/miners were touched). The fix is proven at the
_ensure_hugepages/parse_configunit level, same tier as the#328/#305tests it extends.hugepages_reserve_extra_mbat all after pithead#1306 shipped, so this fix is inert on theappliance until pithead#1103 is revisited (see Sequencing below).
Sequencing (cross-repo)
RigForge miner on the appliance's reduced HugePages tier — the safety net stays in place
regardless of this fix.
again on a constrained box, the pool write cannot exceed an explicitly declared ceiling. Ships
inert (no config sets
hugepages_pool_ceiling_mbtoday).exists, pithead#1103 can be revisited — the reduced tier could declare both
hugepages_reserve_extra_mb(headroom, as today) andhugepages_pool_ceiling_mb(the tier's ownhonest budget, e.g. 5120 MB) and re-enable co-location instead of pithead#1306's outright
refusal. That decision and any pithead code change belongs to pithead#1103.