Skip to content
Merged
2 changes: 2 additions & 0 deletions tensorrt_llm/_torch/pyexecutor/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3038,6 +3038,8 @@ def create_py_executor_instance(
cross_kv_cache_manager=cross_kv_cache_manager,
no_schedule_until_state=no_schedule_until_state,
enable_prefix_aware_scheduling=enable_prefix_aware_scheduling,
# A disaggregated generation worker must not replay context locally.
enable_recompute_pause=not is_disagg,
)
elif (scheduler_config is not None
and scheduler_config.use_python_scheduler):
Expand Down
24 changes: 14 additions & 10 deletions tensorrt_llm/_torch/pyexecutor/kv_cache_manager_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,10 +994,9 @@ def append_to_kv_heads_per_layer(
host_quota = kv_cache_config.host_cache_size
else:
# The V2 MAX_UTILIZATION scheduler relies on suspend/resume to
# evict and later restore KV cache pages. Without a host tier,
# suspended pages have nowhere to be offloaded and resume()
# always fails, causing a scheduling deadlock where no
# generation request can ever make progress.
# evict and later restore KV cache pages. Without a secondary
# tier, suspended held pages cannot migrate out of GPU, so
# suspension cannot free capacity and scheduling can deadlock.
#
# Automatically provision a host tier matching the GPU quota so
# suspend/resume works out of the box. Cap at available host
Expand Down Expand Up @@ -1060,31 +1059,36 @@ def append_to_kv_heads_per_layer(
cache_tiers=cache_tiers,
)
config = self._build_cache_config(config)
has_host_cache_tier = any(
isinstance(tier, HostCacheTierConfig) for tier in config.cache_tiers
)

self.kv_cache_manager_py_config = config

try:
Comment thread
yizhang-nv marked this conversation as resolved.
self.impl = KVCacheManagerPy(config, event_manager=self.event_manager)
except (CuError, KVCacheOutOfMemoryError):
if len(cache_tiers) > 1:
if has_host_cache_tier:
Comment thread
yizhang-nv marked this conversation as resolved.
logger.warning(
"Failed to initialize KV cache manager with host cache "
"tier (cuMemHostRegister may have failed). "
"Retrying without host cache tier."
)
cache_tiers_gpu_only = [t for t in cache_tiers if isinstance(t, GpuCacheTierConfig)]
config = replace(config, cache_tiers=cache_tiers_gpu_only)
cache_tiers = cache_tiers_gpu_only
cache_tiers_without_host = [
tier for tier in config.cache_tiers if not isinstance(tier, HostCacheTierConfig)
]
config = replace(config, cache_tiers=cache_tiers_without_host)
self.kv_cache_manager_py_config = config
self.impl = KVCacheManagerPy(config, event_manager=self.event_manager)
else:
raise
self.can_evict = len(config.cache_tiers) > 1
if self.event_manager is not None:
self.event_manager.set_layer_group_window_sizes(
self._get_event_window_sizes_by_layer_group()
)
self.event_manager.add_created_event(
self._get_event_num_blocks_per_cache_level(cache_tiers, tokens_per_block),
self._get_event_num_blocks_per_cache_level(config.cache_tiers, tokens_per_block),
self._get_event_layer_group_ids(),
)

Expand Down Expand Up @@ -2472,7 +2476,7 @@ def extend_capacity_for_tokens(self, request: LlmRequest) -> None:
)

def suspend_request(self, req: LlmRequest) -> None:
"""Suspend a request's KV cache (move to host tier)."""
"""Suspend a request's KV cache, allowing pages to migrate to a secondary tier."""
kv_cache = self.kv_cache_map.get(req.py_request_id)
if kv_cache is not None and kv_cache.is_active:
kv_cache.suspend()
Expand Down
104 changes: 63 additions & 41 deletions tensorrt_llm/_torch/pyexecutor/llm_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -969,31 +969,10 @@ def __init__(
self.py_request_id = self.request_id
self.py_llm_request_type = self.llm_request_type
self.py_end_id = self.end_id
self.py_prompt_len = self.prompt_len
self.py_orig_prompt_len = self.orig_prompt_len
self.py_max_new_tokens = self.max_new_tokens
self.py_min_length = self.sampling_config.min_length
# `seqlen_this_rank_cp`, `total_input_len_cp`, and `py_helix_is_inactive_rank` are relevant to helix parallelism.
self.seqlen_this_rank_cp = self.prompt_len
self.total_input_len_cp = self.prompt_len
self.py_helix_is_inactive_rank = False
self.py_batch_idx = None
self.py_draft_pages_allocated = 0
self.py_rewind_len = 0
# Tokens physically evicted by KV-cache compression; deducted in the engine.
self.py_num_compressed_tokens = 0
self.py_draft_tokens = [] if self.draft_tokens is None else self.draft_tokens
self.py_last_context_chunk = (None, None)
self.py_draft_logits = None
self.py_target_probs = None
self.py_last_draft_tokens = None
self.py_num_accepted_draft_tokens = 0
# One-model rejection: set per-iteration by _handle_dynamic_draft_len when
# this gen request produced 0 real draft tokens, so _prepare_tp_inputs
# one-hots its stale draft_probs slot. Consumed (and cleared) there.
self.py_needs_onehot_draft_probs = False
self.py_num_accepted_draft_tokens_indices = []
self.py_rewind_draft_token_separate_adjustment = 0
self.py_per_pos_drafted = [0] * MAX_SPEC_DECODE_POSITIONS
self.py_per_pos_accepted = [0] * MAX_SPEC_DECODE_POSITIONS
# Cumulative spec-decode counters backing
Expand All @@ -1005,22 +984,6 @@ def __init__(
# response (see PyExecutor._handle_responses / LlmResult.spec_dec_totals).
self.py_total_draft_tokens = 0
self.py_total_accepted_draft_tokens = 0
# Denominator paired with py_num_accepted_draft_tokens: the number of
# genuinely proposed draft tokens the sampler verified in the step it
# wrote that numerator for (CUDA-graph padding excluded). Written by
# the samplers' update_requests, consumed (and reset to 0) by
# PyExecutor._accumulate_spec_dec_stats so each verified step is
# counted exactly once. py_draft_tokens itself cannot serve as the
# denominator: it is padded for CUDA graphs and, in the one-model
# flow, already holds the NEXT step's drafts by response time.
self.py_num_draft_tokens_verified = 0
# Number of real draft tokens installed for the upcoming step,
# recorded by Drafter.pad_draft_tokens_for_cuda_graph before it pads
# py_draft_tokens to the static max. None when no drafter recorded a
# count (e.g. one-model flow, which tracks lengths in its sample
# state instead).
self.py_draft_tokens_effective_len = None
self.py_decoding_iter = 0
self.is_attention_dp_dummy = False
self.is_cuda_graph_dummy = False
self.py_kv_transfer_start_time = None
Expand Down Expand Up @@ -1057,13 +1020,9 @@ def __init__(

self.py_beam_width = cast(int, self.sampling_config.beam_width)
self.py_is_draft = is_draft
# The request's sequence slot ID, an index between 0 (inclusive) and max_batch_size (exclusive).
self.py_seq_slot = seq_slot
# If the request is a draft request, target_seq_slot is the sequence slot ID of its target request.
self.py_target_seq_slot = target_seq_slot
self.use_draft_model = is_draft
self._cached_tokens = 0
self._cached_tokens_set = False
# Whether the request is for the first forward of the draft model.
self.py_is_first_draft = is_first_draft
self.d2t = None
Expand All @@ -1074,6 +1033,9 @@ def __init__(
self.py_use_chunked_generation_logits = use_chunked_generation_logits
self.py_logits_chunk_size = logits_chunk_size if not self.streaming else 1

self._initialize_execution_state(seq_slot=seq_slot,
orig_prompt_len=self.orig_prompt_len)

# TODO: remove this when use DynamicDecodeOp in pytorch flow.
# currently, keep py_stop_words_list as python list, rather than tensor.
self.py_stop_words_list = stop_words_list
Expand Down Expand Up @@ -1154,6 +1116,66 @@ def cached_tokens(self, value: int):
self._cached_tokens = value
self._cached_tokens_set = True

def _initialize_execution_state(self,
*,
seq_slot: Optional[int],
orig_prompt_len: int,
clear_draft_tokens: bool = False) -> None:
self.py_prompt_len = self.prompt_len
self.py_orig_prompt_len = orig_prompt_len
self.py_max_new_tokens = self.max_new_tokens
# CP sequence lengths are relevant to helix parallelism.
self.seqlen_this_rank_cp = self.prompt_len
self.total_input_len_cp = self.prompt_len
self.py_batch_idx = None
# The request's sequence slot ID, an index between 0 (inclusive) and max_batch_size (exclusive).
self.py_seq_slot = seq_slot
self.py_draft_pages_allocated = 0
self.py_rewind_len = 0
# Tokens physically evicted by KV-cache compression; deducted in the engine.
self.py_num_compressed_tokens = 0
if clear_draft_tokens:
self.draft_tokens = []
self.py_draft_tokens = []
else:
self.py_draft_tokens = ([] if self.draft_tokens is None else
self.draft_tokens)
# Number of real draft tokens installed for the upcoming step,
# recorded by Drafter.pad_draft_tokens_for_cuda_graph before it pads
# py_draft_tokens to the static max. None when no drafter recorded a
# count (e.g. one-model flow, which tracks lengths in its sample
# state instead).
self.py_draft_tokens_effective_len = None
self.py_last_context_chunk = (None, None)
self.py_last_draft_tokens = None
self.py_num_accepted_draft_tokens = 0
# Denominator paired with py_num_accepted_draft_tokens: the number of
# genuinely proposed draft tokens the sampler verified in the step it
# wrote that numerator for (CUDA-graph padding excluded). Written by
# the samplers' update_requests, consumed (and reset to 0) by
# PyExecutor._accumulate_spec_dec_stats so each verified step is
# counted exactly once. py_draft_tokens itself cannot serve as the
# denominator: it is padded for CUDA graphs and, in the one-model
# flow, already holds the NEXT step's drafts by response time.
self.py_num_draft_tokens_verified = 0
# One-model rejection: set per-iteration by _handle_dynamic_draft_len when
# this gen request produced 0 real draft tokens, so _prepare_tp_inputs
# one-hots its stale draft_probs slot. Consumed (and cleared) there.
self.py_needs_onehot_draft_probs = False
self.py_num_accepted_draft_tokens_indices = []
self.py_rewind_draft_token_separate_adjustment = 0
self.py_decoding_iter = 0
self.py_ctx_pre_resize_cap = None
Comment thread
yizhang-nv marked this conversation as resolved.
self._cached_tokens = 0
self._cached_tokens_set = False

def reset_for_recompute(self, max_input_len: int) -> None:
"""Reset Python-side execution state so the request can replay prefill."""
self.pause(max_input_len)
self._initialize_execution_state(seq_slot=None,
orig_prompt_len=self.prompt_len,
clear_draft_tokens=True)

def is_generation_only_request(self):
return self.py_llm_request_type == LlmRequestType.LLMREQUEST_TYPE_GENERATION_ONLY

Expand Down
Loading
Loading