[None][fix] Emit KV cache removed events for silently detached reuse-tree blocks#16335
[None][fix] Emit KV cache removed events for silently detached reuse-tree blocks#16335Shang-Pin wants to merge 6 commits into
Conversation
WindowBlockManager::blockInRadixTree() tested `getPrevBlock() != nullptr`, but getPrevBlock() walks mLookupNode -> parentNode -> getValue(windowSize) and returns nullptr whenever the parent node's value slot is empty — which happens as soon as the parent block is reclaimed. So the check answered "does my parent still hold a block", not "am I in the reuse trie": every descendant of an already-evicted ancestor was misreported as not-in-tree at its own reclaim, and getFreeBlock()/releaseSubtree() then skipped its KV removed event while still detaching it from the trie. Consumers of the KV cache event stream (e.g. external/disaggregated KV routers that mirror worker caches from stored/removed events) therefore never learn those blocks were evicted and leak the hashes indefinitely. Impact scales with how unshared the block chains are. Measured on a disaggregated prefill worker serving diverse prompts (deep, unshared chains whose ancestors are evicted early): ~97% of reclaimed announced blocks emitted no removed event (15,720 suppressed vs 436 emitted in one 30s window once the pool began recycling), and a downstream KV router's radix tree grew to ~950x the worker's physical block capacity before OOM. Aggregated / decode workloads with heavily shared prefixes keep parent chains valued and were largely unaffected, which is why this went unnoticed. Fix: test trie membership directly via getLookupNode() != nullptr. All callers of blockInRadixTree() are removed/updated event-emission guards and want exactly membership semantics. Signed-off-by: Shang-Pin <shang-pin@deepinfra.com>
WindowBlockManager::freeLeafBlock — the partial-leaf reuse path in onboardAndAllocateBlocks — detaches a block from the reuse trie without emitting a KV "removed" event. The block's hash was announced via enqueueStoredEvent when it was stored, so any consumer that mirrors the cache from stored/removed events retains the hash forever. Measured on a disaggregated prefill worker running the companion blockInRadixTree membership fix: this was the only remaining silent trie-detach path (detach_total == getFreeBlock emissions + freeLeafBlock detaches, exactly), at ~4 blocks/s under production load (~350k orphaned hashes per day per worker). Emit the removed event before detaching, under the same mEventManager && blockInRadixTree guard used by getFreeBlock and releaseSubtree. Signed-off-by: Shang-Pin <shang-pin@deepinfra.com>
📝 WalkthroughWalkthrough
ChangesKV cache event handling
Estimated code review effort: 1 (Trivial) | ~5 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cpp/tensorrt_llm/batch_manager/kvCacheManager.cpp`:
- Around line 1233-1243: Remove the newly added enqueueRemovedEvent call from
releaseSubtree’s block-detachment path to prevent duplicate removal events, and
retain a single emission in freeLeafBlock before the block is detached. Preserve
the existing event behavior for blocks removed through releaseSubtree and other
callers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 8a5ffdd3-282f-4f71-bef6-1856cd749204
📒 Files selected for processing (1)
cpp/tensorrt_llm/batch_manager/kvCacheManager.cpp
releaseSubtree's per-block guarded emit + detach is exactly what WindowBlockManager::freeLeafBlock now does; call it instead so the removed-event contract lives in one place. Signed-off-by: Shang-Pin <shang-pin@deepinfra.com>
nvxuanyuc
left a comment
There was a problem hiding this comment.
LGTM. This PR:
- Fixes silent block detaches without a removed event emit.
- Corrects the radix tree membership check.
Both case tests need new plumbing of event manager in current unit testing fixtures, should be low-effort.
|
/bot run --disable-fail-fast |
|
PR_Github #59276 [ run ] triggered by Bot. Commit: |
|
@Shang-Pin Would you like to add test coverage for the code changes? |
SimengLiu-nv
left a comment
There was a problem hiding this comment.
Code changes looks good to me. Request coverage tests to be added.
…leaf reuse paths KVCacheManagerEventRemovedForBlocksWithEvictedAncestors: retention priorities force the eviction policy to reclaim a chain head before its descendant; every hash announced via Stored must still be un-announced via Removed when its block is reclaimed. Fails before the blockInRadixTree membership fix (the descendant's reclaim was misreported as not-in-tree once the ancestor's lookup-node value was cleared) and passes after. KVCacheManagerEventRemovedOnPartialLeafReuse: a request partially matching a stored leaf makes onboardAndAllocateBlocks detach that leaf via freeLeafBlock for re-store by the reuser; the detach must emit a Removed event for the announced hash. Fails before the freeLeafBlock emission fix and passes after. Signed-off-by: Shang-Pin <shang-pin@deepinfra.com>
|
@SimengLiu-nv Added in 359157c — two regression tests in
Validated both directions by compiling the test binary once and running it against a pre-fix and a post-fix
|
|
PR_Github #59276 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #59477 [ run ] triggered by Bot. Commit: |
|
PR_Github #59477 [ run ] completed with state
|
Description
The KV cache event stream (
stored/removedevents) is the contract externalconsumers use to mirror a worker's reuse tree — e.g. a disaggregated KV router
doing prefix-overlap routing. Two paths in
WindowBlockManagerdetach blocksfrom the reuse trie without emitting a
removedevent, so mirrors leak thosehashes forever. This PR fixes both.
Fix 1:
blockInRadixTree()tests the wrong thingWindowBlockManager::blockInRadixTree()usesgetPrevBlock() != nullptrasits "is this block in the reuse trie" test. But
getPrevBlock()walksmLookupNode -> parentNode -> getValue(windowSize)and returnsnullptrwhenever the parent node's value slot is empty — which happens as soon as
the parent block is reclaimed. So the check actually answers "does my parent
still hold a block", not "am I in the trie".
Consequence: every descendant of an already-evicted ancestor is misreported as
not-in-tree at its own reclaim.
getFreeBlock()andreleaseSubtree()guardtheir
enqueueRemovedEvent()calls withblockInRadixTree(), so those blocksare silently detached without a
removedevent.All callers of
blockInRadixTree()areremoved/updatedevent-emissionguards and want exactly membership semantics;
getLookupNode()is thecanonical membership accessor already used elsewhere in this file.
Fix 2:
freeLeafBlock()detaches without an eventWindowBlockManager::freeLeafBlock()(the partial-leaf reuse path inonboardAndAllocateBlocks) callsblock->freeLeafBlock(), which unlinks theblock from the trie directly — bypassing the event-emitting reclaim paths
entirely. Emit a
removedevent before detaching, under the samemEventManager && blockInRadixTree(block)guard used bygetFreeBlock()andreleaseSubtree().With Fix 1 alone in production, per-worker instrumentation still showed
detaches split exactly into evicted-with-event + freeLeafBlock-silent
(counter arithmetic:
detach_total == evict_emit + free_leaf_silent), i.e.this was the one remaining silent path.
Impact
Severity scales with how unshared the cached block chains are:
chains whose ancestors are evicted early. Measured on such a worker: once
the KV pool began recycling announced blocks, ~97% of reclaims emitted no
removed event (15,720 suppressed vs 436 emitted in a single 30s window),
and a downstream KV router's radix tree grew to ~950x the worker's
physical block capacity (tens of millions of tracked blocks vs ~74K real)
before OOM.
chains valued, so removes are emitted normally — which is why this has gone
unnoticed. Verified: an aggregated gpt-oss-120b worker's event stream had a
stored:removed block ratio of 0.99.
Verification
the same pool-full point that previously suppressed 15,720 removes/30s, the
fixed build emitted 11,385 with zero suppression; the publisher's cumulative
stored-minus-removed block count settled at ~the physical pool size (mirror
invariant restored), and the downstream KV router's tracked-block count went
from ~9.4M/hour unbounded growth to flat.
fixes (engine
.soswap only) was placed back in the same fleet — therouter's per-worker tracked-block count immediately resumed unbounded
growth, while fixed workers stayed flat. The regression is attributable to
exactly these two code paths.
Test Coverage
The change is two guarded
enqueueRemovedEvent()emissions on existingreclaim paths; existing KV cache manager event-stream unit tests cover the
emission plumbing. Happy to add targeted unit tests asserting (a) a descendant
of an evicted ancestor emits
removedon reclaim and (b)freeLeafBlockemits
removed, if maintainers can point at the preferred fixture forconstructing a partial-leaf reuse state.
PR Checklist
Summary by CodeRabbit