main
py 548 lines 22.2 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.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}"