main
py 436 lines 16.1 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 on Intel XPU.
4
5 Minimal XPU port of nvidia/dspark.py. Replaces tilelang MHC kernels with
6 the platform-agnostic custom ops (HCHeadOp, MHCPostOp) already used by the
7 XPU MTP path, and uses the XPU Triton-based qnorm_rope_kv_fp8_insert for
8 context KV precomputation.
9 """
10
11 from collections.abc import Iterable
12
13 import regex as re
14 import torch
15 import torch.nn as nn
16
17 from vllm.config import VllmConfig, get_current_vllm_config
18 from vllm.distributed import (
19 get_tensor_model_parallel_rank,
20 get_tensor_model_parallel_world_size,
21 )
22 from vllm.logger import init_logger
23 from vllm.model_executor.layers.fused_moe import (
24 fused_moe_make_expert_params_mapping,
25 )
26 from vllm.model_executor.layers.layernorm import RMSNorm
27 from vllm.model_executor.layers.linear import ReplicatedLinear
28 from vllm.model_executor.layers.logits_processor import LogitsProcessor
29 from vllm.model_executor.layers.mhc import HCHeadOp, MHCPostOp
30 from vllm.model_executor.layers.vocab_parallel_embedding import (
31 ParallelLMHead,
32 VocabParallelEmbedding,
33 )
34 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
35 from vllm.model_executor.models.qwen3_dspark import DSparkMarkovHead
36 from vllm.model_executor.models.utils import maybe_prefix
37
38 from .model import (
39 DeepseekV4DecoderLayer,
40 make_deepseek_v4_expert_params_mapping,
41 )
42
43 logger = init_logger(__name__)
44
45 _EXPERT_SCALE_RE = re.compile(r"\.experts\.\d+\.w[123]\.scale$")
46
47
48 class DSparkDeepseekV4Model(nn.Module):
49 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
50 super().__init__()
51 assert vllm_config.speculative_config is not None
52 config = vllm_config.speculative_config.draft_model_config.hf_config
53 self.config = config
54 self.hidden_size = config.hidden_size
55 self.hc_mult = config.hc_mult
56 self.hc_eps = config.hc_eps
57 self.rms_norm_eps = config.rms_norm_eps
58 self.num_hidden_layers = config.num_hidden_layers
59 self.target_layer_ids = tuple(config.dspark_target_layer_ids)
60
61 self.num_dspark_layers = getattr(config, "n_mtp_layers", None) or 3
62
63 # Shared with target (aliased by speculator loading utility).
64 self.embed_tokens = VocabParallelEmbedding(
65 config.vocab_size,
66 config.hidden_size,
67 prefix=maybe_prefix(prefix, "embed_tokens"),
68 )
69
70 self.main_proj = ReplicatedLinear(
71 config.hidden_size * len(self.target_layer_ids),
72 config.hidden_size,
73 bias=False,
74 return_bias=False,
75 quant_config=vllm_config.quant_config,
76 prefix=maybe_prefix(prefix, "main_proj"),
77 )
78 self.main_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
79
80 current_vllm_config = get_current_vllm_config()
81 self.layers = nn.ModuleList(
82 [
83 DeepseekV4DecoderLayer(
84 current_vllm_config,
85 prefix=maybe_prefix(prefix, f"layers.{self.num_hidden_layers + i}"),
86 )
87 for i in range(self.num_dspark_layers)
88 ]
89 )
90
91 # Heads: final norm + hc_head, and the Markov head
92 self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
93 hc_dim = self.hc_mult * config.hidden_size
94 self.hc_head_fn = nn.Parameter(
95 torch.empty(self.hc_mult, hc_dim, dtype=torch.float32),
96 requires_grad=False,
97 )
98 self.hc_head_base = nn.Parameter(
99 torch.empty(self.hc_mult, dtype=torch.float32), requires_grad=False
100 )
101 self.hc_head_scale = nn.Parameter(
102 torch.empty(1, dtype=torch.float32), requires_grad=False
103 )
104 draft_vocab_size = (
105 getattr(config, "draft_vocab_size", None) or config.vocab_size
106 )
107 self.markov_head = DSparkMarkovHead(
108 config.vocab_size,
109 draft_vocab_size,
110 config.dspark_markov_rank,
111 prefix=maybe_prefix(prefix, "markov_head"),
112 )
113
114 # XPU MHC ops (replaces tilelang)
115 self.mhc_post_op = MHCPostOp()
116 self.hc_head_op = HCHeadOp()
117
118 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
119 return self.embed_tokens(input_ids)
120
121 def combine_hidden_states(self, aux_hidden_states: torch.Tensor) -> torch.Tensor:
122 """main_x = main_norm(main_proj(concat of target aux hidden states))."""
123 return self.main_norm(self.main_proj(aux_hidden_states))
124
125 @torch.inference_mode()
126 def precompute_and_store_context_kv(
127 self,
128 main_x: torch.Tensor,
129 context_positions: torch.Tensor,
130 context_slot_mappings: list[torch.Tensor | None] | None = None,
131 ) -> None:
132 """Insert the sliding-window context KV for every draft layer.
133
134 Each layer derives its context KV from the SAME projected target hidden
135 ``main_x``, via that layer's own wkv + kv_norm + RoPE + quant, then
136 writes it at the layer's context slots.
137 """
138 for i, layer in enumerate(self.layers):
139 slot_mapping = (
140 None if context_slot_mappings is None else context_slot_mappings[i]
141 )
142 attn = layer.attn
143 # wkv part of the fused wq_a|wkv projection (q_lora part discarded)
144 qr_kv, _ = attn.fused_wqa_wkv(main_x)
145 kv = qr_kv[..., attn.q_lora_rank :]
146 kv = attn.kv_norm(kv)
147 if slot_mapping is None:
148 continue
149 _insert_context_kv(attn, kv, context_positions, slot_mapping)
150
151 def forward(
152 self,
153 input_ids: torch.Tensor,
154 positions: torch.Tensor,
155 inputs_embeds: torch.Tensor | None = None,
156 ) -> torch.Tensor:
157 if inputs_embeds is None:
158 inputs_embeds = self.embed_input_ids(input_ids)
159 # Expand to hc_mult copies for hyper-connections ([T, H] -> [T, hc, H]).
160 hidden_states = inputs_embeds.unsqueeze(-2).repeat(1, self.hc_mult, 1)
161
162 residual = post_mix = res_mix = None
163 for layer in self.layers:
164 hidden_states, residual, post_mix, res_mix = layer(
165 hidden_states,
166 positions,
167 input_ids,
168 post_mix,
169 res_mix,
170 residual,
171 )
172 # mhc_post: merge hyper-connection copies
173 hidden_states = self.mhc_post_op(hidden_states, residual, post_mix, res_mix)
174 # hc_head: reduces hc copies; return pre-norm head hidden
175 hidden_states = self.hc_head_op(
176 hidden_states,
177 self.hc_head_fn,
178 self.hc_head_scale,
179 self.hc_head_base,
180 self.rms_norm_eps,
181 self.hc_eps,
182 )
183 return hidden_states
184
185
186 def _insert_context_kv(
187 attn: nn.Module,
188 kv: torch.Tensor,
189 positions: torch.Tensor,
190 slot_mapping: torch.Tensor,
191 ) -> None:
192 """RoPE + quant + paged-cache insert of (already kv_norm'd) context KV.
193
194 On XPU, we reuse the same xpu_qnorm_rope_kv_fp8_insert kernel used in
195 the forward path, passing a dummy q (result discarded).
196 """
197 from vllm.models.deepseek_v4.xpu.xpu_qnorm_rope_kv_fp8_insert import (
198 xpu_qnorm_rope_kv_fp8_insert,
199 )
200
201 swa_cache = attn.swa_cache_layer.kv_cache
202 block_size = attn.swa_cache_layer.block_size
203 cos_sin_cache = attn.rotary_emb.cos_sin_cache
204 n_ctx = kv.shape[0]
205
206 # Dummy q — we only care about the KV insert side effect.
207 dummy_q = torch.empty(
208 (n_ctx, attn.n_local_heads, attn.head_dim),
209 dtype=kv.dtype,
210 device=kv.device,
211 )
212 xpu_qnorm_rope_kv_fp8_insert(
213 dummy_q,
214 kv,
215 swa_cache,
216 slot_mapping,
217 positions,
218 cos_sin_cache,
219 attn.eps,
220 block_size,
221 )
222
223
224 class DSparkDeepseekV4ForCausalLM(nn.Module):
225 """XPU DSpark draft model entry point for DeepSeek-V4."""
226
227 has_own_embed_tokens = False
228 has_own_lm_head = False
229
230 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
231 super().__init__()
232 assert vllm_config.speculative_config is not None
233 self.draft_model_config = vllm_config.speculative_config.draft_model_config
234 self.config = self.draft_model_config.hf_config
235 self.model = DSparkDeepseekV4Model(
236 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
237 )
238 # Shared with the target (aliased by the speculator's load utility).
239 self.lm_head = ParallelLMHead(
240 self.config.vocab_size,
241 self.config.hidden_size,
242 prefix=maybe_prefix(prefix, "lm_head"),
243 )
244 self.logits_processor = LogitsProcessor(self.config.vocab_size)
245
246 # --- Hooks used by the speculator ---
247
248 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
249 return self.model.embed_input_ids(input_ids)
250
251 def combine_hidden_states(self, aux_hidden_states: torch.Tensor) -> torch.Tensor:
252 return self.model.combine_hidden_states(aux_hidden_states)
253
254 def get_draft_kv_cache_layer_names(self) -> list[str]:
255 return [layer.attn.swa_cache_layer.prefix for layer in self.model.layers]
256
257 def precompute_and_store_context_kv(
258 self,
259 context_states: torch.Tensor,
260 context_positions: torch.Tensor,
261 context_slot_mappings: list[torch.Tensor | None] | None = None,
262 ) -> None:
263 self.model.precompute_and_store_context_kv(
264 context_states, context_positions, context_slot_mappings
265 )
266
267 def forward(
268 self,
269 input_ids: torch.Tensor,
270 positions: torch.Tensor,
271 inputs_embeds: torch.Tensor | None = None,
272 ) -> torch.Tensor:
273 return self.model(input_ids, positions, inputs_embeds)
274
275 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
276 """Base logits U_k = lm_head(norm(head_hidden))."""
277 return self.logits_processor(self.lm_head, self.model.norm(hidden_states))
278
279 def compute_draft_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
280 return self.compute_logits(hidden_states)
281
282 def map_draft_to_target(self, draft_ids: torch.Tensor) -> torch.Tensor:
283 return draft_ids # full-vocab: draft ids are target ids
284
285 def markov_embed(self, token_ids: torch.Tensor) -> torch.Tensor:
286 return self.model.markov_head.embed(token_ids)
287
288 def markov_bias(self, markov_embed: torch.Tensor) -> torch.Tensor:
289 return self.model.markov_head.bias(markov_embed, self.logits_processor)
290
291 # --- Weight loading ---
292
293 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
294 """Load ``mtp.{0,1,2}.*`` draft weights from the target checkpoint."""
295 first_layer = self.model.layers[0]
296 use_mega_moe = first_layer.ffn.use_mega_moe
297 if use_mega_moe:
298 expert_mapping = make_deepseek_v4_expert_params_mapping(
299 self.config.n_routed_experts
300 )
301 else:
302 expert_mapping = fused_moe_make_expert_params_mapping(
303 self,
304 ckpt_gate_proj_name="w1",
305 ckpt_down_proj_name="w2",
306 ckpt_up_proj_name="w3",
307 num_experts=self.config.n_routed_experts,
308 )
309 expert_scale_suffix = (
310 ".weight_scale"
311 if getattr(self.config, "expert_dtype", "fp4") == "fp4"
312 else ".weight_scale_inv"
313 )
314
315 stacked_params_mapping = [
316 ("gate_up_proj", "w1", 0),
317 ("gate_up_proj", "w3", 1),
318 ("attn.fused_wqa_wkv", "attn.wq_a", 0),
319 ("attn.fused_wqa_wkv", "attn.wkv", 1),
320 ]
321
322 params_dict = dict(self.named_parameters())
323 loaded_params: set[str] = set()
324
325 tp_size = get_tensor_model_parallel_world_size()
326 tp_rank = get_tensor_model_parallel_rank()
327 n_local_head = self.config.num_attention_heads // tp_size
328 head_start = n_local_head * tp_rank
329 head_end = n_local_head * (tp_rank + 1)
330
331 for name, loaded_weight in weights:
332 mapped = self._remap_dspark_name(name)
333 if mapped is None:
334 continue
335 name = mapped
336
337 # .scale -> per-method scale suffix
338 if name.endswith(".scale"):
339 suffix = (
340 expert_scale_suffix
341 if _EXPERT_SCALE_RE.search(name)
342 else ".weight_scale_inv"
343 )
344 name = name.removesuffix(".scale") + suffix
345
346 # Expert weights
347 if ".experts." in name:
348 if (
349 "weight_scale" in name
350 and loaded_weight.dtype == torch.float8_e8m0fnu
351 ):
352 loaded_weight = loaded_weight.view(torch.uint8)
353 for param_name, weight_name, expert_id, shard_id in expert_mapping:
354 if weight_name not in name:
355 continue
356 name_mapped = name.replace(weight_name, param_name)
357 if name_mapped not in params_dict:
358 continue
359 param = params_dict[name_mapped]
360 success = param.weight_loader(
361 param,
362 loaded_weight,
363 name_mapped,
364 shard_id=shard_id,
365 expert_id=expert_id,
366 return_success=True,
367 )
368 if success:
369 loaded_params.add(name_mapped)
370 break
371 continue
372
373 # Stacked params (decoder-layer only)
374 is_layer_param = name.startswith("model.layers.")
375 for param_name, weight_name, stacked_shard_id in stacked_params_mapping:
376 if not is_layer_param or weight_name not in name:
377 continue
378 name = name.replace(weight_name, param_name)
379 if name not in params_dict:
380 break
381 param = params_dict[name]
382 param.weight_loader(param, loaded_weight, stacked_shard_id)
383 loaded_params.add(name)
384 break
385 else:
386 if "attn_sink" in name:
387 if name not in params_dict:
388 continue
389 narrow = loaded_weight[head_start:head_end]
390 params_dict[name][: narrow.shape[0]].copy_(narrow)
391 loaded_params.add(name)
392 continue
393 if ".shared_experts.w2" in name:
394 name = name.replace(
395 ".shared_experts.w2", ".shared_experts.down_proj"
396 )
397 if name.endswith(".ffn.gate.bias"):
398 name = name.replace(
399 ".ffn.gate.bias", ".ffn.gate.e_score_correction_bias"
400 )
401 if name not in params_dict:
402 continue
403 param = params_dict[name]
404 weight_loader = getattr(param, "weight_loader", default_weight_loader)
405 weight_loader(param, loaded_weight)
406 loaded_params.add(name)
407
408 self._finalize_moe()
409 logger.info_once("DSpark XPU draft model loaded: %d params", len(loaded_params))
410 return loaded_params
411
412 def _finalize_moe(self) -> None:
413 for layer in self.model.layers:
414 layer.ffn.finalize_mega_moe_weights()
415
416 def _remap_dspark_name(self, name: str) -> str | None:
417 """Map checkpoint ``mtp.{i}.*`` name to this model's parameter path."""
418 m = re.match(r"mtp\.(\d+)\.(.*)", name)
419 if m is None:
420 return None
421 stage = int(m.group(1))
422 rest = m.group(2)
423 if rest.startswith("confidence_head."):
424 return None
425 head_prefixes = (
426 "norm.",
427 "hc_head_fn",
428 "hc_head_base",
429 "hc_head_scale",
430 "markov_head.",
431 )
432 if rest.startswith(("main_proj.", "main_norm.")) or rest.startswith(
433 head_prefixes
434 ):
435 return f"model.{rest}"
436 return f"model.layers.{stage}.{rest}"