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
37 changes: 26 additions & 11 deletions tensorrt_llm/_torch/pyexecutor/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2637,17 +2637,28 @@ 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_adp_dummy_fixes(mapping: Mapping) -> bool:
"""Enable transactional ADP dummy handling while PP remains follow-up."""
return not mapping.has_pp()


def should_enable_dsv4_adp_dummy_fixes(model_type: Optional[str],
mapping: Mapping) -> bool:
"""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()
_VALIDATED_OVERLAP_ADP_DUMMY_MODEL_TYPES = ("deepseek_v4", "qwen3_5_moe")


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 in _VALIDATED_OVERLAP_ADP_DUMMY_MODEL_TYPES))


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_disagg_adp_overlap_headroom(
Expand Down Expand Up @@ -2984,8 +2995,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
13 changes: 10 additions & 3 deletions tensorrt_llm/_torch/pyexecutor/model_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,12 +391,15 @@ def __init__(
# Disaggregated attention-DP can backfill a batch before the overlap
# scheduler releases the previous batch's terminal sequence slots.
from ._util import (compute_max_num_sequences,
should_enable_adp_dummy_fixes,
should_enable_disagg_adp_overlap_headroom,
should_enable_dsv4_adp_dummy_fixes)
should_enable_non_overlap_adp_forward_intent,
should_enable_scheduler_aware_adp_dummy)
self._enable_disagg_adp_overlap_headroom = (
should_enable_disagg_adp_overlap_headroom(
mapping, llm_args.cache_transceiver_config,
llm_args.disable_overlap_scheduler))
self._enable_adp_dummy_fixes = should_enable_adp_dummy_fixes(mapping)
self.max_num_seq_slots = compute_max_num_sequences(
mapping,
self.batch_size,
Expand Down Expand Up @@ -486,8 +489,12 @@ def __init__(
self.model = model
pretrained_config = self.model.model_config.pretrained_config
model_type = getattr(pretrained_config, "model_type", None)
self._enable_dsv4_adp_dummy_fixes = should_enable_dsv4_adp_dummy_fixes(
model_type, 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))
if drafting_loop_wrapper is not None:
self.model = drafting_loop_wrapper(self.model)
self.model_is_wrapped = True
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 @@ -98,6 +98,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 @@ -584,8 +591,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 @@ -727,8 +738,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 @@ -3376,7 +3387,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 @@ -5159,7 +5170,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 @@ -6044,31 +6056,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 @@ -6182,8 +6219,18 @@ def _pad_attention_dp_dummy_request(self):
if self._should_skip_dummy_for_benchmark_disagg(num_active_request):
return

needs_dummy = (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 = (expected_num_active_requests > 0
and num_active_request == 0)
if not needs_dummy:
return

Expand Down Expand Up @@ -6216,7 +6263,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 @@ -6251,9 +6298,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
21 changes: 19 additions & 2 deletions tensorrt_llm/_torch/pyexecutor/resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,11 @@ def __init__(
self.indexer_k_cache_local_layer_mask = None

self.kv_connector_manager = kv_connector_manager
# Dummy requests can reserve their V1 sequence before they enter the
# normal context prepare path. Track that ownership per manager so the
# same sequence is not registered twice, while a separate draft or
# cross-cache manager can still prepare its own copy.
self._preprepared_dummy_request_ids: set[int] = set()

tp_size = mapping.tp_size
if mapping.enable_attention_dp:
Expand Down Expand Up @@ -785,6 +790,8 @@ def get_needed_resource_to_completion(self, request: LlmRequest) -> int:
def _context_seq_len(self, req: LlmRequest, is_cross: bool,
is_star_cp: bool) -> Optional[int]:
"""Return the sequence length to pass to add_sequence_batch, or None to skip this request."""
if req.py_request_id in self._preprepared_dummy_request_ids:
return None
if is_cross:
if (getattr(req, "py_skip_cross_kv_projection", False)
or not req.is_first_context_chunk
Expand Down Expand Up @@ -1080,6 +1087,14 @@ def add_dummy_requests(
raise cleanup_error
raise

if batch_request_infos:
self._preprepared_dummy_request_ids.update(
req_id for req_id, _, _ in batch_request_infos)
if (draft_batch_request_infos
and isinstance(draft_kv_cache_manager, KVCacheManager)):
draft_kv_cache_manager._preprepared_dummy_request_ids.update(
req_id for req_id, _, _ in draft_batch_request_infos)

return requests

def update_resources(self,
Expand Down Expand Up @@ -1130,8 +1145,10 @@ def update_resources(self,
self.impl.store_context_blocks(request)

def free_resources(self, request: LlmRequest, pin_on_release: bool = False):
return self.impl.remove_sequence(request.py_request_id, request,
pin_on_release)
result = self.impl.remove_sequence(request.py_request_id, request,
pin_on_release)
self._preprepared_dummy_request_ids.discard(request.py_request_id)
return result

def store_blocks_for_reuse(self,
request: LlmRequest,
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
Loading
Loading