A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 4, 07:30 UTC
[Model Runner V2] Rebuild attn metadata between draft decode steps (#41162)
Signed-off-by: Giancarlo Delfin <gdelfin@inferact.ai>
Giancarlo Delfin committed
May 4, 2026 at 17:44 UTC
e1e4646b06f289475ee57f31e3df817e06351321
3 files changed
+198
-121
vllm/v1/worker/gpu/sample/gumbel.py
+18
-4
@@ -76,6 +76,8 @@ def gumbel_block_argmax(
76
pos_ptr,
77
processed_logits_ptr,
78
processed_logits_stride,
79
+ processed_logits_col_ptr,
80
+ vocab_size,
81
APPLY_TEMPERATURE: tl.constexpr,
82
):
83
req_state_idx = tl.load(expanded_idx_mapping_ptr + token_idx)
@@ -88,8 +90,15 @@ def gumbel_block_argmax(
90
91
if processed_logits_ptr is not None:
92
# Store the temperature-applied logits.
93
+ if processed_logits_col_ptr is not None:
94
+ col = tl.load(processed_logits_col_ptr)
95
+ else:
96
+ col = 0
97
tl.store(
92
- processed_logits_ptr + req_state_idx * processed_logits_stride + block,
98
+ processed_logits_ptr
99
+ + req_state_idx * processed_logits_stride
100
+ + col * vocab_size
101
+ + block,
102
logits,
103
mask=mask,
104
)
@@ -121,6 +130,7 @@ def _gumbel_sample_kernel(
130
local_max_stride,
131
processed_logits_ptr,
132
processed_logits_stride,
133
+ processed_logits_col_ptr,
134
logits_ptr,
135
logits_stride,
136
expanded_idx_mapping_ptr,
@@ -153,6 +163,8 @@ def _gumbel_sample_kernel(
163
pos_ptr,
164
processed_logits_ptr,
165
processed_logits_stride,
166
+ processed_logits_col_ptr,
167
+ vocab_size,
168
APPLY_TEMPERATURE=APPLY_TEMPERATURE,
169
)
170
token_id = block_idx * BLOCK_SIZE + idx
@@ -167,7 +179,8 @@ def gumbel_sample(
179
seed: torch.Tensor, # [max_num_reqs]
180
pos: torch.Tensor, # [num_tokens]
181
apply_temperature: bool,
170
- processed_logits_out: torch.Tensor | None = None, # [num_reqs, vocab_size]
182
+ output_processed_logits: torch.Tensor | None = None,
183
+ output_processed_logits_col: torch.Tensor | None = None,
184
) -> torch.Tensor:
185
num_tokens, vocab_size = logits.shape
186
BLOCK_SIZE = 1024
@@ -179,8 +192,9 @@ def gumbel_sample(
192
local_argmax.stride(0),
193
local_max,
194
local_max.stride(0),
182
- processed_logits_out,
183
- processed_logits_out.stride(0) if processed_logits_out is not None else 0,
195
+ output_processed_logits,
196
+ output_processed_logits.stride(0) if output_processed_logits is not None else 0,
197
+ output_processed_logits_col,
198
logits,
199
logits.stride(0),
200
expanded_idx_mapping,
vllm/v1/worker/gpu/spec_decode/eagle/speculator.py
+176
-115
@@ -89,9 +89,13 @@ class EagleSpeculator:
89
dtype=torch.int64,
90
device=device,
91
)
92
+ self.current_draft_step = torch.tensor(0, dtype=torch.int64, device=device)
93
self.last_token_indices = torch.zeros(
94
self.max_num_reqs, dtype=torch.int64, device=device
95
)
96
+ self.arange = torch.arange(
97
+ self.max_num_reqs + 1, dtype=torch.int32, device="cpu"
98
+ )
99
100
self.supports_mm_inputs = MULTIMODAL_REGISTRY.supports_multimodal_inputs(
101
self.draft_model_config
@@ -228,9 +232,10 @@ class EagleSpeculator:
232
logits: torch.Tensor,
233
idx_mapping: torch.Tensor,
234
pos: torch.Tensor,
231
- step: int,
235
+ draft_step: torch.Tensor,
236
+ draft_logits: torch.Tensor | None,
237
) -> torch.Tensor:
233
- if self.draft_logits is not None:
238
+ if draft_logits is not None:
239
# NOTE(woosuk): We must add 1 to the positions to match the Gumbel noise
240
# used for draft and target sampling.
241
return gumbel_sample(
@@ -240,7 +245,8 @@ class EagleSpeculator:
245
self.seeds,
246
pos + 1,
247
apply_temperature=True,
243
- processed_logits_out=self.draft_logits[:, step],
248
+ output_processed_logits=draft_logits,
249
+ output_processed_logits_col=draft_step,
250
)
251
else:
252
return logits.argmax(dim=-1)
@@ -274,11 +280,63 @@ class EagleSpeculator:
280
logits,
281
idx_mapping,
282
pos,
277
- step=0,
283
+ self.current_draft_step,
284
+ self.draft_logits,
285
)
286
self.hidden_states[:num_reqs] = hidden_states[last_token_indices]
287
self.input_buffers.positions[:num_reqs] = pos
288
289
+ def multi_step_decode(
290
+ self,
291
+ num_reqs: int,
292
+ skip_attn: bool,
293
+ batch_desc: BatchExecutionDescriptor,
294
+ num_tokens_across_dp: torch.Tensor | None,
295
+ ) -> None:
296
+ positions = self.input_buffers.positions[:num_reqs]
297
+ query_start_loc = self.input_buffers.query_start_loc[: num_reqs + 1]
298
+ idx_mapping = self.idx_mapping[:num_reqs]
299
+
300
+ for step in range(1, self.num_speculative_steps):
301
+ attn_metadata = None
302
+ slot_mappings_by_layer = None
303
+ if not skip_attn:
304
+ # Build attention metadata and slot mappings for each draft
305
+ # decode step. It is necessary to rebuild the attention
306
+ # metadata even when replaying the FULL graph so that any
307
+ # attention metadata builder state is updated.
308
+ slot_mappings = self.block_tables.compute_slot_mappings(
309
+ idx_mapping,
310
+ query_start_loc,
311
+ positions,
312
+ batch_desc.num_tokens,
313
+ )
314
+ slot_mappings_by_layer = build_slot_mappings_by_layer(
315
+ slot_mappings, self.kv_cache_config
316
+ )
317
+ attn_metadata = self._build_draft_attn_metadata(
318
+ num_reqs=num_reqs,
319
+ num_reqs_padded=batch_desc.num_reqs or num_reqs,
320
+ num_tokens_padded=batch_desc.num_tokens,
321
+ )
322
+
323
+ # Update the current draft step.
324
+ self.current_draft_step.fill_(step)
325
+
326
+ # Generate draft tokens for the current step.
327
+ if batch_desc.cg_mode == CUDAGraphMode.FULL:
328
+ assert self.decode_cudagraph_manager is not None
329
+ self.decode_cudagraph_manager.run_fullgraph(batch_desc)
330
+ else:
331
+ self.generate_draft(
332
+ num_reqs,
333
+ batch_desc.num_tokens,
334
+ attn_metadata,
335
+ slot_mappings_by_layer,
336
+ num_tokens_across_dp=num_tokens_across_dp,
337
+ cudagraph_runtime_mode=batch_desc.cg_mode,
338
+ )
339
+
340
def generate_draft(
341
self,
342
num_reqs: int,
@@ -288,59 +346,52 @@ class EagleSpeculator:
346
num_tokens_across_dp: torch.Tensor | None,
347
cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
348
) -> None:
291
- pos = self.input_buffers.positions[:num_reqs]
292
- query_start_loc = self.input_buffers.query_start_loc[: num_reqs + 1]
349
idx_mapping = self.idx_mapping[:num_reqs]
294
- for step in range(1, self.num_speculative_steps):
295
- # Run the eagle model.
296
- last_hidden_states, hidden_states = self.run_model(
297
- num_tokens_padded,
298
- attn_metadata,
299
- slot_mappings,
300
- num_tokens_across_dp,
301
- cudagraph_runtime_mode,
302
- )
303
- last_hidden_states = last_hidden_states[:num_reqs]
304
- hidden_states = hidden_states[:num_reqs]
305
- logits = self.model.compute_logits(last_hidden_states)
350
+ positions = self.input_buffers.positions[:num_reqs]
351
+ # Run the eagle model forward pass.
352
+ last_hidden_states, hidden_states = self.run_model(
353
+ num_tokens_padded,
354
+ attn_metadata,
355
+ slot_mappings,
356
+ num_tokens_across_dp,
357
+ cudagraph_runtime_mode,
358
+ )
359
+ last_hidden_states = last_hidden_states[:num_reqs]
360
307
- draft_tokens = self._sample_draft(
308
- logits,
309
- idx_mapping,
310
- pos,
311
- step=step,
312
- )
313
- self.draft_tokens[:num_reqs, step] = draft_tokens
314
-
315
- if step < self.num_speculative_steps - 1:
316
- # Update the inputs for the next step.
317
- update_eagle_inputs(
318
- draft_tokens,
319
- hidden_states,
320
- self.input_buffers,
321
- self.hidden_states,
322
- self.max_model_len,
323
- )
324
- if attn_metadata is not None:
325
- self.block_tables.compute_slot_mappings(
326
- idx_mapping, query_start_loc, pos, num_tokens_padded
327
- )
361
+ # Sample the draft tokens.
362
+ logits = self.model.compute_logits(last_hidden_states)
363
+ draft_tokens = self._sample_draft(
364
+ logits,
365
+ idx_mapping,
366
+ positions,
367
+ self.current_draft_step,
368
+ self.draft_logits,
369
+ )
370
+
371
+ # Update the inputs for the next step.
372
+ update_eagle_draft_inputs(
373
+ draft_tokens,
374
+ self.current_draft_step,
375
+ hidden_states,
376
+ self.draft_tokens,
377
+ self.hidden_states,
378
+ self.input_buffers,
379
+ num_reqs,
380
+ self.max_model_len,
381
+ self.num_speculative_steps,
382
+ )
383
384
def _build_draft_attn_metadata(
385
self,
386
num_reqs: int,
387
num_reqs_padded: int,
388
num_tokens_padded: int,
334
- max_query_len: int,
389
) -> dict[str, Any] | None:
390
if not self.draft_attn_layer_names:
391
return None
392
339
- query_start_loc_cpu = (
340
- torch.arange(num_reqs_padded + 1, dtype=torch.int32, device="cpu").clamp_(
341
- max=num_reqs
342
- )
343
- * max_query_len
393
+ query_start_loc_cpu = torch.clamp(
394
+ self.arange[: num_reqs_padded + 1], max=num_reqs
395
)
396
block_tables = [
397
x[:num_reqs_padded] for x in self.block_tables.input_block_tables
@@ -354,7 +405,7 @@ class EagleSpeculator:
405
: num_reqs_padded + 1
406
],
407
query_start_loc_cpu=query_start_loc_cpu,
357
- max_query_len=max_query_len,
408
+ max_query_len=1,
409
seq_lens=self.input_buffers.seq_lens[:num_reqs_padded],
410
max_seq_len=self.max_model_len,
411
block_tables=block_tables,
@@ -373,7 +424,7 @@ class EagleSpeculator:
424
self.last_token_indices.zero_()
425
426
# Capture the prefill routine (model forward + compute_logits +
376
- # gumbel_sample).
427
+ # sample).
428
# For FULL graphs, the entire routine is recorded as one graph.
429
# For PIECEWISE, only the model's compiled regions are captured
430
# and the rest (compute_logits, gumbel_sample) runs eagerly.
@@ -387,10 +438,9 @@ class EagleSpeculator:
438
if self.num_speculative_steps == 1:
439
return
440
390
- # Capture the decode draft generation loop (model forward +
391
- # compute_logits + gumbel_sample + update_eagle_inputs, for
392
- # each step). For FULL graphs, the entire multi-step loop is
393
- # recorded as one graph.
441
+ # Capture the decode draft generation routine (model forward +
442
+ # compute_logits + sample + update_eagle_inputs) for a single
443
+ # step.
444
assert self.decode_cudagraph_manager is not None
445
self.decode_cudagraph_manager.capture(
446
self.generate_draft,
@@ -461,9 +511,10 @@ class EagleSpeculator:
511
512
# Get the input ids and last token indices for the speculator.
513
prepare_eagle_inputs(
514
+ self.last_token_indices,
515
+ self.current_draft_step,
516
self.input_buffers,
517
input_batch,
466
- self.last_token_indices,
518
num_sampled,
519
num_rejected,
520
last_sampled,
@@ -473,12 +524,18 @@ class EagleSpeculator:
524
525
# When all requests are decoding (no true prefills), each has
526
# num_speculative_steps + 1 tokens, enabling FULL graph replay.
476
- # Mixed or prefill-only batches fall back to PIECEWISE.
527
+ uniform_token_count = get_uniform_token_count(
528
+ num_reqs,
529
+ # Use the actual number of tokens without padding added by
530
+ # the target model during FULL cudagraph.
531
+ input_batch.num_tokens,
532
+ max_query_len,
533
+ )
534
prefill_batch_desc, num_tokens_across_dp = dispatch_cg_and_sync_dp(
535
self.prefill_cudagraph_manager,
536
num_reqs,
537
num_tokens,
481
- get_uniform_token_count(num_reqs, num_tokens, max_query_len),
538
+ uniform_token_count,
539
dp_size=self.dp_size,
540
dp_rank=self.dp_rank,
541
need_eager=is_profile,
@@ -528,48 +585,21 @@ class EagleSpeculator:
585
need_eager=is_profile,
586
)
587
531
- attn_metadata_updated = None
532
- slot_mappings_updated = None
533
- if not (dummy_run and skip_attn_for_dummy_run):
534
- # Build attention metadata and slot mappings for the draft
535
- # decode steps. It is necessary to rebuild the attention
536
- # metadata even when replaying the FULL graph so that any
537
- # attention metadata builder state is updated.
538
- slot_mappings = self.block_tables.compute_slot_mappings(
539
- self.idx_mapping[:num_reqs],
540
- self.input_buffers.query_start_loc[: num_reqs + 1],
541
- self.input_buffers.positions[:num_reqs],
542
- decode_batch_desc.num_tokens,
543
- )
544
- slot_mappings_updated = build_slot_mappings_by_layer(
545
- slot_mappings, self.kv_cache_config
546
- )
547
- attn_metadata_updated = self._build_draft_attn_metadata(
548
- num_reqs=num_reqs,
549
- num_reqs_padded=decode_batch_desc.num_reqs or num_reqs,
550
- num_tokens_padded=decode_batch_desc.num_tokens,
551
- max_query_len=1,
552
- )
588
+ # Generate the remaining num_speculative_steps - 1 draft tokens.
589
+ self.multi_step_decode(
590
+ num_reqs,
591
+ dummy_run and skip_attn_for_dummy_run,
592
+ decode_batch_desc,
593
+ num_tokens_across_dp,
594
+ )
595
554
- if decode_batch_desc.cg_mode == CUDAGraphMode.FULL:
555
- # Replay the full graph for draft generation.
556
- assert self.decode_cudagraph_manager is not None
557
- self.decode_cudagraph_manager.run_fullgraph(decode_batch_desc)
558
- else:
559
- self.generate_draft(
560
- num_reqs,
561
- decode_batch_desc.num_tokens,
562
- attn_metadata_updated,
563
- slot_mappings_updated,
564
- num_tokens_across_dp=num_tokens_across_dp,
565
- cudagraph_runtime_mode=decode_batch_desc.cg_mode,
566
- )
596
return self.draft_tokens[:num_reqs]
597
598
599
@triton.jit
600
def _prepare_eagle_inputs_kernel(
601
last_token_indices_ptr,
602
+ eagle_current_draft_step_ptr,
603
eagle_input_ids_ptr,
604
eagle_positions_ptr,
605
eagle_query_start_loc_ptr,
@@ -630,6 +660,8 @@ def _prepare_eagle_inputs_kernel(
660
# Copy sequence lengths.
661
tl.store(eagle_seq_lens_ptr + req_idx, seq_len)
662
if req_idx == (num_reqs - 1):
663
+ # Reset the current draft step to 0.
664
+ tl.store(eagle_current_draft_step_ptr, 0)
665
# Pad query_start_loc for CUDA graphs.
666
for i in range(num_reqs, max_num_reqs + 1, BLOCK_SIZE):
667
block = i + tl.arange(0, BLOCK_SIZE)
@@ -648,10 +680,11 @@ def _prepare_eagle_inputs_kernel(
680
681
682
def prepare_eagle_inputs(
651
- input_buffers: InputBuffers,
652
- input_batch: InputBatch,
683
# [num_reqs]
684
last_token_indices: torch.Tensor,
685
+ current_draft_step: torch.Tensor,
686
+ input_buffers: InputBuffers,
687
+ input_batch: InputBatch,
688
# [num_reqs]
689
num_sampled: torch.Tensor,
690
# [num_reqs]
@@ -665,6 +698,7 @@ def prepare_eagle_inputs(
698
num_reqs = input_batch.num_reqs
699
_prepare_eagle_inputs_kernel[(num_reqs,)](
700
last_token_indices,
701
+ current_draft_step,
702
input_buffers.input_ids,
703
input_buffers.positions,
704
input_buffers.query_start_loc,
@@ -685,7 +719,7 @@ def prepare_eagle_inputs(
719
720
721
@triton.jit
688
-def _prepare_eagle_docode_kernel(
722
+def _prepare_eagle_decode_kernel(
723
draft_tokens_ptr,
724
draft_tokens_stride,
725
target_seq_lens_ptr,
@@ -742,7 +776,7 @@ def prepare_eagle_decode(
776
max_num_reqs: int,
777
):
778
num_reqs = draft_tokens.shape[0]
745
- _prepare_eagle_docode_kernel[(num_reqs + 1,)](
779
+ _prepare_eagle_decode_kernel[(num_reqs + 1,)](
780
draft_tokens,
781
draft_tokens.stride(0),
782
target_seq_lens,
@@ -758,36 +792,55 @@ def prepare_eagle_decode(
792
793
794
@triton.jit
761
-def _update_eagle_inputs_kernel(
795
+def _update_eagle_draft_inputs_kernel(
796
+ output_draft_tokens_ptr,
797
+ output_draft_tokens_stride,
798
+ next_input_hidden_states_ptr,
799
+ next_input_hidden_states_stride,
800
input_ids_ptr,
801
positions_ptr,
764
- input_hidden_states_ptr,
765
- input_hidden_states_stride,
802
seq_lens_ptr,
767
- max_model_len,
803
draft_tokens_ptr,
769
- output_hidden_states_ptr,
770
- output_hidden_states_stride,
804
+ current_draft_step_ptr,
805
+ hidden_states_ptr,
806
+ hidden_states_stride,
807
hidden_size,
808
+ max_model_len,
809
+ num_speculative_steps,
810
BLOCK_SIZE: tl.constexpr,
811
):
812
req_idx = tl.program_id(0)
813
776
- # Draft token -> Input ID.
814
+ # Write the sampled draft token into self.draft_tokens[req_idx, step].
815
draft_token = tl.load(draft_tokens_ptr + req_idx)
816
+ step = tl.load(current_draft_step_ptr)
817
+ tl.store(
818
+ output_draft_tokens_ptr + req_idx * output_draft_tokens_stride + step,
819
+ draft_token,
820
+ )
821
+
822
+ if step >= num_speculative_steps - 1:
823
+ # This is the final step. Skip updating draft forward inputs.
824
+ return
825
+
826
+ # Write the sampled draft token into the input ids tensor for the next
827
+ # forward pass.
828
tl.store(input_ids_ptr + req_idx, draft_token)
829
780
- # Output hidden states -> Input hidden states.
830
+ # Copy hidden states into the input hidden states tensor for the next
831
+ # forward pass.
832
for i in range(0, hidden_size, BLOCK_SIZE):
833
block = i + tl.arange(0, BLOCK_SIZE)
834
mask = block < hidden_size
784
- output_hidden_states = tl.load(
785
- output_hidden_states_ptr + req_idx * output_hidden_states_stride + block,
835
+ hidden_states = tl.load(
836
+ hidden_states_ptr + req_idx * hidden_states_stride + block,
837
mask=mask,
838
)
839
tl.store(
789
- input_hidden_states_ptr + req_idx * input_hidden_states_stride + block,
790
- output_hidden_states,
840
+ next_input_hidden_states_ptr
841
+ + req_idx * next_input_hidden_states_stride
842
+ + block,
843
+ hidden_states,
844
mask=mask,
845
)
846
@@ -803,24 +856,32 @@ def _update_eagle_inputs_kernel(
856
tl.store(seq_lens_ptr + req_idx, seq_len)
857
858
806
-def update_eagle_inputs(
859
+def update_eagle_draft_inputs(
860
draft_tokens: torch.Tensor,
808
- output_hidden_states: torch.Tensor,
809
- input_buffers: InputBuffers,
861
+ current_draft_step: torch.Tensor,
862
hidden_states: torch.Tensor,
863
+ output_draft_tokens: torch.Tensor,
864
+ next_input_hidden_states: torch.Tensor,
865
+ input_buffers: InputBuffers,
866
+ num_reqs: int,
867
max_model_len: int,
868
+ num_speculative_steps: int,
869
):
813
- num_reqs, hidden_size = output_hidden_states.shape
814
- _update_eagle_inputs_kernel[(num_reqs,)](
870
+ _, hidden_size = hidden_states.shape
871
+ _update_eagle_draft_inputs_kernel[(num_reqs,)](
872
+ output_draft_tokens,
873
+ output_draft_tokens.stride(0),
874
+ next_input_hidden_states,
875
+ next_input_hidden_states.stride(0),
876
input_buffers.input_ids,
877
input_buffers.positions,
817
- hidden_states,
818
- hidden_states.stride(0),
878
input_buffers.seq_lens,
820
- max_model_len,
879
draft_tokens,
822
- output_hidden_states,
823
- output_hidden_states.stride(0),
880
+ current_draft_step,
881
+ hidden_states,
882
+ hidden_states.stride(0),
883
hidden_size,
884
+ max_model_len,
885
+ num_speculative_steps,
886
BLOCK_SIZE=1024,
887
)
vllm/v1/worker/gpu/spec_decode/probabilistic_rejection_sampler_utils.py
+4
-2
@@ -392,8 +392,10 @@ def _resample_kernel(
392
temp_ptr,
393
seed_ptr,
394
pos_ptr,
395
- None,
396
- 0,
395
+ None, # processed_logits_ptr
396
+ 0, # processed_logits_stride
397
+ None, # processed_logits_col_ptr
398
+ vocab_size,
399
APPLY_TEMPERATURE=False,
400
)
401
token_id = block_idx * BLOCK_SIZE + idx