[Spec] Capture DFLASH draft greedy sampling inside the draft decode cuda graph (#29395)

This commit is contained in:
Liangsheng Yin
2026-06-27 19:34:26 -07:00
committed by GitHub
parent 3d922a51fc
commit 4e6ce8e4ab
2 changed files with 109 additions and 12 deletions
@@ -802,12 +802,28 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
):
kwargs["input_embeds"] = self.buffers.input_embeds[:num_tokens]
return forward(
out = forward(
forward_batch.input_ids,
forward_batch.positions,
forward_batch,
**kwargs,
)
dflash_sampler = getattr(
self.model_runner, "dflash_draft_sampler", None
)
if dflash_sampler is not None:
# Must be captured here, or replay leaves a stale output buffer
# the worker would read as valid tokens -- fail loudly instead.
if (
not isinstance(out, LogitsProcessorOutput)
or out.hidden_states is None
):
raise RuntimeError(
"DFLASH draft sampler set but the draft forward has no "
"hidden_states to capture into the graph."
)
dflash_sampler(out.hidden_states)
return out
self.deepep_adapter.capture(is_extend_in_batch=False)
canary_ctx = (
@@ -59,6 +59,40 @@ def _get_fused_kv_materialize_helper():
return _FusedKVMaterializeHelper
class _DflashDraftSampler:
"""Capture-safe greedy argmax over the target LM head, run inside the draft
cuda graph so the draft sampling is captured and counted in fwd_occupancy.
DFLASH's draft has no head of its own; it borrows the target `lm_head`.
tp=1 / no-added-vocab only; TP>1 stays eager in the worker.
"""
def __init__(self, *, weight, block_size, num_org, org_vocab_start, max_bs):
self.weight = weight
self.block_size = int(block_size)
self.num_org = int(num_org)
self.org_vocab_start = int(org_vocab_start)
# Proposed draft tokens: written in-graph, read by the worker after replay.
self.out = torch.empty(
(int(max_bs) * (self.block_size - 1),),
dtype=torch.int64,
device=weight.device,
)
def __call__(self, hidden_states):
# draft tokens are block positions 1: (pos 0 is the seeded bonus token)
bs = hidden_states.shape[0] // self.block_size
hs = hidden_states.view(bs, self.block_size, -1)[:, 1:, :].reshape(
-1, hidden_states.shape[-1]
)
if hs.dtype != self.weight.dtype:
hs = hs.to(self.weight.dtype)
logits = torch.matmul(hs, self.weight[: self.num_org].T)
tokens = torch.argmax(logits, dim=-1).to(torch.long)
if self.org_vocab_start:
tokens += self.org_vocab_start
self.out[: tokens.shape[0]].copy_(tokens)
class DFlashWorkerV2(BaseSpecWorker):
"""DFLASH speculative decoding worker (spec-v2).
@@ -158,6 +192,7 @@ class DFlashWorkerV2(BaseSpecWorker):
)
set_global_server_args_for_scheduler(saved_server_args)
self.draft_model_runner = self._draft_worker.model_runner
self._draft_sampler = None
# Keep the same alias that other spec-v2 workers expose.
self._draft_worker.draft_runner = self.draft_model_runner
self.draft_model = self.draft_model_runner.model
@@ -305,10 +340,50 @@ class DFlashWorkerV2(BaseSpecWorker):
"memory is available after target backend initialization.",
available_mem,
)
if capture_decode_cuda_graph:
# Must run before capture so the draft graph folds the head in.
self._draft_sampler = self._maybe_build_draft_sampler()
self.draft_model_runner.dflash_draft_sampler = self._draft_sampler
self._draft_worker.init_cuda_graphs(
capture_decode_cuda_graph=capture_decode_cuda_graph
)
def _maybe_build_draft_sampler(self):
def _eager(reason):
if self.tp_rank == 0:
logger.info("DFLASH draft greedy head kept eager (reason=%s).", reason)
return None
if get_tp_group().world_size != 1:
return _eager("tp>1")
if self.block_size <= 1:
return _eager("block_size<=1")
target_model = self._target_worker.model_runner.model
lm_head = getattr(target_model, "lm_head", None)
if lm_head is None or not hasattr(lm_head, "weight"):
return _eager("no target lm_head")
if not torch.is_floating_point(lm_head.weight):
# Quantized lm_head (FP8/INT) would break the static matmul.
return _eager("quantized lm_head")
if not hasattr(lm_head, "shard_indices"):
num_org = int(lm_head.weight.shape[0])
org_vocab_start = 0
else:
shard = lm_head.shard_indices
if int(shard.num_added_elements) != 0:
return _eager("added vocab")
num_org = int(shard.num_org_elements)
org_vocab_start = int(shard.org_vocab_start_index)
if self.tp_rank == 0:
logger.info("DFLASH draft greedy head folded into the draft cuda graph.")
return _DflashDraftSampler(
weight=lm_head.weight,
block_size=self.block_size,
num_org=num_org,
org_vocab_start=org_vocab_start,
max_bs=max(self.server_args.cuda_graph_config.decode.bs),
)
def _init_fused_kv_helper(self) -> None:
"""Initialize the fused KV materialization helper with pre-stacked weights."""
try:
@@ -1480,18 +1555,24 @@ class DFlashWorkerV2(BaseSpecWorker):
)
with torch.inference_mode():
draft_logits_output = self.draft_model_runner.forward(
forward_batch
).logits_output
draft_out = self.draft_model_runner.forward(forward_batch)
draft_logits_output = draft_out.logits_output
draft_hidden = draft_logits_output.hidden_states
if draft_hidden is None:
raise RuntimeError("DFLASH draft model returned no hidden states.")
draft_hidden = draft_hidden.view(bs, int(self.block_size), -1)
draft_next = self._greedy_sample_from_vocab_parallel_head(
hidden_states=draft_hidden[:, 1:, :].reshape(-1, draft_hidden.shape[-1]),
lm_head=lm_head,
).view(bs, int(self.block_size) - 1)
if self._draft_sampler is not None and draft_out.can_run_graph:
draft_next = self._draft_sampler.out[
: bs * (int(self.block_size) - 1)
].view(bs, int(self.block_size) - 1)
else:
draft_hidden = draft_logits_output.hidden_states
if draft_hidden is None:
raise RuntimeError("DFLASH draft model returned no hidden states.")
draft_hidden = draft_hidden.view(bs, int(self.block_size), -1)
draft_next = self._greedy_sample_from_vocab_parallel_head(
hidden_states=draft_hidden[:, 1:, :].reshape(
-1, draft_hidden.shape[-1]
),
lm_head=lm_head,
).view(bs, int(self.block_size) - 1)
draft_tokens = self._draft_block_tokens_buf[:bs]
draft_tokens[:, 0].copy_(block_ids[:, 0])