Skip to content

fix(unified-cache): allow multiple concurrent load-back pins per node - #34975

Closed
ffahleraz wants to merge 2 commits into
sgl-project:mainfrom
ffahleraz:fix/unified-loadback-multipin
Closed

ffahleraz wants to merge 2 commits into
sgl-project:mainfrom
ffahleraz:fix/unified-loadback-multipin

Conversation

@ffahleraz

@ffahleraz ffahleraz commented Aug 15, 2026

Copy link
Copy Markdown

Motivation

Under heavy device-eviction↔load-back cycling (small-VRAM GPUs with --enable-hierarchical-cache, hybrid GDN/mamba models), the scheduler crashes with:

AssertionError: node 2498 pinned by load-back 2563, new anchor 2498

at UnifiedTreeCore.commit_load_back (unified_tree_core.py). Hit repeatedly serving nvidia/Qwen3.6-35B-A3B-NVFP4 + EAGLE + hicache ratio-12 on RTX 5090 (32GB) under a production-trace replay; 96GB GPUs never trip it (no deep evicted chains → no overlap).

Root cause

UnifiedTreeNode.load_back_pending_id is single-valued, but two live load-backs can legitimately pin the same node:

  1. Request A load-backs anchored at a descendant; its Full-KV chain covers the evicted ancestor and pins it (load_back_pending_id = A).
  2. Before A's ack, request B anchors at that ancestor to restore its independently-evicted mamba state (mamba_component.build_hicache_transfers pins [node.id]).
  3. The single pin slot can't represent both live pins → assertion (new anchor == the node itself, matching the production message shape exactly).

The overlap is semantically safe: both in-flight DMAs only read the shared host slots, and their writes target disjoint destinations (Full-KV pool vs mamba pool). Only the bookkeeping cannot represent it.

Modifications

  • load_back_pending_id: Optional[int]load_back_pending_ids: set[int].
  • commit_load_back adds the anchor id (idempotent for a node sitting in both the Full and an aux transfer list of one anchor).
  • finish_load_back discards its anchor id along the root path; duplicate tracking updates once the last pin drains.
  • Reclaim guards (_is_settled_full_host_duplicate, _can_reclaim_full_host_duplicate) and the sanity check test set-emptiness; split inheritance copies the set.

Test / repro

test/registered/unit/mem_cache/test_unified_loadback_multipin.py — a deterministic CPU repro built on the existing UnifiedRadixCacheSuite fixtures (FULL+MAMBA): backup → full evict → load-back anchored at the leaf (ack withheld) → load-back anchored at the pinned ancestor.

  • Unpatched: fails in 0.3s with the exact production assertion.
  • Patched: passes; pins drain to empty after both acks; sanity_check clean.
  • Full inherited FULL+MAMBA suite under the new class: 107 tests green.
  • Serving-level: the patched server sustains 400-span production-trace replays at hicache ratio-12 on RTX 5090 with zero assertions (previously crashed mid-run).

Checklist


CI States

Latest PR Test (Base): ❌ Run #31902335393
Latest PR Test (Extra): ❌ Run #31902335225

The load_back_pending_id pin slot was single-valued, so two live
overlapping load-backs on one node hit the commit_load_back assertion
('node X pinned by load-back Y, new anchor X'). This happens under heavy
device-eviction<->load-back cycling on small VRAM: one request's Full-KV
chain pins an evicted ancestor while another request anchors at that
ancestor for its independently-evicted mamba state. Overlapping
transfers only read the shared host slots and write disjoint
destinations, so concurrent pins are safe; track them as a set and
release each anchor's pin at its ack.
A first load-back anchored at a descendant pins the evicted ancestor via
its Full-KV chain; before its ack a second load-back anchors at that
ancestor for its independently-evicted mamba state. On the previous
single-valued pin this reproduced the production crash
'AssertionError: node X pinned by load-back Y, new anchor X' in 0.3s.
@200lz

200lz commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

This looks like the right fix for concurrent ownership. One lifecycle edge case that may be worth making explicit: should a pin identify only the anchor ID, or the specific load-back generation/component as well? If an anchor/node ID can ever be reused while a delayed ack is still in flight, a generation-tagged pin could avoid an ABA-style release of a newer load-back.

@ffahleraz

Copy link
Copy Markdown
Author

Good question — I checked this specifically. Anchor pins are keyed by node id, and UnifiedTreeNode.counter is a process-lifetime monotonic class attribute (unified_tree_core.py: self.id = UnifiedTreeNode.counter; UnifiedTreeNode.counter += 1) that is never rewound — UnifiedTreeCore.reset() rebuilds the node arena/root/LRUs but does not touch the counter. So a node id (and therefore an anchor id) can never be reused within a process, including across cache resets with delayed acks in flight — the ABA release can't occur. If id recycling were ever introduced, a generation tag would indeed be needed, but today monotonicity makes the anchor id itself the generation.

@xiezhq-hermann

Copy link
Copy Markdown
Collaborator

can you confirm this is a fix for write back policy right?

@xiezhq-hermann

Copy link
Copy Markdown
Collaborator

@ziang663 can you help take a look?

@ziang663

Copy link
Copy Markdown
Contributor

I think the fix is correct. Retrieving the set allows the same node to be pinned by multiple requests, while there's an assumption that the components must be different, which I believe can be guaranteed.

@hzh0425 hzh0425 self-assigned this Aug 20, 2026
@hzh0425

hzh0425 commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

@ffahleraz pls fix the lint

Oasis-Git added a commit to SemiAnalysisAI/InferenceX that referenced this pull request Aug 21, 2026
DEP8 at conc 384/512 dies mid-run on an assertion in unified_tree_core.py
commit_load_back: load_back_pending_id holds a single anchor, but a node can
legitimately be pinned by two concurrent H->D load-backs. Eight schedulers
exit together and the surviving DP ranks block forever in the MLP-sync
collective, which surfaces as a frozen in-flight count with zero errors.

sgl-project/sglang#34975 fixes it (the pin becomes a set); it is still open,
so this points at sglang-staging:dev-cu13-pr-35880 -- build commit 2fca6c4e35,
which is dev-nightly-0820 (92eeed41d7) with #35880 cherry-picked on top.
Same CUDA 13.0.3, same layer count. Swap to a released tag once it merges.
@weireweire

Copy link
Copy Markdown
Contributor

Does swa / mamba really need load_back_pending_id ? It should be designed to protect full kv, because the full host duplicate reclaim can evict full-kv under transfer, as the full kv lock only effect anchor node.

But for swa / mamba, 1. they won't be reclaim by dup, 2. their lock will protect full window/state, so the real bug maybe just that they should not participate in the full kv's load_back_pending_id .

Wonder if this simple change can fix the issue you see: e16de90

cquil11 added a commit to SemiAnalysisAI/InferenceX that referenced this pull request Aug 25, 2026
* [NVIDIA][AgentX] DeepSeek-V4 B300 SGLang: mega-MoE + prefill-decode-interval (+28%)

Retunes the existing B300 SGLang AgentX recipe. The headline change is
--prefill-decode-interval 20 (sgl-project/sglang#35017): under speculative
decoding plus DP attention SGLang synchronises decode globally, so a rank
with no prefill work runs an idle batch and the busiest rank sets the clock
for all eight. Measured at conc 128 over 1800 s on 8xB300, that takes output
throughput from 2,433-2,470 to 3,127-3,161 tok/s and closes the gap to the
vLLM recipe from 1.32x to 1.03x.

Every engine-side change is scoped to the DP-attention path; the TP-only
path is left exactly as upstream had it, because all measurements here ran
with DP attention enabled.

Also restructures the search space from 47 points to 12 and raises the CPU
tier to match the vLLM agentic lane on the same runner.

* [NVIDIA][AgentX] Condense the DSv4 HiCache ratio comment

No functional change; retriggers CI.

* [NVIDIA][AgentX] Fix DEP4 startup: mem-fraction-static must clear the weights floor

DEP4 shards the model over half the node, so per-rank weight memory roughly
doubles and the weights-only floor rises above 0.9. The engine refuses to
start with 'Loaded weights leave no GPU memory for the KV cache' and reports
a minimum viable 0.9013. Keep upstream's 0.95 there; raise DEP8 to 0.93
(0.92 at the conc>=512 tail).

* [NVIDIA][AgentX] Lower TP>=8 hicache ratio to 3 to fit host memory

hicache capacity is a host/device token ratio, so host bytes scale with
device KV and therefore with mem-fraction-static -- the two knobs multiply.
ratio=4 at mem-fraction 0.93 left only 5.84 GB free on a 2,964 GB node and
the V4 paged pool failed to allocate (requested 8.70 GB). ratio=3 keeps the
tier near 2 TB with headroom.

* [NVIDIA][AgentX] Scale DEP8 mem-fraction-static down with concurrency

MegaMoE's transient workspace sits outside the static allocation and needs a
single ~7 GB contiguous block, so required headroom grows with batch size.
At conc 256, 0.93 (~16 GB free) and 0.95 (~11 GB free) both OOM one DP rank,
which hangs the engine in the MLP-sync collective; 0.835 (~42 GB free) runs.
Ladder: 0.93 at conc 64/128, 0.9 at 256, 0.89 at 384, 0.875 at 512/576.

* [NVIDIA][AgentX] Size TP-only decode graphs for subagent fan-out

AgentX concurrency counts session trees, not requests, and the recipe already
sets max-running-requests to 2*CONC to allow fan-out. Capturing decode graphs
only up to CONC therefore dropped every larger batch to eager decode on the
conc 1/4/8 rows. Capture to 4*CONC (still capped at 64); the runtime clamps
to the request-pool size, so it cannot over-capture.

* [NVIDIA][AgentX] Point changelog entry at PR #2701

* [NVIDIA][AgentX] Lower DEP4 mem-fraction-static to 0.93

0.95 leaves only ~11 GB of GPU headroom -- the same margin that OOM'd a rank
at DEP8 conc 256 and hung the engine. 0.93 gives ~16 GB, matching what DEP8
conc 128 runs with at an identical per-rank load (max-running/dp = 32).

* [NVIDIA][AgentX] Use dev-nightly-0820 + HiCache load-back fix

DEP8 at conc 384/512 dies mid-run on an assertion in unified_tree_core.py
commit_load_back: load_back_pending_id holds a single anchor, but a node can
legitimately be pinned by two concurrent H->D load-backs. Eight schedulers
exit together and the surviving DP ranks block forever in the MLP-sync
collective, which surfaces as a frozen in-flight count with zero errors.

sgl-project/sglang#34975 fixes it (the pin becomes a set); it is still open,
so this points at sglang-staging:dev-cu13-pr-35880 -- build commit 2fca6c4e35,
which is dev-nightly-0820 (92eeed41d7) with #35880 cherry-picked on top.
Same CUDA 13.0.3, same layer count. Swap to a released tag once it merges.

* [NVIDIA][AgentX] Correct search-space point count in changelog

The removed rows total 42 points (7 + 12 + 6 + 10 + 7), not 47.

* [NVIDIA][AgentX] Lower DEP8 conc 512/576 mem-fraction-static to 0.86

* [NVIDIA][AgentX] Lower DEP8 conc 384 mem-fraction-static to 0.88

* [NVIDIA][AgentX] Drop DEP4 rows; extend TP-8 and DEP8 concurrency

DEP4 cannot serve this trace. Sharding DeepSeek-V4 over half the node leaves
~627k KV tokens per rank at mem-fraction 0.93 (measured from the hicache host
pool / ratio 8), against prompts reaching 950,812 tokens -- the longest
requests cannot be held at all. Raising mem-fraction far enough to hold them
leaves too little room for the mega-MoE workspace, so the two constraints
barely overlap; every DEP4 point failed.

Search space is now TP-8 no-offload at conc 1/4/8/16/32 and DEP8+hicache at
conc 32/64/128/256/384/512/576 -- still 12 points.

* [NVIDIA][AgentX] Enable router retries so one transient send failure cannot abort a run

The recipe launched sglang_router with --disable-retries. A single
transient router->engine send failure ("error sending request") then
surfaces as a 500, and AgentX treats a failed root warmup request as
fatal: "ProfileAborted: A root AgentX warmup request failed, so
profiling was not started." The engine stays healthy throughout -- it
keeps logging prefill batches and serving /metrics 200 OK -- so the run
dies with no result and no crash to point at.

Measured on 8xB300 / DeepSeek-V4-Pro at conc 512: three separate 2h15m
arms were killed this way, each by exactly one failed request (2 ERROR
lines in router.log). After enabling retries, one 3600s run logged 22
such transients spread over all 8 DP workers with zero client-visible
500s and errors=0 -- every one recovered.

Retry is safe here: "error sending request" means the request was never
delivered, so there is no partial state, and it cannot inflate
throughput because it only fires on a failed send. Runs that never hit
a transient are unaffected (the three completed baseline arms logged
zero router errors either way).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Update perf-changelog.yaml

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cameron Quilici <cjquilici@gmail.com>
@ffahleraz

Copy link
Copy Markdown
Author

@weireweire you are right, thanks for #36317. Looking at main again, commit_load_back publishes the Full device value before the ack, so the second anchor's Full chain stops above the pinned ancestor and the only thing that pinned it under a different anchor was the mamba transfer, which #36317 now keeps out of load_back_pending_id. There is no case left for the multi-pin set, so I am dropping it.

@hzh0425 @ziang663 sorry for going quiet on the lint. The same workload had a second crash, Can not alloc mamba cache in prepare_for_caching_req when every cached mamba state is pinned, which is still on main. I put that fix in #36770 together with an end-to-end regression for the scenario from this PR, so closing this one as superseded by #36317. Would appreciate a look at #36770 when you have a moment.

@ffahleraz ffahleraz closed this Aug 28, 2026
cquil11 added a commit to SemiAnalysisAI/InferenceX that referenced this pull request Sep 1, 2026
…新 DSV4 B300 SGLang AgentX 镜像和 HiCache 并发网格 (#2759)

* chore(dsv4-b300): move AgentX HiCache MTP lane to a published nightly image

Bump dsv4-fp4-b300-sglang-agentic-hicache-mtp from the one-off staging tag
lmsysorg/sglang-staging:dev-cu13-pr-35880 to the published nightly
lmsysorg/sglang:nightly-dev-cu13-20260827-20621aa1, so every point is
reproducible from a public image.

Image-only change: the write policy, search space, dram-utilization and all
serving flags from #2701 are left untouched. --prefill-decode-interval is
retained because sgl-project/sglang#35017 merged before this nightly's build
commit. The HiCache load-back fix (sgl-project/sglang#34975 and its
cherry-pick #35880) is still unmerged, so the DEP8 conc 384/512/576 crash is a
known risk; this is recorded in perf-changelog.yaml.

将 dsv4-fp4-b300-sglang-agentic-hicache-mtp 的镜像从一次性构建的 staging 标签
lmsysorg/sglang-staging:dev-cu13-pr-35880 切换到已发布的 nightly
lmsysorg/sglang:nightly-dev-cu13-20260827-20621aa1,使所有数据点均可基于公开
镜像复现。

本次仅改动镜像:#2701 引入的写策略、搜索空间、dram-utilization 及全部服务参数
均保持不变。由于 sgl-project/sglang#35017 已在该 nightly 的构建提交之前合并,
--prefill-decode-interval 得以保留。HiCache load-back 修复(sgl-project/sglang#34975
及其 cherry-pick #35880)仍未合并,因此 DEP8 并发 384/512/576 存在已知的崩溃
风险,该风险已记录在 perf-changelog.yaml 中。

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(changelog): point the entry at PR #2759

Replace the placeholder pr-link now that the PR number exists.

PR 号确定后,将 changelog 条目中的占位 pr-link 替换为实际链接。

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(dsv4-b300): drop DEP8 concurrency 64 and 128 from the hicache row

Concurrency 128 failed on run 33051183882 with a CUDA OOM inside deep_gemm
fp8_fp4_paged_mqa_logits (5.35 GiB requested, 4.55 GiB free) on DP ranks 3, 4
and 5, crashing scheduler_0 and aborting AIPerf during warmup. Concurrency 64
shares the same mem-fraction-static 0.93 tier and is dropped with it.

The changelog entry also corrects the previous entry's prediction: DEP8
concurrency 384, 512 and 576 passed on this image, so the HiCache load-back
failure did not reproduce.

并发 128 在 run 33051183882 上因 deep_gemm fp8_fp4_paged_mqa_logits 内的 CUDA
OOM 失败(请求 5.35 GiB,仅剩 4.55 GiB),DP rank 3、4、5 同时报错,导致
scheduler_0 崩溃并使 AIPerf 在 warmup 阶段中止。并发 64 与其同属
mem-fraction-static 0.93 档位,一并移除。

changelog 同时修正了上一条目的预测:DEP8 并发 384、512、576 在该镜像上均通过,
HiCache load-back 失败并未复现。

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cam Quilici <cjquilici@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants