Skip to content

feat(runtime): NVMe cold tier (ColdPolicy::Disk) under the cold-pool planner - #149

Closed
Astrangemaninhere wants to merge 45 commits into
Neroued:masterfrom
Astrangemaninhere:disk-cold-tier
Closed

feat(runtime): NVMe cold tier (ColdPolicy::Disk) under the cold-pool planner#149
Astrangemaninhere wants to merge 45 commits into
Neroued:masterfrom
Astrangemaninhere:disk-cold-tier

Conversation

@Astrangemaninhere

@Astrangemaninhere Astrangemaninhere commented Sep 1, 2026

Copy link
Copy Markdown

NVMe cold tier under the cold-pool planner (ColdPolicy::Disk)

Adds a disk placement to the existing cold-pool degradation chain:
Device (hot) -> pinned Host (warm) -> NVMe file (cold) -> evict.

What it does

  • ColdPolicy::Disk + --cold-policy disk with --cold-disk-path and
    --cold-disk-bytes (default 32 GiB). Empty path uses the system temp dir
    (WSL2: keep it on ext4, not /mnt/c).
  • Cold pages spill to per-layer files: each slot is a fixed-stride unit
    (compressed bytes, rANS or raw codec unchanged), so the file offset is
    slot * stride. The device slot pool becomes a working set; capacity is
    bounded by the disk budget instead of max_cold_pages.
  • Egress: after the pack kernels commit a slot, its bytes are mirrored to the
    file (D2H via the device stream, then write). Restore reads the file back
    before the decode kernels run.
  • Async prefetch: warm_cold_prefix prefetches every pending slot on the
    transfer stream with double-buffered pinned staging, so the H2D legs of the
    warm path overlap with the previous decode step; the decode stream waits
    once before restoring the batch.
  • restore_cold_page gains a disk_prefetched flag; the checkpoint/rewrite
    path falls back to the synchronous read.

Why

Responds to the design in upstream #143 (NVMe cold tier under the
Device/Host planner). Same shape as vLLM/LMCache, TRT-LLM host offload and
NVIDIA ICMSP: more parked conversations survive (reuse probability),
not higher live decode concurrency. Restore of a few GiB from NVMe is far
cheaper than re-prefilling an 80-120k agent chat (upstream #73 measured
host restore at 422 ms vs 53 s prefill).

Notes

  • Reads stay exact-identity; the slot codecs are byte-identical to the
    device pool (no approximate / block-hash cache).
  • Live decode never streams from the file; cold restore is admission-time
    materialize-to-device, then run.
  • Opt-in, default off: current two-tier behavior is unchanged.

Verified: builds clean, baseline ppl unchanged (1.3353 on the zh corpus),
disk spill/restore exercised with --cold-policy disk --cold-disk-path /tmp.

Verification status

  • Functional (RTX 5090 / WSL2, --cold-policy disk --cold-disk-path /tmp):
    egress compressed 17 prefix pages and mirrored them (1,296,896 B/layer,
    exact pages * slot_stride); restore + prefix reuse verified (4.3k-token
    prompt 2.5 s first request, 0.9 s on reuse).
  • Build: compiled and linked in the integration tree this branch is based on
    (engine + ops). Limitation: the rebased branch (on latest master) has
    not been built standalone yet — the same 8-file diff was built and linked
    in the pre-rebase tree; a clean-master build is pending.
  • Codex review findings (6) addressed in the latest commit; the DFlash2 two
    are pre-existing integration-tree fixes included in the diff.
  • This implements the proposal in Future: NVMe cold tier under Device/Host planner for parked continuations #143; direction confirmed with the
    maintainer is still pending (comment posted on Future: NVMe cold tier under Device/Host planner for parked continuations #143).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d85c8461be

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/targets/qwen3_6/impl/runtime/program_impl.h Outdated
Comment thread src/targets/qwen3_6/impl/runtime/program_impl.h Outdated
Comment thread src/targets/qwen3_6/impl/runtime/program_impl.h
Comment thread src/targets/qwen3_6/impl/runtime/layouts_impl.h
Comment thread src/product/speculative_options.h
Comment thread src/targets/qwen3_6/impl/runtime/program_impl.h Outdated
@Astrangemaninhere
Astrangemaninhere force-pushed the disk-cold-tier branch 3 times, most recently from 08b84f9 to 1f40594 Compare September 1, 2026 01:57
@Astrangemaninhere

Copy link
Copy Markdown
Author

All six review findings are addressed in the updated commit (1f40594). Functional verification on the 5090/WSL2 box with --cold-policy disk:

  • egress: 17 prefix pages compressed and mirrored (1,296,896 B/layer = 17 * slot stride, exact), device slot released after the mirror
  • restore: cold pages read back and decoded, prefix reuse across requests works (long 4.3k prompt: 2.5 s first, 0.9 s on reuse)
  • file mode w+b, staging element addressing, and the two Window-only gates (enqueue_cold_compressions, warm-on-retain) now include the Disk policy

Per finding:

  1. Run cold maintenance for Disk — both gate sites (enqueue trigger + retained-source warm) now accept ColdPolicy::Disk; disk compression is exercised in the run above.
  2. Staging buffer addressing — the egress D2H and the restore H2D now use cold_disk_staging[0] (the pinned allocation), not the array name.
  3. Open spill files read/write — files open with "w+b"; restore reads succeed (verified by the reuse run).
  4. Size the disk tier — rather than inflating the device slot pool, the disk tier now uses an independent monotonic file-slot counter: egress mirrors at file_slot * stride and releases the device slot (the file is the durable copy); restore allocates a temporary device staging slot, reads the file back, decodes, and releases it. Capacity is bounded by the disk budget; the device pool stays the small working set.
  5. DFlash2 draft width — explicit --spec dflash2 without --draft-tokens now normalizes to the fixed 7-draft block in the planner (validation accepts 0/7; the planning inputs map 0 -> 7), so the sequence planner never sees a zero draft window.
  6. DFlash2 long-context demotion — the submission loop now applies the same kSpecDemoteTokens predicate as the budget loop (extent 0 past the threshold), keeping the submitted extent within the computed target envelope.

Thanks for the review.

NInfer Agent added 25 commits September 1, 2026 10:10
Fixed raw slots (9232 B: header + E2M1 nibbles + E4M3 g16 scales) hold
requantized cold pages for both the INT8 and NVFP4 tiers. Requantizing
INT8 planes to g64 E2M1 measures NMSE 0.012-0.014 (inside the accepted
NVFP4-layer envelope) at 1.85-1.99x per head-page, ~1.66x aggregate cold
KV on the 27B production table.

The pack/restore kernels, the per-layer dtype dispatch, the decode and
prefill cold staging (inline nibble->int8 adapter preserving the int8 QK
tensor cores), and the length-based slot sizing are all included;
--cold-policy window|host plus --cold-keep-tokens/--cold-host-bytes
control activation. Three latent v1 cold-addressing bugs (compress_page
slot scaling, decode and prefill flat slot indices) are fixed on the way.
Fixed raw slots (9232 B: header + E2M1 nibbles + E4M3 g16 scales) hold
requantized cold pages for both the INT8 and NVFP4 tiers. Requantizing
INT8 planes to g64 E2M1 measures NMSE 0.012-0.014 (inside the accepted
NVFP4-layer envelope) at 1.85-1.99x per head-page, ~1.66x aggregate cold
KV on the 27B production table.

The pack/restore kernels, the per-layer dtype dispatch, the decode and
prefill cold staging (inline nibble->int8 adapter preserving the int8 QK
tensor cores), and the length-based slot sizing are all included;
--cold-policy window|host plus --cold-keep-tokens/--cold-host-bytes
control activation. Three latent v1 cold-addressing bugs (compress_page
slot scaling, decode and prefill flat slot indices) are fixed on the way.
…only)

The paged-KV cold mechanism (sentinel pages, slot pool, compress)
does not exist in upstream master yet; the cold-compress pass and
its member state are removed until that mechanism lands in a
follow-up PR. This PR keeps the entropy codec ops, the ColdPolicy
option surface, the per-layer KV plumbing, and the op tests.
Cold slots are allocated as per-layer regions (9232 B raw slots +
I32 validity) by the decoder state; the pool exposes allocate/
release with a used bitmap. A decode-boundary pass packs the
retired tail (valid - cold_keep_tokens) of a sequence's text KV
into raw entropy slots, shrinks the address space entitlement,
returns the physical pages to the pool, and publishes block-table
sentinels (entry <= -2, slot base = -2 - entry) via publish_indices.
Attention producers decode sentinel pages inline from the slots
in the cold staging branches (INT8 adapter preserves int8 QK cores;
NVFP4 tier keeps its native E2M1/ISO3 nibble semantics).
NInfer Agent added 20 commits September 1, 2026 10:10
…ctx8k, parity 8L)

Measured 13.3k zh perplexity at ctx 4096 and 8192. 8/8 split
(NVFP4 {0,1,3,4,6,7,8,9}, I8 {2,5,10-15}) scores 1.335 vs all-I8
1.432 at ctx 4096; demoting L13/14 to NVFP4 is free (10L 1.3344,
11L 1.3385, 12L 1.345, 14L 1.375 - all below all-I8). Page pool
shrinks another 12.5%. L2/L5 are the true outliers (demote alone
+0.20/+0.09); the old 10L 0-4,6-9,15 combo degraded to 1.551 by
demoting both. analyze_kv.py NMSE/rank metrics do NOT predict these
combo outcomes - neighborhood marginals do.
The cold pool only carried INT8 planes; NVFP4 layers (10 of 16 in the
default table) skipped cold transfer entirely, so the page pool could
not shrink on the layers that need it most. This wires the previously
orphaned page-slot rANS codec (entropy_nvfp4_slot, ported in batch 1)
into the egress/restore/decode paths:

- egress: NVFP4 layers requantize K (Nvfp4G16) and V (Iso3VG16) to g64
  scales and rANS-encode into entropy slots; INT8 layers keep the raw
  9232 B nibble slots. Per-layer dtype dispatch replaces the all-I8 gate.
- restore: rANS-decode + scale-tail scatter back into native planes.
- slot buffer: one 9536 B size serves both codecs (rANS max = 320 B
  header + 32x256 B streams + 1024 B scale tail).

Fixed pre-existing cold-pool bugs found while validating:
- cold_i8 pack/restore hard-coded the 9232 B slot stride, so heads past
  slot 0 landed at the wrong offset once the buffer grew to 9536 B; both
  kernels now take slot_bytes.
- can_cold_transfer required writer_references == 0, which the paged
  store never clears for live pages, so cold transfer never ran; the
  gate now only requires a device replica and no pins/fork ties (cold
  pages sit before the decode frontier and are never written again).

Verified: 13.1k-token zh needle (紫电青霜) recalled identically with
the cold pool compressing 17/48 prefix pages, INT8-only and mixed
10L tables; cold off matches cold on exactly.
The --cold-policy host tier was parsed but never wired: cold slots always
lived on device, so the device KV footprint never shrank and the 8 GB host
budget (host-kv) sat unused as a backup-only copy.

Host mode allocates the cold-slot payload buffers with cudaHostAllocMapped
(one pinned allocation per layer) instead of device tensors. Pack kernels
write them through UVA; decode kernels read them back over PCIe. Only the
validity plane stays device-resident. The cold-pool policy gates (egress,
warm-restore, decode trigger, scratch allocation) now accept Host alongside
Window.

Functionally verified: 13.1k-token zh needle recalled identically with 48
prefix pages compressed into host slots; device runtime for the cold payload
drops to zero (only hot pages + validity planes occupy device memory), which
is what lets the context ceiling exceed the device KV budget.
device_kv_prefix_pages, shared_device_kv_prefix_pages and
missing_shared_device_kv_prefix_pages treated cold-compressed pages as
non-resident, so a prefix reuse across sequences (request B reusing
request A's checkpoint) computed an inconsistent restore inventory and
aborted with 'Text KV restore inventory is inconsistent'. Cold pages
restore in place from their raw slots (missing_kv_restore already skips
them), so the resident counts must include them.

Known limitation (documented, not fixed): reusing a *catalogued* (shared)
checkpoint that contains cold pages still fails ('cold checkpoint page has
no source bookkeeping') because the catalog entry carries no cold-slot
record; single-sequence cold-pool operation is unaffected. Concurrent
cold-pool reuse of an active source works.
Two DFlash2 defects found while validating the DFlash2/MTP switching path:

1. CUDA-graph preparation segfaulted with --spec dflash2. The graph
   representative (prepare_representative) initialized the DFlash host
   ingress/egress unconditionally under io.dflash_decode, but DFlash2 keeps
   its own dflash2_host_ingress/egress buffers and dflash_host_ingress is
   null when only DFlash2 is active, so *dflash_host_ingress = {} crashed.
   The block now selects ingress/egress by backend, and DFlash2's
   pending_features are zeroed like DFlash's. Verified: dflash2 CLI and
   serve generation run (16 tok, 116 tok/s serve decode).

2. --spec auto failed to load ('loaded weights do not match the frozen
   startup features') because only plan_load resolved auto internally;
   make_sequence_planner and construct_loaded_model still saw
   SpeculativeBackend::Auto, so the planner built an Auto-features plan and
   the startup-features consistency check rejected the loaded weights.
   resolved_auto_speculative is now a public Package static and the
   registry resolves auto once up front, passing the concrete options
   through plan_load, the planner, the loaded model and the instance. The
   35b target gets the same hook (auto -> MTP, no DFlash2 weights).
   Verified: dflash2 artifact with --spec auto serves 360k context (MTP),
   explicit --spec dflash2 serves short context at 116 tok/s.
--spec auto previously chose DFlash2 only when max_context fit the draft
capacity, and otherwise fell back to MTP. For a DFlash2 artifact that
fallback is wrong: the artifact has no MTP draft head (the two are
mutually exclusive), so auto at a long context selected MTP and failed to
load with a confusing weights mismatch. auto now keys purely off the
artifact weights profile: a DFlash2 artifact always picks DFlash2 (a
memory shortfall surfaces as a clear reservation error), any other
artifact defaults to MTP. Verified: base artifact + auto serves 360k
(MTP); DFlash2 artifact + auto serves 16k with speculative=dflash2.
…planner

Cold pages spill to per-layer disk files: the device slot pool becomes a
working set, egress mirrors compressed slots (file offset = slot * stride),
restore reads back before decode. The warm path prefetches pending slots on
the transfer stream with double-buffered staging so H2D overlaps decode.

- ColdPolicy::Disk + --cold-policy disk / --cold-disk-path / --cold-disk-bytes
- per-layer spill files opened once, closed on teardown
- prefetch_cold_pages: async H2D prefetch in warm_cold_prefix
- egress/restore keep the slot codecs (rANS / raw) byte-identical

Responds to upstream Neroued#143 (NVMe cold tier under the Device/Host planner).
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