Repository navigation
Conversation
…s per-CPU lists On a unified-memory GPU on Linux the pool reads MemAvailable, which leaves out the free pages on the per-CPU lists. Since kernel 6.7 those lists tune their own size, and after a bulk free (the previous server exiting, an image load) they can hold GiBs for minutes, so the weights check, the rank room, the streams, the KV pages and the kept states were all budgeted short. parked_pages.bytes sums each zone's count: lines under pagesets less their high_min: lines (512 MiB at rest on kernels that print none; 0 on anything unparsable), and cuda_memory.counting adds it to MemAvailable, at most MemTotal. poolFor, readPool and hostCacheBytes read /proc/zoneinfo right after /proc/meminfo; discrete cards and Windows are unchanged. The pool keeps the counted share and each rank logs it at start-up from 0.25 GiB.
Owner
|
Landed on main (0d24ecb..ab864d4), with you as author. Thank you! On a GB10 with Qwen3.8-27B against main: tokens identical (0 of 20 rows), prefill within 0.3%, and the 1-8 stream sweep within 0.3%. We also captured meminfo/zoneinfo on a GB10 right after a 30B model freed its memory: about 177k pages (690 MiB) sat on the per-CPU lists, still about 46k 30 s later, which is the gap this closes. - Synthema, Ash's AI Assistant. |
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.
What this changes
On a unified-memory GPU on Linux, the CUDA pool now adds to
MemAvailablethe free pages that sit on the kernel'sper-CPU lists beyond their resting level, at most
MemTotal. Every reading that sizes a start goes through that pool:the weights check, the room each rank reports, the streams, the KV pages and the kept prompt states. So a start right
after a bulk free no longer budgets those GiBs short. Each rank logs what it counted from 0.25 GiB. Discrete cards and
Windows are unchanged.
Fixes #1311
What is wrong on main (67cc8fb)
poolFortakesMemTotalandMemAvailablefrom/proc/meminfo(
zig/src/native/cuda_family_memory.zig:22-24,cuda_memory.zig:174-176).openWithreads that pool before the load(
native/cuda.zig:272), in the room probe that a family over hosts gathers, where rank 0 takes the smallest(
:285,families/glm/cuda_native.zig:272), and after the load (:337). That last reading sizes the streams, pagesand kept states (
:340-342,:387) and is the start-up line'sof X GiB left(:391).hostCacheBytes(cuda_memory.zig:84-88) and Drama'sreadPool(:270) read the same counts.The zone's free count leaves those pages out, so neither
MemFreenorMemAvailablereports them. Nothing in thetree reads
/proc/zoneinfo, where they show.main674fb9e, the same budget code), each rank had ~1 GiB parked when rank 0printed its budget line on a warm start, and 2.61 / 1.92 GiB on a cold start right after an image and checkpoint
load (CUDA on unified memory (GB10): the start-up budget does not count free pages parked on the kernel's per-CPU lists #1311).
The change
zig/src/native/parked_pages.zig(new, 138 lines with its tests):bytes(text, page)sums every zone'scount:lines under
pagesetsand subtracts theirhigh_min:lines. Kernels before 6.7 print nohigh_min:, so there itsubtracts a fixed 512 MiB resting level. It returns 0 when the text lists no pages or any field fails to parse or
overflows, so a bad reading never raises a budget.
cuda_memory.counting(m, zones)adds that toMemAvailable, capped atMemTotal.poolFor,readPoolandhostCacheBytesread/proc/zoneinforight after/proc/meminfo. The lists drain intoMemAvailable, so a pagethat moves between the two reads is missed, not counted twice. They read it only where they already read
/proc/meminfo.Pool.parkedkeeps the counted share (default 0).family_memory.noteParkedlogs it from 0.25 GiB, after the load(
the CUDA memory budget counts the X GiB free on the per-CPU page lists beyond their resting level (MemAvailable does not)) and in the room probe ("the room this rank reports"), so each rank's process logs its own. The linescarry no rank number, like the other start-up lines, since GLM's processes take their tensor rank from
TF_GLM_RANKrather than the engine option.zig/src/native/fixtures/gb10_parked/holds/proc/meminfoand/proc/zoneinfofrom a GB10 (kernel 6.17, 4 KiB pages) during a server start: one pair 6.6 s before the budgetwas read, with the lists still draining (1,027,231,744 bytes parked by this rule), and one pair at the read, with
the lists at rest (0). Two tests parse them: the draining pair raises MemAvailable and stays under MemTotal, the
pair at rest counts nothing.
that guards memory at run time changes.
Tests
cuda_family_memory_test.zig,a Linux unified pool counts the free pages parked on the per-CPU lists, at most MemTotal: the fixture'sprocTextnow answers/proc/zoneinfoseparately. With 96 GiB available of 128 GiB, azoneinfo holding 6 GiB beyond
high_minmust give a pool of 102 GiB; 40 GiB parked must stop at 128; no zoneinfo, anempty one or a garbled one must give 96. Windows and a discrete card must not read zoneinfo at all.
On main (this test alone on 67cc8fb,
zig build test-cuda-host; trimmed to the failure, paths relative):On this branch it passes. Also new:
parked_pages.zig: the 6.7+ format (every CPU, zone and node; lists under their resting level count 0); the formatbefore 6.7, without
high_min:(512 MiB at rest); only lines underpagesetscount (straycount:lines, anotherzone's field, a list ending at its zone's next field); no lists, empty, garbled, negative, binary or overflowing text
count 0. A Linux-only test reads this host's
/proc/zoneinfoand checks it parses to less thanMemTotal.cuda_memory.zig,counting: adds, caps atMemTotal, and leaves the reading alone without zoneinfo or with a bad one.cuda_family_memory_test.zig,the pool names the parked share it counted: 6 GiB, 4 GiB when capped, 0 without.Receipt
Environment, host tests: base
67cc8fbce, branch commitb104cf77b; Zig 0.17.0; Apple M4 Mac(16 GiB), macOS 27.0.1. The Xcode Metal Toolchain component is not installed, so the Metal library builds in
zig build testfail on both base and branch.Environment, served run: base
67cc8fbceagainst branchb104cf77b, bothtensorfold-nativebuilt alike (Zig0.17.0,
aarch64-linux-gnu,-Doptimize=fast) with the same 97 embedded CUDA kernel images (one fatbin set, nvcc13.0 V13.0.88, sm_121). Two NVIDIA DGX Spark (GB10, 121 GiB unified memory), driver 580.178.04 (CUDA 13.0),
cuBLASLt 13.1.1.3, NCCL 2.30.7, glibc 2.39, Ubuntu 24.04.5 LTS, kernel 6.17.0-1032-nvidia, aarch64; TP=2 over RoCE,
one model process on each.
Checkpoint and serve:
Mia-AiLab/GLM-5.3-Flash-EXL3-4bpw-TensorFold@078455ff, MTP drafts, the same on both trees:serve <checkpoint> --backend cuda --parallel 8 --context 131072 --no-update-check, the default prompt cache,TENSORFOLD_CUDA_KERNELS=native. (--context 262144 --parallel 8refuses on main at TP=2, so it cannot show abudget.)
Starts, 8, alternated: cold base, cold branch, warm base, warm branch, cold branch, cold base, warm branch, warm base.
Cold: started right after the previous server's exit and one read of the checkpoint on both hosts. Warm: the page
cache dropped on both hosts, then started once both hosts' lists held under 0.25 GiB for 60 s. A 1 s sampler read
/proc/zoneinfoand/proc/meminfoon both hosts from launch to 20 s after ready; the parked figures are its samplenearest rank 0's budget line. Each start served one 32-token
bench_openai.pyrequest per prompt.MemAvailablethen (GiB)of X GiB left after a 12.2 GiB reserveprompt_cache_budget_bytes(GiB)counts the X GiBlines, rank 0 / rank 1Every start:
8 streams at once, 1.77 GiB each at a 131072-token window(windows; GLM keeps windows at two ranks),rank 0's own room the smallest (no start printed the smallest-room clause), and the same
token_shafor bothprompts (
fibonacci-rawe8d0dd8adaba,gpu-chat-no-think397ece16cce9).Warm pairs (the clean comparison): base leaves the parked pages out (3: 0.78 / 0.55 GiB, 8: 1.41 / 0.61 GiB). The
branch counts them and each printed figure is within 0.01 GiB of the sampler (4: 1.06 / 0.78 against 1.054 / 0.769;
7: 0.74 / 0.66 against 0.749 / 0.654). Left: 14.7 → 15.5 GiB (3 → 4) and 14.1 → 15.5 GiB (8 → 7); prompt cache 2.35
→ 3.10 and 1.73 → 3.14 GiB. Rank 0 admits by the smallest room, so the expected gain is about the smaller rank's
count (0.77 and 0.65 GiB); the 8 → 7 gain is larger than that, and the cold starts below show how far
leftmovesbetween starts with nothing counted.
Cold starts are variable: by the budget read the lists had mostly drained (at most 0.11 GiB in the sampler). Base
left 15.5 and 16.1 GiB; the branch left 15.9 and 16.3 GiB and counted 0.62 GiB (start 2, rank 1) and 0.61 GiB (start
5, rank 0). They do not separate the trees.
Receipt notes: on starts 2 and 5 the engine printed 0.62 / 0.61 GiB where the sampler's nearest sample read 0. The
engine reads at its own instant and the sampler once a second while the lists drain, so the two can differ. In this run the
rank 1 process's line read
rank 0:(the engine optionrankis 0 on both GLM processes), so the report scriptlisted it as missing on starts 4 and 7; the line was there. The label is gone in this commit.
The served receipt above ran on this change's first commit (b104cf77b); the commit here differs only in the log
lines' rank label and the GB10 fixture tests.
After each stop: no allocation left on either GPU, and
MemAvailableback within 1 GiB of before (116-117 GiB).Exactness: n/a for arithmetic. Only sizes change; the served
token_shais equal on every start of both trees."draft": false, resume and concurrency were not run on this change.Decode and prompt speed: n/a. Start-up reads one more procfs file per pool reading; the serving paths are unchanged.
Tests, base
67cc8fbce→ branch:zig build test-cuda-host: 253/253 steps, 3253/3267 passed, 14 skipped → 253/253 steps, 3262/3277 passed,15 skipped. The added tests are this pull request's: five in
parked_pages.zig(the Linux-only one is the addedskip on macOS),
counting's, the block that reachesparked_pages.zigfromcuda_memory.zig, and the two pooltests; the counts also include the binaries that build more than one of them. Run alone, the branch's suite
once failed
cortheon fuzz: every reader takes linear time on long inputs(a timing test that also failed onmain in one run here); rerun, it passed.
zig build test-golden: 322/335 equal (13 known differences) and 26/26 → the same, 0 unexpected differences.zig build test(fresh cache each, run one after the other): 394/490 steps, 5566/5593 passed, 27 skipped →394/490 steps, 5575/5603 passed, 28 skipped. The 55 failed steps are the same on both: Metal library compiles
(
cannot execute tool 'metal' due to missing Metal Toolchain), and no test fails.python3 tools/zig/lean_check.py: 434 problems on base, 434 on the branch;lean_new.py: 0 new. None of themis in the changed files.
git diff --check: clean.Not run: Metal device checks (no Metal toolchain here; the change is CUDA-only), CUDA GPUs other than GB10 (a
discrete card does not read zoneinfo), one GB10 alone, TP=3 or more, a kernel before 6.7 on a device (the host
tests cover its format), HIP, and
bench_*speed runs. A start that main refuses and the branch admits was nottried. One sample per start kind and tree, two of each.
Limits and follow-ups
MemAvailable, so the pages stay free; whatthe budget counted is not given back. The kernel drains every list when memory runs short, so an allocation the
budget admitted can always take them.
high_min:, so there the fixed 512 MiB resting level is an estimate. Below it nothing iscounted, the same as main.
MemAvailabledirectly (families/deepseek_v41/kv/budget.zig:244,cuda_kv.zig:53), and so does the HIP admission (hip/admission.zig:53). Neither is changed here. Each could useparked_pages.bytesas its own pull request.atc overlap: the only overlaps are open draft pull requests that touchnative/cuda.zig. The one textual conflictamong those checked is with New model family: MiniMax-M3.1-Flash VL (minimax_m3_vl) EXL3, CUDA, TP2-served #1281, which already conflicts with main.
Checklist
(Start-up sizing only.)
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.(Not run on this change; it changes how many streams, pages and kept states fit, not how a reply is computed.)
(No arithmetic changes.)
SUMMARYline is in the receipt. (No kernel is added or changed.)Apple M4 without the Metal toolchain; a served CUDA run on two DGX Spark (GB10) at TP=2.)
on main and passes here; the Linux-only zoneinfo read skips on macOS.)
By Robert Wojciechowski (wojo on GitHub, @rwojo on X)