Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
04fe30a
[https://nvbugs/6627795][fix] stop charging retiring requests against…
chenfeiz0326 Aug 31, 2026
0dcebd7
[https://nvbugs/6627795][fix] gate the retiring-request filter on pip…
chenfeiz0326 Sep 2, 2026
399890b
[https://nvbugs/6627795][fix] size one-model spec-decode slot pools b…
chenfeiz0326 Sep 2, 2026
e440aa5
[https://nvbugs/6627795][fix] charge LoRA pages for retiring requests…
chenfeiz0326 Sep 2, 2026
154f606
[https://nvbugs/6627795][fix] Apply the retiring-request fix to the p…
chenfeiz0326 Sep 2, 2026
9ef2f5e
[https://nvbugs/6627795][fix] revert the V1 capacity-scheduler change…
chenfeiz0326 Sep 3, 2026
eed1460
[https://nvbugs/6627795][fix] size the remaining spec-dec identity po…
chenfeiz0326 Sep 4, 2026
95e6f7e
Merge branch 'main' into user/chenfeiz/adp-exclude-retiring-from-admi…
chenfeiz0326 Sep 6, 2026
ee62107
Merge branch 'main' into user/chenfeiz/adp-exclude-retiring-from-admi…
chenfeiz0326 Sep 7, 2026
aa0c2a1
[https://nvbugs/6627795][fix] accept num_seq_slots in the qwen4 dynam…
chenfeiz0326 Sep 7, 2026
9f3c3cf
[https://nvbugs/6627795][fix] unify the sequence-slot coefficient and…
chenfeiz0326 Sep 7, 2026
ac72cda
[https://nvbugs/6627795][fix] derive the retiring-request count ident…
chenfeiz0326 Sep 7, 2026
85b9ae1
[https://nvbugs/6627795][feat] extend the attention-DP overlap seat h…
chenfeiz0326 Sep 7, 2026
6c7533e
Merge branch 'main' into user/chenfeiz/adp-exclude-retiring-from-admi…
chenfeiz0326 Sep 7, 2026
166cf0f
[https://nvbugs/6627795][chore] satisfy yapf, ruff and ruff-format on…
chenfeiz0326 Sep 7, 2026
4030e8e
Revert "[https://nvbugs/6627795][feat] extend the attention-DP overla…
chenfeiz0326 Sep 8, 2026
f498e0f
Revert "[https://nvbugs/6627795][fix] derive the retiring-request cou…
chenfeiz0326 Sep 8, 2026
8ffd77b
[https://nvbugs/6627795][fix] make the new seat-pool plumbing tolerat…
chenfeiz0326 Sep 8, 2026
c1c619e
[https://nvbugs/6627795][fix] confine the disagg 2x to the KV index pool
chenfeiz0326 Sep 8, 2026
b01d208
[https://nvbugs/6627795][fix] scope the overlap headroom to non-PP at…
chenfeiz0326 Sep 9, 2026
4088a73
[https://nvbugs/6627795][fix] name the new flag disable_overlap_sched…
chenfeiz0326 Sep 9, 2026
5d34836
[https://nvbugs/6627795][fix] drop the added comments in kv_cache_man…
chenfeiz0326 Sep 9, 2026
206dd2d
[https://nvbugs/6627795][fix] drop the added comments in _util and py…
chenfeiz0326 Sep 9, 2026
2e9f5ee
Merge remote-tracking branch 'origin/main' into user/chenfeiz/adp-exc…
chenfeiz0326 Sep 10, 2026
f8c5080
[None][fix] Align seq-slot sizing with PR 18983
chenfeiz0326 Sep 10, 2026
c17e82f
[None][fix] Register test_spec_slot_pool_sizing.py in l0_h100
chenfeiz0326 Sep 11, 2026
770a6ac
[https://nvbugs/6692514][fix] gate the overlap seat headroom on ADP, …
chenfeiz0326 Sep 13, 2026
436d4c3
Merge branch 'main' into user/chenfeiz/adp-exclude-retiring-from-admi…
chenfeiz0326 Sep 13, 2026
842ccbd
[None][fix] confine the overlap seat headroom to ADP and widen only t…
chenfeiz0326 Sep 13, 2026
a895e49
[None][fix] derive the seat headroom from the same predicate as the i…
chenfeiz0326 Sep 13, 2026
8108410
Merge branch 'main' into user/chenfeiz/adp-exclude-retiring-from-admi…
chenfeiz0326 Sep 14, 2026
d428ed9
[None][test] Register test_spec_slot_pool_sizing.py on the CPU stage
chenfeiz0326 Sep 14, 2026
d5bb747
[None][fix] Restrict the seat-pool headroom to attention DP
chenfeiz0326 Sep 15, 2026
97ff523
Merge remote-tracking branch 'origin/main' into user/chenfeiz/adp-exc…
chenfeiz0326 Sep 15, 2026
5f96a12
[None][chore] trim explanatory comments from the ADP overlap-headroom…
chenfeiz0326 Sep 15, 2026
c3f11a2
[None][fix] Fix the test failures L0 pipeline 60225 reported for this…
chenfeiz0326 Sep 15, 2026
3453805
[None][fix] Gate the overlap seat headroom on the selected KV cache m…
chenfeiz0326 Sep 15, 2026
94527ec
Merge remote-tracking branch 'origin/main' into user/chenfeiz/adp-exc…
chenfeiz0326 Sep 16, 2026
fb49597
[None][fix] Keep the single micro-batch seat pool for MRoPE delta cac…
chenfeiz0326 Sep 16, 2026
a053543
Merge origin/main into user/chenfeiz/adp-exclude-retiring-from-admission
chenfeiz0326 Sep 17, 2026
5111b62
Merge origin/main into user/chenfeiz/adp-exclude-retiring-from-admission
chenfeiz0326 Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 140 additions & 31 deletions tensorrt_llm/_torch/pyexecutor/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,40 @@ def _non_hybrid_kv_cache_manager_cls(config, kv_cache_config: KvCacheConfig):
return KVCacheManagerV2 if needs_v2 else KVCacheManager


def kv_cache_manager_v2_incompatible_features(
max_beam_width: Optional[int]) -> List[str]:
"""Runtime features a V2 manager cannot serve.

``KvCacheCreator._validate_or_fallback_kv_cache_manager_v2`` demotes a plain
V2 manager to ``KVCacheManager`` when this list is non-empty, and rejects the
model families that require V2 outright. ``resolved_kv_cache_manager_is_v2``
reads the same list so that callers sizing pools from the manager version
cannot disagree with the selection itself.

A KV connector is deliberately not a trigger: it is served through the pool
layout registration path and no longer forces a fallback. It is not taken as
a parameter either, so a future caller cannot reintroduce the demotion by
passing it.
"""
incompat: List[str] = []
if max_beam_width is not None and max_beam_width > 1:
incompat.append("max_beam_width > 1")
return incompat


def resolved_kv_cache_manager_is_v2(kv_cache_config: KvCacheConfig,
max_beam_width: Optional[int]) -> bool:
"""Whether the executor will actually hold a V2 manager.

``use_kv_cache_manager_v2`` is a request, not the outcome: model loading has
already resolved ``"auto"``, but a plain model is still demoted to V1 at
manager-selection time when its runtime features are V2-incompatible. Sizing
a pool from the request would leave V2 geometry on a V1 executor.
"""
return (kv_cache_config.use_kv_cache_manager_v2 is True
and not kv_cache_manager_v2_incompatible_features(max_beam_width))


def _resolve_disagg_transceiver_route(
cache_transceiver_config: Optional[CacheTransceiverConfig],
) -> tuple[Optional[str], Optional[str]]:
Expand All @@ -121,6 +155,13 @@ def _resolve_disagg_transceiver_route(
return backend, runtime


def is_disagg_enabled(
cache_transceiver_config: Optional[CacheTransceiverConfig]) -> bool:
"""Whether this executor participates in disaggregated serving."""
return (cache_transceiver_config is not None
and cache_transceiver_config.backend is not None)


def get_kv_cache_manager_cls(
model_config: ModelConfig,
kv_cache_config: KvCacheConfig,
Expand Down Expand Up @@ -737,6 +778,7 @@ def __init__(
model_engine)
self._is_kv_cache_manager_v2 = issubclass(self._kv_cache_manager_cls,
KVCacheManagerV2)
self._disable_overlap_scheduler = llm_args.disable_overlap_scheduler
self._draft_config = draft_config
self._skip_est = skip_est
# Admission cap (tokens of summed context attended-KV) that the fp8 context-MLA workspace reservation
Expand Down Expand Up @@ -791,11 +833,8 @@ def _validate_or_fallback_kv_cache_manager_v2(
# also go through the V2-incompatible-feature gate below.
if issubclass(kv_cache_manager_cls, KVCacheManagerV2):
sparse_attn_config = model_config.sparse_attention_config
# The KV connector is supported through the pool layout
# registration path, so it no longer forces a fallback.
incompat: List[str] = []
if self._max_beam_width is not None and self._max_beam_width > 1:
incompat.append("max_beam_width > 1")
incompat = kv_cache_manager_v2_incompatible_features(
self._max_beam_width)
if incompat:
incompat_str = ", ".join(incompat)
# Never silently replace a sparse V2 manager with V1. Some
Expand Down Expand Up @@ -1575,6 +1614,7 @@ def _create_kv_cache_manager(
execution_stream=self._execution_stream,
layer_mask=spec_dec_layer_mask,
is_disagg=self._is_disagg,
disable_overlap_scheduler=self._disable_overlap_scheduler,
kv_events_config=None
if estimating_kv_cache or model_engine.is_draft_model else
self._llm_args.kv_cache_config.kv_events_config,
Expand Down Expand Up @@ -1795,6 +1835,7 @@ def _create_one_model_draft_kv_cache_manager(
layer_mask=spec_dec_layer_mask,
num_layers=num_draft_layers,
is_disagg=self._is_disagg,
disable_overlap_scheduler=self._disable_overlap_scheduler,
cold_page_codec_provider=cold_page_codec_provider,
joint_kv_cache_reuse=self._joint_kv_cache_reuse,
)
Expand Down Expand Up @@ -2176,6 +2217,7 @@ def _create_cross_kv_cache_manager(
num_layers=num_layers,
num_kv_heads=num_kv_heads,
head_dim=head_dim,
disable_overlap_scheduler=self._disable_overlap_scheduler,
kv_cache_type=tensorrt_llm.bindings.internal.batch_manager.
CacheType.CROSS,
)
Expand Down Expand Up @@ -2495,6 +2537,7 @@ def _create_kv_cache_manager(
head_dim: Optional[int] = None,
kv_cache_type=None,
is_disagg: bool = False,
disable_overlap_scheduler: bool = False,
cold_page_codec_provider: Optional[object] = None,
kv_events_config: Optional[KVEventsConfig] = None,
joint_kv_cache_reuse: bool = False) -> KVCacheManager:
Expand Down Expand Up @@ -2666,6 +2709,8 @@ def _create_kv_cache_manager(
"cold_page_codec_provider"] = cold_page_codec_provider
manager_extra_kwargs["kv_events_config"] = kv_events_config
manager_extra_kwargs["joint_kv_cache_reuse"] = joint_kv_cache_reuse
manager_extra_kwargs[
"disable_overlap_scheduler"] = disable_overlap_scheduler
# V2 builds the block-reuse cache key of a multimodal token run from
# the vocabulary size. Resolve it here rather than per-branch: the
# manager needs it whenever block reuse can meet multimodal input,
Expand Down Expand Up @@ -3211,11 +3256,9 @@ def compute_max_num_sequences(mapping: Mapping,
enable_overlap_headroom: bool = False) -> int:
"""Size the sequence-slot pool (and the sampler state it indexes).

``enable_overlap_headroom`` is intentionally opt-in. Disaggregated
attention-DP needs a second non-PP slot set because the V2 scheduler can
backfill seats before the overlap scheduler releases the previous
iteration's terminal slots. Pipeline parallelism already sizes the pool
by ``pp_size``.
``enable_overlap_headroom`` is intentionally opt-in; see
``should_enable_overlap_headroom``. Pipeline parallelism already sizes the
pool by ``pp_size``.
"""
if mapping.has_pp():
num_micro_batches = mapping.pp_size
Expand All @@ -3225,6 +3268,26 @@ def compute_max_num_sequences(mapping: Mapping,
return max_batch_size * num_micro_batches


def resolve_max_num_sequences(model_engine,
mapping: Mapping,
max_batch_size: int,
llm_args,
max_num_sequences: Optional[int] = None) -> int:
"""Resolve the seat-pool size, preferring an explicit value, then the
engine's published pool, then a fresh ``compute_max_num_sequences``."""
if max_num_sequences is not None:
return max_num_sequences
engine_seats = getattr(model_engine, "max_num_seq_slots", None)
if engine_seats is not None:
return engine_seats
return compute_max_num_sequences(mapping,
max_batch_size,
llm_args.disable_overlap_scheduler,
enable_overlap_headroom=getattr(
model_engine,
"_enable_overlap_headroom", False))


def should_enable_adp_dummy_fixes(mapping: Mapping) -> bool:
"""Enable transactional ADP dummy handling while PP remains follow-up."""
return not mapping.has_pp()
Expand All @@ -3249,17 +3312,59 @@ def should_enable_non_overlap_adp_forward_intent(
and disable_overlap_scheduler)


def should_enable_disagg_adp_overlap_headroom(
mapping: Mapping,
cache_transceiver_config: Optional[CacheTransceiverConfig],
disable_overlap_scheduler: bool) -> bool:
"""Gate extra sequence slots to non-PP disaggregated attention-DP."""
is_disagg = (cache_transceiver_config is not None
and cache_transceiver_config.backend is not None)
return (mapping.enable_attention_dp and is_disagg and not mapping.has_pp()
def should_enable_overlap_headroom(mapping: Mapping,
disable_overlap_scheduler: bool,
kv_cache_manager_is_v2: bool,
is_hybrid: bool = False,
has_mrope_delta_cache: bool = False) -> bool:
"""Gate the extra micro-batch of sequence slots.

True only where a retiring request and the replacement that took its place
can own a seat at the same time: attention DP, non-PP, overlap-on, V2 and
non-hybrid.

Widening the pool is only safe when every ``py_seq_slot``-indexed pool is
sized from ``compute_max_num_sequences``. Two model families size one from
something else instead, so they keep the single-micro-batch pool:

* ``is_hybrid``: ``MambaCacheManager`` re-derives its own capacity as
``max_batch_size * pp_size``, which a doubled non-PP pool would exhaust.
* ``has_mrope_delta_cache``: Qwen2/2.5-VL and Qwen3-VL hold
``max_num_tokens * pp_size + 1`` MRoPE deltas while indexing them by
``py_seq_slot``, relying on ``max_batch_size <= max_num_tokens`` to stay in
bounds. The top entry is the reserved dummy slot, so a doubled pool first
aliases the dummy -- silently giving padded requests a real request's
delta -- and then indexes past the end.
"""
if is_hybrid or has_mrope_delta_cache or not kv_cache_manager_is_v2:
return False
return (mapping.enable_attention_dp and not mapping.has_pp()
and not disable_overlap_scheduler)


def validate_seq_slot_pool_covers_admission(max_num_sequences: int,
kv_cache_manager) -> None:
"""Fail at startup if the KV index pool cannot cover the seat pool.

The check is one-sided on purpose: an index pool narrower than the seat pool
silently defers admitted requests, while a wider one is legitimate. Managers
that do not publish an integer ``max_admissible_sequences`` are skipped.
"""
admissible = getattr(kv_cache_manager, "max_admissible_sequences", None)
if not isinstance(admissible, int):
return
if admissible >= max_num_sequences:
return
raise ValueError(
f"{type(kv_cache_manager).__name__} can lease KV cache indices for "
f"{admissible} concurrent sequences but the executor's sequence-slot "
f"pool holds {max_num_sequences}: the index pool is smaller than the "
"seat pool, so admitted requests would be silently deferred one at a "
"time (nvbug 6627795). The seat pool must come from "
"_util.compute_max_num_sequences and the index pool must cover it; a "
"shortfall means one of them was re-derived from max_batch_size.")


def create_py_executor_instance(
*,
dist,
Expand Down Expand Up @@ -3295,15 +3400,18 @@ def create_py_executor_instance(

spec_config = model_engine.spec_config

if max_num_sequences is None:
max_num_sequences = compute_max_num_sequences(
mapping, max_batch_size, llm_args.disable_overlap_scheduler)
is_disagg = is_disagg_enabled(cache_transceiver_config)

max_num_sequences = resolve_max_num_sequences(
model_engine,
mapping,
max_batch_size,
llm_args,
max_num_sequences=max_num_sequences)

logger.info(
f"max_seq_len={max_seq_len}, max_num_requests={max_num_sequences}, max_num_tokens={max_num_tokens}, max_batch_size={max_batch_size}"
)
is_disagg = (cache_transceiver_config is not None
and cache_transceiver_config.backend is not None)
for key, value in llm_args.extra_resource_managers.items():
if key in resources:
raise ValueError(
Expand Down Expand Up @@ -3461,6 +3569,7 @@ def create_py_executor_instance(
if isinstance(model_engine, PyTorchModelEngine):
model_engine._init_cuda_graph_lora_manager(lora_config)

validate_seq_slot_pool_covers_admission(max_num_sequences, kv_cache_manager)
resources[ResourceManagerType.SEQ_SLOT_MANAGER] = SeqSlotManager(
max_num_sequences)

Expand Down Expand Up @@ -3680,22 +3789,17 @@ def create_py_executor_instance(


def create_torch_sampler_args(
mapping: Mapping,
*,
max_seq_len: int,
max_batch_size: int,
speculative_config: SpeculativeConfig,
max_beam_width: int,
disable_overlap_scheduler: bool,
enable_async_worker: bool,
enable_speculative_beam_history_d2h: bool,
max_num_sequences: Optional[int] = None,
max_num_sequences: int,
):
# The sampler's per-slot state is indexed by sequence slots, so it must
# be sized identically to the executor's slot pool.
if max_num_sequences is None:
max_num_sequences = compute_max_num_sequences(
mapping, max_batch_size, disable_overlap_scheduler)
max_draft_len = (0 if speculative_config is None else
speculative_config.max_draft_len)
max_total_draft_tokens = (0 if speculative_config is None else
Expand Down Expand Up @@ -3727,10 +3831,15 @@ def instantiate_sampler(
enable_async_worker = (confidential_compute_enabled()
or llm_args.sampler_force_async_worker)

sampler_args = create_torch_sampler_args(
max_num_sequences = resolve_max_num_sequences(
engine,
mapping,
max_batch_size,
llm_args,
max_num_sequences=max_num_sequences)

sampler_args = create_torch_sampler_args(
max_seq_len=engine.max_seq_len,
max_batch_size=max_batch_size,
speculative_config=speculative_config,
max_beam_width=max_beam_width,
disable_overlap_scheduler=llm_args.disable_overlap_scheduler,
Expand Down
11 changes: 9 additions & 2 deletions tensorrt_llm/_torch/pyexecutor/kv_cache/kv_cache_manager_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,7 @@ def __init__(
is_disagg: bool = False,
enable_stats: bool = False,
num_reserved_index_slots: int = 1,
disable_overlap_scheduler: bool = False,
kv_events_config: Optional[KVEventsConfig] = None,
is_estimating_kv_cache: bool = False,
cold_page_codec_provider: Optional[object] = None,
Expand Down Expand Up @@ -1713,8 +1714,11 @@ def create_cold_page_codec(cache_config: object) -> Optional[object]:
# up to `max_num_sequences` requests are still in KV transfer
# (TRANS_IN_PROGRESS) and continue to hold their index slots. The 2x
# capacity lets the next batch of active requests acquire slots without
# waiting for the previous batch's transfers to finish.
# waiting for the previous batch's transfers to finish. With the overlap
# scheduler on (non-PP), a retiring request holds its lease one extra
# iteration and needs the same coefficient.
max_num_sequences = max_batch_size * mapping.pp_size
needs_extra_leases = is_disagg or (not disable_overlap_scheduler and not mapping.has_pp())
assert num_reserved_index_slots >= 0, "num_reserved_index_slots must be non-negative"
# Both diagnostics below are off unless their environment variable is
# set; with neither set nothing here changes any allocation, any page
Expand All @@ -1733,18 +1737,21 @@ def create_cold_page_codec(cache_config: object) -> Optional[object]:
self._fresh_pages_filled: Dict[int, Dict[int, np.ndarray]] = {}
self._fresh_fill_announced = False
self._fresh_fill_unavailable_announced = False
self.max_admissible_sequences = max_num_sequences * (2 if needs_extra_leases else 1)
# The guard page is held by a permanent sequence, so it needs an index
# slot of its own. Taking one of the scheduler's would change which
# requests get admitted, so a run with the diagnostic on would no
# longer be comparable with the run it is being read against.
index_mapper_capacity = (
max_num_sequences * (2 if is_disagg else 1)
self.max_admissible_sequences
+ num_reserved_index_slots
+ (1 if self._guard_page_value is not None else 0)
)
logger.info(
f"KVCacheManagerV2: IndexMapper capacity={index_mapper_capacity} "
f"(max_num_sequences={max_num_sequences}, is_disagg={is_disagg}, "
f"disable_overlap_scheduler={disable_overlap_scheduler}, "
f"pp_size={mapping.pp_size}, "
f"num_reserved_index_slots={num_reserved_index_slots}, "
f"max_beam_width={max_beam_width})"
)
Expand Down
Loading
Loading