Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 18 additions & 4 deletions tensorrt_llm/_torch/pyexecutor/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2531,17 +2531,31 @@ def compute_max_num_sequences(mapping: Mapping,
return max_batch_size * num_micro_batches


# Model types whose disaggregated attention-DP path has been measured against
# the ADP dummy fixes. The gate stays an explicit list rather than a capability
# check (``enable_attention_dp and kv_cache_transceiver is not None``) so that
# each entry is added only after its disagg ADP behavior has been exercised.
_ADP_DUMMY_FIX_MODEL_TYPES = ("deepseek_v4", "qwen3_5_moe")


def should_enable_dsv4_adp_dummy_fixes(model_type: Optional[str],
mapping: Mapping) -> bool:
"""Gate DSv4 ADP dummy behavior while PP remains follow-up scope."""
return model_type == "deepseek_v4" and not mapping.has_pp()
"""Gate the ADP dummy fixes while PP remains follow-up scope."""
return model_type in _ADP_DUMMY_FIX_MODEL_TYPES and not mapping.has_pp()


def should_enable_dsv4_overlap_headroom(
model_type: Optional[str], spec_config: Optional[SpeculativeConfig],
mapping: Mapping, disable_overlap_scheduler: bool) -> bool:
"""Gate extra sequence slots to the validated DSv4 MTP overlap path."""
return (should_enable_dsv4_adp_dummy_fixes(model_type, mapping)
"""Gate extra sequence slots to the validated DSv4 MTP overlap path.
Deliberately NOT routed through ``should_enable_dsv4_adp_dummy_fixes``.
That gate now covers more model types, while this one doubles
``max_num_sequences`` (see ``compute_max_num_sequences``) and therefore
changes the memory envelope; it must stay pinned to the one path it was
measured on.
"""
return (model_type == "deepseek_v4" and not mapping.has_pp()
and spec_config is not None
and spec_config.spec_dec_mode.is_mtp_eagle_one_model()
and not disable_overlap_scheduler)
Expand Down
31 changes: 29 additions & 2 deletions tensorrt_llm/_torch/pyexecutor/py_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5870,13 +5870,40 @@ def _pad_attention_dp_dummy_request(self):
if not self.enable_attention_dp:
return

assert self.expected_num_active_requests >= len(self.active_requests)
expected_num_active_requests = self.expected_num_active_requests
if expected_num_active_requests < len(self.active_requests):
# Not fatal, and not a capacity violation. The router derives this
# value as
# min(max(ceil(multiplier * fair_share), max(per_rank_loads)),
# max_num_active_requests)
# (adp_router.AttentionDpRouter._expected_num_active_requests), so
# the per-rank-load floor normally keeps it at or above this rank's
# own load -- but the hard cap is applied last. Any rank that ends
# up holding max_num_active_requests + 1 requests therefore breaks
# the relation, e.g. when an attention-DP pad dummy survives an
# iteration that was skipped fleet-wide (can_queue False skips both
# _forward_step and _update_request_states, and
# _update_request_states_tp is the only place the dummy is removed)
# and the next gather_all_rank_states counts it.
#
# Inside this method the value is consumed only by the idle-rank
# test below, and a rank holding surplus requests needs no dummy,
# so tolerate the surplus. Asserting here took down the executor
# event loop on every affected rank at once, leaving the survivors
# to HangDetector-abort.
logger.warning(
f"active_requests ({len(self.active_requests)}) exceeds "
f"expected_num_active_requests "
f"({expected_num_active_requests}); tolerating (a busy rank "
f"needs no attention-DP dummy).")
expected_num_active_requests = len(self.active_requests)

num_active_request = self._count_schedulable_active_requests()

if self._should_skip_dummy_for_benchmark_disagg(num_active_request):
return

needs_dummy = (self.expected_num_active_requests > 0
needs_dummy = (expected_num_active_requests > 0
and num_active_request == 0)
if not needs_dummy:
return
Expand Down
35 changes: 35 additions & 0 deletions tests/unittest/_torch/executor/test_py_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,41 @@ def test_pad_dummy_added_when_only_wait_scheduler_requests_disagg():
assert len(stub.active_requests) == 2


def test_pad_dummy_tolerates_surplus_over_expected_on_busy_rank() -> None:
# expected_num_active_requests is capped at max_num_active_requests after
# the per-rank-load floor is applied, so a rank can legitimately end up
# holding more requests than the router expected -- e.g. when a pad dummy
# survives an iteration that was skipped fleet-wide and the next
# gather_all_rank_states counts it. This used to trip a bare assert and
# kill the executor event loop; a busy rank needs no dummy, so it must be
# tolerated instead.
stub = _StubADPExecutor()
stub.active_requests = [_make_adp_request(_STATE_GENERATION_IN_PROGRESS) for _ in range(3)]
stub.expected_num_active_requests = 2

_run_pad(stub)

assert stub.add_dummy_calls == []
assert len(stub.active_requests) == 3
# Tolerating must not leak a mutated expectation to downstream consumers.
assert stub.expected_num_active_requests == 2


def test_pad_dummy_still_added_when_surplus_requests_are_unschedulable() -> None:
# Tolerating the surplus must not short-circuit padding. A rank can hold
# more requests than expected AND have none of them schedulable (all parked
# at GENERATION_TO_COMPLETE), in which case it still schedules batch=0 and
# needs a dummy to stay in the forward-pass collectives.
stub = _StubADPExecutor()
stub.active_requests = [_make_adp_request(_STATE_GENERATION_TO_COMPLETE) for _ in range(3)]
stub.expected_num_active_requests = 2

_run_pad(stub)

assert len(stub.add_dummy_calls) == 1
assert stub.expected_num_active_requests == 2


def test_pad_dummy_allocation_failure_skips_padding():
# add_dummy_requests returns None when the rank has no free cache
# resources for even a 1-token dummy (possible while non-schedulable
Expand Down
7 changes: 7 additions & 0 deletions tests/unittest/_torch/executor/test_seq_slot_sizing.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
("deepseek_v4", True, False, 1, False, False),
("deepseek_v4", True, True, 2, False, False),
("deepseek_v4", True, True, 1, True, False),
# The widened ADP dummy gate must not leak into the headroom gate:
# doubling max_num_sequences changes the memory envelope and has only
# been validated on the DSv4 MTP overlap path.
("qwen3_5_moe", True, True, 1, False, False),
],
)
def test_dsv4_overlap_headroom_gate(
Expand All @@ -71,6 +75,9 @@ def test_dsv4_overlap_headroom_gate(
("deepseek_v4", 1, True),
("deepseek_v3", 1, False),
("deepseek_v4", 2, False),
("qwen3_5_moe", 1, True),
("qwen3_5_moe", 2, False),
("llama", 1, False),
],
)
def test_dsv4_adp_dummy_fix_gate(model_type, pp_size, expected):
Expand Down
Loading