Skip to content

[Fix] HiCache: reclaim redundant host mirrors after storage backup (write_through host-pool starvation) - #33862

Open
phlv wants to merge 2 commits into
sgl-project:mainfrom
phlv:fix/hicache-release-host-mirror-after-backup
Open

phlv wants to merge 2 commits into
sgl-project:mainfrom
phlv:fix/hicache-release-host-mirror-after-backup

Conversation

@phlv

@phlv phlv commented Aug 6, 2026

Copy link
Copy Markdown

Motivation

Under --hicache-write-policy write_through with a storage backend (--hicache-storage-backend mooncake etc.), the host (L2) pool deadlocks whenever it is smaller than the device pool, silently starving the storage tier. Three code facts combine into this:

  1. Every inserted node immediately pins a host mirrorwrite_backup() allocates host slots for every node that enters the device radix cache (threshold 1 under write_through).
  2. The backup ack never frees the slot_drain_backup() only calls entry.release_host(), which decrements host_ref_counter; the memory stays attached to node.host_value.
  3. The only reclaimer refuses device-resident nodesevict_host() guards with if not x.evicted: continue, so a mirror can only be reclaimed after the device copy is evicted. The in-band rescue inside write_backup() (evict_host then retry) is a no-op for the same reason.

Under sustained load the device pool stays full, so pinned-mirror demand approaches the full device pool size. With hicache_size below device KV capacity the host pool fills with irreclaimable mirrors, and both staging (hicache_write_rejected_tokens_total{reason="host_alloc_failed"}) and storage prefetch (same reason, stage="storage_prefetch") are rejected wholesale — storage starves in both directions. The default hicache_ratio 2.0 hides the bug by capping mirror demand at half the pool, at the cost of a device-pool-sized slice of host DRAM that is permanently pinned and never reused.

On a production 16-rank deployment (hicache_size ≈ 0.25× device pool, mooncake backend) we measured 329M tokens of write rejections vs 21.6M actually written (94% rejected), hicache_host_reserved_tokens{purpose="write"} pinned at 0, and a storage hit contribution of 0.55%.

Modifications

Make the backup ack meaningful, so the host pool works as a channel into storage instead of a second permanent copy:

  • Durable tracking: pages confirmed written to storage are marked durable in a bounded LRU keyed by page hash (an ack is not success — only the completed_tokens prefix is marked; hash-keying also covers nodes split mid-backup).
  • Release candidates: a node with device copy resident and all pages durable holds a redundant mirror and becomes releasable. Membership is state-derived; busy nodes (lock_ref/host_ref_counter) are kept, not lost.
  • TP-consistent release: candidates are released only under host-pool pressure via a prepare/commit protocol riding the existing storage-drain collective — each rank contributes its pool deficit and last round's release plan (count/tokens/digest as negated pairs, so one MIN all-reduce recovers both min and max), and a plan executes only when every rank reports the identical triple. Mutation never precedes consensus; repeated mismatch fail-closes the feature back to current behavior. Synced ack slots keep MLA-style single-writer deployments (backup_skip) convergent.
  • Eviction integration: demoted nodes leave the candidate set; a released-mirror parent whose children were demoted is evicted by pruning its host-only subtree (data is durable in storage or recomputable), so eviction always makes progress; durable marks are revoked when a node leaves the tree entirely.
  • Anti-churn: a fully-durable node skips re-staging (age-gated so storage-side eviction can eventually be refreshed).

Opt-in, default off: SGLANG_HICACHE_MIRROR_RELEASE=1 enables the feature (_FREE_FRAC, SGLANG_HICACHE_DURABLE_CAPACITY, _REFRESH_ROUNDS tune it). With the flag unset, one added branch runs per drain round and behavior is unchanged. When enabled, check_hicache_events routes storage draining through drain_storage_control_queues (which carries the protocol fields) instead of the fused ready-counts collective.

Results on the same 16-rank deployment, same load, only the flag flipped: storage write-out 11.3% → 100%, host_alloc_failed write rejections 16.7M tokens → 0, warm-round storage hit 0.53% → 3.17% (12.87% across two engines sharing one mooncake backend, with 2× wall-clock speedup on the warm round).

The same pinning pattern exists in unified_radix_cache.py / hi_mamba_radix_cache.py; this PR scopes to HiRadixCache and the others can follow once the approach is agreed on.

Accuracy Tests

Not applicable — no model-output path is touched. Correctness of the cache state machine is covered by the unit tests below; a released mirror is only ever dropped after the storage backend acked the covering pages, and any uncertainty (partial ack, consensus mismatch, busy nodes) keeps the mirror.

Benchmarking and Profiling

Numbers above; measured with the hicache storage metrics (write_rejected/storage hit source deltas per benchmark arm) on 128-session × 2-round shared-prefix load, 8-way concurrency, cold + warm arms. Feature off shows no measurable drain-round overhead (single boolean branch).

Checklist

  • Format your code according to the Code Formatting with Pre-Commit.
  • Add unit tests as outlined in the Running Unit Tests: test/registered/unit/mem_cache/test_hicache_mirror_release.py — 26 falsifiable cases (they fail on the unfixed code) covering durable marking with partial acks, split-during-backup, TP consensus commit/mismatch/fail-close, MLA single-writer, released-parent eviction progress, churn guard, and a concurrency regression for the ack-queue snapshot.
  • Update documentation / docstrings as needed.
  • Provide accuracy and speed benchmark results above.

🤖 Generated with Claude Code


CI States

Latest PR Test (Base): ❌ Run #31091637794
Latest PR Test (Extra): ❌ Run #31091637176

…er write_through

Under write_through with a storage backend enabled, every inserted node
immediately stages a host mirror, but the only reclaim path
(evict_host) skips any node whose device copy is still resident, and
the storage backup ack merely drops a refcount without freeing the
slot.  The mirror therefore stays pinned until the device copy is
evicted.  Under sustained load the device pool stays full, so pinned
mirror demand approaches the full device pool size: whenever the host
pool is smaller than the device pool (hicache_size below device KV
capacity), the host pool fills with irreclaimable mirrors, every new
staging and every storage prefetch fails with host_alloc_failed, and
the storage tier starves in both directions.  The default
hicache_ratio=2.0 hides this by capping mirror demand at half the pool
-- at the cost of a device-pool-sized slice of host memory that is
permanently pinned.

This change makes the backup ack meaningful: pages confirmed written
to storage are marked durable (bounded LRU keyed by page hash, so
mid-backup node splits are covered), and a node whose device copy is
resident and whose pages are all durable becomes a release candidate.
Candidates are released only under host-pool pressure, through a
prepare/commit protocol that rides the existing storage-drain
collective: each rank contributes its pool deficit and last round's
release plan (count/tokens/digest, negated pairs so a single MIN
recovers min and max), and a plan executes only after every rank
reports the identical triple -- mutation never precedes consensus.
Repeated mismatch fail-closes the feature back to today's behavior.
Acked completed-token counts are also carried in the collective so
MLA-style deployments where only one rank writes storage
(backup_skip) still converge.

Opt-in and default-off: set SGLANG_HICACHE_MIRROR_RELEASE=1 to enable
(SGLANG_HICACHE_MIRROR_RELEASE_FREE_FRAC tunes the free-space target,
SGLANG_HICACHE_DURABLE_CAPACITY the durable-hash LRU,
SGLANG_HICACHE_MIRROR_REFRESH_ROUNDS the re-stage age gate).  With the
flag unset a single added branch runs per drain round.

Measured on a 16-rank deployment with hicache_size ~0.25x the device
pool and a mooncake backend (128-session shared-prefix load): storage
write-out rate went from 11.3% to 100%, host_alloc_failed write
rejections from 16.7M tokens to zero, and warm-round storage hit rate
from 0.53% to 3.17% (12.87% across two engines sharing the backend).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A durable node demoted to host-only and later restored on device via the
insert() KV-recomputation paths holds a redundant mirror again, but
neither recompute branch re-registered it -- the mirror stayed pinned
forever, quietly rebuilding the leak this feature removes under
evict-then-recompute churn.  Register both the full-match and the
post-split recompute branches, mirroring what load_back already does,
and add a regression test.

Found by adversarial review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 8, 2026
…project#32035 sgl-project#33656 sgl-project#32183 sgl-project#33145)

Applied PRs (latest from GitHub):
  sgl-project#33288  Indexer logits OOM fix
  sgl-project#30393  HiCache packed/sidecar draft caches
  sgl-project#31170  DPA prefix_affinity load balancing
  sgl-project#33795  DSpark compact ragged-verify CUDA graph JIT race
  sgl-project#32467  C128 plan-kernel warp barrier
  sgl-project#33865  DSpark x prefill CP unblock
  sgl-project#30371  SWA state pool sizing (storage page)
  sgl-project#33358  FlashMLA norm-rope K-tokens-per-block ILP
  sgl-project#33872  num_draft_tokens clamp + extend_len==0 skip (supersede sgl-project#32183)
  sgl-project#34002  Sidecar backup vacuously-successful fix (replaces sgl-project#33656, with tests)
  sgl-project#33862  Reclaim redundant host mirrors after storage backup
  sgl-project#31315  Avoid repeated Mooncake gets after stale hits
  sgl-project#32327  Q8KV8 sparse MLA prefill backend (flashmla_sparse_q8)
  sgl-project#31668  Fix sidecar pool life-time (use-after-free on prefetch abort)
  sgl-project#31195  TP0 verify-token-budget broadcast (adapted to get_schedule() API)

Dropped (per user request or superseded):
  sgl-project#32771  IndexCache C4 top-k reuse — has bug
  sgl-project#32035  DSpark C128 online compressor — has bug
  sgl-project#33656  Superseded by sgl-project#34002 (same fix + unit tests)
  sgl-project#32183  Superseded by sgl-project#33872 (included in supersede PR)
  sgl-project#33145  Base f01f706 already has superior reasoning-effort profile system

Conflicts resolved:
  sgl-project#31195: adapted to base get_schedule().disable_overlap_schedule API
  sgl-project#32327: path remapped jit_kernel/ -> kernels/jit/ and kernels/ops/attention/
  sgl-project#31668: applied cleanly on top of sgl-project#30393+sgl-project#34002+sgl-project#33862 modifications
Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 25, 2026
…1425) not active on unified path

Production DSV4-Flash runs UnifiedRadixCache (hybrid_swa), not
HiRadixCache. These two cherry-picks only modified hiradix_cache.py
(+ cache_controller two-phase API consumed solely by hiradix) and thus
never take effect in production, misleading future readers:

- sgl-project#33862 Host mirror release: hiradix-only (unified path has no
  equivalent; SGLANG_HICACHE_MIRROR_RELEASE env was a no-op). Also
  staged 248 lines of unrelated pyproject.toml dep churn.
- sgl-project#31425 TP/PP write-through & load-back consensus: the all_reduce(MIN)
  consensus lives in hiradix_cache.py only; unified's _execute_kv_backup
  calls hybrid_cache_controller.write() and never triggers it.

Keep: sgl-project#30689 (base_prefix_cache InsertParams.is_finished field is a
dependency of the unified is_finished guard) and sgl-project#26837 (metrics, shared).
Keep: 072c953 unified is_finished guard (the actual production fix).

Reverts commits 4266d89 and 29b0753.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hicache Hierarchical Caching for SGLang

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant