main
py 402 lines 14.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 """Eagle3 speculative decoding model for DeepseekV2/V3 with MLP (no MoE)."""
5
6 from collections.abc import Iterable
7
8 import torch
9 import torch.nn as nn
10 from transformers import DeepseekV2Config, DeepseekV3Config
11
12 from vllm.compilation.decorators import support_torch_compile
13 from vllm.config import VllmConfig, get_current_vllm_config
14 from vllm.model_executor.layers.layernorm import RMSNorm
15 from vllm.model_executor.layers.linear import ReplicatedLinear
16 from vllm.model_executor.layers.logits_processor import LogitsProcessor
17 from vllm.model_executor.layers.vocab_parallel_embedding import (
18 ParallelLMHead,
19 VocabParallelEmbedding,
20 )
21 from vllm.model_executor.models.deepseek_v2 import (
22 DeepseekV2ForCausalLM,
23 DeepseekV2MLAAttention,
24 DeepseekV2MLP,
25 )
26 from vllm.multimodal.inputs import NestedTensors
27
28 from .interfaces import LocalArgmaxMixin
29 from .utils import (
30 AutoWeightsLoader,
31 WeightsMapper,
32 get_draft_quant_config,
33 maybe_prefix,
34 process_eagle_weight,
35 )
36
37
38 class DeepseekV2Eagle3DecoderLayer(nn.Module):
39 """Eagle3 decoder layer for Deepseek that:
40 1. Always uses MLP (not MoE)
41 2. First layer accepts concatenated embeds + hidden_states
42 """
43
44 def __init__(
45 self,
46 vllm_config: VllmConfig,
47 prefix: str,
48 config: DeepseekV2Config | DeepseekV3Config | None = None,
49 layer_idx: int = 0,
50 ) -> None:
51 super().__init__()
52
53 if config is None:
54 config = vllm_config.model_config.hf_config
55 cache_config = vllm_config.cache_config
56 quant_config = get_draft_quant_config(vllm_config)
57
58 self.hidden_size = config.hidden_size
59 max_position_embeddings = getattr(config, "max_position_embeddings", 8192)
60
61 self.layer_idx = layer_idx
62
63 # MLA attention parameters
64 qk_nope_head_dim = getattr(config, "qk_nope_head_dim", 0)
65 qk_rope_head_dim = getattr(config, "qk_rope_head_dim", 0)
66 v_head_dim = getattr(config, "v_head_dim", 0)
67 kv_lora_rank = getattr(config, "kv_lora_rank", 0)
68 self.self_attn = DeepseekV2MLAAttention(
69 vllm_config=vllm_config,
70 config=config,
71 hidden_size=self.hidden_size,
72 num_heads=config.num_attention_heads,
73 qk_nope_head_dim=qk_nope_head_dim,
74 qk_rope_head_dim=qk_rope_head_dim,
75 v_head_dim=v_head_dim,
76 q_lora_rank=config.q_lora_rank if hasattr(config, "q_lora_rank") else None,
77 kv_lora_rank=kv_lora_rank,
78 max_position_embeddings=max_position_embeddings,
79 cache_config=cache_config,
80 quant_config=quant_config,
81 prefix=f"{prefix}.self_attn",
82 input_size=2 * self.hidden_size if layer_idx == 0 else self.hidden_size,
83 )
84
85 # Always use MLP (not MoE) for Eagle3
86 self.mlp = DeepseekV2MLP(
87 hidden_size=config.hidden_size,
88 intermediate_size=config.intermediate_size,
89 hidden_act=config.hidden_act,
90 quant_config=quant_config,
91 prefix=f"{prefix}.mlp",
92 )
93
94 self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
95 self.post_attention_layernorm = RMSNorm(
96 config.hidden_size, eps=config.rms_norm_eps
97 )
98
99 self.hidden_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
100
101 if getattr(config, "norm_before_residual", False):
102 self._residual_norm = self._norm_before_residual
103 else:
104 self._residual_norm = self._norm_after_residual
105
106 def _norm_before_residual(
107 self, hidden_states: torch.Tensor
108 ) -> tuple[torch.Tensor, torch.Tensor]:
109 hidden_states = self.hidden_norm(hidden_states)
110 residual = hidden_states
111 return hidden_states, residual
112
113 def _norm_after_residual(
114 self, hidden_states: torch.Tensor
115 ) -> tuple[torch.Tensor, torch.Tensor]:
116 residual = hidden_states
117 hidden_states = self.hidden_norm(hidden_states)
118 return hidden_states, residual
119
120 def forward(
121 self,
122 positions: torch.Tensor,
123 embeds: torch.Tensor,
124 hidden_states: torch.Tensor,
125 residual: torch.Tensor | None,
126 ) -> tuple[torch.Tensor, torch.Tensor]:
127 if self.layer_idx == 0:
128 # First layer: concatenate embeds with hidden_states
129 embeds = self.input_layernorm(embeds)
130 hidden_states, residual = self._residual_norm(hidden_states=hidden_states)
131 hidden_states = torch.cat([embeds, hidden_states], dim=-1)
132 else:
133 # Subsequent layers: process hidden_states and residuals only
134 hidden_states, residual = self.input_layernorm(hidden_states, residual)
135
136 # Self Attention
137 hidden_states = self.self_attn(
138 positions=positions,
139 hidden_states=hidden_states,
140 llama_4_scaling=None,
141 )
142
143 hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
144
145 # Fully Connected (MLP, not MoE)
146 hidden_states = self.mlp(hidden_states)
147
148 return hidden_states, residual
149
150
151 @support_torch_compile
152 class DeepseekV2Eagle3Model(nn.Module):
153 def __init__(
154 self,
155 *,
156 vllm_config: VllmConfig,
157 start_layer_id: int = 0,
158 prefix: str = "",
159 ) -> None:
160 super().__init__()
161 assert vllm_config.speculative_config is not None
162 self.config = vllm_config.speculative_config.draft_model_config.hf_config
163 self.vocab_size = self.config.vocab_size
164
165 # Get drafter's quantization config
166 self.quant_config = get_draft_quant_config(vllm_config)
167
168 current_vllm_config = get_current_vllm_config()
169
170 self.embed_tokens = VocabParallelEmbedding(
171 self.config.vocab_size,
172 self.config.hidden_size,
173 prefix=maybe_prefix(prefix, "embed_tokens"),
174 )
175
176 self.layers = nn.ModuleList(
177 [
178 DeepseekV2Eagle3DecoderLayer(
179 current_vllm_config,
180 prefix=maybe_prefix(prefix, f"layers.{layer_idx + start_layer_id}"),
181 config=self.config,
182 layer_idx=layer_idx,
183 )
184 for layer_idx in range(self.config.num_hidden_layers)
185 ]
186 )
187
188 # fc layer for combining auxiliary hidden states
189 num_aux_hidden_states = getattr(self.config, "num_aux_hidden_states", None)
190 if num_aux_hidden_states is None:
191 eagle_config = getattr(self.config, "eagle_config", None) or {}
192 layer_ids = eagle_config.get("eagle_aux_hidden_state_layer_ids")
193 num_aux_hidden_states = len(layer_ids) if layer_ids else 3
194 self.num_aux_hidden_states = num_aux_hidden_states
195
196 target_hidden_size = getattr(
197 self.config, "target_hidden_size", self.config.hidden_size
198 )
199 fc_input_size = target_hidden_size * num_aux_hidden_states
200
201 self.fc = ReplicatedLinear(
202 input_size=fc_input_size,
203 output_size=self.config.hidden_size,
204 bias=False,
205 params_dtype=vllm_config.model_config.dtype,
206 quant_config=self.quant_config,
207 prefix=maybe_prefix(prefix, "fc"),
208 return_bias=False,
209 )
210
211 use_fc_norm = getattr(self.config, "fc_norm", False)
212 if use_fc_norm:
213 self.fc_norm = nn.ModuleList(
214 [
215 RMSNorm(target_hidden_size, eps=self.config.rms_norm_eps)
216 for _ in range(self.num_aux_hidden_states)
217 ]
218 )
219 else:
220 self.fc_norm = None
221
222 self.norm_output = getattr(self.config, "norm_output", False)
223 self.norm = RMSNorm(
224 self.config.hidden_size,
225 eps=self.config.rms_norm_eps,
226 )
227
228 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
229 return self.embed_tokens(input_ids)
230
231 def forward(
232 self,
233 input_ids: torch.Tensor,
234 positions: torch.Tensor,
235 hidden_states: torch.Tensor,
236 input_embeds: torch.Tensor | None = None,
237 ) -> tuple[torch.Tensor, torch.Tensor]:
238 if input_embeds is None:
239 input_embeds = self.embed_input_ids(input_ids)
240 assert hidden_states.shape[-1] == input_embeds.shape[-1]
241
242 residual = None
243 for layer in self.layers:
244 hidden_states, residual = layer(
245 positions=positions,
246 embeds=input_embeds,
247 hidden_states=hidden_states,
248 residual=residual,
249 )
250
251 hidden_states, hidden_prenorm = self.norm(hidden_states, residual)
252
253 # norm_output variant uses the post-norm hidden states.
254 aux_output = hidden_states if self.norm_output else hidden_prenorm
255
256 return hidden_states, aux_output
257
258 # midlayer rename + gate_up / MLA fused_qkv_a merges
259 hf_to_vllm_mapper = WeightsMapper(
260 orig_to_new_substr={"midlayer.": "layers.0."},
261 orig_to_new_stacked={
262 ".gate_proj": (".gate_up_proj", 0),
263 ".up_proj": (".gate_up_proj", 1),
264 ".q_a_proj": (".fused_qkv_a_proj", 0),
265 ".kv_a_proj_with_mqa": (".fused_qkv_a_proj", 1),
266 },
267 )
268
269 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
270 loader = AutoWeightsLoader(self)
271 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
272
273
274 class Eagle3DeepseekV2ForCausalLM(LocalArgmaxMixin, DeepseekV2ForCausalLM):
275 """Eagle3 speculative decoding model for DeepseekV2/V3."""
276
277 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
278 nn.Module.__init__(self)
279 assert vllm_config.speculative_config is not None
280 self.config = vllm_config.speculative_config.draft_model_config.hf_config
281
282 # Ensure draft_vocab_size is set
283 if getattr(self.config, "draft_vocab_size", None) is None:
284 base_vocab_size = getattr(self.config, "vocab_size", None)
285 self.config.draft_vocab_size = base_vocab_size
286
287 target_layer_num = vllm_config.model_config.get_total_num_hidden_layers()
288
289 # Store target layer count in draft config
290 self.config.target_layer_count = target_layer_num
291
292 self.model = DeepseekV2Eagle3Model(
293 vllm_config=vllm_config,
294 prefix=maybe_prefix(prefix, "model"),
295 start_layer_id=target_layer_num,
296 )
297
298 logit_scale = getattr(self.config, "logit_scale", 1.0)
299 self.lm_head = ParallelLMHead(
300 self.config.draft_vocab_size,
301 self.config.hidden_size,
302 prefix=maybe_prefix(prefix, "lm_head"),
303 )
304 self.logits_processor = LogitsProcessor(
305 self.config.draft_vocab_size, scale=logit_scale
306 )
307 target_vocab_size = vllm_config.model_config.get_vocab_size()
308 if self.config.draft_vocab_size != target_vocab_size:
309 self.draft_id_to_target_id = nn.Parameter(
310 torch.zeros(self.config.draft_vocab_size, dtype=torch.long),
311 requires_grad=False,
312 )
313 else:
314 self.draft_id_to_target_id = None
315
316 def embed_input_ids(
317 self,
318 input_ids: torch.Tensor,
319 multimodal_embeddings: NestedTensors | None = None,
320 is_multimodal: torch.Tensor | None = None,
321 ) -> torch.Tensor:
322 return self.model.embed_input_ids(input_ids)
323
324 def forward( # type: ignore[override]
325 self,
326 input_ids: torch.Tensor,
327 positions: torch.Tensor,
328 hidden_states: torch.Tensor,
329 inputs_embeds: torch.Tensor | None = None,
330 ) -> tuple[torch.Tensor, torch.Tensor]:
331 return self.model(input_ids, positions, hidden_states, inputs_embeds)
332
333 def compute_logits(
334 self,
335 hidden_states: torch.Tensor,
336 ) -> torch.Tensor | None:
337 logits = self.logits_processor(self.lm_head, hidden_states)
338 if self.draft_id_to_target_id is None:
339 assert logits.shape[1] == self.config.vocab_size, (
340 "Expected logits to have shape "
341 f"(*, {self.config.vocab_size}), but got {logits.shape}"
342 )
343 return logits
344
345 base = torch.arange(self.config.draft_vocab_size, device=logits.device)
346 targets = base + self.draft_id_to_target_id
347 logits_new = logits.new_full(
348 (
349 logits.shape[0],
350 self.config.vocab_size,
351 ),
352 float("-inf"),
353 )
354 logits_new[:, targets] = logits
355 return logits_new
356
357 def combine_hidden_states(
358 self,
359 hidden_states: torch.Tensor,
360 ) -> torch.Tensor:
361 # Combine multiple auxiliary hidden states returned by Eagle3
362 if self.model.fc_norm is not None:
363 chunks = hidden_states.chunk(self.model.num_aux_hidden_states, dim=-1)
364 hidden_states = torch.cat(
365 [norm(chunk) for norm, chunk in zip(self.model.fc_norm, chunks)],
366 dim=-1,
367 )
368 return self.model.fc(hidden_states)
369
370 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
371 model_weights = {}
372 includes_draft_id_mapping = False
373 includes_embed_tokens = False
374
375 for name, loaded_weight in weights:
376 if "t2d" in name:
377 continue
378 if "d2t" in name:
379 name = name.replace("d2t", "draft_id_to_target_id")
380 includes_draft_id_mapping = True
381 elif "lm_head" not in name:
382 name = "model." + name
383 if "embed_tokens" in name:
384 includes_embed_tokens = True
385 model_weights[name] = loaded_weight
386 process_eagle_weight(self, name)
387
388 orig_to_new_substr: dict[str, str | None] = {}
389 if not includes_draft_id_mapping:
390 orig_to_new_substr["draft_id_to_target_id"] = None
391 if not includes_embed_tokens:
392 orig_to_new_substr["embed_tokens"] = None
393
394 loader = AutoWeightsLoader(self)
395 loader.load_weights(
396 model_weights.items(),
397 mapper=WeightsMapper(orig_to_new_substr=orig_to_new_substr),
398 )
399
400
401 # Aliases for compatibility
402 Eagle3DeepseekV3ForCausalLM = Eagle3DeepseekV2ForCausalLM