from __future__ import annotations import json import logging from typing import TYPE_CHECKING, Any, List, Optional import msgspec from sglang.srt.speculative.dflash_utils import parse_dflash_draft_config if TYPE_CHECKING: from sglang.srt.server_args import ServerArgs logger = logging.getLogger(__name__) DEFAULT_DSPARK_GAMMA = 7 SUPPORTED_DSPARK_MARKOV_HEAD_TYPES = ("vanilla", "gated", "rnn") # The dsv4 self-drafting checkpoint runs its draft attention on the dedicated # DeepSeek-V4 backend instead of the generic draft-backend fallback. DSV4_DRAFT_ATTENTION_BACKEND = "dsv4" def draft_is_deepseek_v4(*, server_args: ServerArgs) -> bool: from sglang.srt.configs.model_config import is_deepseek_v4 from sglang.srt.utils.hf_transformers_utils import get_config draft_model_path = server_args.speculative_draft_model_path if not draft_model_path: return False draft_hf_config = get_config( draft_model_path, trust_remote_code=server_args.trust_remote_code, revision=server_args.speculative_draft_model_revision, model_override_args=json.loads(server_args.json_model_override_args), model_config_parser=server_args.model_config_parser, ) return draft_hf_config is not None and is_deepseek_v4(draft_hf_config) def dspark_gamma_from_num_draft_tokens(num_draft_tokens: int) -> int: gamma = int(num_draft_tokens) - 1 if gamma < 1: raise ValueError( "DSpark speculative_num_draft_tokens must be >= 2 (= gamma + 1), " f"got {num_draft_tokens}." ) return gamma class DSparkDraftConfig(msgspec.Struct, frozen=True): num_hidden_layers: Optional[int] num_target_layers: Optional[int] gamma: Optional[int] target_layer_ids: Optional[List[int]] mask_token: str mask_token_id: Optional[int] markov_rank: int markov_head_type: Optional[str] def resolve_gamma(self, *, default: Optional[int] = None) -> Optional[int]: return self.gamma if self.gamma is not None else default def require_markov(self) -> bool: return int(self.markov_rank) > 0 class DSparkRuntimeConfig(msgspec.Struct, frozen=True): gamma: int verify_num_draft_tokens: int mask_token_id: int def resolve_runtime_config( *, draft_hf_config: Any, speculative_num_draft_tokens: Optional[int], target_vocab_size: int, ) -> DSparkRuntimeConfig: """Resolve and validate the worker-facing DSpark runtime knobs (gamma, verify window, mask token) from the draft checkpoint config, with server_args.speculative_num_draft_tokens taking precedence for gamma.""" draft_config = parse_dspark_draft_config(draft_hf_config=draft_hf_config) if not draft_config.require_markov(): raise ValueError( "DSpark draft requires markov_rank > 0; got " f"markov_rank={draft_config.markov_rank}." ) if speculative_num_draft_tokens is None: gamma = int(draft_config.resolve_gamma(default=None) or 0) if gamma < 1: raise ValueError( "DSpark could not resolve gamma from the draft config and " "speculative_num_draft_tokens is unset." ) else: gamma = dspark_gamma_from_num_draft_tokens(int(speculative_num_draft_tokens)) config_gamma = draft_config.resolve_gamma(default=None) if config_gamma is not None and int(config_gamma) != gamma: logger.warning( "DSpark gamma mismatch: using gamma=%s (from " "speculative_num_draft_tokens=%s) but draft config block_size=%s.", gamma, speculative_num_draft_tokens, config_gamma, ) if draft_config.mask_token_id is None: raise ValueError( "DSpark requires mask_token_id to be set in the draft model config." ) mask_token_id = int(draft_config.mask_token_id) if mask_token_id >= target_vocab_size: raise ValueError( f"DSpark mask_token_id={mask_token_id} is outside the target " f"vocab size {target_vocab_size}." ) return DSparkRuntimeConfig( gamma=gamma, verify_num_draft_tokens=gamma + 1, mask_token_id=mask_token_id, ) def read_draft_checkpoint_gamma(*, server_args: ServerArgs) -> Optional[int]: """Load the draft checkpoint's hf config and read its DSpark gamma (block_size). Raises on config-load failure; callers pick the fallback.""" from sglang.srt.utils.hf_transformers_utils import get_config draft_hf_config = get_config( server_args.speculative_draft_model_path, trust_remote_code=server_args.trust_remote_code, revision=server_args.speculative_draft_model_revision, model_override_args=json.loads(server_args.json_model_override_args), ) return parse_dspark_draft_config(draft_hf_config=draft_hf_config).resolve_gamma( default=None ) def checkpoint_bundles_dspark_draft(hf_config: Any) -> bool: """The checkpoint carries a bundled DSpark draft head, marked by the prefixed dspark_* keys on the target hf config. Single source of truth for the bundling convention (draft-path defaulting, draft-arch remap).""" return any( _cfg_get(hf_config, key, None) is not None for key in ( "dspark_block_size", "dspark_markov_rank", "dspark_noise_token_id", "dspark_target_layer_ids", ) ) def _cfg_get(config: Any, key: str, default: Any = None) -> Any: if isinstance(config, dict): return config.get(key, default) return getattr(config, key, default) def _get_text_config(config: Any) -> Any: if config is None: return None if isinstance(config, dict): return config.get("text_config", config) text_config = getattr(config, "text_config", None) if text_config is not None: return text_config return config def _get_dspark_config(config: Any) -> dict: cfg = _cfg_get(config, "dspark_config", None) if cfg is None: return {} if isinstance(cfg, dict): return cfg try: return dict(cfg) except Exception: return {} def parse_dspark_draft_config(*, draft_hf_config: Any) -> DSparkDraftConfig: base = parse_dflash_draft_config(draft_hf_config=draft_hf_config) dspark_cfg = _get_dspark_config(draft_hf_config) text_config = _get_text_config(draft_hf_config) prefixed_block_size = _cfg_get(draft_hf_config, "dspark_block_size", None) prefixed_markov_rank = _cfg_get(draft_hf_config, "dspark_markov_rank", None) prefixed_markov_head_type = _cfg_get( draft_hf_config, "dspark_markov_head_type", None ) prefixed_noise_token_id = _cfg_get(draft_hf_config, "dspark_noise_token_id", None) prefixed_target_layer_ids = _cfg_get( draft_hf_config, "dspark_target_layer_ids", None ) uses_prefixed = any( value is not None for value in ( prefixed_block_size, prefixed_markov_rank, prefixed_noise_token_id, prefixed_target_layer_ids, ) ) raw_markov_rank = ( prefixed_markov_rank if prefixed_markov_rank is not None else dspark_cfg.get( "markov_rank", _cfg_get( text_config, "markov_rank", _cfg_get(draft_hf_config, "markov_rank", 0) ), ) ) markov_rank = int(raw_markov_rank) if raw_markov_rank is not None else 0 if markov_rank < 0: raise ValueError(f"DSpark markov_rank must be >= 0, got {markov_rank}.") markov_head_type = ( prefixed_markov_head_type if prefixed_markov_head_type is not None else dspark_cfg.get( "markov_head_type", _cfg_get( text_config, "markov_head_type", _cfg_get(draft_hf_config, "markov_head_type", None), ), ) ) if markov_rank > 0 and markov_head_type is None and not uses_prefixed: raise ValueError( "DSpark requires markov_head_type when markov_rank > 0, got None." ) if markov_head_type is not None: markov_head_type = str(markov_head_type).lower() if markov_head_type not in SUPPORTED_DSPARK_MARKOV_HEAD_TYPES: raise ValueError( f"Unsupported DSpark markov_head_type={markov_head_type!r}. " f"Supported: {SUPPORTED_DSPARK_MARKOV_HEAD_TYPES}." ) raw_mask_token_id = ( prefixed_noise_token_id if prefixed_noise_token_id is not None else dspark_cfg.get( "mask_token_id", _cfg_get( text_config, "mask_token_id", _cfg_get(draft_hf_config, "mask_token_id", base.mask_token_id), ), ) ) mask_token_id = int(raw_mask_token_id) if raw_mask_token_id is not None else None if mask_token_id is not None and mask_token_id < 0: raise ValueError( f"DSpark mask_token_id must be non-negative, got {mask_token_id}." ) gamma = ( int(prefixed_block_size) if prefixed_block_size is not None else base.block_size ) if prefixed_target_layer_ids is not None: if not isinstance(prefixed_target_layer_ids, (list, tuple)) or not len( prefixed_target_layer_ids ): raise ValueError( "DSpark dspark_target_layer_ids must be a non-empty list of ints, " f"got {prefixed_target_layer_ids!r}." ) target_layer_ids: Optional[List[int]] = [ int(x) for x in prefixed_target_layer_ids ] else: target_layer_ids = base.target_layer_ids return DSparkDraftConfig( num_hidden_layers=base.num_hidden_layers, num_target_layers=base.num_target_layers, gamma=gamma, target_layer_ids=target_layer_ids, mask_token=base.mask_token, mask_token_id=mask_token_id, markov_rank=markov_rank, markov_head_type=markov_head_type, )