Skip to content

Commit 924dfd2

Browse files
[TRTLLM-14620][fix] Preserve decoder construction contracts
Keep input-preparer ownership in the native and encoder-decoder graph factories, and retain optional-config fixture construction without changing configured confidence behavior. Add focused factory and constructor regression coverage. Signed-off-by: Zhaoyuan He <195028209+zhaoyuanh-nvidia@users.noreply.github.com>
1 parent 42ee73f commit 924dfd2

5 files changed

Lines changed: 219 additions & 41 deletions

File tree

‎tensorrt_llm/_torch/pyexecutor/engine/runners/decoder/inputs.py‎

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -357,15 +357,15 @@ def __init__(
357357
self._prepare_inputs_event: torch.cuda.Event | None = None
358358
self._gather_ids_buffer = (
359359
DeviceCopyBuffer(buffers.gather_ids_cuda, pin_memory=prefer_pinned())
360-
if config.is_spec_decode
360+
if config is not None and config.is_spec_decode
361361
else None
362362
)
363363
# Reuse device indices while the scheduled batch stays unchanged.
364364
self._staged_previous_batch_indices: list[int] | None = None
365365
self._staged_previous_pos_indices: list[int] | None = None
366366
self._step_input_gather: spec_step_copies.StepInputGather | None = (
367367
spec_step_copies.StepInputGather()
368-
if config.is_spec_decode and spec_step_copies.is_supported()
368+
if config is not None and config.is_spec_decode and spec_step_copies.is_supported()
369369
else None
370370
)
371371
# Steady-state generation-only prepare cache (non-speculative overlap
@@ -381,7 +381,7 @@ def __init__(
381381
os.getenv("TLLM_LOG_CACHED_KV_TOKENS_PER_REQ", "0") == "1"
382382
)
383383

384-
spec_config = self._config.spec_config
384+
spec_config = self._config.spec_config if self._config is not None else None
385385
self._dspark_confidence_enabled = bool(
386386
spec_config is not None and getattr(spec_config, "enable_confidence_scheduling", False)
387387
)
@@ -1950,23 +1950,27 @@ def previous_seq_slots_device() -> torch.Tensor:
19501950
if host.previous_batch_token_start is not None
19511951
else previous_begin * runtime_tokens_per_gen_step
19521952
)
1953-
if is_ragged_gen or step_gather is None or not step_gather.gather(
1954-
new_tokens_device,
1955-
next_draft_tokens_device,
1956-
new_tokens_lens_device,
1957-
self._buffers.previous_batch_indices_cuda,
1958-
self._buffers.previous_pos_indices_cuda,
1959-
previous_batch_len,
1960-
runtime_tokens_per_gen_step,
1961-
runtime_draft_token_buffer_width,
1962-
self._buffers.input_ids_cuda,
1963-
num_tokens,
1964-
self._buffers.draft_tokens_cuda,
1965-
num_draft_tokens,
1966-
self._buffers.previous_pos_id_offsets_cuda,
1967-
pos_offsets_start,
1968-
self._buffers.previous_kv_lens_offsets_cuda,
1969-
previous_begin,
1953+
if (
1954+
is_ragged_gen
1955+
or step_gather is None
1956+
or not step_gather.gather(
1957+
new_tokens_device,
1958+
next_draft_tokens_device,
1959+
new_tokens_lens_device,
1960+
self._buffers.previous_batch_indices_cuda,
1961+
self._buffers.previous_pos_indices_cuda,
1962+
previous_batch_len,
1963+
runtime_tokens_per_gen_step,
1964+
runtime_draft_token_buffer_width,
1965+
self._buffers.input_ids_cuda,
1966+
num_tokens,
1967+
self._buffers.draft_tokens_cuda,
1968+
num_draft_tokens,
1969+
self._buffers.previous_pos_id_offsets_cuda,
1970+
pos_offsets_start,
1971+
self._buffers.previous_kv_lens_offsets_cuda,
1972+
previous_begin,
1973+
)
19701974
):
19711975
# previous input ids
19721976
if is_ragged_gen:

‎tensorrt_llm/_torch/pyexecutor/engine/runners/decoder/runner.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def _make_input_preparer(
232232

233233
def _make_cuda_graphs(self) -> DecoderCudaGraphs:
234234
"""Build the CUDA graph component; call after the input preparer."""
235-
return DecoderCudaGraphs(**self._cuda_graphs_kwargs())
235+
return DecoderCudaGraphs(**self._cuda_graphs_kwargs(), inputs=self._inputs)
236236

237237
def _dummy_requests_kwargs(self) -> dict[str, Any]:
238238
return dict(
@@ -281,7 +281,6 @@ def _cuda_graphs_kwargs(self) -> dict[str, Any]:
281281
torch_compile_bypass=self._bypass_torch_compile,
282282
context_promotion_supported=self._context_graph_promotion_supported,
283283
warmup_inputs=self._buffers.input_ids_cuda,
284-
inputs=self._inputs,
285284
)
286285

287286
@property

‎tests/unittest/_torch/executor/test_pytorch_model_engine.py‎

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
PrefillCompiledModel
4040
from tensorrt_llm._torch.pyexecutor.engine.runners.decoder.warmup import \
4141
DecoderWarmup
42+
from tensorrt_llm._torch.pyexecutor.engine.runners.encoder_decoder import \
43+
EncoderDecoderRunner
4244
from tensorrt_llm._torch.pyexecutor.engine.runners.interface import \
4345
PreparedInputs
4446
from tensorrt_llm._torch.pyexecutor.engine.runners.pooling import PoolingRunner
@@ -78,17 +80,80 @@
7880
_TWO_MODEL_SPEC_MODE = SimpleNamespace(use_one_engine=lambda: False)
7981

8082

81-
def _input_preparer() -> DecoderInputPreparer:
82-
return DecoderInputPreparer(buffers=None,
83-
config=SimpleNamespace(is_spec_decode=False),
84-
mapping=None,
85-
dist=None,
86-
model=None,
87-
get_runtime_tokens_per_gen_step=None,
88-
iter_states={},
89-
lora=None,
90-
use_beam_search=False,
91-
steady_gen_cache_supported=True)
83+
def _input_preparer(*, without_config: bool = False) -> DecoderInputPreparer:
84+
return DecoderInputPreparer(
85+
buffers=None,
86+
config=(None if without_config else SimpleNamespace(
87+
is_spec_decode=False, spec_config=None)),
88+
mapping=None,
89+
dist=None,
90+
model=None,
91+
get_runtime_tokens_per_gen_step=None,
92+
iter_states={},
93+
lora=None,
94+
use_beam_search=False,
95+
steady_gen_cache_supported=True)
96+
97+
98+
@pytest.mark.parametrize("runner_type,graphs_path", [
99+
(DecoderRunner,
100+
"tensorrt_llm._torch.pyexecutor.engine.runners.decoder.runner.DecoderCudaGraphs"
101+
),
102+
(EncoderDecoderRunner,
103+
"tensorrt_llm._torch.pyexecutor.engine.runners.encoder_decoder.EncoderDecoderCudaGraphs"
104+
)
105+
])
106+
def test_cuda_graph_factory_preserves_input_preparer(runner_type, graphs_path):
107+
runner = Mock()
108+
runner._inputs = object()
109+
runner._cuda_graphs_kwargs.side_effect = lambda: DecoderRunner._cuda_graphs_kwargs(
110+
runner)
111+
112+
with patch(graphs_path) as graphs_type:
113+
graphs = runner_type._make_cuda_graphs(runner)
114+
115+
graphs_type.assert_called_once()
116+
assert graphs_type.call_args.kwargs["inputs"] is runner._inputs
117+
assert graphs_type.call_args.kwargs[
118+
"warmup_inputs"] is runner._buffers.input_ids_cuda
119+
assert graphs is graphs_type.return_value
120+
121+
122+
@pytest.mark.parametrize("enabled,fused", [(None, False), (False, True),
123+
(True, False), (True, True)])
124+
def test_input_preparer_configured_confidence_flags(enabled, fused):
125+
spec_config = (None if enabled is None else SimpleNamespace(
126+
enable_confidence_scheduling=enabled,
127+
enable_fused_confidence_scheduler=fused))
128+
config = SimpleNamespace(is_spec_decode=False, spec_config=spec_config)
129+
inputs = DecoderInputPreparer(buffers=None,
130+
config=config,
131+
mapping=None,
132+
dist=None,
133+
model=None,
134+
get_runtime_tokens_per_gen_step=None,
135+
iter_states={},
136+
lora=None,
137+
use_beam_search=False,
138+
steady_gen_cache_supported=True)
139+
140+
assert inputs._config is config
141+
assert inputs._dspark_confidence_enabled is bool(enabled)
142+
assert inputs._dspark_trims_submitted_tokens is bool(enabled)
143+
assert inputs._dspark_device_windows is bool(enabled and fused)
144+
assert inputs.previous_verify_lens == ([] if enabled else None)
145+
assert inputs.has_previous_device_draft is False
146+
147+
148+
def test_input_preparer_without_config_keeps_confidence_disabled():
149+
inputs = _input_preparer(without_config=True)
150+
151+
assert inputs._config is None
152+
assert inputs._dspark_confidence_enabled is False
153+
assert inputs._dspark_trims_submitted_tokens is False
154+
assert inputs._dspark_device_windows is False
155+
assert inputs.previous_verify_lens is None
156+
assert inputs.has_previous_device_draft is False
92157

93158

94159
class TestCachedKvTokenLogging(unittest.TestCase):

‎tests/unittest/_torch/speculative/hw_agnostic/test_dspark_cuda_graph_runtime.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def num_generation_requests(self):
4242

4343
def _runner_stub(runner_type=DSparkCUDAGraphRunner):
4444
runner = runner_type.__new__(runner_type)
45-
runner.clear = lambda: None
45+
runner.clear = lambda **_kwargs: None
4646
return runner
4747

4848

@@ -220,11 +220,11 @@ def test_decoder_selects_confidence_graph_subclass_only_when_enabled(
220220
decoder = SimpleNamespace(
221221
_dspark_confidence_enabled=confidence_enabled,
222222
_cuda_graph_runner_config=lambda: config,
223-
model=SimpleNamespace(register_cuda_graph_pre_replay_hooks=registered.append),
223+
_model=SimpleNamespace(register_cuda_graph_pre_replay_hooks=registered.append),
224224
)
225225

226226
runner = DecoderRunner._initialize_cuda_graph_runner(decoder)
227-
runner.clear = lambda: None
227+
runner.clear = lambda **_kwargs: None
228228

229229
expected_type = DSparkCUDAGraphRunner if confidence_enabled else CUDAGraphRunner
230230
assert type(runner) is expected_type

‎tests/unittest/_torch/speculative/hw_agnostic/test_dspark_model_engine_runtime.py‎

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ def test_model_engine_delegates_ragged_state_to_decoder_owner() -> None:
373373
inputs._dspark_exact_identity_words = tuple(range(8))
374374
inputs.fit_ragged_verify_lens = lambda *args, **kwargs: (args, kwargs)
375375
worker = object()
376-
runner.model = SimpleNamespace(spec_worker=worker)
376+
runner._model = SimpleNamespace(spec_worker=worker)
377377
engine = object.__new__(PyTorchModelEngine)
378378
engine._cleanup_done = True
379379
engine._runner = runner
@@ -440,7 +440,7 @@ def __init__(self, **kwargs):
440440
metadata_module.build_attention_metadata(model, **kwargs)
441441

442442
assert "enable_ragged_verification" not in seen[0]
443-
assert seen[0]["num_heads_per_kv"] == 4
443+
assert seen[0]["max_num_heads_per_kv"] == 4
444444

445445

446446
def test_metadata_helper_enables_only_supported_ragged_metadata():
@@ -481,6 +481,7 @@ def __init__(self, **kwargs):
481481
**kwargs, spec_config=SimpleNamespace(enable_confidence_scheduling=enabled)
482482
)
483483
decoder.mapping = kwargs["mapping"]
484+
decoder._attention_runtime_features = kwargs["attention_runtime_features"]
484485
decoder._buffers = SimpleNamespace(cache_indirection=None)
485486
monkeypatch.setattr(decoder_metadata_module, "resolve_mamba_metadata_cls", lambda _model: None)
486487
kv = object()
@@ -498,7 +499,7 @@ def __init__(self, **kwargs):
498499
@pytest.mark.parametrize("graph_type", [CUDAGraphRunner, DSparkCUDAGraphRunner])
499500
def test_current_main_warmup_keeps_serving_timeout_only_for_capture(monkeypatch, graph_type):
500501
graph = graph_type.__new__(graph_type)
501-
graph.clear = lambda: None
502+
graph.clear = lambda **_kwargs: None
502503
graph._capture_allowed = False
503504
graph.padding_dummy_requests = {5: object()}
504505
if graph_type is DSparkCUDAGraphRunner:
@@ -569,12 +570,16 @@ def test_confidence_without_an_sps_table_is_eager_only() -> None:
569570
graphs._get_graphs_to_capture([1, 4])
570571

571572

573+
@pytest.mark.parametrize("use_fused_gather", [False, True])
572574
@pytest.mark.parametrize(
573575
("trims", "prefix_window", "previous_windows"),
574576
[(True, 1, [3, 3]), (True, 1, [1, 2]), (False, None, [None, None])],
575577
)
576578
def test_overlap_offsets_follow_the_packed_prefix(
577-
trims: bool, prefix_window: int | None, previous_windows: list[int | None]
579+
trims: bool,
580+
prefix_window: int | None,
581+
previous_windows: list[int | None],
582+
use_fused_gather: bool,
578583
) -> None:
579584
"""Exercise the full input packer with CPU buffers and metadata-only leaves."""
580585
decoder = object.__new__(DecoderInputPreparer)
@@ -618,6 +623,27 @@ def test_overlap_offsets_follow_the_packed_prefix(
618623
decoder._mm_item_scheduler = None
619624
decoder._pinned_host_cache = {}
620625
decoder._pinned_host_active = {}
626+
decoder._staged_previous_batch_indices = None
627+
decoder._staged_previous_pos_indices = None
628+
gather_calls = []
629+
630+
def gather(*args) -> bool:
631+
# A CPU kernel leaf verifies the production caller's packed offsets.
632+
new, draft, lens, slots, positions, rows, width, draft_width = args[:8]
633+
ids, input_begin, drafts, draft_begin, pos, pos_begin, kv, kv_begin = args[8:]
634+
selected_slots = slots[:rows].long()
635+
gather_calls.append((input_begin, pos_begin, kv_begin))
636+
ids[input_begin : input_begin + rows * width].copy_(
637+
new.transpose(0, 1)[selected_slots, :width].flatten()
638+
)
639+
drafts[draft_begin : draft_begin + rows * draft_width].copy_(
640+
draft[selected_slots, :draft_width].flatten()
641+
)
642+
pos[pos_begin : pos_begin + rows * width].copy_(lens[positions[: rows * width].long()])
643+
kv[kv_begin : kv_begin + rows].copy_(lens[selected_slots] - width)
644+
return True
645+
646+
decoder._step_input_gather = SimpleNamespace(gather=gather) if use_fused_gather else None
621647
decoder._ragged_gather_indices = lambda slots, lengths: tuple(
622648
torch.tensor(indices, dtype=torch.long)
623649
for indices in ragged_gather_index_lists(slots, lengths)
@@ -626,7 +652,6 @@ def test_overlap_offsets_follow_the_packed_prefix(
626652
for name in (
627653
"input_ids_cuda",
628654
"draft_tokens_cuda",
629-
"num_accepted_draft_tokens_cuda",
630655
"previous_pos_id_offsets_cuda",
631656
"previous_kv_lens_offsets_cuda",
632657
"previous_pos_indices_cuda",
@@ -635,6 +660,7 @@ def test_overlap_offsets_follow_the_packed_prefix(
635660
"gather_ids_cuda",
636661
):
637662
setattr(decoder._buffers, name, torch.full((32,), -99, dtype=torch.int32))
663+
decoder._gather_ids_buffer = decoder_module.DeviceCopyBuffer(decoder._buffers.gather_ids_cuda)
638664
windows = [prefix_window, *previous_windows]
639665
requests = [
640666
SimpleNamespace(
@@ -699,3 +725,87 @@ def test_overlap_offsets_follow_the_packed_prefix(
699725
assert metadata.kv_cache_params.num_extra_kv_tokens == 2
700726
assert gather_ids.tolist() == list(range(sum(lengths)))
701727
assert draft_len == 3
728+
assert gather_calls == (
729+
[(prefix_len, prefix_len, 1)] if use_fused_gather and previous_lengths == [4, 4] else []
730+
)
731+
732+
733+
@pytest.mark.parametrize("trims", [False, True])
734+
def test_graph_overlap_offset_zeroing_preserves_ragged_bucket_padding(trims: bool) -> None:
735+
decoder = object.__new__(DecoderInputPreparer)
736+
decoder._config = SimpleNamespace(
737+
max_num_tokens=16, max_beam_width=1, disable_overlap_scheduler=False
738+
)
739+
decoder._dspark_trims_submitted_tokens = trims
740+
decoder._staged_previous_batch_indices = None
741+
decoder._staged_previous_pos_indices = None
742+
decoder._step_input_gather = None
743+
decoder.get_runtime_tokens_per_gen_step = lambda _length: 4
744+
decoder._ragged_gather_indices = lambda slots, lengths: tuple(
745+
torch.tensor(indices, dtype=torch.long)
746+
for indices in ragged_gather_index_lists(slots, lengths)
747+
)
748+
decoder._buffers = SimpleNamespace(
749+
**{
750+
name: torch.full((16,), -99, dtype=torch.int32)
751+
for name in (
752+
"input_ids_cuda",
753+
"draft_tokens_cuda",
754+
"previous_batch_indices_cuda",
755+
"previous_pos_indices_cuda",
756+
"previous_pos_id_offsets_cuda",
757+
"previous_kv_lens_offsets_cuda",
758+
)
759+
}
760+
)
761+
lengths = [1, 2] if trims else [4, 4]
762+
host = SimpleNamespace(
763+
lists=SimpleNamespace(
764+
input_ids=[],
765+
draft_tokens=[],
766+
draft_lens=[],
767+
position_ids=list(range(sum(lengths))),
768+
previous_batch_indices=[0, 1],
769+
previous_pos_indices=[0] * lengths[0] + [1] * lengths[1],
770+
extend_requests=[object(), object()],
771+
extend_dummy_requests=[],
772+
generation_requests=[],
773+
unwritten_input_id_rows=[],
774+
),
775+
previous_batch_tokens_per_request=lengths if trims else None,
776+
previous_batch_token_start=0,
777+
)
778+
decoder._copy_token_ids_to_device(
779+
host,
780+
SimpleNamespace(is_attention_dp_phase_idle=False, num_context_requests=0),
781+
torch.arange(8, dtype=torch.int32).reshape(4, 2, 1),
782+
torch.tensor([1, 2], dtype=torch.int32),
783+
torch.arange(6, dtype=torch.int32).reshape(2, 3),
784+
3,
785+
attn_metadata=SimpleNamespace(is_cuda_graph=True),
786+
enable_spec_decode=True,
787+
)
788+
expected_tail = 0 if trims else -99
789+
assert decoder._buffers.previous_pos_id_offsets_cuda[sum(lengths) :].tolist() == [
790+
expected_tail
791+
] * (16 - sum(lengths))
792+
assert decoder._buffers.previous_kv_lens_offsets_cuda[2:].tolist() == [expected_tail] * 14
793+
794+
795+
@pytest.mark.parametrize("release_owners", [False, True])
796+
def test_graph_release_preserves_nccl_window_owner_choice(release_owners: bool) -> None:
797+
calls = []
798+
graphs = object.__new__(DecoderCudaGraphs)
799+
graphs.cuda_graph_runner = SimpleNamespace(
800+
clear=lambda **kwargs: calls.append(("native", kwargs))
801+
)
802+
graphs.breakable_cuda_graph_runner = SimpleNamespace(
803+
clear=lambda **kwargs: calls.append(("breakable", kwargs))
804+
)
805+
806+
graphs.release(release_nccl_window_owners=release_owners)
807+
808+
assert calls == [
809+
("native", {"release_nccl_window_owners": release_owners}),
810+
("breakable", {"release_nccl_window_owners": release_owners}),
811+
]

0 commit comments

Comments
 (0)