main
py 551 lines 22.8 KB
Raw
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}"