[DFLASH] Support grammar-constrained decoding in speculative verify (#30096)

Co-authored-by: hnyls2002 <lsyincs@gmail.com>
This commit is contained in:
Hồ Sỹ Thế
2026-07-25 04:36:46 -07:00
committed by GitHub
co-authored by hnyls2002
parent 17afd8421f
commit d021990bf5
8 changed files with 118 additions and 10 deletions
+3 -1
View File
@@ -2253,7 +2253,9 @@ class Scheduler(
self._maybe_namespace_elastic_radix_cache(req)
if self.spec_algorithm.is_dflash_family():
error_msg = validate_dflash_request(req, self.enable_overlap)
error_msg = validate_dflash_request(
req, self.enable_overlap, self.spec_algorithm
)
if error_msg is not None:
req.set_finish_with_abort(error_msg)
self.init_req_max_new_tokens(req)
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Optional
import torch
from sglang.kernels.ops.attention.utils import create_flashinfer_kv_indices_triton
from sglang.srt.constrained.base_grammar_backend import BaseGrammarObject
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
@@ -44,6 +45,9 @@ class DFlashVerifyInput(SpecInput):
ragged_verify_layout: Optional[RaggedVerifyLayout] = None
# Stamped by generate_token_bitmask during verify, read back to apply the mask.
grammar: Optional[BaseGrammarObject] = None
def __post_init__(self):
super().__init__(spec_input_type=SpecInputType.DFLASH_VERIFY)
if self.num_tokens_per_req == -1:
@@ -12,6 +12,7 @@ import torch.nn.functional as F
from sglang.srt.layers.quantization.unquant import UnquantizedLinearMethod
from sglang.srt.layers.sampler import apply_custom_logit_processor
from sglang.srt.managers.schedule_batch import Req
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.utils import is_cuda, is_musa
DEFAULT_DFLASH_MASK_TOKEN = "<|MASK|>"
@@ -793,21 +794,25 @@ def build_dflash_verify_target_probs(
return target_probs.view(bs, draft_token_num, -1).contiguous()
def validate_dflash_request(req: Req, enable_overlap: bool) -> Optional[str]:
def validate_dflash_request(
req: Req, enable_overlap: bool, spec_algorithm: SpeculativeAlgorithm
) -> Optional[str]:
if req.return_logprob:
return "DFLASH speculative decoding does not support return_logprob yet."
if enable_overlap and req.return_hidden_states:
return "DFLASH speculative decoding does not support return_hidden_states yet."
if (
# Grammar support in this family is the verify-time bitmask plus the grammar
# barrier, so the capability that gates the barrier also gates admission.
if not spec_algorithm.supports_grammar_overlap() and (
req.sampling_params.json_schema is not None
or req.sampling_params.regex is not None
or req.sampling_params.ebnf is not None
or req.sampling_params.structural_tag is not None
):
return (
"DFLASH speculative decoding does not support "
f"{spec_algorithm.name} speculative decoding does not support "
"grammar-constrained decoding yet."
)
@@ -47,7 +47,11 @@ from sglang.srt.speculative.draft_worker_common import (
make_draft_sampler_capture_hook,
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_utils import assign_req_to_token_pool_func
from sglang.srt.speculative.spec_utils import (
GrammarTree,
assign_req_to_token_pool_func,
build_grammar_vocab_mask,
)
from sglang.srt.utils import get_available_gpu_memory, is_cuda, is_hip, is_npu
_is_npu = is_npu()
@@ -1367,6 +1371,7 @@ class DFlashWorkerV2(BaseSpecWorker):
self,
batch: ScheduleBatch,
on_publish=None,
grammar_barrier=None,
) -> GenerationBatchResult:
if getattr(batch, "return_logprob", False):
raise ValueError(
@@ -1633,6 +1638,11 @@ class DFlashWorkerV2(BaseSpecWorker):
draft_tokens[:, 0].copy_(block_ids[:, 0])
draft_tokens[:, 1:].copy_(draft_next)
# Must stay ahead of the target verify launch below.
grammar_tree = (
GrammarTree.from_linear_chain(draft_tokens) if batch.has_grammar else None
)
# --- 2) Target verify.
# TARGET_VERIFY uses standard causal masking; custom masks are unnecessary here.
custom_mask = None
@@ -1678,6 +1688,21 @@ class DFlashWorkerV2(BaseSpecWorker):
logits_output = target_out.logits_output
can_run_cuda_graph = target_out.can_run_cuda_graph
vocab_mask = None
if batch.has_grammar:
# Grammar barrier: advance the previous batch's FSM over its committed
# tokens before building this batch's bitmask. Runs after the target
# launch, so the advance and the traversal both overlap the forward.
if grammar_barrier is not None:
grammar_barrier()
vocab_mask = build_grammar_vocab_mask(
reqs=batch.reqs,
verify_input=verify_input,
tree=grammar_tree,
sampling_info=batch.sampling_info,
device=logits_output.next_token_logits.device,
)
if sampling_info is not None:
apply_dflash_verify_logits_adjustments(
next_token_logits=logits_output.next_token_logits,
@@ -1685,6 +1710,12 @@ class DFlashWorkerV2(BaseSpecWorker):
draft_token_num=int(self.block_size),
)
# Constrain every chain position before accept picks from it.
if vocab_mask is not None:
verify_input.grammar.apply_vocab_mask(
logits=logits_output.next_token_logits, vocab_mask=vocab_mask
)
candidates = draft_tokens
new_seq_lens = None
if (
+1 -1
View File
@@ -139,7 +139,7 @@ class SpeculativeAlgorithm(Enum):
# Needs a GPU draft phase to hide the grammar CPU work under: NGRAM drafts
# from a host corpus lookup, so it stays synchronous by design.
# STANDALONE inherits the EAGLE V2 worker's verify path, barrier included.
return self.is_eagle() or self.is_standalone()
return self.is_eagle() or self.is_standalone() or self.is_dflash()
def has_draft_kv(self) -> bool:
"""Whether the draft phase writes KV chains. NGRAM does not (its tree
@@ -590,6 +590,20 @@ class GrammarTree:
) -> GrammarTree:
return cls((retrieve_next_token, retrieve_next_sibling, draft_token), None)
@classmethod
def from_linear_chain(cls, verify_ids_2d: torch.Tensor) -> GrammarTree:
"""Degenerate tree for chain-verify algorithms: node i's only child is i + 1.
``verify_ids_2d`` is (bs, chain_len) with column 0 the already-committed
token, so mask rows line up with the target's logits rows one-for-one.
Only the ids need a copy; the links are fixed by the shape.
"""
bs, chain_len = verify_ids_2d.shape
next_token = torch.full((bs, chain_len), -1, dtype=torch.int64)
next_token[:, :-1] = torch.arange(1, chain_len, dtype=torch.int64)
next_sibling = torch.full((bs, chain_len), -1, dtype=torch.int64)
return cls.from_device(next_token, next_sibling, verify_ids_2d)
def resolve(self) -> Tuple[torch.Tensor, ...]:
if self._done is not None:
self._done.synchronize()