Skip to content

Commit c2c8ef6

Browse files
committed
[None][fix] Harden dynamic tree kernels and clean up one-model speculative decoding
Fix OOB read in CUDA tree-walk kernels by bounding the while loop and guarding against missing parent tokens. Remove duplicate SpeculativeConfig type alias that shadowed the discriminated union. Replace assert-False error handling with proper RuntimeError, propagate runtime max_batch_size from executor creator to one-model worker, and clean up dead code. Signed-off-by: Qianyi Guan <qguan@nvidia.com> Signed-off-by: qgai <qgai@nvidia.com>
1 parent f431106 commit c2c8ef6

10 files changed

Lines changed: 40 additions & 74 deletions

File tree

cpp/tensorrt_llm/kernels/speculativeDecoding/dynamicTreeKernels.cu

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ __global__ void buildDynamicTreeKernel(int64_t const* parentList, int64_t const*
112112
{
113113
// Walk up to root, setting treeMask ancestor bits and counting depth
114114
int32_t curPosition = tid - 1;
115-
while (true)
115+
while (position < depth + 1)
116116
{
117117
position += 1;
118118
treeMask[tokenTreeIdx + curPosition] = 1;
@@ -131,6 +131,10 @@ __global__ void buildDynamicTreeKernel(int64_t const* parentList, int64_t const*
131131
break;
132132
}
133133
}
134+
if (curPosition == draftTokenNum)
135+
{
136+
break;
137+
}
134138
}
135139
positions[bid * draftTokenNum + tid] = position + seqLen;
136140
}
@@ -204,7 +208,7 @@ __global__ void buildDynamicTreeKernelPacked(int64_t const* parentList, int64_t
204208
else
205209
{
206210
int32_t curPosition = tid - 1;
207-
while (true)
211+
while (position < depth + 1)
208212
{
209213
position += 1;
210214

@@ -231,6 +235,10 @@ __global__ void buildDynamicTreeKernelPacked(int64_t const* parentList, int64_t
231235
break;
232236
}
233237
}
238+
if (curPosition == draftTokenNum)
239+
{
240+
break;
241+
}
234242
}
235243
positions[bid * draftTokenNum + tid] = position + seqLen;
236244
}

examples/llm-api/quickstart_advanced.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"Hello, my name is",
1414
"The capital of France is",
1515
"The future of AI is",
16-
# "A conversation between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions. USER: Hello! ASSISTANT:",
1716
]
1817

1918

tensorrt_llm/_torch/pyexecutor/py_executor_creator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,12 @@ def create_py_executor(
361361
has_draft_model_engine = spec_config.spec_dec_mode.has_draft_model()
362362
has_spec_drafter = spec_config.spec_dec_mode.has_spec_drafter()
363363

364+
# Propagate runtime max_batch_size so one-model workers can pre-allocate
365+
# buffers without hardcoding. Pydantic forbids extra fields, so use
366+
# object.__setattr__ to bypass validation.
367+
object.__setattr__(spec_config, '_runtime_max_batch_size',
368+
max_batch_size)
369+
364370
# WAR for https://nvbugs/5807902
365371
# Disable separate draft KV cache in disaggregated mode
366372
# Enable separate pool for None DI + Non-KVBM and Aggregated + KVBM

tensorrt_llm/_torch/pyexecutor/sampler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3307,7 +3307,7 @@ def update_requests(
33073307
state.sampler_event.synchronize()
33083308

33093309
assert state.host is not None
3310-
new_tokens = state.host.new_tokens # torch.Size([5, 2048, 1])
3310+
new_tokens = state.host.new_tokens
33113311
finish_reasons = state.host.finish_reasons_list()
33123312

33133313
new_tokens_list = new_tokens.tolist()

tensorrt_llm/_torch/speculative/drafting_loops.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -676,9 +676,7 @@ def forward(self, input_ids: torch.Tensor, position_ids: torch.Tensor,
676676

677677
# Build dynamic tree structure using CUDA kernel (in-place, writes to pre-allocated buffers)
678678
tree_structure = None
679-
if hasattr(
680-
self,
681-
'tree_ops_converter') and self.tree_ops_converter is not None:
679+
if self.tree_ops_converter is not None:
682680
try:
683681
# Write directly to spec_tree_manager buffers (target model reads from there).
684682
# use_packed_mask=True: kernel outputs bit-packed mask directly,
@@ -701,7 +699,8 @@ def forward(self, input_ids: torch.Tensor, position_ids: torch.Tensor,
701699
)
702700

703701
except Exception as e:
704-
assert False, f"Dynamic tree CUDA kernel failed: {e}"
702+
raise RuntimeError(
703+
f"Dynamic tree CUDA kernel failed: {e}") from e
705704

706705
# return_new_draft_tokens: [max_total_draft_tokens, batch_size]
707706
return_new_draft_tokens = torch.transpose(real_draft_tokens, 0, 1)
@@ -735,7 +734,8 @@ def forward(self, input_ids: torch.Tensor, position_ids: torch.Tensor,
735734
}
736735
}
737736

738-
def sample(self, logits: torch.Tensor, max_top_k: int) -> torch.Tensor:
737+
def sample(self, logits: torch.Tensor,
738+
max_top_k: int) -> tuple[torch.Tensor, torch.Tensor]:
739739
# TODO: inject the sampler here so we can support non-greedy
740740

741741
# for draft_layer_idx == 0, logits is of shape [batch_size, vocab_size]

tensorrt_llm/_torch/speculative/dynamic_tree_ops.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,6 @@ def __init__(
8181
self.max_batch_size = max_batch_size
8282
self.device = device
8383

84-
# Pre-allocate buffers for tree building
85-
self._preallocate_buffers()
86-
87-
def _preallocate_buffers(self):
88-
"""Pre-allocate reusable buffers to minimize runtime allocation."""
89-
# Preallocate parent_list buffer (max size)
90-
# Size: [max_batch_size, K * (depth - 1) + 1]
91-
# Note: Only first max_total_draft_tokens are used
92-
self.parent_list_buffer = torch.full(
93-
(self.max_batch_size, self.K * (self.depth - 1) + 1),
94-
-1,
95-
dtype=torch.int32,
96-
device=self.device,
97-
)
98-
9984
def build_dynamic_tree(
10085
self,
10186
history_draft_tokens_parent_buffer: torch.Tensor,

tensorrt_llm/_torch/speculative/eagle3_dynamic_tree.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ def __init__(
105105
K = spec_config.dynamic_tree_max_topK
106106
max_draft_len = spec_config.max_draft_len
107107
max_total_draft_tokens = spec_config.tokens_per_gen_step - 1
108-
max_batch_size = 256
108+
# Read runtime max_batch_size set by py_executor_creator; fall back to 256
109+
max_batch_size = getattr(spec_config, "_runtime_max_batch_size", 256)
110+
self._max_batch_size = max_batch_size
109111

110112
self.K = K
111113
self.max_total_draft_tokens = max_total_draft_tokens
@@ -207,6 +209,7 @@ def _ensure_spec_tree_manager(self, resource_manager):
207209

208210
# ---- Overridden dispatch methods ----
209211

212+
@override
210213
def _forward_draft_loop(
211214
self,
212215
inputs,
@@ -236,6 +239,7 @@ def _forward_draft_loop(
236239
resource_manager,
237240
)
238241

242+
@override
239243
def forward(
240244
self,
241245
input_ids,
@@ -267,6 +271,7 @@ def forward(
267271
]
268272
return output
269273

274+
@override
270275
def sample_and_accept_draft_tokens(self, logits, attn_metadata, spec_metadata):
271276
"""Override to handle dynamic tree verification."""
272277
batch_size = attn_metadata.num_seqs
@@ -277,6 +282,7 @@ def sample_and_accept_draft_tokens(self, logits, attn_metadata, spec_metadata):
277282
logits, attn_metadata, spec_metadata, batch_size, num_contexts, num_gens
278283
)
279284

285+
@override
280286
def prepare_1st_drafter_inputs(
281287
self,
282288
input_ids,
@@ -434,6 +440,10 @@ def _forward_dynamic_tree_draft_loop(
434440
self._ensure_spec_tree_manager(resource_manager)
435441
spec_tree_manager = self.spec_tree_manager
436442

443+
assert batch_size <= self._max_batch_size, (
444+
f"batch_size {batch_size} exceeds pre-allocated max_batch_size {self._max_batch_size}"
445+
)
446+
437447
# === Step 0: Initial forward ===
438448
# Inputs already in uniform-padded layout from prepare_1st_drafter_inputs:
439449
# max_draft_len + 1 tokens per gen request (contiguous accepted + zero padding).
@@ -703,7 +713,9 @@ def _sample_and_accept_dynamic_tree(
703713

704714
# ---- Dynamic tree helper methods (matching two-model naming) ----
705715

706-
def sample(self, logits: torch.Tensor, max_top_k: int, draft_model=None) -> torch.Tensor:
716+
def sample(
717+
self, logits: torch.Tensor, max_top_k: int, draft_model=None
718+
) -> tuple[torch.Tensor, torch.Tensor]:
707719
"""TopK sampling with log softmax for dynamic tree."""
708720
last_p = self.logsoftmax(logits)
709721
topk_values, topk_indices = torch.topk(last_p, k=max_top_k, dim=-1)

tensorrt_llm/_torch/speculative/spec_tree_manager.py

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -317,41 +317,10 @@ def compute_spec_dec_mask_matrix(self, tree_idx=0):
317317
indices = path[path > -1]
318318
self.spec_dec_mask_matrix[actual_idx][i, indices] = 1
319319

320-
# # Compute the packed mask according to the mask matrix
321-
# def compute_spec_dec_packed_mask(self, mask_matrix, packed_mask):
322-
# # mask_matrix: shape: [bs, num_tokens, num_tokens]
323-
# # packed_mask: output buffer, shape: [bs, max_total_draft_tokens + 1, num_blocks]
324-
# # This function pads last dim to padded_len and packs each row into 32-bit integers
325-
# # Only the first num_tokens rows of packed_mask are filled
326-
327-
# num_tokens = mask_matrix.shape[1]
328-
# padded_len = self.max_total_draft_tokens + 1
329-
# num_blocks = math.ceil(padded_len / 32)
330-
331-
# # Pad last dim: [bs, num_tokens, num_tokens] -> [bs, num_tokens, padded_len]
332-
333-
# pad_cols = padded_len - num_tokens
334-
# padded_matrix = torch.nn.functional.pad(mask_matrix, (0, pad_cols), value=0)
335-
336-
# # Flatten to [bs * num_tokens, padded_len] for packing
337-
# int_tensor = padded_matrix.reshape(-1, padded_len)
338-
# packed_mask = packed_mask[:,:num_tokens,:].reshape(-1, num_blocks)
339-
340-
# # Pack each 32-bit block
341-
# for block_idx in range(num_blocks):
342-
# start_idx = block_idx * 32
343-
# end_idx = min(start_idx + 32, padded_len)
344-
# block_bits = int_tensor[:, start_idx:end_idx]
345-
# weight = torch.pow(
346-
# 2,
347-
# torch.arange(end_idx - start_idx,
348-
# dtype=torch.int32,
349-
# device=int_tensor.device))
350-
# block_value = torch.sum(block_bits * weight, dim=-1)
351-
# packed_mask[:, block_idx] = block_value
352-
353320
def compute_spec_dec_packed_mask(self, mask_matrix, packed_mask):
354321
bs, num_tokens, num_tokens_attend = mask_matrix.shape
322+
assert mask_matrix.ndim == 3, f"Expected 3D mask_matrix, got {mask_matrix.ndim}D"
323+
assert packed_mask.ndim == 3, f"Expected 3D packed_mask, got {packed_mask.ndim}D"
355324
num_blocks = packed_mask.shape[-1]
356325

357326
# 1. Prepare bit weights (1, 1, 32)

tensorrt_llm/llmapi/llm_args.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1821,8 +1821,8 @@ def supports_backend(self, backend: str) -> bool:
18211821
SpeculativeConfig: TypeAlias = Annotated[
18221822
Union[
18231823
DraftTargetDecodingConfig,
1824+
Eagle3DecodingConfig, # Must be before EagleDecodingConfig since it's a subclass
18241825
EagleDecodingConfig,
1825-
Eagle3DecodingConfig,
18261826
LookaheadDecodingConfig,
18271827
MedusaDecodingConfig,
18281828
MTPDecodingConfig,
@@ -1836,19 +1836,6 @@ def supports_backend(self, backend: str) -> bool:
18361836
Field(discriminator="decoding_type"),
18371837
]
18381838

1839-
SpeculativeConfig: TypeAlias = Optional[Union[
1840-
DraftTargetDecodingConfig,
1841-
Eagle3DecodingConfig, # Must be before EagleDecodingConfig since it's a subclass
1842-
EagleDecodingConfig,
1843-
LookaheadDecodingConfig,
1844-
MedusaDecodingConfig,
1845-
MTPDecodingConfig,
1846-
NGramDecodingConfig,
1847-
UserProvidedDecodingConfig,
1848-
SaveHiddenStatesDecodingConfig,
1849-
AutoDecodingConfig,
1850-
]]
1851-
18521839
SparseAttentionConfig: TypeAlias = Annotated[
18531840
Union[
18541841
RocketSparseAttentionConfig,

tests/unittest/_torch/speculative/test_draft_token_prepare_for_generation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ def run_test(
825825
ref_hidden_states_read_indices,
826826
)
827827

828-
##### CASE 2 dynamic tree, batch size = 1, cur_draft_idx = 1 #############
828+
##### CASE 3 dynamic tree, batch size = 1, cur_draft_idx = 2 #############
829829
max_batch_size = 1
830830
max_draft_len = 3
831831
max_total_draft_tokens = 15

0 commit comments

Comments
 (0)