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