Skip to content

[None][fix] Agree the KVCacheManagerV2 rebalance trigger across TP ranks - #17391

Open
thorjohnsen wants to merge 6 commits into
NVIDIA:mainfrom
thorjohnsen:thor/kvcmv2-tp-rebalance-agreement
Open

[None][fix] Agree the KVCacheManagerV2 rebalance trigger across TP ranks#17391
thorjohnsen wants to merge 6 commits into
NVIDIA:mainfrom
thorjohnsen:thor/kvcmv2-tp-rebalance-agreement

Conversation

@thorjohnsen

@thorjohnsen thorjohnsen commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Description

TP ranks are deliberately kept identical — same layers, same KV cache geometry, same request stream — so the KV pool rebalance hook should fire on the same iteration on every rank and compute the same pool ratios by construction, without coordination. Auditing that property found it holds for every input to need_adjustment except one.

Deterministic (identical across TP ranks given identical requests):

Input Where Why
mNumSampledKvCaches kvCacheManager.h:290, incremented at kvCache.cpp:563 plain ++; no RNG anywhere in the V2 tree
mAvgReusedLength, mAvgSqrCapacity, mAvgSqrHistoryLength kvCache.cpp:105,561-562 fed from request-derived values
2000-sample / 100-sample gates kvCacheManager.cpp:853,784 counter comparisons
target-ratio math, 1.25 skew threshold kvCacheManager.cpp:781-796,838 pure float ops, same binary, same order → bit-identical

Not deterministic — the 120 s cooldown:

double now = nowSeconds();                    // kvCacheManager.cpp:855 — steady_clock, per rank
if (now - mLastAdjustmentTime < 120.0) return false;

mLastAdjustmentTime is stamped per rank, in the constructor (:126) and at the end of adjust() (:873). Nothing ties those readings together.

Why one iteration of skew is enough to break TP

Two ranks can straddle the cooldown boundary on different iterations and rebalance one iteration apart. In that window they hold different pool geometry, and _prepare_and_schedule_batch (py_executor.py:3618) runs _schedule() on every rank independently with no broadcast — so they can admit different requests and issue mismatched collectives. (_pp_schedule_and_propagate at :2436 does broadcast, which is why PP is structurally immune.) The window is small, but it gets a fresh roll every 120 s.

This was reachable in practice: _can_pause_for_rebalance's docstring said "MVP scope: single-GPU aggregated", but the predicate never checked tp_size, and no other guard did either — the only additional constraint on enable_kv_pool_rebalance is Mamba + block reuse (_util.py:236).

Fix

TP rank 0 decides and broadcasts (_agreed_need_adjustment). Only the trigger is agreed — the ratios are still computed independently on every rank and never exchanged, because they are pure functions of statistics that are already identical. Two properties make that safe:

  • needAdjustment() is const (kvCacheManager.h:238) — no side effects, so only rank 0's value mattering is harmless.
  • tryUpdateTargetRatios() runs from the KvCache close path (kvCache.cpp:564), not from this read, so every rank keeps maintaining its ratios regardless of who decides.

Attention DP is excluded, matching the predicate the scheduler's own tp_broadcast already uses (py_executor.py:2459): ADP ranks own independent request streams and independent KV caches, so forcing rank 0's decision on them would starve a rank that needs to rebalance when rank 0 does not.

To keep the broadcast off the hot path, the check is throttled to once every KV_POOL_REBALANCE_CHECK_INTERVAL (10) iterations. Rebalance is already rate-limited to once per 120 s, so this costs a fraction of a second of latency while cutting the collective rate by an order of magnitude. Every condition in _can_pause_for_rebalance is rank-uniform, which keeps the throttle counter — and therefore the iteration the collective runs on — identical across ranks.

For context on cost: the rebalance stall was measured at 28–45 ms, ~99 % of it inside adjust() itself (drain/suspend/resume are sub-millisecond).

Test Coverage

tests/unittest/_torch/multi_gpu/test_kv_pool_rebalance_tp.py (new) — 14 cases on real MPI ranks with a real MPIDist, following the test_allgather.py harness. Needs no model weights or KV cache. Picked up automatically by l0_dgx_h100.yml (unittest/_torch/multi_gpu); max world size 4.

Test TP Pins down
test_tp_ranks_agree_on_rebalance_trigger 2, 4 rank 0's decision wins on every rank, across 4 skew scenarios
test_attention_dp_ranks_decide_independently 2, 4 ADP opts out — each rank keeps its own answer
test_rebalance_check_stays_in_lockstep_across_ranks 2, 4 × interval 1, 8 ranks fire the check on identical iterations

The scenario that matters is only_rank0_false: every follower's local clock says "rebalance now" but rank 0's does not. Without agreement the followers would resize while rank 0 did not. The lockstep test drives the real collective inside the loop, so a cadence divergence hangs rather than passing quietly.

tests/unittest/_torch/executor/test_kv_pool_rebalance.py — 15 → 26 tests, covering the throttle and the agreement helper.

tests/integration/defs/accuracy/test_kv_pool_rebalance_accuracy.py — the throttle would otherwise have made this test vacuous: it runs fewer iterations than a large interval, so adjust() would never fire and the token comparison would pass while testing nothing. It now un-throttles explicitly, and asserts the GPU pool ratio actually moved in the rebalance arm and stayed fixed in the baseline arm. Verified by mutation — removing the un-throttle while keeping the assertion produces:

E  AssertionError: rebalance never fired: GPU pool ratio unchanged at
   [0.5000054240226746, 0.49999457597732544]. The token comparison would pass vacuously.

Results (8×H100, sm90)

Suite Result
test_kv_pool_rebalance_tp.py 14 passed, 201 s
test_kv_pool_rebalance.py 26 passed
test_kv_pool_rebalance_accuracy.py 2 passed, 105 s
kv_cache_manager_v2_tests (C++ backend, regression check) 127 passed, OK (skipped=13)

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.

🤖 Generated with Claude Code

Dev Engineer Review

  • Synchronizes KV pool rebalance decisions across TP and CP ranks.
  • Preserves independent decisions for attention-DP ranks.
  • Preserves CP agreement within each attention-DP replica.
  • Checks rebalance eligibility every 10 executor iterations.
  • Preserves the 120-second rebalance cooldown.
  • Keeps target-ratio calculations local and deterministic.
  • Accuracy tests verify pool-ratio changes with rebalance enabled and stability in the baseline case.
  • No configuration or test-list changes require follow-up.

QA Engineer Review

Added test coverage includes:

  • TP, CP, and combined CP×TP agreement tests.
  • Attention-DP independence tests.
  • Attention-DP with CP agreement tests.
  • Lockstep throttle-cadence tests.
  • Unit tests for throttling, broadcast agreement, disagreement suppression, and helper behavior.
  • Accuracy tests for pool-ratio changes and baseline stability.

No tests/integration/test_lists/, test-db/, or qa/ files were modified. Test-list coverage for the added test functions is not provided. Reported results include 14 TP tests, 26 executor tests, 2 accuracy tests, and 127 C++ backend regression tests passing.

Verdict: needs follow-up.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The executor now throttles KV pool rebalance checks and synchronizes tensor-parallel and context-parallel decisions. Unit, accuracy, and real-MPI tests validate interval behavior, rank agreement, attention-DP independence, and pool-ratio changes.

Changes

KV pool rebalance coordination

Layer / File(s) Summary
Executor rebalance coordination
tensorrt_llm/_torch/pyexecutor/py_executor.py
Adds a rebalance-check interval and counter. Throttles eligibility checks. Broadcasts rank-0 decisions for tensor- and context-parallel execution while preserving local decisions for single-rank and attention-DP paths.
Local and accuracy validation
tests/unittest/_torch/executor/test_kv_pool_rebalance.py, tests/integration/defs/accuracy/test_kv_pool_rebalance_accuracy.py
Tests interval and counter behavior, distributed decision modes, rejected agreement, and pool-ratio changes with and without rebalance.
Distributed MPI validation
tests/unittest/_torch/multi_gpu/test_kv_pool_rebalance_tp.py
Adds GPU-backed MPI tests for tensor-parallel, context-parallel, combined CP×TP, attention-DP, and lockstep throttle behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PyExecutor
  participant KVCacheManager
  participant MPIDist
  PyExecutor->>KVCacheManager: read local adjustment state
  PyExecutor->>MPIDist: broadcast rank-0 decision for tensor or context parallelism
  MPIDist-->>PyExecutor: return agreed decision
  PyExecutor->>KVCacheManager: rebalance when the decision is true
Loading

Suggested labels: ci: full pre-merge approved

Suggested reviewers: qijune, shixiaowei02, tabrizian

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the fix for KVCacheManagerV2 rebalance-trigger agreement across TP ranks.
Description check ✅ Passed The description explains the issue, solution, scope, attention-DP behavior, test coverage, results, and checklist status in detail.
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: 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 `@tests/unittest/_torch/multi_gpu/test_kv_pool_rebalance_tp.py`:
- Around line 77-81: Remove the broad try/except block from run_single_rank and
invoke single_rank_forward_func directly, allowing MPIPoolExecutor to propagate
the original failure unchanged; only add a handler if a documented specific
exception requires handling.
🪄 Autofix

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: a9e5c753-c65b-457e-ae26-c5efdc6a2333

📥 Commits

Reviewing files that changed from the base of the PR and between 5cc76ef and ac3e393.

📒 Files selected for processing (4)
  • tensorrt_llm/_torch/pyexecutor/py_executor.py
  • tests/integration/defs/accuracy/test_kv_pool_rebalance_accuracy.py
  • tests/unittest/_torch/executor/test_kv_pool_rebalance.py
  • tests/unittest/_torch/multi_gpu/test_kv_pool_rebalance_tp.py

Comment thread tests/unittest/_torch/multi_gpu/test_kv_pool_rebalance_tp.py

@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: 2

🤖 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 `@tests/unittest/_torch/multi_gpu/test_kv_pool_rebalance_tp.py`:
- Around line 119-122: Update
tests/unittest/_torch/multi_gpu/test_kv_pool_rebalance_tp.py at lines 119-122
for _cp_dist: add Google-style Args and Returns sections while preserving its
existing behavior; at lines 152-159 annotate every parameter and the return type
as None, then add a Google-style docstring; at lines 284-285 annotate world_size
and case plus -> None, then add a Google-style docstring.
- Around line 279-296: Add a real-MPI CP-then-TP agreement case alongside
test_cp_ranks_agree_on_rebalance_trigger using world_size=4, cp_size=2, and
tp_size=2, with divergent flags such as [True, False, False, False]. Exercise
the production collective chain through cp_broadcast() followed by
tp_broadcast(), and assert every rank receives rank 0’s decision; retain the
existing pure-CP coverage.
🪄 Autofix

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: 8c9b6071-a5f0-4f25-b6b0-9e9d6d2a759b

📥 Commits

Reviewing files that changed from the base of the PR and between ac3e393 and 43f8135.

📒 Files selected for processing (3)
  • tensorrt_llm/_torch/pyexecutor/py_executor.py
  • tests/unittest/_torch/executor/test_kv_pool_rebalance.py
  • tests/unittest/_torch/multi_gpu/test_kv_pool_rebalance_tp.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unittest/_torch/executor/test_kv_pool_rebalance.py
  • tensorrt_llm/_torch/pyexecutor/py_executor.py

Comment thread tests/unittest/_torch/multi_gpu/test_kv_pool_rebalance_tp.py
Comment thread tests/unittest/_torch/multi_gpu/test_kv_pool_rebalance_tp.py
Comment thread tensorrt_llm/_torch/pyexecutor/py_executor.py Outdated

@brnguyen2 brnguyen2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thorough work — the PR description alone settles most questions a reviewer would have, and the anti-vacuity assertions in the accuracy test plus the real-MPI lockstep tests are exactly the right coverage for this kind of collective.

One correctness concern (inline at [py_executor.py:4461](https://github.com/NVIDIA/TensorRT-LLM/pull/17391/files#diff-f0b4c3c02708916fd189f863c48b982a55f34ae94996089b23aa0a6f0571fe19R4461)): the attention-DP early-return skips the CP broadcast as well as the TP one, but the scheduler propagation cited as precedent gates only the TP hop on ADP — cp_broadcast at [py_executor.py:2475](https://github.com/NVIDIA/TensorRT-LLM/pull/17391/files#diff-f0b4c3c02708916fd189f863c48b982a55f34ae94996089b23aa0a6f0571fe19R2475) runs unconditionally. Under ADP + CP, CP ranks share a request stream, so they need the trigger agreement even though the TP-dimension exclusion is correct. Details and a suggested fix inline.

Also verified while reviewing (no action needed): adjust() re-checks only the deterministic per-level ratio-skew test, not the cooldown or the 2000-sample gate (kvCacheManager.cpp:862-874), so a follower whose local cooldown hadn't expired but is forced True by rank 0 performs the same _adjustLevel work rather than no-opping — and re-stamps mLastAdjustmentTime, which conveniently re-syncs the per-rank clocks after the first agreed adjustment. That property is what makes trigger-only agreement sufficient; might be worth a sentence in the _agreed_need_adjustment docstring, since the mechanism silently depends on it.

On the [None] tag: this is a real bug fix (cross-rank divergence reachable in any TP job with the feature enabled). Worth filing a tracking ticket and referencing it in the title, per the usual convention for fixes.


Broadcasting over the CP group and then the TP group propagates global
rank 0's decision to everyone: after the CP step each rank holds
``V(its tp_rank, cp_rank 0)``, and the TP step then replaces that with

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This early return skips the CP broadcast too, but the scheduler propagation cited as the precedent gates only the TP hop on attention DP — cp_broadcast at [py_executor.py:2475](https://github.com/NVIDIA/TensorRT-LLM/pull/17391/files#diff-f0b4c3c02708916fd189f863c48b982a55f34ae94996089b23aa0a6f0571fe19R2475) runs unconditionally, ADP or not. That's because ADP gives each TP rank its own request stream, but CP ranks within a DP group still split the same requests and must admit them together. Under ADP + CP (nothing in Mapping or llm_args rejects the combination), CP ranks would decide the rebalance trigger independently here — the same divergence class this PR fixes.

Suggested structure, mirroring [py_executor.py:2471](https://github.com/NVIDIA/TensorRT-LLM/pull/17391/files#diff-f0b4c3c02708916fd189f863c48b982a55f34ae94996089b23aa0a6f0571fe19R2471)-2478:

need = self.kv_cache_manager.impl.need_adjustment
if self.dist.cp_size > 1:
    need = self.dist.cp_broadcast(need, root=0)
if self.dist.tp_size > 1 and not self.enable_attention_dp:
    need = self.dist.tp_broadcast(need, root=0)
return need

If ADP + CP + rebalance is instead considered unreachable today, a comment saying so (and why) would do — but test_attention_dp_skips_cp_broadcast_too and test_attention_dp_ranks_decide_independently currently bake the skip in as intended behavior, so either the code or the tests should change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — you're right, and I had the precedent backwards. Fixed in ba6fff1.

I cited the scheduler's tp_broadcast as justification for excluding ADP outright and missed that the cp_broadcast immediately below it (py_executor.py:2474) runs unconditionally. That asymmetry is the whole answer, and the reasoning you give for it is the same one this PR already makes for pure CP — I just failed to carry it into the ADP branch.

Confirmed reachable: nothing in Mapping or LlmArgs rejects enable_attention_dp with cp_size > 1. Mapping.__init__'s only ADP-related assert is enable_lm_head_tp_in_adp requires enable_attention_dp (mapping.py:190), and dp_size = tp_size if enable_attention_dp else 1 (:296) makes CP orthogonal to the DP dimension rather than folded into it.

Took your structure verbatim:

need = self.kv_cache_manager.impl.need_adjustment
if self.dist.cp_size > 1:
    need = self.dist.cp_broadcast(need, root=0)
if self.dist.tp_size > 1 and not self.enable_attention_dp:
    need = self.dist.tp_broadcast(need, root=0)
return need

The CP-then-TP order differs from the scheduler's TP-then-CP, but the two agree in both regimes: without ADP, TP-then-CP gives rank(t,c) <- V(0,c) <- V(0,0) and CP-then-TP gives rank(t,c) <- V(t,0) <- V(0,0); with the TP hop suppressed both reduce to rank(t,c) <- V(t,0), i.e. each replica decides on its own cp_rank-0 reading.

On tests — you were right that they baked the skip in, so I inverted rather than extended:

  • test_attention_dp_skips_cp_broadcast_too -> test_attention_dp_still_broadcasts_over_cp (asserts the CP result overrides the local reading and that tp_broadcast stays uncalled).
  • Added test_attention_dp_without_cp_touches_no_collective so the genuine no-collective case keeps coverage.
  • Added a real-MPI case, test_attention_dp_agrees_over_cp_but_not_tp (world_size=4, cp_size=2, tp_size=2). Flags are [True, False, False, True] so it fails in both directions: ranks 1 and 3 are overridden by their CP root (proving the CP hop ran) while the two replicas end on different answers (proving the TP hop did not).

Mutation-verified — restoring the early return gives:

AssertionError: ADP+CP decisions were [True, False, False, True],
expected [True, True, False, False]: CP ranks must follow their replica's
root while replicas stay independent

which is exactly the raw local flags, i.e. no broadcast at all. The pure-TP, pure-CP and CP-x-TP cases all stay green under that mutation, which is your point that nothing covered this topology.

One note for precision: the propagation you cite lives in _pp_schedule_and_propagate, the PP scheduling path, and rebalance is gated off for pp_size > 1. So it's a semantic precedent rather than an operative one — in the non-PP loops _schedule() runs per rank with no broadcast at all, which is this PR's premise. Doesn't change the conclusion; the ADP+CP semantics stand on their own.

Suites after the fix: 30 mock tests, 20 real-MPI tests, pre-commit clean.

@BowenFu BowenFu 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.

Verified existing/default behavior remains unchanged behind the default-off rebalance gate; when enabled, the loop-wide cadence and CP/TP agreement are covered by focused unit and MPI tests, including ADP+CP.
The unresolved py_executor.py thread is non-blocking: ba6fff1 fixes the cadence/ADP issue it raised, and the author response documents the correction.

@yufeiwu-nv
yufeiwu-nv removed their request for review August 10, 2026 02:31
@thorjohnsen

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65044 [ run ] triggered by Bot. Commit: ba6fff1 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65044 [ run ] completed with state SUCCESS. Commit: ba6fff1
/LLM/main/L0_MergeRequest_PR pipeline #52849 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@thorjohnsen

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65127 [ run ] triggered by Bot. Commit: ba6fff1 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65127 [ run ] completed with state SUCCESS. Commit: ba6fff1
/LLM/main/L0_MergeRequest_PR pipeline #52922 completed with status: 'UNSTABLE'

CI Report

⚠️ Multi-GPU Label Required:
Multi-GPU tests require the ci: full pre-merge approved label on this PR. Ask a member of NVIDIA/trt-llm-ci-approvers to add the label, then re-trigger CI with the same bot command (no rebase needed).

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

Link to invocation

@thorjohnsen

Copy link
Copy Markdown
Collaborator Author

@mikeiovine @cascade812 @Tabrizian @QiJune — could one of you approve? Need a trt-llm-runtime-devs approval to cover py_executor.py and test_kv_pool_rebalance_tp.py.

@liji-nv @SimengLiu-nv — could one of you approve? Need a trt-llm-kv-cache-manager-devs approval to cover test_kv_pool_rebalance.py.

@tburt-nv @dpitman-nvda @QiJune @BowenFu — could one of you add the ci: full pre-merge approved label to enable multi-GPU test stages?

@github-actions

Copy link
Copy Markdown

Removed the "ci: full pre-merge approved" label because @BowenFu could not be verified as an active member of NVIDIA/trt-llm-ci-approvers. Ask a member of that team to apply it.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

TP ranks are deliberately kept identical -- same layers, same KV cache
geometry, same request stream -- so the rebalance hook should fire on the
same iteration on every rank and compute the same pool ratios without any
coordination. Auditing that property found it holds for every input to
need_adjustment except one.

The sample counters and the moving averages behind the target ratios are all
fed from request-derived values (kvCache.cpp:105,561-563) with no randomness
anywhere in the V2 tree, and the ratio math is pure float arithmetic on those
values, so it is bit-identical across ranks. The 120s cooldown is not: it
compares against a per-rank steady_clock reading (kvCacheManager.cpp:855),
stamped per rank at construction (:126) and at the end of adjust() (:873).

Two TP ranks can therefore straddle the cooldown boundary on different
iterations and rebalance one iteration apart. That window is enough to break
TP, because _prepare_and_schedule_batch runs _schedule() on every rank
independently with no broadcast: ranks holding different pool geometry can
admit different requests and issue mismatched collectives. The window is
small but it gets a fresh roll every 120s.

Note this was reachable in practice. _can_pause_for_rebalance's docstring
said "MVP scope: single-GPU aggregated", but the predicate never checked
tp_size, and no other guard did either -- the only additional constraint on
enable_kv_pool_rebalance is Mamba + block reuse.

Fix: TP rank 0 decides and broadcasts (_agreed_need_adjustment). Only the
trigger is agreed; the ratios are still computed independently on every rank
and never exchanged, because they are pure functions of statistics that are
already identical. That is safe because needAdjustment() is const
(kvCacheManager.h:238) and tryUpdateTargetRatios() runs from the KvCache
close path (kvCache.cpp:564), not from this read, so every rank keeps
maintaining its ratios regardless of which rank decides.

Attention DP is excluded, matching the predicate the scheduler's own
tp_broadcast already uses: ADP ranks own independent request streams and
independent KV caches, so forcing rank 0's decision on them would starve a
rank that needs to rebalance when rank 0 does not.

To keep the broadcast off the hot path, the check is throttled to once every
KV_POOL_REBALANCE_CHECK_INTERVAL iterations. Rebalance is already rate-limited
to once per 120s, so this costs a fraction of a second of latency while
cutting the collective rate by an order of magnitude. Every condition in
_can_pause_for_rebalance is rank-uniform, which keeps the throttle counter --
and therefore the iteration the collective runs on -- identical across ranks.

Tests:

- test_kv_pool_rebalance_tp.py (new): multi-rank TP coverage on real MPI ranks
  with a real MPIDist, at TP=2 and TP=4. Injects the skew the mechanism exists
  to absorb, including the case where every follower's local clock says
  "rebalance now" but rank 0's does not. The lockstep test drives the real
  collective inside the loop, so a cadence divergence hangs rather than passing
  quietly. Needs no model weights.
- test_kv_pool_rebalance.py: coverage for the throttle and the agreement helper
  (15 -> 26 tests).
- test_kv_pool_rebalance_accuracy.py: the throttle would otherwise have made
  this test vacuous, since it runs fewer iterations than a large interval. It
  now un-throttles explicitly and asserts the GPU pool ratio actually moved in
  the rebalance arm and stayed fixed in the baseline arm, so a future change
  that stops adjust() from firing fails instead of silently passing.

Signed-off-by: Thor Johnsen <41591019+thorjohnsen@users.noreply.github.com>
…lism

The agreement added in the previous commit keyed on dist.tp_size, which is
mapping.tp_size. A pure context-parallel job has tp_size == 1, so it took the
local-read path and did no collective at all, leaving CP ranks with exactly
the per-rank clock race the mechanism was built to eliminate.

CP meets both conditions that make agreement necessary:

- A request is split across CP ranks, so they must admit it together.
- CP ranks schedule independently. Executor loop selection keys only on
  pp_size, so a CP job with pp_size == 1 runs _executor_loop /
  _executor_loop_overlap, whose _prepare_and_schedule_batch calls _schedule()
  on every rank with no broadcast.

Under Ulysses this is doubly true: attn_tp_size = tp_size * cp_size and
attn_cp_size = 1, so CP ranks are TP-shaped for KV purposes while
mapping.tp_size is still 1.

Broadcast over the CP group and then the TP group. With pipeline parallelism
already gated off, world_size == tp_size * cp_size, so a rank is identified by
(tp_rank, cp_rank): after the CP step a rank holds V(its tp_rank, 0), and the
TP step replaces that with V(0, 0), which is global rank 0's decision. Each
step is skipped when its dimension is 1, so a pure-TP job still pays exactly
one collective and a single-rank job pays none.

The dedicated sub-communicators are used rather than the global one, which
carries regular executor traffic including request broadcasting -- the same
consideration that makes the sleep/wakeup protocol duplicate its own
communicator.

Tests:

- Multi-rank coverage over a pure-CP mapping at CP=2 and CP=4, asserting rank
  0's decision wins. Verified by mutation: restoring the tp_size-only
  predicate fails all four cases with "CP rank 1: local reading was False,
  rank 0 said True, so the agreed decision should be True, got False".
- Unit coverage for the CP-only path, the CP-then-TP chain, and that attention
  DP skips both collectives.
- The multi-rank workers now allgather before asserting. Previously a rank
  that failed its assert never reached the collective, so the ranks that
  passed blocked until the pytest timeout; a clean failure was reported as a
  five-minute hang. The mutation run above now fails in 59s.
- The CP-then-TP chain test deliberately gives the local reading and the CP
  result different values. With both set to True it passed whether or not the
  TP step consumed the CP step's result, leaving the chaining unverified.

Signed-off-by: Thor Johnsen <41591019+thorjohnsen@users.noreply.github.com>
Throttle the rebalance check on iter_counter instead of a private
counter.  A private counter only advanced on iterations where every gate
in _can_pause_for_rebalance had already passed, so one iteration on which
a single rank returned early -- is_shutdown is set per rank on the fatal
error path -- left that rank's counter permanently offset from its peers.
The ranks would then reach _agreed_need_adjustment's broadcast on
different iterations from then on.  iter_counter is bumped
unconditionally at the end of every executor loop iteration, so the
firing schedule becomes a pure function of the iteration index and cannot
accumulate an offset.  This matches how iter stats are already throttled
in the same file.  _rebalance_check_counter is now dead and removed.

Add a CP x TP multi-rank case.  The pure-TP test has cp_size == 1 and the
pure-CP test has tp_size == 1, so between them the chained
cp_broadcast -> tp_broadcast the production path actually takes was never
executed; neither could show that the TP step consumes the CP step's
result rather than the rank's own local reading.  Verified by mutation:
broadcasting the local reading in the TP step fails the new case on the
cp_rank == 1 ranks while both single-dimension tests stay green.

Rework TestRebalanceCheckThrottle for the new cadence and add a
regression test asserting a rank that skips checks never fires on an
iteration its peers do not.  Verified by mutation: restoring the private
counter fails it with the drifted schedule in the message.

Annotate and document the multi-GPU test helpers per the coding
guidelines.

Signed-off-by: Thor Johnsen <41591019+thorjohnsen@users.noreply.github.com>
_agreed_need_adjustment returned early on enable_attention_dp, which
skipped the CP broadcast as well as the TP one.  Only the TP hop should
be suppressed.

Under ADP the TP dimension is the DP dimension (mapping.py:
dp_size = tp_size if enable_attention_dp else 1), so those ranks own
independent request streams and KV caches and must decide independently.
CP is orthogonal: inside a single DP replica the CP ranks still split the
same request along the sequence dimension and have to admit it together,
which is the same argument this mechanism already makes for pure CP.
Nothing in Mapping or LlmArgs rejects enable_attention_dp with
cp_size > 1, so the early return left every replica's CP ranks deciding
the trigger independently -- the divergence class this change exists to
remove.

This also aligns with the scheduler's own propagation at
py_executor.py:2470-2477, which gates its tp_broadcast on
not enable_attention_dp but runs its cp_broadcast unconditionally.  The
original rationale cited that tp_broadcast as precedent for excluding ADP
outright and missed the cp_broadcast directly below it.

Invert test_attention_dp_skips_cp_broadcast_too, which asserted the old
behaviour, into test_attention_dp_still_broadcasts_over_cp, and add
test_attention_dp_without_cp_touches_no_collective to keep the
no-collective case covered.

Add a real-MPI case, test_attention_dp_agrees_over_cp_but_not_tp
(world_size=4, cp_size=2, tp_size=2), with flags chosen to fail in both
directions: two ranks are overridden by their CP root, proving the CP hop
ran, while the two replicas end on different answers, proving the TP hop
did not.  Verified by mutation: restoring the early return yields
[True, False, False, True] against the expected [True, True, False, False]
while the pure-TP, pure-CP and CP-x-TP cases all stay green.

Signed-off-by: Thor Johnsen <41591019+thorjohnsen@users.noreply.github.com>
@thorjohnsen
thorjohnsen force-pushed the thor/kvcmv2-tp-rebalance-agreement branch from ce68152 to 1f1cb52 Compare August 12, 2026 19:55
@thorjohnsen

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65686 [ run ] triggered by Bot. Commit: 1f1cb52 Link to invocation

…elism

_executor_loop_pp had no rebalance hook and _can_pause_for_rebalance
rejected pp_size > 1 outright, so pipeline-parallel jobs never rebalanced
their KV pools.

The other two loops rebalance inline: at the top of an iteration at most
one batch is in flight (previous_batch) and _consume_previous_batch_for_
rebalance retires it on the spot.  The PP loop keeps up to
num_micro_batches batches in flight in a ring, and a microbatch is retired
only when its sample state finishes travelling that ring, which advances
just as iterations run.  There is no inline call that can drain it, and
adjust() cannot run while any of them is outstanding because it moves KV
pages underneath whatever holds them.

So rather than drain inline, stop feeding the ring and let the loop drain
itself.  While a rebalance is pending the loop forces can_queue to False,
which is its existing "skip this microbatch slot" path -- the same one
every idle iteration already takes, since _can_queue is just
batch_size > 0.  Each iteration then retires one slot and queues nothing
new, so num_micro_batches iterations empty the ring, after which a new
Stage 3.4 suspends, adjusts and resumes.

Every rank must do this on the same iteration.  A rank that drained alone
while its peers kept queueing would desynchronize the per-iteration
send/recv chain and hang the pipeline, so this is a stronger requirement
than the TP case, where divergence merely risks mismatched scheduling.
Three things provide it: _agreed_need_adjustment gains a pp_broadcast hop
so the decision is shared; the drain countdown is a rank-independent
constant rather than a per-rank condition; and the ring state the
quiescence test reads is already symmetric across ranks.

The PP hop must not be suppressed under attention DP, unlike the TP hop.
ADP replicates along the TP dimension, so a replica's pipeline stages all
serve that replica's one request stream and must drain together; pp_group
holds exactly those stages.

If the ring is somehow still busy when the countdown expires the rebalance
is skipped with a warning rather than forced: adjusting underneath a live
_KVCache would corrupt it, while skipping costs only a delay, since the
auto-tuner asks again on the next check interval.

The suspend/adjust/resume core is split out as _rebalance_kv_pools_now so
both paths share it.  The PP path must not run _consume_previous_batch_for_
rebalance: under PP a microbatch is retired by the relay and previous_batch
is kept only to synchronize its sampler event, so running the overlap
loop's helper on it would double-handle a batch the ring already completed.

Tests: 16 new unit tests covering the drain countdown, the quiescence
predicate, the busy-ring skip and the sample-stream sync, plus 10 new
multi-rank cases over real MPI communicators -- pure PP at widths 2 and 4,
a TP2xPP2 chain that only passes if the PP hop consumes the TP hop's
result, and a TP2xPP2 ADP case expecting [True, False, True, False], which
pins down both that the PP hop ran and that the TP hop did not.

Signed-off-by: Thor Johnsen <41591019+thorjohnsen@users.noreply.github.com>
@thorjohnsen

Copy link
Copy Markdown
Collaborator Author

/bot kill

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65693 [ kill ] triggered by Bot. Commit: ef96782 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65686 [ run ] completed with state ABORTED. Commit: 1f1cb52

Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65693 [ kill ] completed with state SUCCESS. Commit: ef96782
Successfully killed previous jobs for commit ef96782

Link to invocation

The drain's pieces were unit-tested but its wiring was not.  Starting the
drain, forcing can_queue to False while it runs, and completing it in
Stage 3.4 all live inside _executor_loop_pp, which no unit test drove, so
only live multi-GPU runs covered them and a regression would have reached
CI unnoticed.

TestPpLoopDrainWiring drives the real loop over an object.__new__
(PyExecutor) instance populated with just what one iteration touches --
the approach test_py_executor.py already uses for the PP scheduling path.
Two choices make it discriminating: _can_queue returns True, so every
iteration wants to queue and an empty slot can only be the drain's doing;
and the mocked forward raises, so reaching it both records that the
iteration queued and ends the loop.

The main case walks a two-slot ring end to end -- iterations 1 and 2 queue
nothing despite _can_queue saying they could, the rebalance lands at the
end of iteration 2, and iteration 3 returns to queueing.  The others cover
the control case (no drain pending reaches the forward), the countdown
tracking ring depth rather than a constant, and the decision being taken
once per rebalance instead of once per iteration -- which matters because
_agreed_need_adjustment runs a collective, so re-entering it mid-drain
would put a rank into a broadcast its peers are not in.

The fixture shuts the loop down after a bounded number of iterations.
Without that, dropping the Stage 3.4 call makes the drain never complete,
so nothing is queued, the forward is never reached and the loop spins
forever -- which turns a clean assertion failure into a hung stage.  Each
of the wiring mutations now fails in well under a second instead.

Signed-off-by: Thor Johnsen <41591019+thorjohnsen@users.noreply.github.com>
@thorjohnsen

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65701 [ run ] triggered by Bot. Commit: c21efaa Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #65701 [ run ] completed with state FAILURE. Commit: c21efaa
/LLM/main/L0_MergeRequest_PR pipeline #53418 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@nvpohanh
nvpohanh requested a review from lowsfer August 13, 2026 05:43
@nvpohanh

Copy link
Copy Markdown
Collaborator

[by Codex] @lowsfer Could you review this PR? Thanks!

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