Skip to content

[https://nvbugs/6550275][fix] Add KVCacheManagerV2.max_resident_sequences() from per-pool-group page counts… - #17211

Open
trtllm-agent wants to merge 1 commit into
NVIDIA:mainfrom
tensorrt-cicd:repair-bot-bug6550275
Open

[https://nvbugs/6550275][fix] Add KVCacheManagerV2.max_resident_sequences() from per-pool-group page counts…#17211
trtllm-agent wants to merge 1 commit into
NVIDIA:mainfrom
tensorrt-cicd:repair-bot-bug6550275

Conversation

@trtllm-agent

@trtllm-agent trtllm-agent commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Root cause: V2's MAX_UTILIZATION admits sequences up to max_batch_size (485) with no bound on resident KV capacity (~118), and a hybrid Mamba recurrent state is non-droppable, so once all sequences suspend nothing is evictable and the pool can never drain.
  • Fix: Add KVCacheManagerV2.max_resident_sequences() from per-pool-group page counts and gate new-sequence admission on it in _schedule_loop Phase 2; returns None (unchanged behavior) for models without a non-droppable state pool.
  • Automated fix generated by repair-bot

Test plan

  • Verify fix on the same GPU type as the original failure
  • Check for regressions in related tests

Links

Dev Engineer Review

  • Added KVCacheManagerV2.max_resident_sequences() for models with non-droppable state pools.
  • The method returns None for attention-only models and preserves existing scheduler behavior.
  • The scheduler applies the limit during deferred context admission.
  • Existing sequences retain their resident slots.
  • Continuing context chunks do not consume additional slots.
  • The change addresses over-admission when MAX_UTILIZATION exceeds resident KV capacity.
  • No configuration or test-list changes were identified.
  • The API and error-handling changes are consistent with the described behavior.

QA Engineer Review

  • Modified tests/unittest/_torch/executor/test_kv_cache_v2_scheduler.py.
  • Updated the KV-cache manager mock factory to configure max_resident_sequences.
  • Added coverage for:
    • Unlimited admission.
    • Limiting newly started sequences.
    • Counting active generations against the limit.
    • Allowing continuing context chunks without consuming another slot.
  • No test-list coverage in tests/integration/test_lists/ was identified.
  • Verdict: needs follow-up.

…state pools

MAX_UTILIZATION admits new sequences up to max_batch_size and relies on
suspend/resume to survive over-subscription. That recovery needs some
resident sequence to still be evictable so its pages can be freed for a
suspended one to resume. A hybrid Mamba recurrent state is fixed-size per
sequence and cannot be recomputed from tokens, so such a sequence yields no
evictable pages; once every sequence is suspended the pool can no longer
drain and the scheduler stops making progress.

On L40S, qwen3.5_9b at 500-in/2000-out admitted 485 sequences while the
attention pool holds only ~118 at max_seq_len, then spun 8676 iterations
scheduling nothing before raising 'V2 scheduler deadlock'.

Add KVCacheManagerV2.max_resident_sequences(), derived from per-pool-group
page counts, and gate new-sequence admission on it. Returns None (unbounded,
unchanged behavior) for models without a non-droppable state pool.

Pin the return value on the scheduler test's manager double: a bare Mock() is
not None, so the new gate would otherwise compare an int against a child Mock
and raise TypeError in every test that schedules a first context chunk. Add
direct coverage for the cap, which had none.

Two config levers suggested by triage were measured and do not fix this:
avg_seq_len=2500 reproduces the failure identically, and
max_util_for_resume=1.0 replaces the raise with an unbounded livelock.

Signed-off-by: trtllm-agent <296075020+trtllm-agent@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

KVCacheManagerV2 now computes maximum resident sequences. KVCacheV2Scheduler uses this limit during context admission and logs it. Tests cover unlimited and capped residency, active generations, and continuing context chunks.

Changes

Resident-sequence admission

Layer / File(s) Summary
Capacity calculation
tensorrt_llm/_torch/pyexecutor/kv_cache_manager_v2.py
KVCacheManagerV2.max_resident_sequences() returns a conservative capacity for models with non-attention state pools and None for attention-only models.
Scheduler enforcement and validation
tensorrt_llm/_torch/pyexecutor/scheduler/scheduler_v2.py, tests/unittest/_torch/executor/test_kv_cache_v2_scheduler.py
KVCacheV2Scheduler records and logs the capacity, limits new first-context-chunk admissions, preserves slots for started requests, and increments residency after successful admission. Tests cover these cases.

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

Possibly related PRs

Suggested labels: api-compatible

Suggested reviewers: juney-nvidia, bowenfu, liji-nv

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the bug, fix type, and added max_resident_sequences() functionality.
Description check ✅ Passed The description explains the root cause and fix, lists test coverage, and links the bug, but omits the template checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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: 3

🤖 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/_torch/pyexecutor/kv_cache_manager_v2.py`:
- Around line 2177-2188: Update the capacity calculation around
max_blocks_per_seq so attention pools are charged using the full maximum
per-sequence allocation, including num_extra_kv_tokens,
_kv_reserve_draft_tokens, and the base decode token, matching max_seq_capacity
and max_blocks_per_seq. Preserve the fixed one-slot divisor for state pools, and
add a unit test where the extra allocation crosses a block boundary to verify
the scheduler cannot over-admit.

In `@tensorrt_llm/_torch/pyexecutor/scheduler/scheduler_v2.py`:
- Around line 407-425: Extend resident-sequence accounting to the Phase 1
DISAGG_GENERATION_INIT path: initialize the count before Phase 1, reject or
defer new disaggregated requests when the cap is reached, and increment/account
only successful prepare_disagg_gen_init() admissions. Ensure already initialized
disaggregated requests are included in num_started on later scheduler
iterations, and add a regression test covering capped DISAGG_GENERATION_INIT
admission.

In `@tests/unittest/_torch/executor/test_kv_cache_v2_scheduler.py`:
- Around line 2229-2268: Extend TestResidencyCap with direct
KVCacheManagerV2.max_resident_sequences() tests covering storage statistics,
block-size rounding, and extra KV allocation, asserting the calculated residency
cap. Add a scheduler regression test for capped DISAGG_GENERATION_INIT
admission, verifying that generation initialization requests respect
max_resident_sequences while preserving existing admission behavior.
🪄 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: bd455d53-e826-4bbf-8e40-e075cea0e56c

📥 Commits

Reviewing files that changed from the base of the PR and between c5427c5 and ded67ab.

📒 Files selected for processing (3)
  • tensorrt_llm/_torch/pyexecutor/kv_cache_manager_v2.py
  • tensorrt_llm/_torch/pyexecutor/scheduler/scheduler_v2.py
  • tests/unittest/_torch/executor/test_kv_cache_v2_scheduler.py

Comment on lines +2177 to +2188
# Charging every attention pool the full max_seq_len is deliberately
# worst-case: a genuinely sliding-window pool needs fewer pages per
# sequence, so the bound errs low rather than over-admitting.
pages_per_seq = math.ceil(self.max_seq_len / self.tokens_per_block)
# A state pool holds one fixed slot per sequence; an attention pool
# must hold every page of each resident sequence.
return max(
1,
min(
stat.total // (1 if pool_group_id in state_pool_groups else pages_per_seq)
for pool_group_id, stat in enumerate(self._get_storage_statistics(GPU_LEVEL))
),

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Charge attention pools for the full maximum sequence allocation.

Line 2180 excludes num_extra_kv_tokens, _kv_reserve_draft_tokens, and the base decode token. The manager reserves all of these in max_seq_capacity at Lines 1121-1127.

If an attention pool is the limiting pool, this calculation can return a limit that exceeds the pages required by fully allocated resident sequences. The scheduler can then over-admit and reintroduce the allocation deadlock that this limit is intended to prevent.

Use the same maximum capacity basis as max_blocks_per_seq. Add a unit test where the extra allocation crosses a block boundary.

Proposed fix
-        pages_per_seq = math.ceil(self.max_seq_len / self.tokens_per_block)
+        max_sequence_capacity = (
+            self.max_seq_len
+            + self.num_extra_kv_tokens
+            + self._kv_reserve_draft_tokens
+            + 1
+        )
+        pages_per_seq = math.ceil(max_sequence_capacity / self.tokens_per_block)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Charging every attention pool the full max_seq_len is deliberately
# worst-case: a genuinely sliding-window pool needs fewer pages per
# sequence, so the bound errs low rather than over-admitting.
pages_per_seq = math.ceil(self.max_seq_len / self.tokens_per_block)
# A state pool holds one fixed slot per sequence; an attention pool
# must hold every page of each resident sequence.
return max(
1,
min(
stat.total // (1 if pool_group_id in state_pool_groups else pages_per_seq)
for pool_group_id, stat in enumerate(self._get_storage_statistics(GPU_LEVEL))
),
# Charging every attention pool the full max_seq_len is deliberately
# worst-case: a genuinely sliding-window pool needs fewer pages per
# sequence, so the bound errs low rather than over-admitting.
max_sequence_capacity = (
self.max_seq_len
self.num_extra_kv_tokens
self._kv_reserve_draft_tokens
1
)
pages_per_seq = math.ceil(max_sequence_capacity / self.tokens_per_block)
# A state pool holds one fixed slot per sequence; an attention pool
# must hold every page of each resident sequence.
return max(
1,
min(
stat.total // (1 if pool_group_id in state_pool_groups else pages_per_seq)
for pool_group_id, stat in enumerate(self._get_storage_statistics(GPU_LEVEL))
),
🤖 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/_torch/pyexecutor/kv_cache_manager_v2.py` around lines 2177 -
2188, Update the capacity calculation around max_blocks_per_seq so attention
pools are charged using the full maximum per-sequence allocation, including
num_extra_kv_tokens, _kv_reserve_draft_tokens, and the base decode token,
matching max_seq_capacity and max_blocks_per_seq. Preserve the fixed one-slot
divisor for state pools, and add a unit test where the extra allocation crosses
a block boundary to verify the scheduler cannot over-admit.

Comment on lines +407 to +425
#
# Starting a new sequence is what grows the resident set, so the
# residency cap is enforced here. Requests already started keep
# their slot; the rest wait in the queue until one drains.
residency_cap = self.max_resident_sequences
num_started = (
sum(1 for r in requests_list if self._is_started_request(r))
if residency_cap is not None
else 0
)

for req in pending_ctx:
if budget.requests_full:
break
# Read before scheduling: _try_schedule_context advances the
# request past its first chunk.
is_new_sequence = residency_cap is not None and req.is_first_context_chunk
if is_new_sequence and num_started >= residency_cap:
break

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Apply the residency cap to disaggregated generation initialization.

DISAGG_GENERATION_INIT runs in Phase 1 at Lines 338-357. It calls prepare_disagg_gen_init(), which creates and resizes a primary KV cache. Lines 411-425 run only after every Phase 1 candidate has been processed.

A hybrid disaggregated generation worker can therefore admit new resident sequences without this cap. This path can exceed the non-droppable-state capacity and reproduce the suspend/resume deadlock.

Initialize resident accounting before Phase 1. Gate successful disaggregated generation initialization with the same limit. Include already initialized disaggregated requests in subsequent scheduler iterations. Add a regression test for capped DISAGG_GENERATION_INIT admission.

Also applies to: 441-442

🤖 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/_torch/pyexecutor/scheduler/scheduler_v2.py` around lines 407 -
425, Extend resident-sequence accounting to the Phase 1 DISAGG_GENERATION_INIT
path: initialize the count before Phase 1, reject or defer new disaggregated
requests when the cap is reached, and increment/account only successful
prepare_disagg_gen_init() admissions. Ensure already initialized disaggregated
requests are included in num_started on later scheduler iterations, and add a
regression test covering capped DISAGG_GENERATION_INIT admission.

Comment on lines +2229 to +2268
class TestResidencyCap:
"""A manager reporting max_resident_sequences bounds newly started
sequences, so a pool whose state cannot be evicted never over-subscribes.
"""

def test_none_leaves_admission_unbounded(self):
"""Plain attention models report None and keep MAX_UTILIZATION behavior."""
mgr = make_kv_cache_manager(max_resident_sequences=None)
sched = make_scheduler(mgr)
reqs = [make_ctx_request(i, context_remaining_length=10) for i in range(20)]
out = sched.schedule_request(reqs, set())
assert len(out.context_requests) == 20

def test_caps_newly_started_sequences(self):
mgr = make_kv_cache_manager(max_resident_sequences=3)
sched = make_scheduler(mgr)
reqs = [make_ctx_request(i, context_remaining_length=10) for i in range(20)]
out = sched.schedule_request(reqs, set())
assert ids(out.context_requests) == [0, 1, 2]

def test_already_started_sequences_consume_the_cap(self):
"""In-progress generation holds its slot, so no new context is admitted."""
mgr = make_kv_cache_manager(max_resident_sequences=2)
sched = make_scheduler(mgr)
reqs = [
make_gen_request(0),
make_gen_request(1),
make_ctx_request(2, context_remaining_length=10),
]
out = sched.schedule_request(reqs, set())
assert ids(out.generation_requests) == [0, 1]
assert ids(out.context_requests) == []

def test_continuing_chunk_is_not_charged_again(self):
"""Only a first chunk starts a sequence; later chunks keep their slot."""
mgr = make_kv_cache_manager(max_resident_sequences=1)
sched = make_scheduler(mgr)
req = make_ctx_request(0, context_remaining_length=10, is_first_context_chunk=False)
out = sched.schedule_request([req], set())
assert ids(out.context_requests) == [0]

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Test coverage is insufficient.

The added tests cover unbounded admission, capped new context admission, active generation occupancy, and continuing context chunks.

They do not test KVCacheManagerV2.max_resident_sequences() with storage statistics, block rounding, and extra KV allocation. They also do not test capped DISAGG_GENERATION_INIT admission.

Test coverage summary

  • Added tests: test_none_leaves_admission_unbounded, test_caps_newly_started_sequences, test_already_started_sequences_consume_the_cap, and test_continuing_chunk_is_not_charged_again.
  • Test-list status: No relevant tests/integration/test_lists/test-db/ or tests/integration/test_lists/qa/ file was included for review.
  • Verdict: insufficient.

Add direct manager calculation tests and a disaggregated generation initialization regression test.

🤖 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/_torch/executor/test_kv_cache_v2_scheduler.py` around lines
2229 - 2268, Extend TestResidencyCap with direct
KVCacheManagerV2.max_resident_sequences() tests covering storage statistics,
block-size rounding, and extra KV allocation, asserting the calculated residency
cap. Add a scheduler regression test for capped DISAGG_GENERATION_INIT
admission, verifying that generation initialization requests respect
max_resident_sequences while preserving existing admission behavior.

Source: Path instructions

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.

2 participants