[DSPARK] Grammar-constrained decoding, incl. tool_choice=auto (#31753)
Co-authored-by: shanemort1982 <shanemort1982@users.noreply.github.com> Co-authored-by: hnyls2002 <lsyincs@gmail.com>
This commit is contained in:
co-authored by
shanemort1982
hnyls2002
parent
a678a42033
commit
e943e609dc
@@ -2253,9 +2253,7 @@ class Scheduler(
|
|||||||
self._maybe_namespace_elastic_radix_cache(req)
|
self._maybe_namespace_elastic_radix_cache(req)
|
||||||
|
|
||||||
if self.spec_algorithm.is_dflash_family():
|
if self.spec_algorithm.is_dflash_family():
|
||||||
error_msg = validate_dflash_request(
|
error_msg = validate_dflash_request(req, self.enable_overlap)
|
||||||
req, self.enable_overlap, self.spec_algorithm
|
|
||||||
)
|
|
||||||
if error_msg is not None:
|
if error_msg is not None:
|
||||||
req.set_finish_with_abort(error_msg)
|
req.set_finish_with_abort(error_msg)
|
||||||
self.init_req_max_new_tokens(req)
|
self.init_req_max_new_tokens(req)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import List, Optional
|
|||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
|
from sglang.srt.constrained.base_grammar_backend import BaseGrammarObject
|
||||||
from sglang.srt.environ import envs
|
from sglang.srt.environ import envs
|
||||||
from sglang.srt.managers.schedule_batch import ScheduleBatch
|
from sglang.srt.managers.schedule_batch import ScheduleBatch
|
||||||
from sglang.srt.mem_cache.allocation import alloc_for_spec_decode
|
from sglang.srt.mem_cache.allocation import alloc_for_spec_decode
|
||||||
@@ -56,6 +57,9 @@ class DFlashDraftInputV2(SpecInput):
|
|||||||
|
|
||||||
verify_token_budget: Optional[int] = None
|
verify_token_budget: Optional[int] = None
|
||||||
|
|
||||||
|
# Stamped by generate_token_bitmask during verify, read back to apply the mask.
|
||||||
|
grammar: Optional[BaseGrammarObject] = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
super().__init__(spec_input_type=SpecInputType.DFLASH_DRAFT)
|
super().__init__(spec_input_type=SpecInputType.DFLASH_DRAFT)
|
||||||
# Spec v2 draft state itself does not change token accounting.
|
# Spec v2 draft state itself does not change token accounting.
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import torch.nn.functional as F
|
|||||||
from sglang.srt.layers.quantization.unquant import UnquantizedLinearMethod
|
from sglang.srt.layers.quantization.unquant import UnquantizedLinearMethod
|
||||||
from sglang.srt.layers.sampler import apply_custom_logit_processor
|
from sglang.srt.layers.sampler import apply_custom_logit_processor
|
||||||
from sglang.srt.managers.schedule_batch import Req
|
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
|
from sglang.srt.utils import is_cuda, is_musa
|
||||||
|
|
||||||
DEFAULT_DFLASH_MASK_TOKEN = "<|MASK|>"
|
DEFAULT_DFLASH_MASK_TOKEN = "<|MASK|>"
|
||||||
@@ -794,26 +793,11 @@ def build_dflash_verify_target_probs(
|
|||||||
return target_probs.view(bs, draft_token_num, -1).contiguous()
|
return target_probs.view(bs, draft_token_num, -1).contiguous()
|
||||||
|
|
||||||
|
|
||||||
def validate_dflash_request(
|
def validate_dflash_request(req: Req, enable_overlap: bool) -> Optional[str]:
|
||||||
req: Req, enable_overlap: bool, spec_algorithm: SpeculativeAlgorithm
|
|
||||||
) -> Optional[str]:
|
|
||||||
if req.return_logprob:
|
if req.return_logprob:
|
||||||
return "DFLASH speculative decoding does not support return_logprob yet."
|
return "DFLASH speculative decoding does not support return_logprob yet."
|
||||||
|
|
||||||
if enable_overlap and req.return_hidden_states:
|
if enable_overlap and req.return_hidden_states:
|
||||||
return "DFLASH speculative decoding does not support return_hidden_states yet."
|
return "DFLASH speculative decoding does not support return_hidden_states yet."
|
||||||
|
|
||||||
# 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 (
|
|
||||||
f"{spec_algorithm.name} speculative decoding does not support "
|
|
||||||
"grammar-constrained decoding yet."
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ from sglang.srt.speculative.dspark_components.dspark_verify import (
|
|||||||
TargetVerifyExecutor,
|
TargetVerifyExecutor,
|
||||||
verify_logits_adjustments_are_noop,
|
verify_logits_adjustments_are_noop,
|
||||||
)
|
)
|
||||||
from sglang.srt.speculative.spec_utils import draft_tp_context
|
from sglang.srt.speculative.spec_utils import (
|
||||||
|
GrammarTree,
|
||||||
|
build_grammar_vocab_mask,
|
||||||
|
draft_tp_context,
|
||||||
|
)
|
||||||
from sglang.srt.utils import get_available_gpu_memory, is_cuda
|
from sglang.srt.utils import get_available_gpu_memory, is_cuda
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -358,6 +362,7 @@ class DSparkWorkerV2(BaseSpecWorker):
|
|||||||
self,
|
self,
|
||||||
batch: ScheduleBatch,
|
batch: ScheduleBatch,
|
||||||
on_publish=None,
|
on_publish=None,
|
||||||
|
grammar_barrier=None,
|
||||||
) -> GenerationBatchResult:
|
) -> GenerationBatchResult:
|
||||||
if getattr(batch, "return_logprob", False):
|
if getattr(batch, "return_logprob", False):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -369,7 +374,7 @@ class DSparkWorkerV2(BaseSpecWorker):
|
|||||||
self._observers.note_prefill_step()
|
self._observers.note_prefill_step()
|
||||||
return self._forward_prefill(batch, on_publish)
|
return self._forward_prefill(batch, on_publish)
|
||||||
|
|
||||||
return self._forward_decode(batch, on_publish)
|
return self._forward_decode(batch, on_publish, grammar_barrier)
|
||||||
|
|
||||||
def _forward_prefill(
|
def _forward_prefill(
|
||||||
self, batch: ScheduleBatch, on_publish
|
self, batch: ScheduleBatch, on_publish
|
||||||
@@ -475,7 +480,7 @@ class DSparkWorkerV2(BaseSpecWorker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _forward_decode(
|
def _forward_decode(
|
||||||
self, batch: ScheduleBatch, on_publish
|
self, batch: ScheduleBatch, on_publish, grammar_barrier=None
|
||||||
) -> GenerationBatchResult:
|
) -> GenerationBatchResult:
|
||||||
if batch.spec_info is None:
|
if batch.spec_info is None:
|
||||||
batch.spec_info = DFlashDraftInputV2.create_idle_input(device=self.device)
|
batch.spec_info = DFlashDraftInputV2.create_idle_input(device=self.device)
|
||||||
@@ -568,11 +573,19 @@ class DSparkWorkerV2(BaseSpecWorker):
|
|||||||
[draft_block_ids[:, :1], draft_tokens], dim=1
|
[draft_block_ids[:, :1], draft_tokens], dim=1
|
||||||
).contiguous()
|
).contiguous()
|
||||||
|
|
||||||
|
# Must stay ahead of the target verify launch below.
|
||||||
|
grammar_tree = (
|
||||||
|
GrammarTree.from_linear_chain(verify_ids_2d) if batch.has_grammar else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# A live grammar forces the eager path: the folded epilogue accepts inside
|
||||||
|
# the cuda graph off its own buffers, where the mask below never lands.
|
||||||
fold_eligible = (
|
fold_eligible = (
|
||||||
self._verify_executor.verify_epilogue is not None
|
self._verify_executor.verify_epilogue is not None
|
||||||
and proposal.folded
|
and proposal.folded
|
||||||
and verify_logits_adjustments_are_noop(sampling_info)
|
and verify_logits_adjustments_are_noop(sampling_info)
|
||||||
and self._simulate_acc_len <= 0
|
and self._simulate_acc_len <= 0
|
||||||
|
and not batch.has_grammar
|
||||||
)
|
)
|
||||||
with self._observers.segment(InfoSegment.TARGET_VERIFY):
|
with self._observers.segment(InfoSegment.TARGET_VERIFY):
|
||||||
if run_compact:
|
if run_compact:
|
||||||
@@ -598,6 +611,25 @@ class DSparkWorkerV2(BaseSpecWorker):
|
|||||||
logits_output = target_verify.logits_output
|
logits_output = target_verify.logits_output
|
||||||
can_run_cuda_graph = target_verify.can_run_cuda_graph
|
can_run_cuda_graph = target_verify.can_run_cuda_graph
|
||||||
|
|
||||||
|
if batch.has_grammar:
|
||||||
|
# Both the FSM advance over the previous batch's committed tokens and
|
||||||
|
# the traversal below are host work, so they overlap the launch above.
|
||||||
|
if grammar_barrier is not None:
|
||||||
|
grammar_barrier()
|
||||||
|
# run_compact scatters its rows back to (bs * chain_len), so the mask
|
||||||
|
# lines up with the logits on both verify paths.
|
||||||
|
vocab_mask = build_grammar_vocab_mask(
|
||||||
|
reqs=batch.reqs,
|
||||||
|
verify_input=draft_input,
|
||||||
|
tree=grammar_tree,
|
||||||
|
sampling_info=sampling_info,
|
||||||
|
device=logits_output.next_token_logits.device,
|
||||||
|
)
|
||||||
|
if vocab_mask is not None:
|
||||||
|
draft_input.grammar.apply_vocab_mask(
|
||||||
|
logits=logits_output.next_token_logits, vocab_mask=vocab_mask
|
||||||
|
)
|
||||||
|
|
||||||
epilogue = self._verify_executor.verify_epilogue
|
epilogue = self._verify_executor.verify_epilogue
|
||||||
folded_accept = fold_eligible and run_compact and can_run_cuda_graph
|
folded_accept = fold_eligible and run_compact and can_run_cuda_graph
|
||||||
accept = self._verify_executor.accept_and_finalize(
|
accept = self._verify_executor.accept_and_finalize(
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class SpeculativeAlgorithm(Enum):
|
|||||||
# Needs a GPU draft phase to hide the grammar CPU work under: NGRAM drafts
|
# 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.
|
# from a host corpus lookup, so it stays synchronous by design.
|
||||||
# STANDALONE inherits the EAGLE V2 worker's verify path, barrier included.
|
# STANDALONE inherits the EAGLE V2 worker's verify path, barrier included.
|
||||||
return self.is_eagle() or self.is_standalone() or self.is_dflash()
|
return self.is_eagle() or self.is_standalone() or self.is_dflash_family()
|
||||||
|
|
||||||
def has_draft_kv(self) -> bool:
|
def has_draft_kv(self) -> bool:
|
||||||
"""Whether the draft phase writes KV chains. NGRAM does not (its tree
|
"""Whether the draft phase writes KV chains. NGRAM does not (its tree
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from sglang.test.kits.basic_decode_correctness_kit import BasicDecodeCorrectness
|
|||||||
from sglang.test.kits.basic_scheduler_stress_kit import BasicSchedulerStressMixin
|
from sglang.test.kits.basic_scheduler_stress_kit import BasicSchedulerStressMixin
|
||||||
from sglang.test.kits.eval_accuracy_kit import GSM8KMixin
|
from sglang.test.kits.eval_accuracy_kit import GSM8KMixin
|
||||||
from sglang.test.kits.fwd_occupancy_kit import FwdOccupancyMixin
|
from sglang.test.kits.fwd_occupancy_kit import FwdOccupancyMixin
|
||||||
|
from sglang.test.kits.json_constrained_kit import JSONConstrainedMixin
|
||||||
|
from sglang.test.kits.spec_server_kits import SpecGrammarKit
|
||||||
from sglang.test.test_utils import (
|
from sglang.test.test_utils import (
|
||||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||||
DEFAULT_URL_FOR_TEST,
|
DEFAULT_URL_FOR_TEST,
|
||||||
@@ -14,7 +16,7 @@ from sglang.test.test_utils import (
|
|||||||
popen_launch_server,
|
popen_launch_server,
|
||||||
)
|
)
|
||||||
|
|
||||||
register_cuda_ci(est_time=120, stage="base-b", runner_config="1-gpu-large")
|
register_cuda_ci(est_time=180, stage="base-b", runner_config="1-gpu-large")
|
||||||
|
|
||||||
TARGET_MODEL = "Qwen/Qwen3-14B"
|
TARGET_MODEL = "Qwen/Qwen3-14B"
|
||||||
DRAFT_MODEL = "deepseek-ai/dspark_qwen3_14b_block7"
|
DRAFT_MODEL = "deepseek-ai/dspark_qwen3_14b_block7"
|
||||||
@@ -34,6 +36,8 @@ class TestBasicSanityDSpark(
|
|||||||
BasicSchedulerStressMixin,
|
BasicSchedulerStressMixin,
|
||||||
FwdOccupancyMixin,
|
FwdOccupancyMixin,
|
||||||
GSM8KMixin,
|
GSM8KMixin,
|
||||||
|
JSONConstrainedMixin,
|
||||||
|
SpecGrammarKit,
|
||||||
CustomTestCase,
|
CustomTestCase,
|
||||||
):
|
):
|
||||||
served_model_name = TARGET_MODEL
|
served_model_name = TARGET_MODEL
|
||||||
@@ -84,6 +88,10 @@ class TestBasicSanityDSpark(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@unittest.skip("DSPARK rejects return_logprob at admission")
|
||||||
|
def test_grammar_logprob_count_matches_completion_tokens(self):
|
||||||
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
if cls.process is not None:
|
if cls.process is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user