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
33 changes: 26 additions & 7 deletions tensorrt_llm/_torch/pyexecutor/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2637,17 +2637,32 @@ def compute_max_num_sequences(mapping: Mapping,
return max_batch_size * num_micro_batches


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()
def should_enable_adp_dummy_fixes(mapping: Mapping) -> bool:
"""Enable transactional ADP dummy handling while PP remains follow-up."""
return not mapping.has_pp()


def should_enable_scheduler_aware_adp_dummy(
model_type: Optional[str], mapping: Mapping,
disable_overlap_scheduler: bool) -> bool:
"""Enable scheduler-aware padding for validated lifecycle configurations."""
return (should_enable_adp_dummy_fixes(mapping)
and (disable_overlap_scheduler or model_type == "deepseek_v4"))


def should_enable_non_overlap_adp_forward_intent(
mapping: Mapping, disable_overlap_scheduler: bool) -> bool:
"""Enable fresh cross-rank dummy intent for the generic non-overlap path."""
return (should_enable_adp_dummy_fixes(mapping)
and disable_overlap_scheduler)


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)
return (model_type == "deepseek_v4"
and should_enable_adp_dummy_fixes(mapping)
and spec_config is not None
and spec_config.spec_dec_mode.is_mtp_eagle_one_model()
and not disable_overlap_scheduler)
Expand Down Expand Up @@ -2976,8 +2991,12 @@ def create_py_executor_instance(
enable_prefix_aware_scheduling=enable_prefix_aware_scheduling,
)

mb_scheduler = BindMicroBatchScheduler(max_batch_size, max_num_tokens,
ctx_chunk_config)
mb_scheduler = BindMicroBatchScheduler(
max_batch_size,
max_num_tokens,
ctx_chunk_config,
no_schedule_until_state=no_schedule_until_state,
)

reorder_policy_config = llm_args.reorder_policy_config
if reorder_policy_config is not None:
Expand Down
21 changes: 14 additions & 7 deletions tensorrt_llm/_torch/pyexecutor/model_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,10 @@ def __init__(
# Start with the established pool size. Once the model is loaded we
# selectively enable headroom for the non-PP DeepSeek-V4 overlap path.
from ._util import (compute_max_num_sequences,
should_enable_dsv4_adp_dummy_fixes,
should_enable_dsv4_overlap_headroom)
should_enable_adp_dummy_fixes,
should_enable_dsv4_overlap_headroom,
should_enable_non_overlap_adp_forward_intent,
should_enable_scheduler_aware_adp_dummy)
self.max_num_seq_slots = compute_max_num_sequences(
mapping, self.batch_size, llm_args.disable_overlap_scheduler)
self.dist = dist
Expand Down Expand Up @@ -446,11 +448,16 @@ def __init__(
self.model = model
pretrained_config = self.model.model_config.pretrained_config
model_type = getattr(pretrained_config, "model_type", None)
# Keep the scheduler/dummy fix model-scoped, while the larger slot pool
# is restricted to the validated MTP overlap configuration. PP remains
# on its established path for follow-up changes.
self._enable_dsv4_adp_dummy_fixes = (should_enable_dsv4_adp_dummy_fixes(
model_type, mapping))
# Apply transactional dummy handling to every non-PP disaggregated ADP
# model. The larger slot pool remains restricted to the validated
# DeepSeek-V4 MTP overlap configuration.
self._enable_adp_dummy_fixes = should_enable_adp_dummy_fixes(mapping)
self._enable_scheduler_aware_adp_dummy = (
should_enable_scheduler_aware_adp_dummy(
model_type, mapping, llm_args.disable_overlap_scheduler))
self._enable_non_overlap_adp_forward_intent = (
should_enable_non_overlap_adp_forward_intent(
mapping, llm_args.disable_overlap_scheduler))
self._enable_dsv4_overlap_headroom = (
should_enable_dsv4_overlap_headroom(
model_type, spec_config, mapping,
Expand Down
92 changes: 69 additions & 23 deletions tensorrt_llm/_torch/pyexecutor/py_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@
_UNBOUNDED_STATS_MAX_LEN = -1


class _ADPForwardIntent(IntEnum):
# MAX reduction gives context precedence when ADP ranks have mixed work.
NONE = 0
GENERATION = 1
CONTEXT = 2


def _stats_buffer_is_unbounded(max_stats_len: int) -> bool:
return max_stats_len == _UNBOUNDED_STATS_MAX_LEN

Expand Down Expand Up @@ -569,8 +576,12 @@ def __init__(
self.resource_manager = resource_manager
self.scheduler = scheduler
self.model_engine = model_engine
self._enable_dsv4_adp_dummy_fixes = getattr(
model_engine, "_enable_dsv4_adp_dummy_fixes", False)
self._enable_adp_dummy_fixes = getattr(model_engine,
"_enable_adp_dummy_fixes", False)
self._enable_scheduler_aware_adp_dummy = getattr(
model_engine, "_enable_scheduler_aware_adp_dummy", False)
self._enable_non_overlap_adp_forward_intent = getattr(
model_engine, "_enable_non_overlap_adp_forward_intent", False)
self.enable_attention_dp = model_engine.enable_attention_dp
self.dist = dist
self.sampler = sampler
Expand Down Expand Up @@ -712,8 +723,8 @@ def __init__(
# lifted to _handle_kv_transfer_timeouts_synced / _flush_iter_stats_synced.
self._pending_timed_out_requests: List[LlmRequest] = []
self._pending_iter_stats_dict: Optional[Dict] = None
# ADP dummy role for _pad_attention_dp_dummy_request. Default is gen;
# updated from observed request types.
# Legacy ADP dummy role for overlap and PP fallback paths. The generic
# non-overlap path derives its role from fresh per-iteration intent.
self._adp_dummy_is_gen: bool = True
# Dummy allocated by the current scheduling iteration. It is committed
# to the normal forward/termination lifecycle only after every ADP rank
Expand Down Expand Up @@ -3344,7 +3355,7 @@ def _finalize_adp_dummy_allocation(self, can_queue: bool) -> None:
must release theirs before retrying or the fixed dummy request ID leaks
cache resources on every skipped iteration.
"""
if not self._enable_dsv4_adp_dummy_fixes:
if not self._enable_adp_dummy_fixes:
return

dummy_request = self._pending_adp_dummy_request
Expand Down Expand Up @@ -5113,7 +5124,8 @@ def _fetch_new_requests(
all_new_flat = [
req for reqs in all_ranks_new_requests.values() for req in reqs
]
self._update_adp_dummy_role(all_new_flat)
if not self._enable_non_overlap_adp_forward_intent:
self._update_adp_dummy_role(all_new_flat)

# Update per-rank counter for DP
self.num_fetch_requests_cur_rank += len(new_requests_cur_rank)
Expand Down Expand Up @@ -5765,31 +5777,56 @@ def _check_disagg_ctx_schedulable_status(self,
def _count_schedulable_active_requests(self) -> int:
"""Count active requests that are ready for scheduling.

The non-PP DeepSeek-V4 disaggregated ADP path mirrors the decoder
scheduler's state window [CONTEXT_INIT, GENERATION_TO_COMPLETE). This
covers generation-first context requests below the lower bound and
terminal requests at the upper bound. Other configurations retain the
established ADP behavior; PP eligibility remains follow-up scope.
The non-PP disaggregated ADP path uses the scheduler's state-
eligibility contract. This keeps decoder-only and encoder-decoder
boundaries and special exclusions aligned without duplicating them
here. PP eligibility remains follow-up scope.

Returns:
The number of active requests eligible for scheduling.
"""
if (not self._enable_dsv4_adp_dummy_fixes
if (not self._enable_scheduler_aware_adp_dummy
or self.kv_cache_transceiver is None):
if self.kv_cache_transceiver is None:
return len(self.active_requests)

# PP intentionally preserves its established ADP padding behavior
# until its dummy lifecycle is generalized. Keep this fallback on
# semantic request properties so enum reordering cannot silently
# change which transfer states it excludes.
return sum(
1 for req in self.active_requests
if not (req.is_disagg_generation_init_state
Comment thread
chienchunhung marked this conversation as resolved.
or req.is_disagg_generation_transmission_in_progress))

schedule_from_value = LlmRequestState.CONTEXT_INIT.value
to_complete_value = LlmRequestState.GENERATION_TO_COMPLETE.value
return sum(1 for req in self.active_requests
if self.scheduler.is_request_in_schedulable_state(req))

return sum(
1 for req in self.active_requests
if schedule_from_value <= req.state_value < to_complete_value)
def _get_non_overlap_adp_forward_intent(
self) -> tuple[int, _ADPForwardIntent]:
"""Return local eligible-real count and fresh TP-wide forward role.

This runs before capacity scheduling so the result is forward intent,
not a guarantee that every eligible request will be admitted. The
post-schedule queue vote commits or rolls back the tentative dummy.
"""
local_schedulable_count = 0
local_intent = _ADPForwardIntent.NONE
for request in self.active_requests:
if (request.is_attention_dp_dummy or
not self.scheduler.is_request_in_schedulable_state(request)
):
continue

local_schedulable_count += 1
if request.is_encoder_init_state or request.is_context_init_state:
local_intent = _ADPForwardIntent.CONTEXT
elif local_intent == _ADPForwardIntent.NONE:
local_intent = _ADPForwardIntent.GENERATION

global_intent = self.dist.tp_allreduce(int(local_intent),
op=ReduceOp.MAX)
return local_schedulable_count, _ADPForwardIntent(global_intent)

def _has_adp_dummy_kv_capacity(self,
token_nums: Optional[List[int]]) -> bool:
Expand Down Expand Up @@ -5876,8 +5913,18 @@ def _pad_attention_dp_dummy_request(self):
if self._should_skip_dummy_for_benchmark_disagg(num_active_request):
return

needs_dummy = (self.expected_num_active_requests > 0
and num_active_request == 0)
if (self._enable_non_overlap_adp_forward_intent
and self.kv_cache_transceiver is not None):
num_active_request, global_intent = (
self._get_non_overlap_adp_forward_intent())
if global_intent != _ADPForwardIntent.NONE:
self._adp_dummy_is_gen = (
global_intent == _ADPForwardIntent.GENERATION)
needs_dummy = (global_intent != _ADPForwardIntent.NONE
and num_active_request == 0)
else:
needs_dummy = (self.expected_num_active_requests > 0
and num_active_request == 0)
if not needs_dummy:
return

Expand Down Expand Up @@ -5910,7 +5957,7 @@ def _pad_attention_dp_dummy_request(self):
key="attention_dp_dummy_insufficient_kv_capacity")
return

if (not self._enable_dsv4_adp_dummy_fixes
if (not self._enable_adp_dummy_fixes
or self.kv_cache_transceiver is None):
llm_request = self.kv_cache_manager.add_dummy_requests(
request_ids=dummy_request_ids,
Expand Down Expand Up @@ -5945,9 +5992,8 @@ def _pad_attention_dp_dummy_request(self):
except OutOfPagesError:
dummy_requests = None
if not dummy_requests:
logger.warning(
"Cannot allocate DeepSeek-V4 ADP pad dummy; rank schedules "
"an empty batch and the fleet will retry.")
logger.warning("Cannot allocate ADP pad dummy; rank schedules "
"an empty batch and the fleet will retry.")
return

dummy_request = dummy_requests[0]
Expand Down
44 changes: 43 additions & 1 deletion tensorrt_llm/_torch/pyexecutor/scheduler/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,25 @@ def reset_context_requests(self, context_requests: RequestList | None = None) ->


class RequestScheduler(ABC):
@property
@abstractmethod
def scheduling_state_range(self) -> tuple[LlmRequestState, LlmRequestState]:
"""Return the half-open state range admitted to a forward batch."""
raise NotImplementedError

def is_request_in_schedulable_state(self, request: LlmRequest) -> bool:
Comment thread
chienchunhung marked this conversation as resolved.
"""Return whether request state permits admission to a forward batch."""
if is_decoder_context_request_waiting_for_encoder_output(request):
return False
if request.state in (
LlmRequestState.DISAGG_CONTEXT_WAIT_SCHEDULER,
LlmRequestState.DISAGG_GENERATION_INIT,
LlmRequestState.DISAGG_GENERATION_TRANS_IN_PROGRESS,
):
return False
schedule_from, schedule_to = self.scheduling_state_range
return schedule_from.value <= request.state_value < schedule_to.value

@abstractmethod
def schedule_request(
self, active_requests: RequestList, inflight_request_ids: set[int]
Expand Down Expand Up @@ -392,18 +411,27 @@ def __init__(
max_batch_size: int,
max_num_tokens: int = None,
ctx_chunk_config: Optional[tuple[StrEnum, int]] = None,
no_schedule_until_state: LlmRequestState = LlmRequestState.CONTEXT_INIT,
no_schedule_after_state: LlmRequestState = LlmRequestState.GENERATION_TO_COMPLETE,
) -> None:
super(BindMicroBatchScheduler, self).__init__()
self.max_batch_size = max_batch_size
self.max_num_tokens = max_num_tokens
self.no_schedule_until_state = no_schedule_until_state
self.no_schedule_after_state = no_schedule_after_state

ctx_chunk_config_cpp = None
if ctx_chunk_config is not None:
ctx_chunk_config_cpp = tb_internal.batch_manager.ContextChunkingConfig(
ctx_chunk_config[0]._to_pybind(), ctx_chunk_config[1]
)

self.impl = tb_internal.algorithms.MicroBatchScheduler(ctx_chunk_config_cpp, max_num_tokens)
self.impl = tb_internal.algorithms.MicroBatchScheduler(
ctx_chunk_config=ctx_chunk_config_cpp,
max_context_length=max_num_tokens,
no_schedule_until_state=no_schedule_until_state,
no_schedule_after_state=no_schedule_after_state,
)

def schedule(
self, active_requests: RequestList, inflight_request_ids: set[int]
Expand All @@ -427,6 +455,13 @@ def __init__(
self.capacity_scheduler = capacity_scheduler
self.micro_batch_scheduler = micro_batch_scheduler

@property
def scheduling_state_range(self) -> tuple[LlmRequestState, LlmRequestState]:
return (
self.micro_batch_scheduler.no_schedule_until_state,
self.micro_batch_scheduler.no_schedule_after_state,
)

def schedule_request(
self, active_requests: RequestList, inflight_request_ids: set[int]
) -> SchedulerOutput:
Expand Down Expand Up @@ -1867,6 +1902,13 @@ def __init__(
no_schedule_until_state=no_schedule_until_state,
)

@property
def scheduling_state_range(self) -> tuple[LlmRequestState, LlmRequestState]:
return (
self.micro_batch_scheduler.no_schedule_until_state,
self.micro_batch_scheduler.no_schedule_after_state,
)

def schedule_request(
self, active_requests: RequestList, inflight_request_ids: set[int]
) -> SchedulerOutput:
Expand Down
8 changes: 8 additions & 0 deletions tensorrt_llm/_torch/pyexecutor/scheduler/scheduler_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ def __init__(
# MicroBatchScheduler. For encoder-decoder models, caller should pass
# no_schedule_until_state=ENCODER_INIT to widen the range (same as
# C++ trtEncoderModel which passes kENCODER_INIT).
self.no_schedule_until_state = no_schedule_until_state
self.no_schedule_after_state = no_schedule_after_state
self._no_schedule_until_state_value = no_schedule_until_state.value
self._no_schedule_after_state_value = no_schedule_after_state.value
self._context_init_state_value = LlmRequestState.CONTEXT_INIT.value
Expand All @@ -220,6 +222,12 @@ def __init__(
os.environ.get("TLLM_DISAGG_GEN_PRIORITIZE_FIRST_TOKEN", "0") == "1"
)

@property
def scheduling_state_range(
self,
) -> tuple[LlmRequestState, LlmRequestState]:
return self.no_schedule_until_state, self.no_schedule_after_state

def schedule_request(
self, active_requests: RequestList, inflight_request_ids: set[int]
) -> SchedulerOutput:
Expand Down
Loading
Loading