| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """DSpark draft model for DeepSeek-V4.1 (semi-autoregressive speculative decoding). |
| 4 | |
| 5 | See: qwen3_dspark.py for base architecture. This one is specialized to the |
| 6 | DSV4.1 DSpark, which reuses the target model's architecture similarly to MTP. |
| 7 | Its weights ship in the target checkpoint under the ``mtp.{0,1,2}.*`` prefix. |
| 8 | |
| 9 | To implement non-causal attention, we leverage the sparse attention implementation to |
| 10 | include the future query tokens in the top-k indices for each query token. |
| 11 | """ |
| 12 | |
| 13 | from collections.abc import Iterable |
| 14 | |
| 15 | import regex as re |
| 16 | import torch |
| 17 | import torch.nn as nn |
| 18 | |
| 19 | import vllm.envs as envs |
| 20 | from vllm.config import VllmConfig, get_current_vllm_config |
| 21 | from vllm.distributed import ( |
| 22 | get_tensor_model_parallel_rank, |
| 23 | get_tensor_model_parallel_world_size, |
| 24 | ) |
| 25 | from vllm.forward_context import get_forward_context, is_forward_context_available |
| 26 | from vllm.logger import init_logger |
| 27 | from vllm.model_executor.kernels.mhc.torch import mhc_post_torch |
| 28 | from vllm.model_executor.layers.fused_moe import ( |
| 29 | fused_moe_make_expert_params_mapping, |
| 30 | ) |
| 31 | from vllm.model_executor.layers.layernorm import RMSNorm |
| 32 | from vllm.model_executor.layers.linear import ReplicatedLinear |
| 33 | from vllm.model_executor.layers.logits_processor import LogitsProcessor |
| 34 | from vllm.model_executor.layers.vocab_parallel_embedding import ( |
| 35 | ParallelLMHead, |
| 36 | VocabParallelEmbedding, |
| 37 | ) |
| 38 | from vllm.model_executor.model_loader.weight_utils import default_weight_loader |
| 39 | from vllm.model_executor.models.qwen3_dspark import ( |
| 40 | DSparkConfidenceHead, |
| 41 | DSparkMarkovHead, |
| 42 | ) |
| 43 | from vllm.model_executor.models.utils import maybe_prefix |
| 44 | from vllm.models.common.ops.sequence_parallel import ( |
| 45 | sp_all_gather, |
| 46 | sp_padding_mask, |
| 47 | sp_shard, |
| 48 | ) |
| 49 | |
| 50 | from .model import ( |
| 51 | DeepseekV4DecoderLayer, |
| 52 | DeepseekV4Model, |
| 53 | _linear_scale_param_name, |
| 54 | _use_sequence_parallel, |
| 55 | ) |
| 56 | |
| 57 | logger = init_logger(__name__) |
| 58 | |
| 59 | # MoE expert scale suffix differs by expert dtype (mirrors deepseek_v4 loaders): |
| 60 | # fp4 experts register ``.weight_scale``; block-fp8 experts ``.weight_scale_inv``. |
| 61 | _EXPERT_SCALE_RE = re.compile(r"\.experts\.\d+\.w[123]\.scale$") |
| 62 | |
| 63 | |
| 64 | class DSparkDeepseekV4Model(nn.Module): |
| 65 | def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None: |
| 66 | super().__init__() |
| 67 | assert vllm_config.speculative_config is not None |
| 68 | config = vllm_config.speculative_config.draft_model_config.hf_config |
| 69 | self.config = config |
| 70 | self.hidden_size = config.hidden_size |
| 71 | self.hc_mult = config.hc_mult |
| 72 | self.hc_eps = config.hc_eps |
| 73 | self.rms_norm_eps = config.rms_norm_eps |
| 74 | self.num_hidden_layers = config.num_hidden_layers |
| 75 | self.target_layer_ids = tuple(config.dspark_target_layer_ids) |
| 76 | self.use_sequence_parallel = _use_sequence_parallel(vllm_config) |
| 77 | |
| 78 | self.num_dspark_layers = ( |
| 79 | getattr(config, "n_mtp_layers", None) |
| 80 | or getattr(config, "num_nextn_predict_layers", None) |
| 81 | or 3 |
| 82 | ) |
| 83 | |
| 84 | # Shared with the target (aliased by the speculator's loading utility). |
| 85 | self.embed_tokens = VocabParallelEmbedding( |
| 86 | config.vocab_size, |
| 87 | config.hidden_size, |
| 88 | prefix=maybe_prefix(prefix, "embed_tokens"), |
| 89 | ) |
| 90 | |
| 91 | self.main_proj = ReplicatedLinear( |
| 92 | config.hidden_size * len(self.target_layer_ids), |
| 93 | config.hidden_size, |
| 94 | bias=False, |
| 95 | return_bias=False, |
| 96 | quant_config=vllm_config.quant_config, |
| 97 | prefix=maybe_prefix(prefix, "main_proj"), |
| 98 | ) |
| 99 | self.main_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) |
| 100 | |
| 101 | self.topk_indices_buffer = torch.empty( |
| 102 | vllm_config.scheduler_config.max_num_batched_tokens, |
| 103 | config.index_topk, |
| 104 | dtype=torch.int32, |
| 105 | ) |
| 106 | |
| 107 | current_vllm_config = get_current_vllm_config() |
| 108 | self.layers = nn.ModuleList( |
| 109 | [ |
| 110 | DeepseekV4DecoderLayer( |
| 111 | current_vllm_config, |
| 112 | prefix=maybe_prefix(prefix, f"layers.{self.num_hidden_layers + i}"), |
| 113 | topk_indices_buffer=self.topk_indices_buffer, |
| 114 | ) |
| 115 | for i in range(self.num_dspark_layers) |
| 116 | ] |
| 117 | ) |
| 118 | |
| 119 | # Heads: final norm, and the Markov + confidence heads. |
| 120 | # Loaded from the "final" MTP layer weights (mtp.*) in the target |
| 121 | # checkpoint. v4.1 has no learned hc_head: the hc copies are |
| 122 | # collapsed with a pre-mix derived from the last layer's hc_ffn |
| 123 | # projection at the end of forward(). |
| 124 | self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) |
| 125 | draft_vocab_size = ( |
| 126 | getattr(config, "draft_vocab_size", None) or config.vocab_size |
| 127 | ) |
| 128 | self.markov_head = DSparkMarkovHead( |
| 129 | config.vocab_size, |
| 130 | draft_vocab_size, |
| 131 | config.dspark_markov_rank, |
| 132 | prefix=maybe_prefix(prefix, "markov_head"), |
| 133 | ) |
| 134 | self.confidence_head: DSparkConfidenceHead | None = None |
| 135 | if getattr(config, "enable_confidence_head", True): |
| 136 | self.confidence_head = DSparkConfidenceHead( |
| 137 | config.hidden_size + config.dspark_markov_rank, |
| 138 | prefix=maybe_prefix(prefix, "confidence_head"), |
| 139 | ) |
| 140 | |
| 141 | def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor: |
| 142 | return self.embed_tokens(input_ids) |
| 143 | |
| 144 | def combine_hidden_states(self, aux_hidden_states: torch.Tensor) -> torch.Tensor: |
| 145 | """main_x = main_norm(main_proj(concat of target aux hidden states)). |
| 146 | |
| 147 | ``aux_hidden_states`` is [T, hidden_size * len(target_layer_ids)]. |
| 148 | """ |
| 149 | return self.main_norm(self.main_proj(aux_hidden_states)) |
| 150 | |
| 151 | @torch.inference_mode() |
| 152 | def precompute_and_store_context_kv( |
| 153 | self, |
| 154 | main_x: torch.Tensor, |
| 155 | context_positions: torch.Tensor, |
| 156 | context_slot_mappings: list[torch.Tensor | None] | None = None, |
| 157 | ) -> None: |
| 158 | """Insert the sliding-window context KV for every draft layer. |
| 159 | |
| 160 | Mirrors the reference DSparkAttention: each layer derives its context KV |
| 161 | from the SAME projected target hidden ``main_x``, via that layer's own |
| 162 | ``wkv`` + ``kv_norm`` + RoPE + quant, then writes it at the |
| 163 | layer's context slots. |
| 164 | |
| 165 | ``context_slot_mappings`` is a per-layer list (each entry is the context |
| 166 | slot mapping for that layer's kv-cache group, since the hybrid manager may |
| 167 | place draft layers in different groups). ``None`` (or a ``None`` entry) |
| 168 | runs the projection to reserve workspace but writes nothing (profiling). |
| 169 | """ |
| 170 | for i, layer in enumerate(self.layers): |
| 171 | slot_mapping = ( |
| 172 | None if context_slot_mappings is None else context_slot_mappings[i] |
| 173 | ) |
| 174 | attn = layer.attn |
| 175 | # Optimized DSV4 MLA path: wkv part of the fused wq_a|wkv projection |
| 176 | # (q_lora part discarded), then RoPE/quant/insert via the fused op. |
| 177 | qr_kv, _ = attn.fused_wqa_wkv(main_x) |
| 178 | kv = qr_kv[..., attn.q_lora_rank :] |
| 179 | kv = attn.kv_norm(kv) |
| 180 | if slot_mapping is None: |
| 181 | continue |
| 182 | _insert_context_kv(attn, kv, context_positions, slot_mapping) |
| 183 | |
| 184 | def forward( |
| 185 | self, |
| 186 | input_ids: torch.Tensor, |
| 187 | positions: torch.Tensor, |
| 188 | inputs_embeds: torch.Tensor | None = None, |
| 189 | ) -> torch.Tensor: |
| 190 | if inputs_embeds is None: |
| 191 | inputs_embeds = self.embed_input_ids(input_ids) |
| 192 | full_num_tokens = positions.shape[0] |
| 193 | if self.use_sequence_parallel: |
| 194 | if envs.VLLM_MOE_SKIP_PADDING and is_forward_context_available(): |
| 195 | forward_context = get_forward_context() |
| 196 | forward_context.is_padding = sp_padding_mask( |
| 197 | forward_context.is_padding, inputs_embeds |
| 198 | ) |
| 199 | inputs_embeds = sp_shard(inputs_embeds) |
| 200 | input_ids = sp_shard(input_ids) |
| 201 | # Expand to hc_mult copies for hyper-connections ([T, H] -> [T, hc, H]). |
| 202 | hidden_states = inputs_embeds.unsqueeze(-2).repeat(1, self.hc_mult, 1) |
| 203 | |
| 204 | residual = post_mix = res_mix = pre_mix = None |
| 205 | for layer in self.layers: |
| 206 | hidden_states, residual, post_mix, res_mix, pre_mix = layer( |
| 207 | hidden_states, |
| 208 | positions, |
| 209 | input_ids, |
| 210 | pre_mix, |
| 211 | post_mix, |
| 212 | res_mix, |
| 213 | residual, |
| 214 | ) |
| 215 | hidden_states = mhc_post_torch(hidden_states, residual, post_mix, res_mix) |
| 216 | if self.use_sequence_parallel: |
| 217 | hidden_states = sp_all_gather(hidden_states)[:full_num_tokens] |
| 218 | pre_mix = sp_all_gather(pre_mix)[:full_num_tokens] |
| 219 | # Collapse the hc copies with the pre-mix from the last layer's FFN |
| 220 | # mixes — the mix the reference forward_head applies (hc_pre with the |
| 221 | # last block's ffn pre-mix). Return the PRE-norm head hidden; |
| 222 | # compute_logits applies self.norm. |
| 223 | hidden_states = DeepseekV4DecoderLayer._hc_collapse(hidden_states, pre_mix) |
| 224 | return hidden_states |
| 225 | |
| 226 | |
| 227 | def _insert_context_kv( |
| 228 | attn: nn.Module, |
| 229 | kv: torch.Tensor, |
| 230 | positions: torch.Tensor, |
| 231 | slot_mapping: torch.Tensor, |
| 232 | ) -> None: |
| 233 | """RoPE + quant + paged-cache insert of (already kv_norm'd) context KV. |
| 234 | |
| 235 | Reuses the DSV4 fused insert ops (which also process a query; we pass a dummy |
| 236 | query and discard it, since context tokens have no query). Mirrors |
| 237 | ``DeepseekV4Attention._fused_qnorm_rope_kv_insert``. |
| 238 | """ |
| 239 | swa_cache = attn.swa_cache_layer.kv_cache |
| 240 | block_size = attn.swa_cache_layer.block_size |
| 241 | cos_sin_cache = attn.rotary_emb.cos_sin_cache |
| 242 | cache_dtype = swa_cache.dtype |
| 243 | n_ctx = kv.shape[0] |
| 244 | dummy_q = torch.zeros( |
| 245 | (n_ctx, attn.n_local_heads, attn.head_dim), |
| 246 | dtype=kv.dtype, |
| 247 | device=kv.device, |
| 248 | ) |
| 249 | if cache_dtype == torch.uint8: |
| 250 | # fp8_ds_mla UE8M0 paged layout |
| 251 | swa_2d = swa_cache.view(swa_cache.shape[0], -1) |
| 252 | torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert( |
| 253 | dummy_q, |
| 254 | kv, |
| 255 | swa_2d, |
| 256 | slot_mapping, |
| 257 | positions, |
| 258 | cos_sin_cache, |
| 259 | attn.padded_heads, |
| 260 | attn.eps, |
| 261 | block_size, |
| 262 | True, # apply_q_norm (unused: the query is a discarded dummy) |
| 263 | attn.kv_mxfp8, |
| 264 | ) |
| 265 | elif cache_dtype == torch.bfloat16: |
| 266 | swa_3d = swa_cache.view(-1, block_size, attn.head_dim) |
| 267 | torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_full_cache_bf16_insert( |
| 268 | dummy_q, |
| 269 | kv, |
| 270 | swa_3d, |
| 271 | slot_mapping, |
| 272 | positions, |
| 273 | cos_sin_cache, |
| 274 | attn.eps, |
| 275 | block_size, |
| 276 | ) |
| 277 | else: # per-tensor fp8 (torch.float8_e4m3fn) |
| 278 | # TODO(ben): double-check if this is being dispatched correctly for FI backend |
| 279 | swa_3d = swa_cache.view(-1, block_size, attn.head_dim) |
| 280 | dummy_q_fp8 = torch.zeros_like(dummy_q, dtype=torch.float8_e4m3fn) |
| 281 | torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_full_cache_fp8_insert( |
| 282 | dummy_q, |
| 283 | kv, |
| 284 | dummy_q_fp8, |
| 285 | swa_3d, |
| 286 | slot_mapping, |
| 287 | positions, |
| 288 | cos_sin_cache, |
| 289 | attn._flashinfer_fp8_kv_scale, |
| 290 | attn._flashinfer_fp8_q_scale_inv, |
| 291 | attn.eps, |
| 292 | block_size, |
| 293 | ) |
| 294 | |
| 295 | |
| 296 | class DSparkDeepseekV4ForCausalLM(nn.Module): |
| 297 | # Draft weights ship in the target checkpoint (mtp.*) without embed/head, so |
| 298 | # load_dspark_model always aliases the target's. |
| 299 | has_own_embed_tokens = False |
| 300 | has_own_lm_head = False |
| 301 | # Full-vocab draft: draft ids are target ids, no remapping needed. |
| 302 | draft_id_to_target_id = None |
| 303 | |
| 304 | def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None: |
| 305 | super().__init__() |
| 306 | assert vllm_config.speculative_config is not None |
| 307 | self.draft_model_config = vllm_config.speculative_config.draft_model_config |
| 308 | self.config = self.draft_model_config.hf_config |
| 309 | self.quant_config = vllm_config.quant_config |
| 310 | self.linear_scale_name = _linear_scale_param_name( |
| 311 | vllm_config, getattr(self.config, "expert_dtype", "fp4") |
| 312 | ) |
| 313 | self.pad_shared_expert = getattr( |
| 314 | self.quant_config, "weight_block_size", None |
| 315 | ) is not None and not _use_sequence_parallel(vllm_config) |
| 316 | self.model = DSparkDeepseekV4Model( |
| 317 | vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model") |
| 318 | ) |
| 319 | # Shared with the target (aliased by the speculator's load utility). |
| 320 | self.lm_head = ParallelLMHead( |
| 321 | self.config.vocab_size, |
| 322 | self.config.hidden_size, |
| 323 | prefix=maybe_prefix(prefix, "lm_head"), |
| 324 | ) |
| 325 | self.logits_processor = LogitsProcessor(self.config.vocab_size) |
| 326 | |
| 327 | # --- Hooks used by the speculator ------------------------------------- |
| 328 | |
| 329 | def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor: |
| 330 | return self.model.embed_input_ids(input_ids) |
| 331 | |
| 332 | def combine_hidden_states(self, aux_hidden_states: torch.Tensor) -> torch.Tensor: |
| 333 | return self.model.combine_hidden_states(aux_hidden_states) |
| 334 | |
| 335 | def get_draft_kv_cache_layer_names(self) -> list[str]: |
| 336 | # DSV4 MLA path: each draft layer's sliding-window cache is a separate |
| 337 | # layer, named by its prefix. |
| 338 | return [layer.attn.swa_cache_layer.prefix for layer in self.model.layers] |
| 339 | |
| 340 | def precompute_and_store_context_kv( |
| 341 | self, |
| 342 | context_states: torch.Tensor, |
| 343 | context_positions: torch.Tensor, |
| 344 | context_slot_mappings: list[torch.Tensor | None] | None = None, |
| 345 | ) -> None: |
| 346 | self.model.precompute_and_store_context_kv( |
| 347 | context_states, context_positions, context_slot_mappings |
| 348 | ) |
| 349 | |
| 350 | def forward( |
| 351 | self, |
| 352 | input_ids: torch.Tensor, |
| 353 | positions: torch.Tensor, |
| 354 | inputs_embeds: torch.Tensor | None = None, |
| 355 | ) -> torch.Tensor: |
| 356 | # Returns the pre-norm collapsed head hidden ([T, hidden_size]). |
| 357 | return self.model(input_ids, positions, inputs_embeds) |
| 358 | |
| 359 | def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor: |
| 360 | """Base logits U_k = lm_head(norm(head_hidden)).""" |
| 361 | return self.logits_processor(self.lm_head, self.model.norm(hidden_states)) |
| 362 | |
| 363 | def compute_draft_logits(self, hidden_states: torch.Tensor) -> torch.Tensor: |
| 364 | # Full-vocab draft: base logits, no d2t scatter. |
| 365 | return self.compute_logits(hidden_states) |
| 366 | |
| 367 | def map_draft_to_target(self, draft_ids: torch.Tensor) -> torch.Tensor: |
| 368 | return draft_ids # full-vocab: draft ids are target ids |
| 369 | |
| 370 | def markov_embed(self, token_ids: torch.Tensor) -> torch.Tensor: |
| 371 | return self.model.markov_head.embed(token_ids) |
| 372 | |
| 373 | def markov_bias(self, markov_embed: torch.Tensor) -> torch.Tensor: |
| 374 | return self.model.markov_head.bias(markov_embed, self.logits_processor) |
| 375 | |
| 376 | def compute_confidence( |
| 377 | self, head_hidden: torch.Tensor, markov_embed: torch.Tensor |
| 378 | ) -> torch.Tensor: |
| 379 | """Per-position acceptance probability for each drafted token.""" |
| 380 | assert self.model.confidence_head is not None |
| 381 | return torch.sigmoid(self.model.confidence_head(head_hidden, markov_embed)) |
| 382 | |
| 383 | # --- Weight loading ---------------------------------------------------- |
| 384 | |
| 385 | def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: |
| 386 | """Load the ``mtp.{0,1,2}.*`` draft weights from the target checkpoint. |
| 387 | |
| 388 | Non-mtp weights (embed/head/main layers) belong to the target model and |
| 389 | are skipped here. ``embed_tokens``/``lm_head`` are aliased from the target. |
| 390 | """ |
| 391 | # Draft MoE layers use the dspark_* expert counts, not the |
| 392 | # backbone's (see DeepseekV4MoE and the reference |
| 393 | # ModelArgs.get_moe_config). |
| 394 | n_draft_experts = ( |
| 395 | getattr(self.config, "dspark_n_routed_experts", 0) |
| 396 | or self.config.n_routed_experts |
| 397 | ) |
| 398 | expert_mapping = fused_moe_make_expert_params_mapping( |
| 399 | self, |
| 400 | ckpt_gate_proj_name="w1", |
| 401 | ckpt_down_proj_name="w2", |
| 402 | ckpt_up_proj_name="w3", |
| 403 | num_experts=n_draft_experts, |
| 404 | ) |
| 405 | expert_scale_suffix = ( |
| 406 | ".weight_scale" |
| 407 | if getattr(self.config, "expert_dtype", "fp4") == "fp4" |
| 408 | else ".weight_scale_inv" |
| 409 | ) |
| 410 | |
| 411 | # (param_name, ckpt_shard_name, shard_id) for non-expert stacked params. |
| 412 | stacked_params_mapping = [ |
| 413 | ("gate_up_proj", "w1", 0), |
| 414 | ("gate_up_proj", "w3", 1), |
| 415 | ("attn.fused_wqa_wkv", "attn.wq_a", 0), |
| 416 | ("attn.fused_wqa_wkv", "attn.wkv", 1), |
| 417 | ] |
| 418 | |
| 419 | params_dict = dict(self.named_parameters()) |
| 420 | loaded_params: set[str] = set() |
| 421 | loaded_confidence_head = False |
| 422 | |
| 423 | tp_size = get_tensor_model_parallel_world_size() |
| 424 | tp_rank = get_tensor_model_parallel_rank() |
| 425 | n_local_head = self.config.num_attention_heads // tp_size |
| 426 | head_start = n_local_head * tp_rank |
| 427 | head_end = n_local_head * (tp_rank + 1) |
| 428 | |
| 429 | for name, loaded_weight in weights: |
| 430 | mapped = self._remap_dspark_name(name) |
| 431 | if mapped is None: |
| 432 | continue |
| 433 | name = mapped |
| 434 | if "confidence_head." in name: |
| 435 | loaded_confidence_head = True |
| 436 | |
| 437 | # ``.scale`` -> per-method scale suffix. |
| 438 | if name.endswith(".scale"): |
| 439 | suffix = ( |
| 440 | expert_scale_suffix |
| 441 | if _EXPERT_SCALE_RE.search(name) |
| 442 | else f".{self.linear_scale_name}" |
| 443 | ) |
| 444 | name = name.removesuffix(".scale") + suffix |
| 445 | if ".shared_experts.w2" in name: |
| 446 | name = name.replace(".shared_experts.w2", ".shared_experts.down_proj") |
| 447 | if self.pad_shared_expert and ".shared_experts." in name: |
| 448 | loaded_weight = DeepseekV4Model._pad_shared_expert_weight( |
| 449 | self.quant_config, name, loaded_weight |
| 450 | ) |
| 451 | |
| 452 | # E8M0 expert scales: keep raw exponent bytes. |
| 453 | if ".experts." in name: |
| 454 | if ( |
| 455 | "weight_scale" in name |
| 456 | and loaded_weight.dtype == torch.float8_e8m0fnu |
| 457 | ): |
| 458 | loaded_weight = loaded_weight.view(torch.uint8) |
| 459 | for param_name, weight_name, expert_id, shard_id in expert_mapping: |
| 460 | if weight_name not in name: |
| 461 | continue |
| 462 | name_mapped = name.replace(weight_name, param_name) |
| 463 | param = params_dict[name_mapped] |
| 464 | success = param.weight_loader( |
| 465 | param, |
| 466 | loaded_weight, |
| 467 | name_mapped, |
| 468 | shard_id=shard_id, |
| 469 | expert_id=expert_id, |
| 470 | return_success=True, |
| 471 | ) |
| 472 | if success: |
| 473 | loaded_params.add(name_mapped) |
| 474 | break |
| 475 | continue |
| 476 | |
| 477 | # Stacked rules only apply to decoder-layer weights. Head-stack params |
| 478 | # (main_proj/norm/markov_head/confidence_head) load directly — |
| 479 | # otherwise e.g. "markov_w1" would collide with the "w1" shard rule. |
| 480 | is_layer_param = name.startswith("model.layers.") |
| 481 | for param_name, weight_name, stacked_shard_id in stacked_params_mapping: |
| 482 | if not is_layer_param or weight_name not in name: |
| 483 | continue |
| 484 | name = name.replace(weight_name, param_name) |
| 485 | param = params_dict[name] |
| 486 | param.weight_loader(param, loaded_weight, stacked_shard_id) |
| 487 | loaded_params.add(name) |
| 488 | break |
| 489 | else: |
| 490 | if "attn_sink" in name: |
| 491 | narrow = loaded_weight[head_start:head_end] |
| 492 | params_dict[name][: narrow.shape[0]].copy_(narrow) |
| 493 | loaded_params.add(name) |
| 494 | continue |
| 495 | if name.endswith(".ffn.gate.bias"): |
| 496 | name = name.replace( |
| 497 | ".ffn.gate.bias", ".ffn.gate.e_score_correction_bias" |
| 498 | ) |
| 499 | param = params_dict[name] |
| 500 | weight_loader = getattr(param, "weight_loader", default_weight_loader) |
| 501 | weight_loader(param, loaded_weight) |
| 502 | loaded_params.add(name) |
| 503 | |
| 504 | if self.model.confidence_head is not None and not loaded_confidence_head: |
| 505 | self.model.confidence_head = None |
| 506 | self.process_weights_after_loading() |
| 507 | logger.info_once("DSpark draft model loaded: %d params", len(loaded_params)) |
| 508 | return loaded_params |
| 509 | |
| 510 | def process_weights_after_loading(self) -> None: |
| 511 | # ROCm linears and fused-MoE runners finalize their own quantized |
| 512 | # parameters. NVIDIA-only MegaMoE/WO-A requantization is not needed. |
| 513 | return |
| 514 | |
| 515 | def _remap_dspark_name(self, name: str) -> str | None: |
| 516 | """Map a checkpoint ``mtp.{i}.*`` name to this model's parameter path. |
| 517 | |
| 518 | Returns None for non-mtp weights (owned by the target model). |
| 519 | """ |
| 520 | m = re.match(r"mtp\.(\d+)\.(.*)", name) |
| 521 | if m is None: |
| 522 | return None |
| 523 | stage = int(m.group(1)) |
| 524 | rest = m.group(2) |
| 525 | if rest.startswith("confidence_head.") and self.model.confidence_head is None: |
| 526 | return None |
| 527 | # The checkpoint calls the Markov head's factors ``embed``/``head``; |
| 528 | # DSparkMarkovHead registers them as ``markov_w1``/``markov_w2``. |
| 529 | if rest.startswith("markov_head.embed."): |
| 530 | return "model.markov_head.markov_w1." + rest.removeprefix( |
| 531 | "markov_head.embed." |
| 532 | ) |
| 533 | if rest.startswith("markov_head.head."): |
| 534 | return "model.markov_head.markov_w2." + rest.removeprefix( |
| 535 | "markov_head.head." |
| 536 | ) |
| 537 | # Head-stack params live at model level (mtp.last), context combiner at |
| 538 | # model level (mtp.0); everything else is a per-layer decoder block. |
| 539 | head_prefixes = ( |
| 540 | "norm.", |
| 541 | "markov_head.", |
| 542 | "confidence_head.", |
| 543 | ) |
| 544 | if rest.startswith(("main_proj.", "main_norm.")) or rest.startswith( |
| 545 | head_prefixes |
| 546 | ): |
| 547 | return f"model.{rest}" |
| 548 | return f"model.layers.{stage}.{rest}" |