Skip to content
Merged
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
5 changes: 4 additions & 1 deletion tensorrt_llm/_torch/attention/backends/fmha/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,10 @@ def __init__(self, attn: TrtllmAttention) -> None:
# normal forwarding never pays for an environment lookup.
self._cache_sanity_check_enabled = _is_fmha_cache_sanity_check_enabled()
self.fmha_libs: list[Fmha] = []
for fmha_cls in get_enabled_fmha_lib_classes():
for fmha_cls in (
*getattr(attn, "prepended_fmha_lib_classes", ()),
*get_enabled_fmha_lib_classes(),
):
if fmha_cls.is_available(attn):
self.fmha_libs.append(fmha_cls(attn))
self._layer_idx = getattr(attn, "layer_idx", None)
Expand Down
116 changes: 92 additions & 24 deletions tensorrt_llm/_torch/attention/backends/sparse/dsa/indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1987,9 +1987,24 @@ def sparse_attn_indexer(
)

if has_decode and not metadata.skip_indexer_for_gen_reqs and reuse_topk:
topk_indices_buffer[token_offset : token_offset + num_gen_tokens, :] = (
metadata.shared_topk_indices[:num_generations, :]
self._copy_reused_topk(
metadata,
topk_indices_buffer,
token_offset,
num_gen_tokens,
num_generations,
has_prefill,
gvr_prior_indices,
)
elif (
has_decode
and not metadata.skip_indexer_for_gen_reqs
and self._fill_decode_topk(
metadata, q_fp8, weights, topk_indices_buffer, token_offset, num_gen_tokens
)
):
# A subclass filled the generation rows.
pass
elif has_decode and not metadata.skip_indexer_for_gen_reqs:
# Get decode lengths per request (from seq_lens) for validation
gen_seq_lens = metadata.seq_lens[num_contexts : num_contexts + num_generations]
Expand Down Expand Up @@ -2268,30 +2283,83 @@ def sparse_attn_indexer(
prev_topk_dst.copy_(last_mtp_topk)

if self.mtp_index_share and metadata.in_mtp_draft_loop and not reuse_topk:
rows = None
if has_decode:
next_n = num_gen_tokens // num_generations
gen_topk = topk_indices_buffer[token_offset : token_offset + num_gen_tokens]
rows = self._mtp_last_accepted_rows(
gen_topk, metadata, num_contexts, num_generations, next_n
)
if has_prefill:
ctx_last = (
torch.cumsum(metadata.seq_lens_cuda[:num_contexts].to(torch.long), dim=0) - 1
)
ctx_rows = topk_indices_buffer[ctx_last]
rows = ctx_rows if rows is None else torch.cat([ctx_rows, rows])
if rows is not None:
shared_topk_indices = metadata.shared_topk_indices
if shared_topk_indices is None:
metadata.shared_topk_indices = rows.contiguous()
else:
row_start = num_contexts if is_generation else 0
shared_topk_indices[
row_start : row_start + rows.shape[0], : rows.shape[1]
].copy_(rows)
self._store_mtp_shared_rows(
topk_indices_buffer,
metadata,
num_contexts,
num_generations,
num_gen_tokens,
token_offset,
has_prefill,
has_decode,
is_generation,
)
return topk_indices_buffer

def _copy_reused_topk(
self,
metadata: DSAtrtllmAttentionMetadata,
topk_indices_buffer: torch.Tensor,
token_offset: int,
num_gen_tokens: int,
num_generations: int,
has_prefill: bool,
gvr_prior_indices: Optional[torch.Tensor],
) -> None:
"""Copy the shared TopK rows an MTP draft step reuses into its generation rows."""
topk_indices_buffer[token_offset : token_offset + num_gen_tokens, :] = (
metadata.shared_topk_indices[:num_generations, :]
)

def _fill_decode_topk(
self,
metadata: DSAtrtllmAttentionMetadata,
q_fp8: torch.Tensor,
weights: torch.Tensor,
topk_indices_buffer: torch.Tensor,
token_offset: int,
num_gen_tokens: int,
) -> bool:
"""Fill the generation TopK rows ahead of the decode branch; True if it filled them.

This class leaves them to the decode branch of ``sparse_attn_indexer``.
"""
return False

def _store_mtp_shared_rows(
self,
topk_indices_buffer: torch.Tensor,
metadata: DSAtrtllmAttentionMetadata,
num_contexts: int,
num_generations: int,
num_gen_tokens: int,
token_offset: int,
has_prefill: bool,
has_decode: bool,
is_generation: Optional[bool],
) -> None:
"""Store each request's last accepted TopK row for the next MTP draft step to reuse."""
rows = None
if has_decode:
next_n = num_gen_tokens // num_generations
gen_topk = topk_indices_buffer[token_offset : token_offset + num_gen_tokens]
rows = self._mtp_last_accepted_rows(
gen_topk, metadata, num_contexts, num_generations, next_n
)
if has_prefill:
ctx_last = torch.cumsum(metadata.seq_lens_cuda[:num_contexts].to(torch.long), dim=0) - 1
ctx_rows = topk_indices_buffer[ctx_last]
rows = ctx_rows if rows is None else torch.cat([ctx_rows, rows])
if rows is not None:
shared_topk_indices = metadata.shared_topk_indices
if shared_topk_indices is None:
metadata.shared_topk_indices = rows.contiguous()
else:
row_start = num_contexts if is_generation else 0
shared_topk_indices[row_start : row_start + rows.shape[0], : rows.shape[1]].copy_(
rows
)

def _mtp_last_accepted_rows(
self,
gen_topk: torch.Tensor,
Expand Down
2 changes: 2 additions & 0 deletions tensorrt_llm/_torch/attention/backends/trtllm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,8 @@ def is_sm_version_trtllm_gen_kernel(self, sm):
class TrtllmAttention(AttentionBackend[TrtllmAttentionMetadata]):

Metadata = TrtllmAttentionMetadata
# FMHA library classes this instance tries before the registry's libraries.
prepended_fmha_lib_classes: tuple = ()

@staticmethod
def is_sm_version_trtllm_gen_kernel(sm):
Expand Down
14 changes: 14 additions & 0 deletions tensorrt_llm/_torch/pyexecutor/engine/runners/decoder/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
ResourceManager,
ResourceManagerType,
)
from tensorrt_llm._torch.pyexecutor.runtime_hooks import DecodePrologue
from tensorrt_llm._torch.pyexecutor.sampler import SampleStateTensors
from tensorrt_llm._torch.pyexecutor.scheduler import ScheduledRequests
from tensorrt_llm._torch.pyexecutor.scheduler.scheduler import RemoteTailPhase
Expand Down Expand Up @@ -324,6 +325,9 @@ class DecoderInputPreparer:
path overrides ``_new_extra_inputs`` and ``_prepare_inputs_fast_path``.
"""

# The model's decode prologue (RuntimeHooks.decode_prologue), set by the runner.
decode_prologue: DecodePrologue | None = None

def __init__(
self,
*,
Expand Down Expand Up @@ -2561,6 +2565,16 @@ def preprocess_inputs(
Make some changes to the device inputs and avoid blocking the async data transfer
"""
attn_meta = inputs.get("attn_metadata")
# A model's decode prologue may replace the device work below with one
# launch; it decides on the host before any device work and returns
# False to keep the code below.
prologue = self.decode_prologue
if prologue is not None and prologue(
self, inputs, enable_spec_decode=enable_spec_decode, runtime_draft_len=runtime_draft_len
):
if self.guided_decoder is not None:
self.guided_decoder.token_event.record()
return inputs
# Invalidate per-forward-pass caches so they are recomputed (and captured) on every _forward_step.
if attn_meta is not None:
attn_meta.on_update_kv_lens()
Expand Down
10 changes: 10 additions & 0 deletions tensorrt_llm/_torch/pyexecutor/engine/runners/decoder/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
from tensorrt_llm._torch.pyexecutor.guided_decoder import CapturableGuidedDecoder
from tensorrt_llm._torch.pyexecutor.llm_request import LlmRequest
from tensorrt_llm._torch.pyexecutor.resource_manager import ResourceManager, ResourceManagerType
from tensorrt_llm._torch.pyexecutor.runtime_hooks import (
DEFAULT_RUNTIME_HOOKS,
RuntimeHooks,
runtime_hooks_of,
)
from tensorrt_llm._torch.pyexecutor.sampler.beam_search import PendingBeamHistories
from tensorrt_llm._torch.pyexecutor.sampler.sampler_common import SampleType
from tensorrt_llm._torch.pyexecutor.scheduler import ScheduledRequests
Expand Down Expand Up @@ -127,6 +132,9 @@ def __getattr__(self, name: str) -> Any:
class DecoderRunner(ScheduledModelRunner):
"""Run scheduled decoder generation with its own buffers, graphs and metadata."""

# Step-loop behaviour the model opted into (RuntimeHooks); all off unless it declares otherwise.
runtime_hooks: RuntimeHooks = DEFAULT_RUNTIME_HOOKS

# Optional execution paths that a subclass can turn off.
_context_warmups_supported = True
_context_graph_promotion_supported = True
Expand Down Expand Up @@ -204,6 +212,8 @@ def __init__(
self._dummy_requests = self._make_dummy_requests()
self._init_decoder_state()
self._inputs = self._make_input_preparer(mm_item_scheduler)
self.runtime_hooks = runtime_hooks_of(self._model)
self._inputs.decode_prologue = self.runtime_hooks.decode_prologue
self._graphs = self._make_cuda_graphs()
self._inputs.cuda_graph_runner = self.cuda_graph_runner
self._initialize_moe_workspace_reclaimer()
Expand Down
67 changes: 63 additions & 4 deletions tensorrt_llm/_torch/pyexecutor/py_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
get_from_waiting_queue, merge_requests)
from .resource_manager import (NoFreeSlotsError, ResourceManager,
ResourceManagerType, request_context)
from .runtime_hooks import ExecutorEvents, runtime_hooks_of
from .sampler import (AsyncWorkerMixin, Sampler, SamplerEvent, SampleState,
SampleStateTensors)
from .scheduler import (RequestScheduler, ScheduledRequests,
Expand Down Expand Up @@ -451,6 +452,16 @@ def _single_rank_adp_skips_padding(executor: "PyExecutor") -> bool:


class PyExecutor:
# True while the overlap loop runs with the execution stream current
# (_overlap_loop_stream_scope).
_loop_on_execution_stream = False
# Whether the model opted into running the overlap loop on the execution
# stream (RuntimeHooks.loop_on_execution_stream); read once at construction.
_execution_stream_loop_armed = False
# The model's executor events (RuntimeHooks.executor_events); read once at
# construction.
_executor_events: Optional[ExecutorEvents] = None

# Minimum number of async micro batches for async PP execution.
# This is a trade-off between memory usage and performance.
# If the number of micro batches is too small, the executor will spend too much time in synchronization.
Expand Down Expand Up @@ -571,6 +582,9 @@ def __init__(
# related modules
self.resource_manager = resource_manager
self.model_engine = model_engine
hooks = runtime_hooks_of(model_engine.model)
self._execution_stream_loop_armed = hooks.loop_on_execution_stream
self._executor_events = hooks.executor_events
self._enable_adp_dummy_fixes = enable_adp_dummy_fixes
self._enable_scheduler_aware_adp_dummy = enable_scheduler_aware_adp_dummy
self._enable_non_overlap_adp_forward_intent = (
Expand Down Expand Up @@ -980,6 +994,12 @@ def __init__(
# Ensure the default stream waits for execution_stream to complete
# before subsequent operations.
torch.cuda.current_stream().wait_stream(self.execution_stream)
if self._executor_events is not None:
# The model's actions after warmup and graph capture, on every
# rank (some are collective, so every rank reaches this point,
# and a rank-local failure arms the rank-crash watchdog).
with _distributed_warmup_guard(self.dist, self.dist.mapping):
self._executor_events.after_warmup()
self.encoder_launch_executor = (ThreadPoolExecutor(
max_workers=1, thread_name_prefix="encoder-launch") if
self.is_encoder_decoder else None)
Expand Down Expand Up @@ -3144,6 +3164,9 @@ def _executor_loop_cleanup(self):
f"Error waiting on PP send handles during cleanup: "
f"{traceback.format_exc()}")

if self._executor_events is not None and self._event_loop_completed:
self._executor_events.at_shutdown()

def _pp_rank_schedules(self) -> bool:
"""Whether this rank computes the schedule that the other PP ranks receive.

Expand Down Expand Up @@ -6197,11 +6220,44 @@ def control_action(self,
def _wait_for_model_engine_input_copy(self):
self.model_engine.wait_for_input_copy()

@contextmanager
def _overlap_loop_stream_scope(self):
"""Run the overlap loop with the execution stream current, when the model opted in.

_forward_step makes the execution stream wait for the current stream
before every forward and the current stream wait for the execution
stream after it. On the thread's default stream, which in steady decode
carries only these waits and timing events, step N+1's inputs wait for
decode graph N through two cross-stream event hops. With the execution
stream current the loop's stream work is queued in the same order on one
stream and _forward_step skips both waits.

Not taken with a two-model drafter, a guided decoder, a KV connector,
an encoder-decoder model or multimodal encoder item scheduling, whose
side streams synchronize with the current stream.
"""
if not (self._execution_stream_loop_armed and self.drafter is None and
self.draft_model_engine is None and self.guided_decoder is None
and self.kv_connector_manager is None
and not self.is_encoder_decoder
and not self._mm_encoder_item_scheduling_enabled):
yield
return
with torch.cuda.stream(self.execution_stream):
self._loop_on_execution_stream = True
try:
yield
finally:
self._loop_on_execution_stream = False

def _executor_loop_overlap(self) -> None:
torch.cuda.set_device(self.device_id)
# ensure the context is created, otherwise, some MPI calls will fail.
CUASSERT(cudart.cudaSetDevice(self.device_id))
with self._profiler() as profile_step, self.hang_detector:
if self._executor_events is not None:
self._executor_events.before_overlap_loop()
with self._overlap_loop_stream_scope(), self._profiler() as profile_step, \
self.hang_detector:
iter_start_time = time.time()
iter_stats = None
previous_tensors_device = None
Expand Down Expand Up @@ -8946,8 +9002,10 @@ def forward(scheduled_requests, resource_manager, new_tensors_device,
cache_indirection_buffer = self.sampler.get_cache_indirection()

# Run model forward on the execution stream for proper synchronization
# with KVCacheTransferManager's onboard/offload operations.
self.execution_stream.wait_stream(torch.cuda.current_stream())
# with KVCacheTransferManager's onboard/offload operations. With the
# loop on the execution stream both waits are program order.
if not self._loop_on_execution_stream:
self.execution_stream.wait_stream(torch.cuda.current_stream())
self._attach_encoder_output_to_execution_stream(scheduled_requests)
with torch.cuda.stream(self.execution_stream):
outputs = forward(scheduled_requests, self.resource_manager,
Expand All @@ -8964,7 +9022,8 @@ def forward(scheduled_requests, resource_manager, new_tensors_device,

# Ensure the default stream waits for execution_stream to complete
# before downstream operations use the outputs.
torch.cuda.current_stream().wait_stream(self.execution_stream)
if not self._loop_on_execution_stream:
torch.cuda.current_stream().wait_stream(self.execution_stream)

self._kv_connector_wait_for_save()
if route_capture is not None:
Expand Down
Loading
Loading