Skip to content

[None][feat] Reuse freshly out-of-window SWA pages for new decode blocks in KV cache manager v2#16288

Open
lishicheng1996-nv wants to merge 1 commit into
NVIDIA:mainfrom
lishicheng1996-nv:feat/swa-decode-ring-reuse
Open

[None][feat] Reuse freshly out-of-window SWA pages for new decode blocks in KV cache manager v2#16288
lishicheng1996-nv wants to merge 1 commit into
NVIDIA:mainfrom
lishicheng1996-nv:feat/swa-decode-ring-reuse

Conversation

@lishicheng1996-nv

@lishicheng1996-nv lishicheng1996-nv commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Description

During decode with a sliding-window-attention (SWA) layer, every tokens_per_block tokens the window slides across a block boundary: one out-of-window block is retired and one new block is allocated. Today the staled page takes a full free-list round-trip (release with a ready event, later re-allocation with cross-stream event bookkeeping) while the new block draws a different slot from the global allocator.

This PR adds an opt-in per-sequence reuse pocket to KV cache manager v2 (kv_cache_config.enable_swa_decode_slot_reuse, default off): a resize() that orphans a freshly staled page parks it; a later resize() that needs a new block consumes the parked page's slot directly. On the same CUDA stream the recycled slot is immediately safe to overwrite, so e.g. for a 131-token window a sequence's SWA footprint is served by the same ~3 physical blocks in rotation instead of continuous allocate-then-free.

Safety gating (behavior is unchanged except for which physical slot backs the new block):

  • only fully-orphaned pages are parked: uncommitted, with committing disallowed for the cache (so the block radix tree cannot retain them for prefix reuse) and GPU-resident;
  • the pocket is bounded (2 pages per life cycle), consumed and refilled only on resize() success paths, so the OutOfPagesError revert path (_lock_held_blocks) is unaffected;
  • the pocket is drained on suspend()/close().

Plumbed through KvCacheConfig.enable_swa_decode_slot_reuse (prototype) -> pyexecutor KVCacheManagerV2 -> KVCacheManagerConfig.

Test Coverage

  • New TestSwaDecodeSlotReuse in tests/unittest/kv_cache_manager_v2_tests/test_kv_cache_manager_v2.py: drives a disaggregated-decode-style uncommitted generation across 8 window advances with FakeEngine refcheck at every step, plus deterministic allocator-traffic assertions:

    • flag off: one slot request per boundary crossing per life cycle (2 x crossings);
    • flag on: the windowed life cycle allocates only until its first page is parked (crossings + 1).
  • Existing TestNoBatching battery passes unchanged under the patched module (flag off is a no-op: two cheap branches).

  • Perf A/B on DeepSeek-V4 disaggregated GEN (GB200, dep32/EP32, 256K ISL, per-rank batch 84, MTP3, MoE backend MEGAMOE_DEEPGEMM, 2688/2688 requests completed on both arms):

    arm output tok/s per GEN GPU mean TPOT
    flag off 3481.6 26.932 ms
    flag on 3513.2 (+0.9%) 26.900 ms

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • If PR introduces API changes, an appropriate PR label is added - either api-compatible or api-breaking. For api-breaking, include BREAKING in the PR title.

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, please comment /bot help.

Summary by CodeRabbit

  • New Features

    • Added an experimental option to reuse stale sliding-window-attention cache pages during decoding.
    • Reused pages can reduce new cache-slot allocations and improve memory efficiency.
    • The option is disabled by default and only applies to eligible, fully released pages.
  • Bug Fixes

    • Ensured the new cache-reuse setting is consistently passed through runtime configuration.
  • Tests

    • Added coverage verifying both disabled behavior and reduced allocation activity when reuse is enabled.

…cks in KV cache manager v2

During decode with a sliding window, every tokens_per_block tokens retire one
out-of-window block and allocate one new block. Today the staled page takes a
full free-list round-trip (release with a ready event, later re-allocation with
cross-stream event bookkeeping) while the new block draws a different slot from
the allocator.

This change adds an opt-in per-sequence reuse pocket
(kv_cache_config.enable_swa_decode_slot_reuse, default off): a resize() that
orphans a freshly staled page parks it, and a later resize() that needs a new
block consumes the parked page's slot directly. On the same CUDA stream the
recycled slot is immediately safe to overwrite, so for a 131-token window the
sequence's SWA footprint is served by the same ~3 physical blocks in rotation.

Only fully-orphaned pages are parked: uncommitted, with committing disallowed
for the cache (so the block radix tree cannot retain them) and GPU-resident.
Committed pages kept for prefix reuse are never touched. The pocket is bounded
(2 pages per life cycle), consumed and refilled only on resize() success paths
so the OutOfPagesError revert is unaffected, and drained on suspend()/close().

Adds TestSwaDecodeSlotReuse: a disaggregated-decode-style uncommitted
generation with FakeEngine refcheck at every step plus deterministic allocator
traffic assertions (flag off: one allocation per boundary crossing per life
cycle; flag on: the windowed life cycle allocates only until its first page is
parked).

Signed-off-by: Shicheng Li <shicli@nvidia.com>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an experimental SWA decode slot-reuse configuration, wires it through KV cache manager construction, recycles eligible stale pages during resize, clears parked pages on lifecycle transitions, and tests allocator traffic with the option enabled and disabled.

Changes

SWA decode slot reuse

Layer / File(s) Summary
Configuration and manager wiring
tensorrt_llm/llmapi/llm_args.py, tensorrt_llm/runtime/kv_cache_manager_v2/..., tensorrt_llm/_torch/pyexecutor/kv_cache_manager_v2.py
Adds the experimental enable_swa_decode_slot_reuse option to public configuration types and passes it into KVCacheManagerConfigPy.
Reuse pocket lifecycle
tensorrt_llm/runtime/kv_cache_manager_v2/_core/_kv_cache.py
Tracks parked stale SWA pages per life cycle, consumes them during resize, limits eligibility and capacity, and clears them on close or suspend.
Allocator traffic validation
tests/unittest/kv_cache_manager_v2_tests/test_kv_cache_manager_v2.py
Exercises uncommitted decode with the option enabled and disabled, asserting the expected GPU slot allocation counts.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant KVCacheManagerV2
  participant _KVCache
  participant StorageManager
  participant SWAReusePocket
  KVCacheManagerV2->>_KVCache: resize with reuse enabled
  _KVCache->>SWAReusePocket: consume parked page slots
  _KVCache->>StorageManager: request remaining GPU slots
  _KVCache->>SWAReusePocket: park eligible stale orphaned pages
Loading

Suggested reviewers: mikeiovine

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, follows the required format, and accurately summarizes the main change.
Description check ✅ Passed The description includes the required sections and clearly explains the change, tests, and checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unittest/kv_cache_manager_v2_tests/test_kv_cache_manager_v2.py (1)

658-712: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Solid steady-state coverage; consider adding pocket-lifecycle edge cases in tests/unittest/kv_cache_manager_v2_tests/test_kv_cache_manager_v2.py.

Coverage for the primary decode steady-state (disabled vs. enabled allocator traffic + refcheck correctness) is sufficient and the math checks out. However, the PR's own safety claims aren't directly exercised here:

  • suspend()/close() draining _swa_reuse_pocket (e.g., suspend mid-decode with a warmed-up pocket, then resume and verify the next crossing allocates fresh rather than reusing a stale/invalid pocket entry).
  • The 2-entries-per-life-cycle pocket cap (e.g., a resize() that stales two blocks in one call, such as a larger history_length jump).
  • OutOfPagesError rollback interaction with the pocket (PR description states rollback is unchanged, but no test asserts pocket state survives a failed resize() untouched).

These are follow-up-worthy rather than blocking, given the core mechanism is already well-covered.

🤖 Prompt for 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.

In `@tests/unittest/kv_cache_manager_v2_tests/test_kv_cache_manager_v2.py` around
lines 658 - 712, Extend the KV cache manager tests around
_run_uncommitted_decode to cover pocket lifecycle safety: suspend and close must
drain _swa_reuse_pocket, and after resume the next block crossing must allocate
fresh storage rather than reuse stale entries. Add a resize scenario that stales
two blocks in one call to verify the two-entry-per-lifecycle cap. Add an
OutOfPagesError resize test asserting rollback leaves the pocket state
unchanged.

Source: Path instructions

🤖 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 `@tensorrt_llm/runtime/kv_cache_manager_v2/_core/_kv_cache.py`:
- Around line 888-905: Revalidate each pocketed holder’s page immediately before
reuse in the `_swa_reuse_pocket` pop path, including `cache_level` and
`has_valid_slot` (and existing commitment checks), before calling
`move_to_new_slot()`. Remove stale or no-longer-eligible holders instead of
reusing them, or otherwise pin them against migration so pocketed pages remain
valid until reuse.

---

Nitpick comments:
In `@tests/unittest/kv_cache_manager_v2_tests/test_kv_cache_manager_v2.py`:
- Around line 658-712: Extend the KV cache manager tests around
_run_uncommitted_decode to cover pocket lifecycle safety: suspend and close must
drain _swa_reuse_pocket, and after resume the next block crossing must allocate
fresh storage rather than reuse stale entries. Add a resize scenario that stales
two blocks in one call to verify the two-entry-per-lifecycle cap. Add an
OutOfPagesError resize test asserting rollback leaves the pocket state
unchanged.
🪄 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: 7c06be1f-ab1e-49b8-b52b-8a2d1ccc7082

📥 Commits

Reviewing files that changed from the base of the PR and between 5e77882 and 75943b8.

📒 Files selected for processing (6)
  • tensorrt_llm/_torch/pyexecutor/kv_cache_manager_v2.py
  • tensorrt_llm/llmapi/llm_args.py
  • tensorrt_llm/runtime/kv_cache_manager_v2/__init__.pyi
  • tensorrt_llm/runtime/kv_cache_manager_v2/_config.py
  • tensorrt_llm/runtime/kv_cache_manager_v2/_core/_kv_cache.py
  • tests/unittest/kv_cache_manager_v2_tests/test_kv_cache_manager_v2.py

Comment on lines +888 to +905
# Park this call's freshly staled, fully-orphaned pages for reuse by upcoming
# resize() calls. Qualifying pages are uncommitted with committing disallowed
# (the radix tree cannot retain them; backup_holders holds the sole reference)
# and GPU-resident. Holding the _PageHolder keeps the slot alive. Bounded per
# life cycle to the ring working set (one block retired per boundary crossing,
# +1 for straddle).
if enable_slot_reuse and self._commit_state != self.CommitState.ALLOWED:
for _, _, holder_lc, holder in backup_holders:
if len(self._swa_reuse_pocket[holder_lc]) >= 2:
continue
page = holder.page
if (
page.is_committed()
or not page.has_valid_slot
or page.cache_level != GPU_LEVEL
):
continue
self._swa_reuse_pocket[holder_lc].append(holder)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how held (unlocked) uncommitted pages interact with eviction scheduling.
rg -n "scheduled_for_eviction|schedule_for_eviction|exclude_from_eviction" tensorrt_llm/runtime/kv_cache_manager_v2/_page.py tensorrt_llm/runtime/kv_cache_manager_v2/_storage_manager.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 4142


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== _core/_kv_cache.py around the pocket consumer =="
sed -n '790,835p' tensorrt_llm/runtime/kv_cache_manager_v2/_core/_kv_cache.py

echo
echo "== _page.py around page state, eviction, and _PageHolder =="
sed -n '1,220p' tensorrt_llm/runtime/kv_cache_manager_v2/_page.py
echo
sed -n '220,380p' tensorrt_llm/runtime/kv_cache_manager_v2/_page.py

echo
echo "== _storage_manager.py around schedule/exclude and migration paths =="
sed -n '520,720p' tensorrt_llm/runtime/kv_cache_manager_v2/_storage_manager.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 23938


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant definitions and references =="
rg -n "class _PageHolder|def __del__|has_valid_slot|scheduled_for_eviction|lock|unlock|move_to_new_slot|is_evictable|exclude_from_eviction|schedule_for_eviction" \
  tensorrt_llm/runtime/kv_cache_manager_v2/_page.py \
  tensorrt_llm/runtime/kv_cache_manager_v2/_storage_manager.py \
  tensorrt_llm/runtime/kv_cache_manager_v2/_core/_kv_cache.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== storage is_evictable and eviction controller hooks =="
sed -n '1,220p' tensorrt_llm/runtime/kv_cache_manager_v2/_eviction_controller.py
echo
sed -n '220,360p' tensorrt_llm/runtime/kv_cache_manager_v2/_eviction_controller.py

echo
echo "== storage_manager migrations around last-level pages =="
sed -n '1020,1085p' tensorrt_llm/runtime/kv_cache_manager_v2/_storage_manager.py

echo
echo "== page unlock/destructor paths =="
sed -n '240,320p' tensorrt_llm/runtime/kv_cache_manager_v2/_page.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 322


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== kv_cache_manager_v2 file tree =="
fd -a . tensorrt_llm/runtime/kv_cache_manager_v2

echo
echo "== likely eviction-controller references =="
rg -n "def is_evictable|class .*Controller|schedule_for_eviction|exclude_from_eviction|node_ref" tensorrt_llm/runtime/kv_cache_manager_v2

Repository: NVIDIA/TensorRT-LLM

Length of output: 8203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== storage_manager is_evictable =="
sed -n '380,430p' tensorrt_llm/runtime/kv_cache_manager_v2/_storage_manager.py

echo
echo "== eviction controller =="
sed -n '156,230p' tensorrt_llm/runtime/kv_cache_manager_v2/_eviction_controller/_eviction_controller.py

echo
echo "== page holder / page destructor paths =="
sed -n '60,110p' tensorrt_llm/runtime/kv_cache_manager_v2/_page.py
echo
sed -n '260,310p' tensorrt_llm/runtime/kv_cache_manager_v2/_page.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 9569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Slot methods and move_to_new_slot semantics =="
sed -n '1,220p' tensorrt_llm/runtime/kv_cache_manager_v2/_storage/_core.py

echo
echo "== any direct callers of move_to_new_slot =="
rg -n "move_to_new_slot\(" tensorrt_llm/runtime/kv_cache_manager_v2

Repository: NVIDIA/TensorRT-LLM

Length of output: 6893


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Slot.move_to_new_slot =="
sed -n '250,310p' tensorrt_llm/runtime/kv_cache_manager_v2/_storage/_core.py

echo
echo "== page slot-transfer helpers =="
sed -n '480,520p' tensorrt_llm/runtime/kv_cache_manager_v2/_page.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 3460


Re-check pocketed pages before reuse

Held pages at GPU_LEVEL are still evictable when lower cache levels exist, so _swa_reuse_pocket can’t assume cache_level and has_valid_slot stay unchanged between park and pop. Re-validate before move_to_new_slot() or pin pocketed pages against migration.

🤖 Prompt for 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.

In `@tensorrt_llm/runtime/kv_cache_manager_v2/_core/_kv_cache.py` around lines 888
- 905, Revalidate each pocketed holder’s page immediately before reuse in the
`_swa_reuse_pocket` pop path, including `cache_level` and `has_valid_slot` (and
existing commitment checks), before calling `move_to_new_slot()`. Remove stale
or no-longer-eligible holders instead of reusing them, or otherwise pin them
against migration so pocketed pages remain valid until reuse.

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