Skip to content

feat(kvcache): host-resident prefix tier for hybrid GDN models (snapshots + KV pages) - #525

Open
alvarorsouza-arch wants to merge 3 commits into
FlashML-org:mainfrom
alvarorsouza-arch:host-prefix-tier
Open

alvarorsouza-arch wants to merge 3 commits into
FlashML-org:mainfrom
alvarorsouza-arch:host-prefix-tier

Conversation

@alvarorsouza-arch

Copy link
Copy Markdown

Note: stacked on #499 (KV host-offload). The GDN-snapshot tier works standalone; the KV-page tier composes with the offloader.

Problem

On hybrid GDN models (qwen4_exp), prefix reuse needs two things alive at once: the KV pages in the radix tree and a GDN state snapshot at that point. The snapshot pool is small and global LRU. Any auxiliary call (WorkBuddy's title generator is our real-world case) drains the main conversation's snapshots between turns, and the match truncates to zero. Measured before the patch: agent turns with 20 to 150 s TTFT. After: stable single-digit seconds.

What the patch does

  1. New hooks on HybridRadixCache: on_evict_node (fired before unlink/free) and on_match_dead_snapshot (fired during the match walk). With the tier on, an evicted leaf becomes a tombstone and its KV is no longer cascade-freed.
  2. HostPrefixTier (new, kvcache/host_prefix_tier.py): a pinned host bank of GDN snapshots plus an index of cold prefixes with its own KV slot space. Scheduler logical page ids get reused, so content is copied D2H on eviction and never referenced by logical id afterwards.
  3. Rehydration at admission: if the tree match falls short, we check the host index; on a hit we restore the GDN snapshot (H2D into a fresh slot) and bring the prefix back. With KVHostOffloader active the copy is host-to-host into its mirror, no bulk H2D; the offloader's lazy fetch covers residency.
  4. Eviction copies run in 128-page slices. The first version staged the whole prefix on the GPU and OOMed exactly under memory pressure, which is when eviction runs.

Flags: FT_GDN_HOST_TIER=1, FT_GDN_HOST_SLOTS=32, FT_PREFIX_HOST=1, FT_PREFIX_HOST_GB=16. All off by default.

Measured numbers (RTX 4070 Ti SUPER 16 GB, 251 GB RAM, PCIe Gen4)

Churn test (24k-token conversation, 10 auxiliary calls in between, then back):

cached-token wall time
without the tier 0 16.5 s (full recompute)
with the tier 23,936 3.8 s

Total flush test (two ~130k-token prefills through a 200k pool; every page of the conversation leaves the GPU): the conversation comes back with cached-token 24,000 in 4.4 s.

Regressions: our quality suite (40 known-answer questions + 30 cloze) is identical to baseline (39/40 both). Decode steady at 33-37 t/s.

Known limitations

  • Rehydrating a prefix larger than the physical pool requires the KVHostOffloader active; without it the conservative path applies.
  • The host index is process memory; a server restart clears the tier.
  • mr=5 breaks at CUDA graph capture on this build (bs=5 not captured); mr=4 is the safe ceiling. Details in a separate issue.

Files

  • New: kvcache/host_prefix_tier.py, tests/kvcache/test_host_prefix_tier.py, tests/kvcache/test_tree_hooks.py, tests/kvcache/test_tier3.py
  • Modified: kvcache/hybrid_radix_cache.py, scheduler/cache.py, engine/config.py

alvarorsouza-arch and others added 3 commits September 17, 2026 06:03
- mirror_store_quant (new Triton kernel, graph-safe UVA): the write-through
  mirror now copies e4m3 codes + fp32 scales read from the pool AFTER
  quantization (the pool is the source of truth). The bf16-era mirror
  corrupted fp8 pages silently on rehydrate.
- mirror_scales host bank + fused-copy descriptor extended with the scale
  banks (per-bank row sizes).
- set_select_width takes max_bs: the whole batch selection must fit the
  physical pool by construction (fixes assert-crash on multi-row decode with
  small pools).
- measured: 299,648 logical tokens with kv-cache-dtype fp8 + kv-host-pages,
  needle at 253k tokens answered correctly (~1026 t/s), quality suite
  identical to baseline.
Prefix reuse on hybrid GDN models needs KV pages AND a GDN state snapshot
alive at the match point; the small global snapshot pool gets drained by
interleaved auxiliary calls (agent title generators) and every match
truncates to zero. Measured on real WorkBuddy agent turns: TTFT 20-150s
recompute per message.

This adds a host-resident tier: evicted GDN snapshots and KV prefix pages
move to pinned RAM instead of dying. Matches rehydrate from host in ~4s
(24k-token prefix) instead of a full recompute.

- HybridRadixCache hooks: on_evict_node / on_match_dead_snapshot; evicted
  leaves become tombstones instead of cascade-freeing KV
- HostPrefixTier: pinned GDN snapshot bank + cold-prefix index with its own
  KV slot space (logical ids get reused by the scheduler, so content is
  copied D2H on eviction, never referenced by id)
- Admission rehydration; with KVHostOffloader active the copy is
  host-to-host into its mirror (no bulk H2D; lazy fetch covers residency)
- Eviction copies run in 128-page slices (staging the whole prefix OOMed
  exactly under memory pressure, which is when eviction runs)

Measured: churn test cached-token 0 -> 23,936 (16.5s -> 3.8s); total pool
flush rehydrates at 24,000 cached tokens in 4.4s; quality suite identical
(39/40 QA, 30/30 cloze); decode steady 33-37 t/s.

All off by default (FT_GDN_HOST_TIER / FT_PREFIX_HOST envs).
gdevenyi added a commit to gdevenyi/FreeToken that referenced this pull request Sep 19, 2026
gdevenyi added a commit to gdevenyi/FreeToken that referenced this pull request Sep 19, 2026
…cales they cannot mirror

Upstream FlashML-org#499/FlashML-org#525 mirror KV codes plus the fp8 pool's per-token _scale_buffer; the nvfp4
pool (FlashML-org#408) keeps per-block scales in _block_scale_buffer, which neither the offloader nor
the prefix tier copies, so --kv-host-pages or FT_PREFIX_HOST would rehydrate codes without
scales. Both constructors now raise a clear error naming the knob; the GDN-snapshot-only
tier (FT_GDN_HOST_TIER) never reads the KV pool and stays usable.

Assisted-by: Claude Fable 5.1

This branch has not been deployed

No deployments
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.

1 participant