Repository navigation
Conversation
Backs vLLM's CPU offload staging region with a sparse file on each node's own NVMe, so a prefix evicted from the GPU pool is restored from disk instead of recomputed. Capacity becomes the file size rather than RAM. Off by default; GLM53_OFFLOAD_MMAP_DIR turns it on. Measured here (105k-token prompt, evicted by 6 x 168k floods, 100 GB store): restore 8.54 s vs 130 s to recompute (15x), output byte-correct, 1.68 GB read back from disk, host stable. 160 GB/node holds ~11.3M tokens. Decode unchanged: bench_decode median of 5x400 gives structured 66.4 / prose 26.7. overlay/patch_kv_offload_groups.py fixes what blocks offload on this model (see MiaAI-Lab#57): - the hash-alignment assert aborts the boot, because KpoolTailSpec's 4-token block can never align to the 64-token hash granularity - sub-block groups (kpool tail, DFlash2 drafter) emit ~56x more keys than the 3584-token groups and starve the staging tier, so MLA/mamba chunks never reach disk; both are excluded. Drafter KV rebuilds in its own window after a restore and the target verifies every token, so this costs acceptance at worst, never correctness. - the packed whole-row copy path clobbers other groups' live pages on restore for a multi-block-size hybrid; the per-layer path is coherent and moves ~10x less data - the staging region's page cache is bounded with msync + MADV_DONTNEED, which GB10 needs: CPU and GPU share one pool, and an unbounded region starves the GPU until the worker rank dies mid-prefill with no traceback Per-node rather than an fs secondary tier because those run scheduler-side on the head only: under 2-node TP rank 1's shard is never written and its restores return stale bytes as confidently wrong output, with healthy-looking counters. Backing the CPU primary tier per node is correct by construction. The patch is fail-closed (a drifted anchor patches nothing and exits non-zero), idempotent, and skippable via GLM53_SKIP_KV_OFFLOAD_GROUPS_PATCH=1. Wired into start.sh on both ranks alongside the existing overlay patches.
|
| same 337k-token request | expandable_segments:False |
expandable_segments:True |
|---|---|---|
| host available at start | 10358 MB | 10358 MB |
| at t+60s | 9244 MB | 9682 MB |
| at t+240s | 1352 MB | ~10098 MB |
| outcome | never completed (600 s timeout), host wedged | completed 397 s, memory recovered |
Memory did not return after the request finished (num_requests_running=0, still pinned at ~5 GB below baseline), which is the caching-allocator-retention signature rather than page cache or a mapping leak. We wedged both Sparks six times before isolating it — SSH and SSM die and it needs a physical power cycle, so it is worth flagging loudly.
The fix (upstream's own exemption, not a hack)
The same guard exempts the CuMem allocator, because CuMemAllocator.use_memory_pool disables expandable segments around the KV pool so KV lands on stable physical pages — which is precisely what the guard protects (registered/pinned KV for RDMA connectors like NIXL/Mooncake). So the working combination is:
GLM53_ALLOC_CONF=expandable_segments:True # or just leave start.sh's default
# and add to the serve flags:
--enable-cumem-allocatorVerified live: offload active (OffloadingConnector, primary tier 3162 blocks), expandable_segments:True, 12.6 GB stored to the disk tier, and host memory flat through a 337k-token prefill.
I'll fold this into the PR (recipe comment + .env.example note + README section) so the offload path ships with the allocator guidance attached rather than as a footgun. Flagging it here first in case you'd rather handle the interaction differently — e.g. having start.sh set --enable-cumem-allocator automatically whenever --kv-transfer-config is present, which would make the combination impossible to get wrong.
…e on GB10)
The OffloadingConnector rejects PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True,
so enabling offload pushes you onto expandable_segments:False — and this repo's
start.sh defaults to True for good reason. Without expandable segments the
caching allocator keeps reserved blocks after a large prefill and never returns
them; on GB10's unified memory that starves the host rather than merely
fragmenting it.
Measured on 2x DGX Spark, same 337k-token request:
expandable_segments:False -> host available 10358 MB -> 1352 MB, no recovery
after the request finished, never completed
(600 s timeout), both Sparks wedged
expandable_segments:True -> dipped ~560 MB, completed in 397 s, recovered
Six wedges before this was isolated, each needing a physical power cycle, so it
is documented loudly rather than as a footnote.
The fix is the guard's own exemption: keep expandable_segments:True and pass
--enable-cumem-allocator. CuMemAllocator disables expandable segments around the
KV pool, so KV lands on stable physical pages -- which is what the guard exists
to protect (registered KV for RDMA connectors) -- while the rest of the engine
keeps expandable segments. Verified live with offload active: OffloadingConnector
loaded, primary tier 3162 blocks, 12.6 GB stored to the disk tier, host memory
flat through a 337k-token prefill.
Status update: offload verified working — and this PR depends on #80Since opening this, we ran it in production and then to a proper restore proof. Two things worth adding here. It works, with numbers169,328-token prompt, evicted from a 1,096,618-token GPU cache by 9 floods (1.9M tokens), then re-asked: Host memory flat ~10.4 GB across the flood. Decode unaffected: structured 65.63 tok/s, prose 27.91 (vs 65.1 / 27.1 published).
|
Status updateOffload has now run in production on our kit and been root-caused properly. Full detail in #57, but the parts that matter for this PR: The docs in this PR are still correct and still necessary. The allocator pairing ( One claim of mine that this PR's discussion inherited was wrong. I reported that the restore path stalls the engine. It does not. That was queue wait behind another session's long chunked prefill (root cause + isolation in #110), and the restore itself fires in seconds — measured 318,976 tokens restored from disk, and 10.4 s vs 228.7 s cold on an earlier test. Worth adding to the docs if you take this PR: once #110's scheduling fix and #86's right-sizing are both in, offload largely stops firing — our final two-session workload wrote 80.12 GB to the store and restored 0.00 GB, because nothing was ever evicted. Offload becomes insurance rather than a throughput feature, and anyone benchmarking it needs to force an eviction first or they will measure nothing. Happy to rebase this on current |
|
Feel free to take any of my work here: https://github.com/smartprogrammer93/vllm-bounded-fs-kv-tier/tree/main |
Review follow-up —
|
Maintainer direction — pursue the experimental offload pathWe have selected #58 as the offload path to pursue, rather than adopting #99's store-only stage without a usable hybrid restore path. This is a direction decision, not current-image qualification, merge approval, or a scheduled live run. Thank you @jdecker76 for the per-node primary-tier design, restore measurements, allocator findings, and corrected stall attribution; those receipts remain historical evidence at their recorded identities. The next work is maintainer-owned: correct both the PR body's testing note and README quickstart, then integrate the mechanism into canonical current main before deciding any live qualification schedule. The copy-paste quickstart at this head still recommends We also own selecting and documenting the mixed-prefill policy for that integration: main's Please do not rebase or produce new measurements now. This supersedes the earlier author-side documentation request: maintainer @plotarmordev owns the correction and integration, and the PR stays open and unqualified in the meantime. |
…anup-7c8b # Conflicts: # start.sh
The quickstart and .env.example told readers GLM53_ALLOC_CONF=expandable_segments:False, contradicting this PR's own warning block (False is fatal on GB10) and duplicating main's PYTORCH_CUDA_ALLOC_CONF caller override with worse empty-value semantics (:- falls back on empty; main's - honors an explicit empty as disable). - README quickstart and .env.example: PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True plus --enable-cumem-allocator in the offload serve flags; state the unset-only default and the explicit-empty disable instead of a second knob. - .env.example: quote the EXTRA_ARGS example. .env is sourced as shell code, so the unquoted JSON ran `kv_connector:OffloadingConnector` as a command and killed the launcher under set -euo pipefail. - Register GLM53_OFFLOAD_MMAP_DIR / GLM53_OFFLOAD_RELEASE_BYTES in the README knob table and scope the section to the TP=2 launcher (start-tp3.sh / start-tp4.sh do not forward these knobs or mount the overlay). Docs only; no runtime behavior change.
plotarmordev
left a comment
There was a problem hiding this comment.
Source/publication review at 6662b7d7aa5837340be6f3299e5310abf616c015: fast-forward publication and updated description verified; merge-hold remains. Experimental offload is not approved for merge: current-main integration, RELEASE_BYTES admission/domain decision, and separately authorized forced-eviction/restore plus allocator qualification remain maintainer plotarmordev-owned. Four passing staged-source tests are not runtime qualification. No author rebase, download or benchmark requested; no merge performed.
Documentation correction published — HOLD remainsExact head Next-action owner: maintainer plotarmordev. Decide the supported release-threshold domain and pre-stop handling before commissioning an authorized current-image forced-eviction/restore and allocator window. Unblock requires reviewed current-main integration and accepted actual disk-to-GPU restore, output-correctness, and host-memory evidence under the selected recipe; stores/counters alone do not qualify it. Existing marker short-circuit and per-file rather than transactional patching limits remain explicit in the description. Main's prior four pinned-source patch/compile/idempotence checks apply to unchanged code, not runtime qualification. No tests, live operations, approval, or merge were performed in this documentation review. Experimental |
plotarmordev
left a comment
There was a problem hiding this comment.
Hold update
Public head checked: 0bef1aa251b15036a85cff249125b0769b532190.
Why it is held: We observed disk-backed storage activity, but did not demonstrate a successful restore from it.
What we tested: The CPU/numeric checks and bounded shared-memory and file-backed probes passed their answer checks. The file-backed mappings gained allocated data. However, the repeated request reported zero cached tokens and no additional measured block-device reads. Allocation or writes alone are not restore proof; zero device reads alone also does not distinguish all page-cache behavior.
Next action — me, @plotarmordev: Finish integration and prepare a test that identifies the actual restore path. The test must store known data, remove resident-cache explanations within the supported design, observe restore-specific counters/events, and check the returned data/answers. Record source and runtime identities and both participating ranks. Use disposable storage, not live cache deletion.
Author action now: No immediate rebase or rerun. We own this qualification step. Mark ready only after the selected final version demonstrates the claimed restore behavior; the current result is incomplete evidence, not a proven restore failure.
Please keep the hold until the conditions above are met. When a new version is ready for review, include its commit ID, the checks actually run, their results, and anything not tested. Keep failed or incomplete runs in the report. Share only sanitized results—not private prompts, credentials, or machine configuration.
Maintainer priority triage — 2026-09-26Reviewed head: P2, retained alongside #232 pending a maintainer integration choice. The previous selection of this path is not erased merely because a newer PR exists; conversely, #232 now reports restart-restore receipts worth evaluating. Next owner: @plotarmordev decides the supported offload architecture and restore-validation contract before requesting either author to rebase or rerun. Unblocks on that decision and the chosen path proving actual restored cache hits, safe invalidation, and integration with current-main cache behavior. Existing missing-restore evidence here is not relabeled as passing. This is prioritization only; no merge or implementation work is authorized by it. |
|
Closing in favor of #232, which we have now selected as the KV-offload direction. When we chose this PR on 2026-09-15, its unblock condition was an accepted disk-to-GPU restore on current main; that was never shown here. #232 reports a restore after restart, so we are consolidating on one offload path instead of maintaining two. Thank you @jdecker76: your per-node primary-tier design, allocator findings ( |
Experimental offload candidate for #57; not current-image runtime qualification.
Off by default.
GLM53_OFFLOAD_MMAP_DIRbacks the CPU offload staging region with a sparse file on each node's own NVMe, so a prefix that has been evicted from the GPU pool is restored from disk instead of recomputed. Capacity becomes the size of that file rather than RAM, which is what makes a shared box practical.Historical author measurements (August 2026 image)
105k-token prompt, evicted by 6 × 168k-token floods, 100 GB per-node store, thinking off, temp 0:
kv_offloadCPU_to_GPU)Historical decode observations (not a current-stack equivalence claim) —
tests/bench_decode.py, median of 5 × 400, offload on: structured 66.4 tok/s (0.938 accept / 6.57 per step), prose 26.7, against the 65.1 / 27.1 in the README. Also re-run at 1M context on our launcher: boots clean,GPU KV cache size: 1,347,826 tokens.What the patch does
overlay/patch_kv_offload_groups.py, 27 anchored edits over 5 vLLM files:build_offloading_configasserts every group is hash-aligned;KpoolTailSpec's 4-token block never can be. Non-alignable groups are treated as per-request scratch instead of aborting._g6. Both are excluded. Drafter KV rebuilds inside its own window after a restore, and the target verifies every token, so this costs acceptance at worst, never correctness.msync+MADV_DONTNEEDeveryGLM53_OFFLOAD_RELEASE_BYTES). Required on GB10: CPU and GPU share one pool, so an unbounded region starves the GPU and the worker rank dies mid-prefill with no traceback.Why per-node instead of an
fssecondary tierSecondary tiers and their promotions run scheduler-side on the head only. Under 2-node TP that means rank 1's shard is never written (every file lands
..._r0) and its restores read stale bytes — the request is admitted as a hit and generates confidently wrong output whilekv_offloadcounters look healthy. Ours degenerated into"lockhandlehandle...". Backing the CPU primary tier per node sidesteps this by construction, since store/load jobs are broadcast to every rank. Detail in #57.Safety
# [glm53-kv-offload-scratch].GLM53_SKIP_KV_OFFLOAD_GROUPS_PATCH=1.GLM53_OFFLOAD_MMAP_DIRand the--kv-transfer-configflag, behaviour is unchanged.tests/test_kv_offload_groups.pycovers idempotency, fail-closed drift, the kill switch, and (inside the image) that every anchor is still present. The original30 passedsuite claim is historical, not the current review result.Notes for anyone testing
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:Trueand add--enable-cumem-allocator. The launcher uses its existing caller override: unset selects that default; explicit empty disables it. Do not disable expandable segments merely to bypass the connector guard.PYTHONHASHSEED(the tier docs suggest it for cross-process key stability) — it invalidates the Triton JIT cache keys and the mid-collective recompile kills the worker rank.cpu_bytes_to_use ≥ row_bytes × (restore_tokens / 3584 + 8). Undersized, stores still succeed and metrics still climb while no restore ever serves.vm.dirty_bytes(we use 4 GB / 1 GB background). Unbounded dirty pages livelocked both our Sparks twice;drop_cachescan't help because the pages are dirty.Tested on 2× DGX Spark (GB10, 121 GiB unified), TP=2 over CX7, DFlash2 k=7,
KV_CACHE_DTYPE=fp8, at 400k and 1M context.Maintainer source review — publication under HOLD
Published source head
6662b7d7aa5837340be6f3299e5310abf616c015preserves author history through normal main merges and a fast-forward publication; integrated base isa9bbbadc4c7253870d0d519c3d445e4ee7b97540, not current main. Maintainerplotarmordevowns current-main integration and any later qualification scheduling; SIX-GlmLandingE alone owns the merge lane.merge-holdremains.Corrected both allocator recipes, retained main caller precedence and replay/retention wiring, and added the missing KVOFF artifact-table/preflight admission. Main's supplied receipts report launcher parity passing and four offload tests passing on copies of all five Python sources from the immutable wheel-layer pin; absent runtime source explicitly skips its source-dependent test. These establish patch/compile/idempotence only, not allocator behavior or eviction/restore qualification. The final commit changes README historical scoping only.
Unresolved source contract:
GLM53_OFFLOAD_RELEASE_BYTESis forwarded without launcher pre-stop validation. Runtime usesint(value or 0): empty/zero disables release, noninteger raises ValueError during worker construction, and negative values are accepted and trigger release on each completion poll with a region. Both load and store transfer sizes contribute, so the old “bytes stored” wording is imprecise. No positive-only restriction was invented in this review. Maintainer owns deciding/documenting the supported domain and pre-stop admission before live qualification.Unblock requires separately reviewed current-main integration and separately authorized current-image forced eviction/restore showing actual disk-to-GPU transfer, correct output, and host allocator behavior under a selected scheduling policy. Historical tables are retained as historical author observations; stores/counters alone are insufficient, and no author rebase or benchmark is requested.