main
py 803 lines 32.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import copy
4 from typing import Any
5
6 import torch
7 import torch.nn as nn
8
9 from vllm.config import VllmConfig, replace
10 from vllm.config.compilation import CUDAGraphMode
11 from vllm.forward_context import BatchDescriptor, set_forward_context
12 from vllm.logger import init_logger
13 from vllm.triton_utils import tl, triton
14 from vllm.v1.attention.backend import AttentionCGSupport
15 from vllm.v1.attention.backends.utils import PAD_SLOT_ID
16 from vllm.v1.kv_cache_interface import (
17 KVCacheConfig,
18 get_kv_cache_spec_sliding_window,
19 )
20 from vllm.v1.worker.gpu.attn_utils import build_slot_mappings_by_layer
21 from vllm.v1.worker.gpu.block_table import BlockTables
22 from vllm.v1.worker.gpu.cp_utils import cp_local_slot
23 from vllm.v1.worker.gpu.dp_utils import DPSyncState, dispatch_cg_and_sync_dp
24 from vllm.v1.worker.gpu.input_batch import InputBatch, InputBuffers
25 from vllm.v1.worker.gpu.model_states.interface import ModelState
26 from vllm.v1.worker.gpu.spec_decode.dflash.cudagraph import DFlashCudaGraphManager
27 from vllm.v1.worker.gpu.spec_decode.dflash.utils import load_dflash_model
28 from vllm.v1.worker.gpu.spec_decode.speculator import DraftModelSpeculator
29 from vllm.v1.worker.gpu.spec_decode.utils import get_parallel_drafting_token_id
30 from vllm.v1.worker.utils import AttentionGroup
31
32 logger = init_logger(__name__)
33
34
35 class DFlashSpeculator(DraftModelSpeculator):
36 _speculator_name = "DFlash" # For logging, so we can share methods with subclasses
37
38 def __init__(self, vllm_config: VllmConfig, device: torch.device):
39 parallel_config = vllm_config.parallel_config
40 speculative_config = vllm_config.speculative_config
41 assert speculative_config is not None
42 vllm_config = copy.copy(vllm_config)
43 vllm_config.parallel_config = replace(
44 parallel_config,
45 prefill_context_parallel_size=1,
46 decode_context_parallel_size=(
47 parallel_config.decode_context_parallel_size
48 if speculative_config.draft_model_config.use_mla
49 else 1
50 ),
51 )
52 super().__init__(vllm_config, device)
53
54 self.hidden_states = torch.zeros(
55 self.max_num_tokens, self.hidden_size, dtype=self.dtype, device=device
56 )
57
58 # Multimodal inputs not currently supported.
59 self.supports_mm_inputs = False
60
61 # Each request emits exactly (bonus + N mask) query tokens per step.
62 self.num_query_per_req = 1 + self.num_speculative_steps
63
64 self.parallel_drafting_token_id = get_parallel_drafting_token_id(
65 self.draft_model_config.hf_config
66 )
67
68 from vllm.model_executor.models.qwen3_dflash import dflash_has_any_non_causal
69
70 self.requires_non_causal = dflash_has_any_non_causal(
71 self.draft_model_config.hf_config
72 )
73
74 # Whether the anchor query position is itself a prediction. DFlash default uses
75 # the anchor as the bonus token (only mask tokens predict); DSpark samples from
76 # the anchor and the N-1 mask token positions. See _prepare_dflash_inputs_kernel
77 dflash_config = (
78 getattr(self.draft_model_config.hf_config, "dflash_config", None) or {}
79 )
80 if dflash_config.get("sample_from_anchor", False):
81 raise ValueError(
82 "sample_from_anchor=True is not supported for DFlash. "
83 "DFlash uses a fixed 1+N query layout where the anchor "
84 "is the bonus token."
85 )
86 self.sample_from_anchor = False
87
88 # Context positions for the K/V precompute, populated by prepare_dflash_inputs.
89 self.context_positions = torch.zeros(
90 self.max_num_tokens, dtype=torch.int64, device=device
91 )
92
93 # Per-mask-token sampling buffers. Flattened from (num_reqs, num_spec_tokens).
94 max_num_sampled_tokens = self.max_num_reqs * self.num_speculative_steps
95 self.sample_indices = torch.zeros(
96 max_num_sampled_tokens, dtype=torch.int64, device=device
97 )
98 self.sample_pos = torch.zeros(
99 max_num_sampled_tokens, dtype=torch.int64, device=device
100 )
101 # -1 marks an inert sampling row. CUDA graph capture can execute the
102 # full buffer before a real batch has populated it, so zero would make
103 # every padding row scatter into request slot 0.
104 self.sample_idx_mapping = torch.full(
105 (max_num_sampled_tokens,), -1, dtype=torch.int32, device=device
106 )
107 # [0, 1, ..., N-1, 0, 1, ..., N-1, ...] -> the per-token column index into
108 # draft_logits[req, step, :].
109 self.sample_col = torch.arange(
110 self.num_speculative_steps, dtype=torch.int32, device=device
111 ).repeat(self.max_num_reqs)
112
113 self.query_cudagraph_manager: DFlashCudaGraphManager | None = None
114 self.draft_kv_cache_group_id: int = -1
115
116 @property
117 def attn_vllm_config(self) -> VllmConfig:
118 # The draft's attention differs from the target's in causality.
119 config = copy.copy(super().attn_vllm_config)
120 config.attention_config = replace(
121 self.vllm_config.attention_config,
122 use_non_causal=self.requires_non_causal,
123 )
124 return config
125
126 def init_cudagraph_manager(self, cudagraph_mode: CUDAGraphMode) -> None:
127 wants_full = cudagraph_mode.decode_mode() == CUDAGraphMode.FULL
128 supports_full = (
129 self.attn_cg_support.min_cg_support.value
130 >= AttentionCGSupport.UNIFORM_BATCH.value
131 )
132 if wants_full and not supports_full:
133 logger.warning(
134 "%s draft attention (%s) does not support full CUDA graphs; "
135 "running the draft eagerly.",
136 self._speculator_name,
137 self.attn_cg_support.min_cg_attn_backend,
138 )
139 # PIECEWISE cudagraphs are not supported for dflash.
140 if wants_full and supports_full:
141 cudagraph_mode = CUDAGraphMode.FULL_DECODE_ONLY
142 else:
143 cudagraph_mode = CUDAGraphMode.NONE
144
145 self.query_cudagraph_manager = DFlashCudaGraphManager(
146 self.vllm_config,
147 self.device,
148 cudagraph_mode,
149 decode_query_len=self.num_query_per_req,
150 )
151
152 def capture(self) -> None:
153 logger.info("Capturing model for %s speculator...", self._speculator_name)
154 # Padded sample rows must not scatter into a live request during capture.
155 self.sample_indices.zero_()
156 self.sample_pos.zero_()
157 self.sample_idx_mapping.fill_(-1)
158 # Capture must not write context K/V.
159 self._context_slot_mappings.fill_(PAD_SLOT_ID)
160 assert self.query_cudagraph_manager is not None
161 self.query_cudagraph_manager.capture(
162 self._generate_draft,
163 self.input_buffers,
164 self.block_tables,
165 self.attn_groups,
166 self.kv_cache_config,
167 self.max_model_len,
168 causal=self._group_causal,
169 precompute_context_kv=lambda num_reqs: self._precompute_context_kv(
170 0, self._num_graph_context_tokens(num_reqs)
171 ),
172 progress_bar_desc=f"Capturing {self._speculator_name.lower()} CUDA graphs",
173 )
174
175 def load_draft_model(
176 self,
177 target_model: nn.Module,
178 target_attn_layer_names: set[str],
179 ) -> nn.Module:
180 return load_dflash_model(target_model, self.vllm_config)
181
182 def set_attn(
183 self,
184 model_state: ModelState,
185 kv_cache_config: KVCacheConfig,
186 block_tables: BlockTables,
187 target_input_buffers: InputBuffers,
188 target_attn_groups: list[list[AttentionGroup]],
189 ) -> None:
190 super().set_attn(
191 model_state,
192 kv_cache_config,
193 block_tables,
194 target_input_buffers,
195 target_attn_groups,
196 )
197
198 # FlashAttention's AOT split schedule is wrong for a windowed drafter,
199 # and `_get_sliding_window_configs` leaves it on or off depending on
200 # whether the target also runs FlashAttention. Decide it here instead.
201 for groups in self.attn_groups:
202 for group in groups:
203 builder = group.get_metadata_builder()
204 if getattr(
205 builder, "aot_schedule", False
206 ) and get_kv_cache_spec_sliding_window(builder.kv_cache_spec):
207 # `aot_schedule` belongs to FlashAttention's builder, not
208 # to the base class this loop is typed against.
209 builder.aot_schedule = False # type: ignore[attr-defined]
210
211 self.draft_kv_cache_group_ids = [
212 gid for gid, g in enumerate(self.attn_groups) if g
213 ]
214 assert self.draft_kv_cache_group_ids, "No draft attention groups found."
215 self.draft_kv_cache_group_id = self.draft_kv_cache_group_ids[0]
216
217 # Per-group context slot buffers for the precompute (one row per group).
218 self._context_slot_mappings = torch.zeros(
219 len(self.draft_kv_cache_group_ids),
220 self.max_num_tokens,
221 dtype=torch.int64,
222 device=self.device,
223 )
224
225 # Map each draft decoder layer to the index (within draft_kv_cache_group_ids)
226 # of the kv-cache group its cache belongs to. Models that share a single group
227 # leave this as None and share one context slot mapping.
228 self._layer_group_idx: list[int] | None = None
229 # Per-KV-group causal, falling back to whether the drafter is all-causal.
230 self._group_causal: dict[int, bool] | bool = not self.requires_non_causal
231 if hasattr(self.model, "get_draft_kv_cache_layer_names"):
232 layer_names = self.model.get_draft_kv_cache_layer_names()
233 name_to_gid = {
234 ln: gid
235 for gid, group in enumerate(kv_cache_config.kv_cache_groups)
236 for ln in group.layer_names
237 }
238 gid_to_idx = {gid: i for i, gid in enumerate(self.draft_kv_cache_group_ids)}
239 self._layer_group_idx = [
240 gid_to_idx[name_to_gid[name]] for name in layer_names
241 ]
242 if hasattr(self.model, "get_draft_attn_causal"):
243 self._group_causal = {
244 name_to_gid[name]: layer_causal
245 for name, layer_causal in zip(
246 layer_names, self.model.get_draft_attn_causal()
247 )
248 }
249
250 @torch.inference_mode()
251 def _run_model(
252 self,
253 num_tokens: int,
254 attn_metadata: dict[str, Any] | None,
255 slot_mappings: dict[str, torch.Tensor] | None,
256 num_tokens_across_dp: torch.Tensor | None,
257 cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
258 ) -> torch.Tensor:
259 batch_descriptor = BatchDescriptor(num_tokens=num_tokens)
260 with set_forward_context(
261 attn_metadata,
262 self.vllm_config,
263 num_tokens=num_tokens,
264 cudagraph_runtime_mode=cudagraph_runtime_mode,
265 num_tokens_across_dp=num_tokens_across_dp,
266 slot_mapping=slot_mappings,
267 batch_descriptor=batch_descriptor,
268 ):
269 last_hidden_states = self.model(
270 input_ids=self.input_buffers.input_ids[:num_tokens],
271 positions=self.input_buffers.positions[:num_tokens],
272 inputs_embeds=None,
273 )
274 return last_hidden_states
275
276 def _generate_draft(
277 self,
278 num_reqs: int,
279 num_tokens_padded: int,
280 attn_metadata: dict[str, Any] | None,
281 slot_mappings: dict[str, torch.Tensor] | None,
282 num_tokens_across_dp: torch.Tensor | None,
283 cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
284 ) -> None:
285 last_hidden_states = self._run_model(
286 num_tokens_padded,
287 attn_metadata,
288 slot_mappings,
289 num_tokens_across_dp,
290 cudagraph_runtime_mode,
291 )
292 num_sample = num_reqs * self.num_speculative_steps
293 sample_hidden_states = last_hidden_states[self.sample_indices[:num_sample]]
294 # sample_pos is the predicted token's position P. Sampling keys a draw
295 # by the position before the sampled token, P-1.
296 draft_tokens = self.sample_draft(
297 sample_hidden_states,
298 self.sample_pos[:num_sample] - 1,
299 self.sample_idx_mapping[:num_sample],
300 self.temperature,
301 self.seeds,
302 self.sample_col[:num_sample],
303 self.draft_logits,
304 )
305 self.draft_tokens[:num_reqs] = draft_tokens.view(
306 num_reqs, self.num_speculative_steps
307 )
308
309 def _num_graph_context_tokens(self, num_reqs: int) -> int:
310 # Context rows a captured draft step stores: one full verify per request.
311 return min(num_reqs * (self.num_speculative_steps + 1), self.max_num_tokens)
312
313 def _precompute_context_kv(
314 self, start: int, end: int, dummy_run: bool = False
315 ) -> None:
316 if dummy_run:
317 context_slots: torch.Tensor | list[torch.Tensor | None] | None = None
318 elif self._layer_group_idx is not None:
319 context_slots = [
320 self._context_slot_mappings[gidx][start:end]
321 for gidx in self._layer_group_idx
322 ]
323 else:
324 context_slots = self._context_slot_mappings[0][start:end]
325 self.model.precompute_and_store_context_kv(
326 self.hidden_states[start:end],
327 self.context_positions[start:end],
328 context_slots,
329 )
330
331 def prepare_context_anchor(
332 self, input_batch: InputBatch, num_rejected: torch.Tensor
333 ) -> None:
334 """Publish context features required by a draft's candidate head."""
335
336 @torch.inference_mode()
337 def propose(
338 self,
339 input_batch: InputBatch,
340 attn_metadata: dict[str, Any],
341 slot_mappings: dict[str, torch.Tensor],
342 # [num_tokens, hidden_size]
343 last_hidden_states: torch.Tensor,
344 # num_layers x [num_tokens, hidden_size]
345 aux_hidden_states: list[torch.Tensor] | None,
346 # [num_reqs]
347 num_sampled: torch.Tensor,
348 # [num_reqs]
349 num_rejected: torch.Tensor,
350 # [max_num_reqs]
351 last_sampled: torch.Tensor,
352 # [max_num_reqs]
353 next_prefill_tokens: torch.Tensor,
354 # [max_num_reqs]
355 temperature: torch.Tensor,
356 # [max_num_reqs]
357 seeds: torch.Tensor,
358 dp_sync: DPSyncState | None = None,
359 dummy_run: bool = False,
360 skip_attn_for_dummy_run: bool = False,
361 mm_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None,
362 is_profile: bool = False,
363 ) -> torch.Tensor:
364 num_reqs = input_batch.num_reqs
365 num_target_tokens = input_batch.num_tokens
366 num_query_tokens = num_reqs * self.num_query_per_req
367 max_seq_len = input_batch.seq_lens_cpu_upper_bound[:num_reqs].max().item()
368 self.draft_max_seq_len = min(
369 max_seq_len + self.num_query_per_req, self.max_model_len
370 )
371
372 # NOTE: To avoid CPU-GPU synchronization without CPU knowing the
373 # number of rejected tokens, we maintain the size of input_ids and
374 # hidden_states the same as the target model's. This means, we pad each
375 # request's query length to include any rejected positions.
376 if aux_hidden_states:
377 hidden_states = self.model.combine_hidden_states(
378 torch.cat(aux_hidden_states, dim=-1)
379 )
380 else:
381 hidden_states = last_hidden_states
382 self.hidden_states[:num_target_tokens].copy_(hidden_states[:num_target_tokens])
383 self.prepare_context_anchor(input_batch, num_rejected)
384
385 if dummy_run and skip_attn_for_dummy_run:
386 # Memory profiling path: block_tables / kv_cache_config are not initialized.
387 # Since DFlash needs to build its own attention metadata, we must skip the
388 # preparation in this path and run a minimal forward pass.
389 self.model.precompute_and_store_context_kv(
390 self.hidden_states[:num_target_tokens],
391 self.context_positions[:num_target_tokens],
392 )
393 # DFlash processes all speculative tokens in one forward pass,
394 # so the real token count is num_query_tokens.
395 self._prepare_eplb_forward(num_query_tokens)
396 self._generate_draft(
397 num_reqs,
398 num_query_tokens,
399 attn_metadata=None,
400 slot_mappings=None,
401 num_tokens_across_dp=None,
402 cudagraph_runtime_mode=CUDAGraphMode.NONE,
403 )
404 return self.draft_tokens[:num_reqs]
405
406 if self.pcp_manager is not None and not dummy_run:
407 self.block_tables.gather_block_tables(
408 input_batch.idx_mapping, num_reqs_padded=num_reqs
409 )
410
411 # The query slot mapping is written into the shared BlockTables slot_mappings.
412 # That buffer's address is what the captured CUDA graph reads from at replay.
413 assert self.draft_kv_cache_group_id >= 0
414 # Support multiple draft KV cache groups by preparing inputs once for each
415 for i, gid in enumerate(self.draft_kv_cache_group_ids):
416 prepare_dflash_inputs(
417 self.input_buffers,
418 self.block_tables.slot_mappings[gid],
419 self.context_positions,
420 self._context_slot_mappings[i],
421 self.sample_indices,
422 self.sample_pos,
423 self.sample_idx_mapping,
424 self.temperature,
425 self.seeds,
426 input_batch,
427 num_sampled,
428 num_rejected,
429 last_sampled,
430 next_prefill_tokens,
431 temperature,
432 seeds,
433 self.block_tables.input_block_tables[gid],
434 self.block_tables.kernel_block_sizes[gid],
435 self.block_tables.cp_rank,
436 self.dcp_size,
437 self.block_tables.cp_interleave,
438 self.parallel_drafting_token_id,
439 self.num_query_per_req,
440 self.num_speculative_steps,
441 self.max_num_reqs,
442 self.max_num_tokens,
443 self.max_model_len,
444 self.sample_from_anchor,
445 )
446
447 batch_sync, num_batch_tokens = (
448 self._build_uniform_batch_dp_sync(dp_sync, num_reqs, self.num_query_per_req)
449 if dp_sync is not None
450 else (None, num_query_tokens)
451 )
452 # Every DFlash step has exactly num_query_per_req tokens, so we can use FULL CGs
453 batch_desc, batch_sync = dispatch_cg_and_sync_dp(
454 self.query_cudagraph_manager,
455 num_reqs,
456 num_batch_tokens,
457 uniform_token_count=self.num_query_per_req,
458 dp_size=self.dp_size,
459 dp_rank=self.dp_rank,
460 need_eager=is_profile,
461 dp_sync=batch_sync,
462 )
463 num_tokens_padded = batch_desc.num_tokens
464 num_tokens_across_dp = (
465 batch_sync.num_tokens_across_dp if batch_sync is not None else None
466 )
467
468 if batch_desc.cg_mode == CUDAGraphMode.FULL:
469 # The graph stores the first num_context context rows.
470 assert batch_desc.num_reqs is not None
471 num_context = self._num_graph_context_tokens(batch_desc.num_reqs)
472 if dummy_run:
473 # Dummy block tables are placeholders: write no context K/V.
474 self._context_slot_mappings[:, :num_context].fill_(PAD_SLOT_ID)
475 elif num_target_tokens <= num_context:
476 # Rows past the batch keep stale positions but write no K/V.
477 self._context_slot_mappings[:, num_target_tokens:num_context].fill_(
478 PAD_SLOT_ID
479 )
480 else:
481 # Prefill context beyond the graph's rows is stored before replay.
482 self._precompute_context_kv(num_context, num_target_tokens)
483 else:
484 self._precompute_context_kv(0, num_target_tokens, dummy_run)
485
486 # Rebuild the draft attention metadata even when replaying the FULL
487 # graph so that any attention metadata builder state is updated.
488 draft_attn_metadata = self._build_uniform_attn_metadata(
489 num_reqs=num_reqs,
490 batch_desc=batch_desc,
491 num_query_per_req=self.num_query_per_req,
492 seq_lens_cpu_upper_bound=input_batch.seq_lens_cpu_upper_bound,
493 step=self.num_query_per_req,
494 causal=self._group_causal,
495 )
496 draft_slot_mappings_by_layer = build_slot_mappings_by_layer(
497 self.block_tables.slot_mappings[:, :num_tokens_padded],
498 self.kv_cache_config,
499 )
500
501 # DFlash processes all speculative tokens in one forward pass,
502 # so the real token count is num_query_tokens.
503 self._prepare_eplb_forward(num_query_tokens)
504
505 if batch_desc.cg_mode == CUDAGraphMode.FULL:
506 assert self.query_cudagraph_manager is not None
507 self.query_cudagraph_manager.run_fullgraph(batch_desc)
508 else:
509 self._generate_draft(
510 num_reqs,
511 num_tokens_padded,
512 draft_attn_metadata,
513 draft_slot_mappings_by_layer,
514 num_tokens_across_dp=num_tokens_across_dp,
515 cudagraph_runtime_mode=batch_desc.cg_mode,
516 )
517
518 return self.draft_tokens[:num_reqs]
519
520
521 @triton.jit
522 def _prepare_dflash_inputs_kernel(
523 # Outputs
524 out_input_ids_ptr,
525 out_query_positions_ptr,
526 out_query_start_loc_ptr,
527 out_seq_lens_ptr,
528 out_query_slot_mapping_ptr,
529 out_context_positions_ptr,
530 out_context_slot_mapping_ptr,
531 out_sample_indices_ptr,
532 out_sample_pos_ptr,
533 out_sample_idx_mapping_ptr,
534 out_temperature_ptr,
535 out_seeds_ptr,
536 # Inputs from target batch
537 target_positions_ptr,
538 target_query_start_loc_ptr,
539 idx_mapping_ptr,
540 last_sampled_ptr,
541 next_prefill_tokens_ptr,
542 num_sampled_ptr,
543 num_rejected_ptr,
544 # Sampling params
545 temperature_ptr,
546 seeds_ptr,
547 # Block table for slot mapping lookup.
548 block_table_ptr,
549 block_table_stride,
550 # Scalars
551 parallel_drafting_token_id,
552 block_size,
553 num_query_per_req,
554 num_speculative_steps,
555 max_num_reqs,
556 max_num_tokens,
557 max_model_len,
558 cp_rank,
559 SAMPLE_FROM_ANCHOR: tl.constexpr,
560 PAD_SLOT_ID: tl.constexpr,
561 CP_SIZE: tl.constexpr,
562 CP_INTERLEAVE: tl.constexpr,
563 BLOCK_SIZE: tl.constexpr,
564 ):
565 req_idx = tl.program_id(0)
566 block_idx = tl.program_id(1)
567 num_reqs = tl.num_programs(0)
568 req_state_idx = tl.load(idx_mapping_ptr + req_idx)
569
570 ctx_start = tl.load(target_query_start_loc_ptr + req_idx)
571 ctx_end = tl.load(target_query_start_loc_ptr + req_idx + 1)
572 num_ctx = ctx_end - ctx_start
573
574 num_rejected = tl.load(num_rejected_ptr + req_idx)
575 valid_ctx_end = ctx_end - num_rejected
576 num_valid_ctx = valid_ctx_end - ctx_start
577
578 num_sampled = tl.load(num_sampled_ptr + req_idx)
579 if num_sampled > 0:
580 bonus_token = tl.load(last_sampled_ptr + req_state_idx).to(tl.int32)
581 else:
582 # Chunked prefilling: splice in the next prefill token.
583 bonus_token = tl.load(next_prefill_tokens_ptr + req_state_idx).to(tl.int32)
584
585 last_valid_pos = tl.load(target_positions_ptr + valid_ctx_end - 1)
586 query_base = req_idx * num_query_per_req
587
588 j = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
589 is_ctx = j < num_ctx
590 is_valid_ctx = j < num_valid_ctx
591 is_query = (j >= num_valid_ctx) & (j < num_valid_ctx + num_query_per_req)
592 query_off = j - num_valid_ctx
593
594 # --- Context positions / slots ---
595 ctx_pos_idx = ctx_start + tl.where(is_ctx, j, 0)
596 ctx_pos = tl.load(target_positions_ptr + ctx_pos_idx, mask=is_valid_ctx, other=0)
597 ctx_block_num = ctx_pos // (block_size * CP_SIZE)
598 ctx_block_num = tl.minimum(ctx_block_num, block_table_stride - 1)
599 ctx_block_id = tl.load(
600 block_table_ptr + req_idx * block_table_stride + ctx_block_num,
601 mask=is_valid_ctx,
602 other=0,
603 ).to(tl.int64)
604 # Block 0 is the null block. Old sliding-window context positions can map
605 # to it after eviction; rejected suffix rows are invalid context as well.
606 # Neither kind of row may write draft KV into physical block 0.
607 ctx_resident = is_valid_ctx & (ctx_block_id != 0)
608 local_ctx_slot = cp_local_slot(
609 ctx_pos, ctx_block_id, block_size, cp_rank, CP_SIZE, CP_INTERLEAVE, PAD_SLOT_ID
610 )
611 ctx_slot = tl.where(
612 ctx_resident,
613 local_ctx_slot,
614 PAD_SLOT_ID,
615 )
616 # Stored over the full [0, num_ctx) span while the loads above are masked to
617 # [0, num_valid_ctx): the rejected suffix rows in between get position 0 and
618 # PAD_SLOT_ID. That is intentional — those rows write no KV and their
619 # positions are never consumed, but the span must stay fully initialized so
620 # a replayed graph cannot observe a stale value from an earlier batch.
621 tl.store(out_context_positions_ptr + ctx_start + j, ctx_pos, mask=is_ctx)
622 tl.store(out_context_slot_mapping_ptr + ctx_start + j, ctx_slot, mask=is_ctx)
623
624 # --- Query positions / input_ids / slots ---
625 query_pos = last_valid_pos + 1 + query_off
626 query_idx = query_base + query_off
627 is_bonus = is_query & (query_off == 0)
628 input_id = tl.where(is_bonus, bonus_token, parallel_drafting_token_id)
629
630 q_block_num = query_pos // (block_size * CP_SIZE)
631 q_block_num = tl.minimum(q_block_num, block_table_stride - 1)
632 q_block_id = tl.load(
633 block_table_ptr + req_idx * block_table_stride + q_block_num,
634 mask=is_query,
635 other=0,
636 ).to(tl.int64)
637 # A null block is never a writable cache slot. This can occur when a
638 # sliding-window block table contains evicted/global padding entries.
639 q_resident = is_query & (q_block_id != 0)
640 local_q_slot = cp_local_slot(
641 query_pos,
642 q_block_id,
643 block_size,
644 cp_rank,
645 CP_SIZE,
646 CP_INTERLEAVE,
647 PAD_SLOT_ID,
648 )
649 q_slot = tl.where(
650 q_resident,
651 local_q_slot,
652 PAD_SLOT_ID,
653 )
654
655 tl.store(out_input_ids_ptr + query_idx, input_id, mask=is_query)
656 clamped_query_pos = tl.minimum(query_pos, max_model_len - 1)
657 tl.store(out_query_positions_ptr + query_idx, clamped_query_pos, mask=is_query)
658 tl.store(out_query_slot_mapping_ptr + query_idx, q_slot, mask=is_query)
659
660 # --- Sample indices / positions / idx_mapping ---
661 # When SAMPLE_FROM_ANCHOR (DSpark), so we sample at EVERY query position
662 # and each position k predicts the NEXT token (sampled position = query_pos + 1).
663 # Otherwise (DFlash default) the anchor is the bonus token and only the mask tokens
664 # at offsets > 0 are sampled from, each AT its own position.
665 sample_off = 0 if SAMPLE_FROM_ANCHOR else 1
666 is_sample = is_query & (query_off >= sample_off)
667 sample_idx = req_idx * num_speculative_steps + (query_off - sample_off)
668 sample_pos = query_pos + 1 if SAMPLE_FROM_ANCHOR else query_pos
669 tl.store(out_sample_indices_ptr + sample_idx, query_idx, mask=is_sample)
670 tl.store(out_sample_pos_ptr + sample_idx, sample_pos, mask=is_sample)
671 tl.store(out_sample_idx_mapping_ptr + sample_idx, req_state_idx, mask=is_sample)
672
673 if block_idx == 0:
674 tl.store(out_query_start_loc_ptr + req_idx, query_base)
675 # seq_lens is the absolute sequence length the draft attention
676 # reads up to (context + query), not just the count of accepted
677 # tokens this step.
678 tl.store(
679 out_seq_lens_ptr + req_idx,
680 tl.minimum(last_valid_pos + 1 + num_query_per_req, max_model_len),
681 )
682 # Copy sampling state.
683 tl.store(
684 out_temperature_ptr + req_state_idx,
685 tl.load(temperature_ptr + req_state_idx),
686 )
687 tl.store(out_seeds_ptr + req_state_idx, tl.load(seeds_ptr + req_state_idx))
688 if req_idx == num_reqs - 1:
689 # Pad per-request buffers to max_num_reqs for CUDA graph safety.
690 last_query_end = num_reqs * num_query_per_req
691 for i in range(num_reqs, max_num_reqs + 1, BLOCK_SIZE):
692 block = i + tl.arange(0, BLOCK_SIZE)
693 mask = block < max_num_reqs + 1
694 tl.store(out_query_start_loc_ptr + block, last_query_end, mask=mask)
695 for i in range(num_reqs, max_num_reqs, BLOCK_SIZE):
696 block = i + tl.arange(0, BLOCK_SIZE)
697 mask = block < max_num_reqs
698 tl.store(out_seq_lens_ptr + block, 0, mask=mask)
699 # Padded sample slots point at query index 0 (a valid row in
700 # last_hidden_states) so CG replay never reads OOB. Padded
701 # sample idx mappings point to -1, which is ignored during
702 # sampling to prevent writing stale values to draft logits.
703 pad_start = num_reqs * num_speculative_steps
704 pad_end = max_num_reqs * num_speculative_steps
705 for i in range(pad_start, pad_end, BLOCK_SIZE):
706 block = i + tl.arange(0, BLOCK_SIZE)
707 mask = block < pad_end
708 tl.store(out_sample_indices_ptr + block, 0, mask=mask)
709 tl.store(out_sample_pos_ptr + block, 0, mask=mask)
710 tl.store(out_sample_idx_mapping_ptr + block, -1, mask=mask)
711 # Pad query slot mappings past num_query_tokens with PAD so the
712 # captured CG sees PAD slots (no K/V write) for replay sizes
713 # larger than the current request count.
714 q_pad_start = num_reqs * num_query_per_req
715 for i in range(q_pad_start, max_num_tokens, BLOCK_SIZE):
716 block = i + tl.arange(0, BLOCK_SIZE)
717 mask = block < max_num_tokens
718 tl.store(out_query_slot_mapping_ptr + block, PAD_SLOT_ID, mask=mask)
719
720
721 def prepare_dflash_inputs(
722 input_buffers: InputBuffers,
723 query_slot_mapping: torch.Tensor,
724 context_positions: torch.Tensor,
725 context_slot_mapping: torch.Tensor,
726 sample_indices: torch.Tensor,
727 sample_pos: torch.Tensor,
728 sample_idx_mapping: torch.Tensor,
729 temperature: torch.Tensor,
730 seeds: torch.Tensor,
731 input_batch: InputBatch,
732 # [num_reqs]
733 num_sampled: torch.Tensor,
734 # [num_reqs]
735 num_rejected: torch.Tensor,
736 # [max_num_reqs]
737 last_sampled: torch.Tensor,
738 # [max_num_reqs]
739 next_prefill_tokens: torch.Tensor,
740 # [max_num_reqs]
741 input_temperature: torch.Tensor,
742 # [max_num_reqs]
743 input_seeds: torch.Tensor,
744 # [max_num_reqs, max_num_blocks]
745 block_table: torch.Tensor,
746 block_size: int,
747 cp_rank: int,
748 cp_size: int,
749 cp_interleave: int,
750 parallel_drafting_token_id: int,
751 num_query_per_req: int,
752 num_speculative_steps: int,
753 max_num_reqs: int,
754 max_num_tokens: int,
755 max_model_len: int,
756 sample_from_anchor: bool = False,
757 ) -> None:
758 num_reqs = input_batch.num_reqs
759 assert num_reqs > 0
760 # Cover the longest possible per-request span (ctx + query). Use the max
761 # per-request query length, not the total token count across the batch.
762 max_target_query_len = int(input_batch.num_scheduled_tokens.max())
763 max_tokens_per_req = max_target_query_len + num_query_per_req
764 BLOCK_SIZE = min(256, triton.next_power_of_2(max(1, max_tokens_per_req)))
765 num_blocks = triton.cdiv(max_tokens_per_req, BLOCK_SIZE)
766 _prepare_dflash_inputs_kernel[(num_reqs, num_blocks)](
767 input_buffers.input_ids,
768 input_buffers.positions,
769 input_buffers.query_start_loc,
770 input_buffers.seq_lens,
771 query_slot_mapping,
772 context_positions,
773 context_slot_mapping,
774 sample_indices,
775 sample_pos,
776 sample_idx_mapping,
777 temperature,
778 seeds,
779 input_batch.positions,
780 input_batch.query_start_loc,
781 input_batch.idx_mapping,
782 last_sampled,
783 next_prefill_tokens,
784 num_sampled,
785 num_rejected,
786 input_temperature,
787 input_seeds,
788 block_table,
789 block_table.stride(0),
790 parallel_drafting_token_id,
791 block_size,
792 num_query_per_req,
793 num_speculative_steps,
794 max_num_reqs,
795 max_num_tokens,
796 max_model_len,
797 cp_rank,
798 SAMPLE_FROM_ANCHOR=sample_from_anchor,
799 PAD_SLOT_ID=PAD_SLOT_ID,
800 CP_SIZE=cp_size,
801 CP_INTERLEAVE=cp_interleave,
802 BLOCK_SIZE=BLOCK_SIZE,
803 )