[Model] Support Nemotron 3.5 Lightning speculative decoding (#36186)

Co-authored-by: Ryan Stewart <rystewart@nvidia.com>
This commit is contained in:
Baizhou Zhang
2026-08-25 16:43:58 -07:00
committed by GitHub
co-authored by Ryan Stewart
parent 2d88c79b3e
commit 41e7612dee
18 changed files with 792 additions and 122 deletions
+26 -2
View File
@@ -1482,8 +1482,32 @@ def _nemotron_h_overrides(server_args: Any, hf_config: Any) -> dict:
else:
overrides["moe_runner_backend"] = "flashinfer_cutlass"
if is_sm100_supported() and server_args.attention_backend is None:
overrides["attention_backend"] = "flashinfer"
if is_blackwell_supported() and server_args.is_attention_backend_not_set():
if server_args.speculative_algorithm is not None:
speculative_algorithm = server_args.speculative_algorithm.upper()
if is_sm100_supported() and server_args.speculative_eagle_topk in (
None,
1,
):
overrides["attention_backend"] = "trtllm_mha"
if server_args.page_size is None:
overrides["page_size"] = 64
if server_args.mamba_radix_cache_strategy == "auto":
overrides["mamba_radix_cache_strategy"] = "extra_buffer"
if (
server_args.speculative_draft_attention_backend is None
and speculative_algorithm in ("EAGLE", "NEXTN", "DSPARK")
):
overrides["speculative_draft_attention_backend"] = "trtllm_mha"
else:
overrides["attention_backend"] = "triton"
if (
server_args.speculative_draft_attention_backend is None
and speculative_algorithm in ("EAGLE", "NEXTN", "DFLASH", "DSPARK")
):
overrides["speculative_draft_attention_backend"] = "flashinfer"
elif is_sm100_supported():
overrides["attention_backend"] = "trtllm_mha"
return overrides
@@ -447,6 +447,21 @@ def _handle_dspark(server_args: ServerArgs) -> None:
speculative_eagle_topk=1,
)
from sglang.srt.speculative.dspark_components.dspark_config import (
DEFAULT_DSPARK_GAMMA,
read_draft_checkpoint_config,
)
draft_config = None
try:
draft_config = read_draft_checkpoint_config(server_args=server_args)
except Exception as e:
logger.warning(
"Failed to read DSpark draft config; preserving explicit/default "
"gamma resolution. Error: %s",
e,
)
gamma: Optional[int] = None
if server_args.speculative_dspark_block_size is not None:
if int(server_args.speculative_dspark_block_size) <= 0:
@@ -456,19 +471,8 @@ def _handle_dspark(server_args: ServerArgs) -> None:
)
gamma = int(server_args.speculative_dspark_block_size)
else:
from sglang.srt.speculative.dspark_components.dspark_config import (
DEFAULT_DSPARK_GAMMA,
read_draft_checkpoint_gamma,
)
try:
gamma = read_draft_checkpoint_gamma(server_args=server_args)
except Exception as e:
logger.warning(
"Failed to read DSpark gamma from draft model config; "
"cannot cross-check --speculative-num-draft-tokens. Error: %s",
e,
)
if draft_config is not None:
gamma = draft_config.resolve_gamma(default=None)
if gamma is None and server_args.speculative_num_draft_tokens is None:
gamma = DEFAULT_DSPARK_GAMMA
logger.warning(
@@ -1407,6 +1407,7 @@ class ModelOptFp4Config(ModelOptQuantConfig):
"format is experimental and subject to change."
)
self.is_awq = is_awq
self.is_w4a16 = False
self.group_size = group_size
if not is_checkpoint_nvfp4_serialized:
if use_per_token_activation:
@@ -1561,9 +1562,10 @@ class ModelOptFp4Config(ModelOptQuantConfig):
"Expected either flat format (config.json) or nested format (hf_quant_config.json)."
)
if quant_method not in ["FP8", "NVFP4", "NVFP4_AWQ"]:
if quant_method not in ["FP8", "NVFP4", "NVFP4_AWQ", "W4A16_NVFP4"]:
raise ValueError(
"ModelOpt currently only supports: FP8, NVFP4, NVFP4_AWQ "
"ModelOpt currently only supports: FP8, NVFP4, NVFP4_AWQ, "
"W4A16_NVFP4 "
"quantizations in sglang. Please check the "
"quantization config for your model's configuration."
)
@@ -1579,14 +1581,17 @@ class ModelOptFp4Config(ModelOptQuantConfig):
"NVFP4 quantization requires group_size and exclude_modules "
"specified in the quantization config"
)
return cls(
quant_config = cls(
is_checkpoint_nvfp4_serialized,
kv_cache_quant_algo,
group_size,
exclude_modules,
config.get("packed_modules_mapping"),
is_awq="AWQ" in quant_method,
use_per_token_activation=(False if quant_method == "W4A16_NVFP4" else None),
)
quant_config.is_w4a16 = quant_method == "W4A16_NVFP4"
return quant_config
def get_quant_method(self, layer: torch.nn.Module, prefix: str):
from sglang.srt.layers.linear import LinearBase
@@ -1606,7 +1611,11 @@ class ModelOptFp4Config(ModelOptQuantConfig):
return self._get_quant_method(
layer,
prefix,
Linear=ModelOptFp4LinearMethod,
Linear=(
ModelOptNvFp4A16LinearMethod
if self.is_w4a16
else ModelOptFp4LinearMethod
),
Moe=ModelOptNvFp4FusedMoEMethod,
)
@@ -807,6 +807,15 @@ class ModelRunner:
) -> int:
"""Logits rows per decode batch slot."""
if self.spec_algorithm.is_speculative():
if self.spec_algorithm.is_dspark() and self.is_draft_worker:
from sglang.srt.speculative.dspark_components.dspark_config import (
get_dspark_sample_from_anchor,
)
if not get_dspark_sample_from_anchor(self.model_config.hf_config):
if num_draft_tokens is None:
num_draft_tokens = get_spec().speculative_num_draft_tokens
return int(num_draft_tokens)
return resolve_num_tokens_per_req(
phase="target_verify",
spec_algorithm=self.spec_algorithm,
+154 -34
View File
@@ -1,7 +1,6 @@
# Adapted from the DFlash reference implementation (HF) but implemented with
# SGLang primitives (RadixAttention + SGLang KV cache). This model intentionally
# does not include token embeddings or an LM head; DFlash uses the target model's
# embedding/lm_head.
# SGLang primitives (RadixAttention + SGLang KV cache). Most drafts borrow the
# target embedding and LM head; Nemotron 3.5 drafts carry their own embedding.
from __future__ import annotations
@@ -21,6 +20,7 @@ from sglang.srt.layers.linear import (
ColumnParallelLinear,
MergedColumnParallelLinear,
QKVParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.srt.layers.logits_processor import (
@@ -29,18 +29,23 @@ from sglang.srt.layers.logits_processor import (
)
from sglang.srt.layers.radix_attention import AttentionType, RadixAttention
from sglang.srt.layers.rotary_embedding import get_rope
from sglang.srt.layers.vocab_parallel_embedding import VocabParallelEmbedding
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.model_loader.weight_utils import default_weight_loader
from sglang.srt.model_loader.weight_utils import (
default_weight_loader,
sharded_weight_loader,
)
from sglang.srt.models.utils import apply_qk_norm
from sglang.srt.runtime_context import get_parallel
from sglang.srt.runtime_context import get_parallel, get_spec
from sglang.srt.speculative.dflash_utils import (
can_dflash_slice_qkv_weight,
get_dflash_attention_sliding_window_size,
get_dflash_layer_types,
is_dense_head_weight,
is_nemotron_35_draft_config,
parse_dflash_draft_config,
)
from sglang.srt.utils import is_npu
from sglang.srt.utils import is_npu, set_weight_attrs
from sglang.srt.utils.common import get_compiler_backend
from sglang.srt.utils.hf_transformers_utils import get_rope_config
@@ -62,6 +67,24 @@ def _radix_topk(scores: torch.Tensor, k: int) -> Tuple[torch.Tensor, torch.Tenso
return torch.topk(scores, k, dim=-1)
def _logical_linear_weight_shape(
param: torch.Tensor,
loaded_weight: torch.Tensor,
*,
output_features: int,
) -> Tuple[int, ...]:
"""Return a checkpoint linear weight shape in logical elements."""
loaded_shape = tuple(loaded_weight.shape)
pack_factor = getattr(param, "pack_factor", None)
if pack_factor is None or loaded_shape != tuple(param.shape):
return loaded_shape
logical_numel = int(loaded_weight.numel() * pack_factor)
if logical_numel % output_features == 0:
return (output_features, logical_numel // output_features)
return (logical_numel,)
def _project_candidate_logits(
hidden: torch.Tensor, lm_head: nn.Module, *, num_org: int, use_quant_head: bool
) -> torch.Tensor:
@@ -117,7 +140,9 @@ def _get_dflash_layer_attention_params(
class DFlashAttention(nn.Module):
def __init__(self, config, layer_id: int, quant_config=None) -> None:
def __init__(
self, config, layer_id: int, quant_config=None, prefix: str = ""
) -> None:
super().__init__()
hidden_size = int(config.hidden_size)
tp_size = int(get_parallel().tp_size)
@@ -160,14 +185,14 @@ class DFlashAttention(nn.Module):
total_num_kv_heads=self.total_num_kv_heads,
bias=attention_bias,
quant_config=quant_config,
prefix="qkv_proj",
prefix=f"{prefix}.qkv_proj" if prefix else "qkv_proj",
)
self.o_proj = RowParallelLinear(
self.total_num_heads * head_dim,
hidden_size,
bias=attention_bias,
quant_config=quant_config,
prefix="o_proj",
prefix=f"{prefix}.o_proj" if prefix else "o_proj",
)
# Per-head Q/K RMSNorm, matching HF Qwen3.
@@ -201,6 +226,24 @@ class DFlashAttention(nn.Module):
self.sliding_window_size, self.attn_type = _get_dflash_layer_attention_params(
config, layer_id
)
self.attention_sink_bias = None
if is_nemotron_35_draft_config(config) and bool(
getattr(config, "attention_sink_bias", False)
):
draft_attention_backend = get_spec().speculative_draft_attention_backend
if draft_attention_backend != "trtllm_mha":
raise ValueError(
"Nemotron 3.5 DSpark attention sinks require "
"--speculative-draft-attention-backend trtllm_mha, "
f"got {draft_attention_backend!r}."
)
self.attention_sink_bias = nn.Parameter(
torch.empty(self.num_heads, dtype=torch.float32), requires_grad=False
)
set_weight_attrs(
self.attention_sink_bias,
{"weight_loader": sharded_weight_loader(0)},
)
self.attn = RadixAttention(
num_heads=self.num_heads,
head_dim=head_dim,
@@ -259,7 +302,12 @@ class DFlashAttention(nn.Module):
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
q, k = apply_qk_norm(q, k, self.q_norm, self.k_norm, self.head_dim)
q, k = self.rotary_emb(positions, q, k)
attn_output = self.attn(q, k, v, forward_batch)
if self.attention_sink_bias is None:
attn_output = self.attn(q, k, v, forward_batch)
else:
attn_output = self.attn(
q, k, v, forward_batch, sinks=self.attention_sink_bias
)
attn_output = self.apply_attention_output(attn_output, hidden_states)
output, _ = self.o_proj(attn_output)
return output
@@ -428,17 +476,25 @@ class DFlashDecoderLayer(nn.Module):
attention_conv: Optional[DFlashGroupedConv] = None,
mlp_conv: Optional[DFlashGroupedConv] = None,
quant_config=None,
prefix: str = "",
) -> None:
super().__init__()
hidden_size = int(config.hidden_size)
rms_norm_eps = float(getattr(config, "rms_norm_eps", 1e-6))
self.input_layernorm = RMSNorm(hidden_size, eps=rms_norm_eps)
attention_prefix = f"{prefix}.self_attn" if prefix else ""
self.self_attn = self.attention_cls(
config=config, layer_id=layer_id, quant_config=quant_config
config=config,
layer_id=layer_id,
quant_config=quant_config,
prefix=attention_prefix,
)
self.post_attention_layernorm = RMSNorm(hidden_size, eps=rms_norm_eps)
self.mlp = DFlashMLP(config=config, quant_config=quant_config)
mlp_prefix = f"{prefix}.mlp" if prefix else ""
self.mlp = DFlashMLP(
config=config, quant_config=quant_config, prefix=mlp_prefix
)
self.attention_conv = attention_conv
self.mlp_conv = mlp_conv
@@ -487,7 +543,7 @@ class DFlashDecoderLayer(nn.Module):
class DFlashDraftModel(nn.Module):
"""SGLang DFlash draft model (no embedding / lm_head weights).
"""SGLang DFlash draft model with an optional Nemotron embedding.
The checkpoint provides:
- transformer weights for `layers.*`
@@ -510,6 +566,16 @@ class DFlashDraftModel(nn.Module):
)
self.block_size = draft_config.resolve_block_size(default=16)
self.candidate_selector: Optional[nn.Module] = None
self.is_nemotron_35_draft = is_nemotron_35_draft_config(config)
self.embed_tokens: Optional[VocabParallelEmbedding] = None
if self.is_nemotron_35_draft:
embed_prefix = f"{prefix}.embed_tokens" if prefix else "embed_tokens"
self.embed_tokens = VocabParallelEmbedding(
config.vocab_size,
hidden_size,
quant_config=quant_config,
prefix=embed_prefix,
)
def grouped_conv():
if not draft_config.conv_kernel_size:
@@ -529,6 +595,11 @@ class DFlashDraftModel(nn.Module):
attention_conv=grouped_conv(),
mlp_conv=grouped_conv(),
quant_config=quant_config,
prefix=(
(f"{prefix}.layers.{i}" if prefix else f"layers.{i}")
if self.is_nemotron_35_draft
else ""
),
)
for i in range(num_layers)
]
@@ -550,9 +621,19 @@ class DFlashDraftModel(nn.Module):
num_context_features = len(target_layer_ids)
self.num_context_features = int(num_context_features)
self.fc = nn.Linear(
self.num_context_features * hidden_size, hidden_size, bias=False
)
if self.is_nemotron_35_draft:
fc_prefix = f"{prefix}.fc" if prefix else "fc"
self.fc = ReplicatedLinear(
self.num_context_features * hidden_size,
hidden_size,
bias=False,
quant_config=quant_config,
prefix=fc_prefix,
)
else:
self.fc = nn.Linear(
self.num_context_features * hidden_size, hidden_size, bias=False
)
self.hidden_norm = RMSNorm(hidden_size, eps=rms_norm_eps)
def set_block_size(self, block_size: int) -> None:
@@ -571,6 +652,9 @@ class DFlashDraftModel(nn.Module):
def get_attention_sliding_window_size(self) -> Optional[int]:
return get_dflash_attention_sliding_window_size(self.config)
def get_input_embeddings(self) -> Optional[VocabParallelEmbedding]:
return self.embed_tokens
def prepare_context_hidden_for_kv(
self, layer: DFlashDecoderLayer, ctx_hidden: torch.Tensor
) -> torch.Tensor:
@@ -578,7 +662,9 @@ class DFlashDraftModel(nn.Module):
def project_target_hidden(self, target_hidden: torch.Tensor) -> torch.Tensor:
"""Project concatenated target-layer hidden states into draft hidden_size."""
expected = int(self.fc.in_features)
expected = int(
self.fc.input_size if self.is_nemotron_35_draft else self.fc.in_features
)
if target_hidden.ndim != 2 or int(target_hidden.shape[-1]) != expected:
raise ValueError(
"DFLASH target_hidden feature dim mismatch. "
@@ -588,7 +674,10 @@ class DFlashDraftModel(nn.Module):
"This usually means the target model is capturing a different number of layer features than "
"the draft checkpoint/config expects."
)
return self.hidden_norm(self.fc(target_hidden))
projected = self.fc(target_hidden)
if self.is_nemotron_35_draft:
projected = projected[0]
return self.hidden_norm(projected)
@torch.no_grad()
def forward(
@@ -601,7 +690,9 @@ class DFlashDraftModel(nn.Module):
pp_proxy_tensors=None,
) -> LogitsProcessorOutput:
if input_embeds is None:
if hasattr(self, "forward_embed"):
if self.embed_tokens is not None:
input_embeds = self.embed_tokens(input_ids)
elif hasattr(self, "forward_embed"):
input_embeds = self.forward_embed(input_ids)
else:
raise ValueError(
@@ -679,16 +770,33 @@ class DFlashDraftModel(nn.Module):
# Ignore unexpected weights (e.g., HF rotary caches).
continue
param = params_dict[resolved_name]
if resolved_name.endswith("fc.weight") and tuple(
loaded_weight.shape
) != tuple(param.shape):
raise ValueError(
"DFLASH fc.weight shape mismatch. This usually means the draft checkpoint's "
"number of context features (K) does not match this config. "
f"Expected fc.weight.shape={tuple(param.shape)} "
f"(num_context_features={self.num_context_features}, hidden_size={int(self.config.hidden_size)}), "
f"but got {tuple(loaded_weight.shape)} for weight '{name}'."
)
if resolved_name.endswith("fc.weight"):
if self.is_nemotron_35_draft:
expected_shape = (
int(self.config.hidden_size),
int(self.num_context_features * self.config.hidden_size),
)
loaded_shape = _logical_linear_weight_shape(
param,
loaded_weight,
output_features=expected_shape[0],
)
shape_matches = loaded_shape == expected_shape or (
getattr(param, "pack_factor", None) is None
and tuple(loaded_weight.shape) == tuple(param.shape)
)
else:
expected_shape = tuple(param.shape)
loaded_shape = tuple(loaded_weight.shape)
shape_matches = loaded_shape == expected_shape
if not shape_matches:
raise ValueError(
"DFLASH fc.weight shape mismatch. This usually means the draft checkpoint's "
"number of context features (K) does not match this config. "
f"Expected fc.weight.shape={expected_shape} "
f"(num_context_features={self.num_context_features}, hidden_size={int(self.config.hidden_size)}), "
f"but got {loaded_shape} for weight '{name}'."
)
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
@@ -696,8 +804,15 @@ class DFlashDraftModel(nn.Module):
class DFlashLagunaAttention(DFlashAttention):
"""Laguna DFlash attention with the trained Laguna softplus gate."""
def __init__(self, config, layer_id: int, quant_config=None) -> None:
super().__init__(config=config, layer_id=layer_id, quant_config=quant_config)
def __init__(
self, config, layer_id: int, quant_config=None, prefix: str = ""
) -> None:
super().__init__(
config=config,
layer_id=layer_id,
quant_config=quant_config,
prefix=prefix,
)
hidden_size = int(config.hidden_size)
total_num_heads = self.total_num_heads
gating = normalize_gating(getattr(config, "gating", True))
@@ -716,7 +831,7 @@ class DFlashLagunaAttention(DFlashAttention):
g_out,
bias=False,
quant_config=quant_config,
prefix="g_proj",
prefix=f"{prefix}.g_proj" if prefix else "g_proj",
)
def apply_attention_output(
@@ -764,7 +879,9 @@ class DFlashLagunaForCausalLM(DFlashDraftModel):
return layer.input_layernorm(ctx_hidden)
def project_target_hidden(self, target_hidden: torch.Tensor) -> torch.Tensor:
expected = int(self.fc.in_features)
expected = int(
self.fc.input_size if self.is_nemotron_35_draft else self.fc.in_features
)
if target_hidden.ndim != 2 or int(target_hidden.shape[-1]) != expected:
raise ValueError(
"Laguna DFLASH target_hidden feature dim mismatch. "
@@ -783,7 +900,10 @@ class DFlashLagunaForCausalLM(DFlashDraftModel):
for i, norm in enumerate(self.aux_hidden_norms):
normed[:, i, :] = norm(slices[:, i, :])
fused = normed.reshape(target_hidden.shape[0], -1)
return self.hidden_norm(self.fc(fused))
projected = self.fc(fused)
if self.is_nemotron_35_draft:
projected = projected[0]
return self.hidden_norm(projected)
@torch.compile(dynamic=True, backend=get_compiler_backend(), disable=_is_npu)
+69 -8
View File
@@ -9,11 +9,13 @@ from torch import nn
from sglang.srt.distributed.communication_op import tensor_model_parallel_all_gather
from sglang.srt.environ import envs
from sglang.srt.layers.linear import ReplicatedLinear
from sglang.srt.layers.logits_processor import should_apply_lm_head_quant_method
from sglang.srt.model_loader.weight_utils import default_weight_loader
from sglang.srt.models.dflash import DFlashDraftModel
from sglang.srt.speculative.dflash_utils import can_dflash_slice_qkv_weight
from sglang.srt.speculative.dspark_components.dspark_config import (
get_dspark_sample_from_anchor,
parse_dspark_draft_config,
)
from sglang.srt.speculative.ragged_verify import (
@@ -142,6 +144,39 @@ class VanillaMarkov(nn.Module):
)
class Nemotron35VanillaMarkov(VanillaMarkov):
"""Checkpoint-quantized Markov head used only by Nemotron 3.5 DSpark."""
def __init__(
self,
*,
vocab_size: int,
markov_rank: int,
quant_config,
prefix: str,
) -> None:
nn.Module.__init__(self)
self.vocab_size = int(vocab_size)
self.markov_rank = int(markov_rank)
if self.markov_rank <= 0:
raise ValueError(
"Nemotron35VanillaMarkov requires markov_rank > 0, "
f"got {self.markov_rank}."
)
self.markov_w1 = nn.Embedding(self.vocab_size, self.markov_rank)
self.markov_w2 = ReplicatedLinear(
self.markov_rank,
self.vocab_size,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.markov_w2" if prefix else "markov_w2",
)
def project_bias(self, latent_states: torch.Tensor) -> torch.Tensor:
bias, _ = self.markov_w2(latent_states)
return bias
class GatedMarkovHead(VanillaMarkov):
markov_head_type = "gated"
@@ -298,6 +333,22 @@ def build_markov_head(config) -> Optional[nn.Module]:
raise ValueError(f"Unsupported DSpark markov_head_type={markov_head_type!r}.")
def build_nemotron_35_markov_head(config, quant_config, prefix: str) -> nn.Module:
markov_head_type = str(getattr(config, "markov_head_type", "vanilla")).lower()
if markov_head_type != "vanilla":
raise ValueError(
"Nemotron 3.5 DSpark requires markov_head_type='vanilla', "
f"got {markov_head_type!r}."
)
markov_prefix = f"{prefix}.markov_head" if prefix else "markov_head"
return Nemotron35VanillaMarkov(
vocab_size=int(config.vocab_size),
markov_rank=int(config.markov_rank),
quant_config=quant_config,
prefix=markov_prefix,
)
class DSparkConfidenceHead(nn.Module):
def __init__(
@@ -365,11 +416,7 @@ def build_confidence_head(config) -> Optional[nn.Module]:
)
_DSPARK_SKIPPED_WEIGHT_PREFIXES = (
"embed_tokens.",
"lm_head.",
"rotary_emb.",
)
_DSPARK_SKIPPED_WEIGHT_PREFIXES = ("lm_head.", "rotary_emb.")
class DSparkDraftMixin:
@@ -385,14 +432,21 @@ class DSparkDraftMixin:
f"got markov_rank={dspark_config.markov_rank}."
)
self.gamma = int(dspark_config.resolve_gamma(default=self.block_size))
self.markov_head = build_markov_head(config)
self.sample_from_anchor = get_dspark_sample_from_anchor(config)
if self.is_nemotron_35_draft:
self.markov_head = build_nemotron_35_markov_head(
config, quant_config, prefix
)
else:
self.markov_head = build_markov_head(config)
self.confidence_head = build_confidence_head(config)
self.lm_head: Optional[nn.Module] = None
def attach_shared_modules(
self, *, embed_tokens: nn.Module, lm_head: nn.Module
) -> None:
self.embed_tokens = embed_tokens
if not self.is_nemotron_35_draft:
self.embed_tokens = embed_tokens
self.lm_head = lm_head
def forward_embed(self, input_ids: torch.Tensor) -> torch.Tensor:
@@ -430,7 +484,14 @@ class DSparkDraftMixin:
backbone_weights = []
params_dict = dict(self.named_parameters())
for name, loaded_weight in weights:
if any(name.startswith(p) for p in _DSPARK_SKIPPED_WEIGHT_PREFIXES):
normalized_name = name.removeprefix("model.")
if any(
normalized_name.startswith(p) for p in _DSPARK_SKIPPED_WEIGHT_PREFIXES
):
continue
if normalized_name.startswith("embed_tokens.") and not (
self.is_nemotron_35_draft
):
continue
if name.startswith("confidence_head."):
if self.confidence_head is None:
+42 -1
View File
@@ -833,6 +833,7 @@ class NemotronHModel(nn.Module):
self.norm_f = RMSNorm(config.hidden_size, eps=config.layer_norm_epsilon)
else:
self.norm_f = PPMissingLayer(return_tuple=True)
self.layers_to_capture: set[int] = set()
def forward(
self,
@@ -853,7 +854,17 @@ class NemotronHModel(nn.Module):
hidden_states = pp_proxy_tensors["hidden_states"]
residual = pp_proxy_tensors["residual"]
aux_hidden_states = []
for i in range(self.start_layer, self.end_layer):
if i in self.layers_to_capture:
if residual is not None and getattr(
hidden_states, "_sglang_needs_allreduce_fusion", False
):
hidden_states = tensor_model_parallel_all_reduce(hidden_states)
hidden_states._sglang_needs_allreduce_fusion = False
aux_hidden_states.append(
hidden_states if residual is None else hidden_states + residual
)
layer = self.layers[i]
if not isinstance(layer, Layers):
raise ValueError(f"Unknown layer type: {type(layer)}")
@@ -867,7 +878,18 @@ class NemotronHModel(nn.Module):
return PPProxyTensors(
{"hidden_states": hidden_states, "residual": residual}
)
if self.end_layer in self.layers_to_capture:
if residual is not None and getattr(
hidden_states, "_sglang_needs_allreduce_fusion", False
):
hidden_states = tensor_model_parallel_all_reduce(hidden_states)
hidden_states._sglang_needs_allreduce_fusion = False
aux_hidden_states.append(
hidden_states if residual is None else hidden_states + residual
)
hidden_states, _ = self.norm_f(hidden_states, residual)
if aux_hidden_states:
return hidden_states, aux_hidden_states
return hidden_states
@@ -962,6 +984,7 @@ class NemotronHForCausalLM(nn.Module):
self.lm_head.weight.copy_(emb_token_weight)
self.logits_processor = LogitsProcessor(config)
self.capture_aux_hidden_states = False
def _init_model(
self,
@@ -1082,9 +1105,16 @@ class NemotronHForCausalLM(nn.Module):
hidden_states = self.model.forward(
input_ids, positions, forward_batch, pp_proxy_tensors, input_embeds
)
aux_hidden_states = None
if self.capture_aux_hidden_states:
hidden_states, aux_hidden_states = hidden_states
if self.pp_group.is_last_rank:
return self.logits_processor(
input_ids, hidden_states, self.lm_head, forward_batch
input_ids,
hidden_states,
self.lm_head,
forward_batch,
aux_hidden_states,
)
else:
return hidden_states
@@ -1106,6 +1136,17 @@ class NemotronHForCausalLM(nn.Module):
torch.cuda.empty_cache()
torch.cuda.synchronize()
def set_dflash_layers_to_capture(self, layer_ids: list[int]):
if not self.pp_group.is_last_rank:
return
if layer_ids is None:
raise ValueError(
"DFLASH requires explicit layer_ids for aux hidden capture."
)
self.capture_aux_hidden_states = True
self.model.layers_to_capture = {layer_id + 1 for layer_id in layer_ids}
def load_weights(
self, weights: Iterable[tuple[str, torch.Tensor]], is_mtp: bool = False
) -> None:
+4 -9
View File
@@ -2249,7 +2249,10 @@ class ServerArgs:
] = "prefill"
speculative_draft_attention_backend: A[
Optional[str],
"Attention backend for speculative decoding drafting.",
Arg(
help="Attention backend for speculative decoding drafting.",
resolvable=True,
),
NS("spec"),
] = None
speculative_draft_kv_cache_dtype: A[
@@ -6069,14 +6072,6 @@ class ServerArgs:
logger.info(
f"Using {attention_backend} as attention backend for {model_arch}."
)
elif model_arch in ["NemotronHForCausalLM", "NemotronHPuzzleForCausalLM"]:
# Quantization / MoE runner / attention backend defaults moved to
# the override registry (arg_groups/overrides.py:
# _nemotron_h_overrides).
assert resolved_view(self).attention_backend != "triton", (
"NemotronHForCausalLM does not support triton attention backend,"
"as the first layer might not be an attention layer"
)
elif model_arch in [
"Qwen3MoeForCausalLM",
"Qwen3VLMoeForConditionalGeneration",
@@ -413,6 +413,8 @@ def get_dflash_attention_sliding_window_size(config: Any) -> Optional[int]:
sliding_window = _cfg_get(
text_config, "sliding_window", _cfg_get(config, "sliding_window")
)
if sliding_window is None and is_nemotron_35_draft_config(config):
sliding_window = _get_dflash_config(config).get("swa_window_size")
if sliding_window is None:
raise ValueError(
"DFLASH sliding_attention layers require config.sliding_window."
@@ -463,6 +465,46 @@ def _get_dflash_config(config: Any) -> dict:
return {}
def is_nemotron_35_draft_config(config: Any) -> bool:
"""Identify the published Nemotron 3.5 DFlash/DSpark draft layout.
Keep the non-anchor query layout and checkpoint-local vocabulary modules
scoped to this structurally distinct family instead of changing every
DFlash/DSpark checkpoint that happens to expose one of these fields.
"""
architectures = _cfg_get(config, "architectures", None) or []
if not {"DFlashDraftModel", "Qwen3DSparkModel"}.intersection(architectures):
return False
if not bool(_cfg_get(config, "has_embed_tokens", False)):
return False
if bool(_cfg_get(config, "has_lm_head", False)):
return False
quant_config = _cfg_get(config, "quantization_config", None) or {}
if _cfg_get(quant_config, "quant_algo", None) != "W4A16_NVFP4":
return False
dflash_config = _get_dflash_config(config)
target_layer_ids = dflash_config.get(
"target_layer_ids", _cfg_get(config, "target_layer_ids", None)
)
aux_layer_ids = _cfg_get(config, "eagle_aux_hidden_state_layer_ids", None)
if not target_layer_ids or not aux_layer_ids:
return False
if len(target_layer_ids) != len(aux_layer_ids):
return False
if any(
int(target) + 1 != int(aux)
for target, aux in zip(target_layer_ids, aux_layer_ids)
):
return False
sample_from_anchor = dflash_config.get(
"sample_from_anchor", _cfg_get(config, "sample_from_anchor", True)
)
return sample_from_anchor is False
def _parse_optional_int(
value: Any,
*,
@@ -178,6 +178,17 @@ def _commit_accept(candidates, accept_len, bonus_tokens):
return out_tokens, accept_len.to(torch.int32) + 1
def _resolve_dflash_embedding_module(draft_model, target_model):
if getattr(draft_model, "is_nemotron_35_draft", False):
embed_module = draft_model.get_input_embeddings()
if embed_module is None:
raise RuntimeError(
"Nemotron 3.5 DFLASH draft requires its checkpoint embedding."
)
return embed_module
return target_model.get_input_embeddings()
def _is_all_greedy(sampling_info) -> bool:
return sampling_info is None or sampling_info.is_all_greedy
@@ -1753,7 +1764,9 @@ class DFlashWorkerV2(BaseSpecWorker):
# --- 1) Draft a fixed block with the draft model.
target_model = self.target_worker.model_runner.model
embed_module = unwrap_lora_layer(target_model.get_input_embeddings())
embed_module = unwrap_lora_layer(
_resolve_dflash_embedding_module(self.draft_model, target_model)
)
lm_head = unwrap_lora_layer(getattr(target_model, "lm_head", None))
if lm_head is None or not (
hasattr(lm_head, "weight")
@@ -25,6 +25,11 @@ SUPPORTED_DSPARK_MARKOV_HEAD_TYPES = ("vanilla", "gated", "rnn")
DSV4_DRAFT_ATTENTION_BACKEND = "dsv4"
def get_dspark_sample_from_anchor(draft_hf_config: Any) -> bool:
"""Return whether a DSpark checkpoint samples the anchor query row."""
return bool(_cfg_get(draft_hf_config, "sample_from_anchor", True))
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
@@ -128,9 +133,8 @@ def resolve_runtime_config(
)
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.
def read_draft_checkpoint_config(*, server_args: ServerArgs) -> DSparkDraftConfig:
"""Load and normalize the DSpark draft checkpoint configuration.
Reads the *resolving* configuration, not the bags: the speculative hook
calls this from inside resolution, where no bag exists yet -- and the
@@ -148,7 +152,11 @@ def read_draft_checkpoint_gamma(*, server_args: ServerArgs) -> Optional[int]:
revision=resolving.speculative_draft_model_revision,
model_override_args=json.loads(resolving.json_model_override_args),
)
return parse_dspark_draft_config(draft_hf_config=draft_hf_config).resolve_gamma(
return parse_dspark_draft_config(draft_hf_config=draft_hf_config)
def read_draft_checkpoint_gamma(*, server_args: ServerArgs) -> Optional[int]:
return read_draft_checkpoint_config(server_args=server_args).resolve_gamma(
default=None
)
@@ -81,6 +81,27 @@ class DraftProposal(msgspec.Struct, frozen=True):
folded: bool = False
def select_draft_hidden_without_anchor(
hidden_states: torch.Tensor,
*,
bs: int,
gamma: int,
) -> tuple[torch.Tensor, torch.Tensor]:
query_token_num = gamma + 1
expected_rows = bs * query_token_num
if hidden_states.shape[0] != expected_rows:
raise RuntimeError(
f"DSpark draft returned {hidden_states.shape[0]} hidden rows, "
f"expected {expected_rows}."
)
hidden_by_query = hidden_states.view(bs, query_token_num, *hidden_states.shape[1:])
selected = hidden_by_query[:, 1:].contiguous()
return (
selected.view(bs * gamma, *hidden_states.shape[1:]),
selected.view(bs, gamma, -1),
)
def make_next_draft_input(
*,
bonus_tokens: torch.Tensor,
@@ -178,6 +199,8 @@ class DraftBlockProposer:
self.draft_model = draft_model
self.draft_model_runner = draft_model_runner
self.gamma = gamma
self.sample_from_anchor = bool(draft_model.sample_from_anchor)
self.query_token_num = self.gamma if self.sample_from_anchor else self.gamma + 1
self._mask_token_id = mask_token_id
self._draft_block_spec_info = draft_block_spec_info
self._draft_sampler = None
@@ -202,7 +225,11 @@ class DraftBlockProposer:
target_model,
sampling_info,
) -> DraftProposal:
embed_module = unwrap_lora_layer(target_model.get_input_embeddings())
embed_module = unwrap_lora_layer(
self.draft_model.embed_tokens
if not self.sample_from_anchor
else target_model.get_input_embeddings()
)
draft_sampler = self._draft_sampler
all_greedy = sampling_info is None or sampling_info.is_all_greedy
fwd = self._run_forward(
@@ -276,8 +303,13 @@ class DraftBlockProposer:
markov_head=self.draft_model.markov_head,
device=device,
)
proposal_block_ids = (
draft_block_ids
if self.sample_from_anchor
else draft_block_ids[:, : self.gamma].contiguous()
)
return DraftProposal(
draft_block_ids=draft_block_ids,
draft_block_ids=proposal_block_ids,
draft_block=draft_block,
draft_hidden=fwd.draft_hidden_3d,
confidence=folded_confidence,
@@ -321,16 +353,20 @@ class DraftBlockProposer:
sampling_info=None,
) -> DraftForwardResult:
gamma = self.gamma
query_token_num = self.query_token_num
prefix_lens = batch.seq_lens
positions_2d = verify_window.positions_2d
verify_cache_loc_2d = verify_window.verify_cache_loc_2d
draft_block_ids = torch.full(
(bs, gamma), int(self._mask_token_id), dtype=torch.long, device=device
(bs, query_token_num),
int(self._mask_token_id),
dtype=torch.long,
device=device,
)
draft_block_ids[:, 0].copy_(draft_input.bonus_tokens.view(-1))
draft_positions = positions_2d[:, :gamma].reshape(-1)
draft_cache_loc = verify_cache_loc_2d[:, :gamma].reshape(-1)
draft_positions = positions_2d[:, :query_token_num].reshape(-1)
draft_cache_loc = verify_cache_loc_2d[:, :query_token_num].reshape(-1)
draft_owns_embed = envs.SGLANG_DSPARK_EMBED_IN_GRAPH.get() and hasattr(
self.draft_model, "forward_embed"
@@ -341,7 +377,7 @@ class DraftBlockProposer:
draft_input_embeds = noise_embedding.view(-1, noise_embedding.shape[-1])
if batch.seq_lens_cpu is not None:
draft_seq_lens_cpu = batch.seq_lens_cpu + gamma
draft_seq_lens_cpu = batch.seq_lens_cpu + query_token_num
draft_seq_lens_sum = int(draft_seq_lens_cpu.sum())
elif draft_input.nxt_kv_lens_cpu is not None:
draft_seq_lens_cpu = draft_input.nxt_kv_lens_cpu
@@ -349,7 +385,7 @@ class DraftBlockProposer:
else:
raise RuntimeError("DSpark decode expected batch.seq_lens_cpu, got None")
draft_num_tokens = bs * gamma
draft_num_tokens = bs * query_token_num
draft_forward_batch = ForwardBatch(
forward_mode=ForwardMode.TARGET_VERIFY,
batch_size=bs,
@@ -381,10 +417,24 @@ class DraftBlockProposer:
raw_hidden = logits_output.hidden_states
if raw_hidden is None:
raise RuntimeError("DSpark draft model returned no hidden states.")
draft_hidden_3d = raw_hidden.view(bs, gamma, -1)
if self.sample_from_anchor:
expected_rows = bs * gamma
if raw_hidden.shape[0] != expected_rows:
raise RuntimeError(
f"DSpark draft returned {raw_hidden.shape[0]} hidden rows, "
f"expected {expected_rows}."
)
model_hidden = raw_hidden
draft_hidden_3d = raw_hidden.view(bs, gamma, -1)
else:
model_hidden, draft_hidden_3d = select_draft_hidden_without_anchor(
raw_hidden,
bs=bs,
gamma=gamma,
)
return DraftForwardResult(
draft_block_ids=draft_block_ids,
raw_hidden=raw_hidden,
raw_hidden=model_hidden,
draft_hidden_3d=draft_hidden_3d,
can_run_graph=draft_out.can_run_graph,
)
@@ -9,6 +9,9 @@ from sglang.kernels.ops.speculative.dspark.dspark_draft_model import (
SampleStepTokens,
)
from sglang.srt.environ import DsparkFoldedSampling, envs
from sglang.srt.speculative.dspark_components.dspark_draft import (
select_draft_hidden_without_anchor,
)
from sglang.srt.utils import get_available_gpu_memory
logger = logging.getLogger(__name__)
@@ -49,6 +52,8 @@ class DsparkDraftSampler:
self.model = model
self.markov_head = model.markov_head
self.gamma = int(gamma)
self.sample_from_anchor = bool(model.sample_from_anchor)
self.query_token_num = self.gamma if self.sample_from_anchor else self.gamma + 1
max_bs = int(max_bs)
if out is not None:
assert out.shape == (max_bs * self.gamma,) and out.dtype == torch.int64
@@ -100,10 +105,19 @@ class DsparkDraftSampler:
self.greedy_mask[:bs].copy_((sampling_info.top_ks <= 1).view(-1)[:bs])
def __call__(self, hidden_states, input_ids):
bs = hidden_states.shape[0] // self.gamma
base_logits, confidence_tap = self.model.compute_base_logits(hidden_states)
bs = hidden_states.shape[0] // self.query_token_num
if self.sample_from_anchor:
model_hidden = hidden_states
sample_hidden = hidden_states.view(bs, self.gamma, -1)
else:
model_hidden, sample_hidden = select_draft_hidden_without_anchor(
hidden_states,
bs=bs,
gamma=self.gamma,
)
base_logits, confidence_tap = self.model.compute_base_logits(model_hidden)
base_logits = base_logits.view(bs, self.gamma, -1)
anchor = input_ids.view(bs, self.gamma)[:, 0]
anchor = input_ids.view(bs, self.query_token_num)[:, 0]
if self.folded_sampling:
@@ -125,7 +139,7 @@ class DsparkDraftSampler:
draft_tokens, corrected_logits = self.markov_head.sample_block(
base_logits,
first_prev_tokens=anchor,
hidden_states=hidden_states.view(bs, self.gamma, -1),
hidden_states=sample_hidden,
sampler=sampler,
)
self.out[: draft_tokens.numel()].copy_(draft_tokens.reshape(-1))
@@ -135,7 +149,7 @@ class DsparkDraftSampler:
)
if self.confidence_out is not None:
confidence = self.confidence_fn(
draft_hidden=hidden_states.view(bs, self.gamma, -1),
draft_hidden=sample_hidden,
anchor_tokens=anchor,
draft_tokens=draft_tokens,
confidence_tap=confidence_tap,
@@ -162,18 +162,22 @@ class DSparkWorkerV2(BaseSpecWorker):
)
self.gamma = runtime_config.gamma
self.verify_num_draft_tokens = runtime_config.verify_num_draft_tokens
self.sample_from_anchor = bool(self.draft_model.sample_from_anchor)
self.query_token_num = self.gamma if self.sample_from_anchor else self.gamma + 1
self.speculative_num_draft_tokens = self.verify_num_draft_tokens
self._mask_token_id = runtime_config.mask_token_id
if self.ps.tp_rank == 0:
logger.info(
"Initialized DSpark draft runner. attention_backend=%s, model=%s, "
"gamma=%s, verify_num_draft_tokens=%s, mask_token_id=%s, "
"markov_head=%s",
"gamma=%s, verify_num_draft_tokens=%s, query_token_num=%s, "
"sample_from_anchor=%s, mask_token_id=%s, markov_head=%s",
bundle.resolved_attention_backend,
self.draft_model.__class__.__name__,
self.gamma,
self.verify_num_draft_tokens,
self.query_token_num,
self.sample_from_anchor,
self._mask_token_id,
type(self.draft_model.markov_head).__name__,
)
@@ -182,7 +186,7 @@ class DSparkWorkerV2(BaseSpecWorker):
length=self.verify_num_draft_tokens, device=self.device
)
self._draft_block_spec_info = make_draft_block_spec_info(
draft_token_num=int(self.gamma), device=self.device
draft_token_num=int(self.query_token_num), device=self.device
)
if getattr(self.draft_model, "uses_own_vocab_modules", False):
@@ -192,6 +192,7 @@ class GSM8KMixin:
gsm8k_num_shots: int = 5 # run_eval backend only
gsm8k_backend: str = "run_eval" # "run_eval" | "sgl_eval"
gsm8k_thinking: bool = False # sgl_eval backend
gsm8k_max_tokens: Optional[int] = None # sgl_eval backend
gsm8k_n_repeats: int = 1 # sgl_eval backend
def test_gsm8k(self):
@@ -213,6 +214,7 @@ class GSM8KMixin:
num_examples=num_examples,
num_threads=self.gsm8k_num_threads,
thinking=self.gsm8k_thinking,
max_tokens=self.gsm8k_max_tokens,
accept_length_thres=self.gsm8k_accept_length_thres,
)
else: