main
py 333 lines 13 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Gemma4 DSpark draft model for speculative decoding."""
4
5 from collections.abc import Iterable
6
7 import torch
8 import torch.nn as nn
9 import torch.nn.functional as F
10
11 from vllm import _custom_ops as ops
12 from vllm.compilation.decorators import support_torch_compile
13 from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
14 from vllm.model_executor.layers.layernorm import RMSNorm
15 from vllm.model_executor.layers.linear import ColumnParallelLinear, ReplicatedLinear
16 from vllm.model_executor.layers.logits_processor import LogitsProcessor
17 from vllm.model_executor.layers.quantization import QuantizationConfig
18 from vllm.model_executor.layers.vocab_parallel_embedding import (
19 ParallelLMHead,
20 VocabParallelEmbedding,
21 )
22 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
23 from vllm.transformers_utils.configs.gemma4 import gemma4_layer_config
24
25 from .gemma4_mtp import Gemma4MTPAttention, Gemma4MTPDecoderLayer
26 from .qwen3_dflash import DFlashQwen3Model, _dflash_layer_causal
27 from .qwen3_dspark import (
28 DSparkConfidenceHead,
29 DSparkMarkovHead,
30 Qwen3DSparkForCausalLM,
31 )
32 from .utils import extract_layer_index, maybe_prefix
33
34
35 class Gemma4DSparkAttention(Gemma4MTPAttention):
36 """Gemma4 attention with its own KV cache and K/V projections."""
37
38 def __init__(
39 self,
40 config,
41 cache_config: CacheConfig | None,
42 quant_config: QuantizationConfig | None,
43 prefix: str,
44 ) -> None:
45 layer_idx = extract_layer_index(prefix)
46 layer_type = config.layer_types[layer_idx]
47 self.use_k_eq_v = layer_type == "full_attention" and getattr(
48 config, "attention_k_eq_v", False
49 )
50
51 layer_config = gemma4_layer_config(config, layer_idx)
52 head_dim = layer_config.head_dim
53 num_kv_heads = layer_config.num_key_value_heads
54 super().__init__(
55 config=config,
56 hidden_size=config.hidden_size,
57 num_heads=config.num_attention_heads,
58 num_kv_heads=num_kv_heads,
59 head_dim=head_dim,
60 max_position_embeddings=config.max_position_embeddings,
61 cache_config=cache_config,
62 quant_config=quant_config,
63 attn_logits_soft_cap=getattr(config, "attn_logit_softcapping", None),
64 prefix=prefix,
65 )
66 self.is_kv_shared_layer = False
67 self.causal = _dflash_layer_causal(config, layer_idx)
68 self.kv_size = self.num_kv_heads * self.head_dim
69 attn_bias = getattr(config, "attention_bias", False)
70 self.k_proj = ColumnParallelLinear(
71 config.hidden_size,
72 self.total_num_kv_heads * self.head_dim,
73 bias=attn_bias,
74 quant_config=quant_config,
75 prefix=f"{prefix}.k_proj",
76 )
77 self.v_proj = (
78 None
79 if self.use_k_eq_v
80 else ColumnParallelLinear(
81 config.hidden_size,
82 self.total_num_kv_heads * self.head_dim,
83 bias=attn_bias,
84 quant_config=quant_config,
85 prefix=f"{prefix}.v_proj",
86 )
87 )
88 self.k_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
89 self.v_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps, has_weight=False)
90
91 def _kv_proj(
92 self, hidden_states: torch.Tensor
93 ) -> tuple[torch.Tensor, torch.Tensor]:
94 k, _ = self.k_proj(hidden_states)
95 k_normed = self.k_norm(k.unflatten(-1, (self.num_kv_heads, self.head_dim)))
96 if self.use_k_eq_v:
97 v_src = k
98 else:
99 assert self.v_proj is not None
100 v_src = self.v_proj(hidden_states)[0]
101 v_normed = self.v_norm(v_src.unflatten(-1, (self.num_kv_heads, self.head_dim)))
102 return k_normed.flatten(-2, -1), v_normed.flatten(-2, -1)
103
104 def forward(
105 self, positions: torch.Tensor, hidden_states: torch.Tensor, **kwargs
106 ) -> torch.Tensor:
107 q, _ = self.q_proj(hidden_states)
108 q = self.q_norm(q.unflatten(-1, (self.num_heads, self.head_dim))).flatten(
109 -2, -1
110 )
111 k, v = self._kv_proj(hidden_states)
112 q, k = self.rotary_emb(positions, q, k)
113 attn_output = self.attn(q, k, v)
114 output, _ = self.o_proj(attn_output)
115 return output
116
117
118 class Gemma4DSparkDecoderLayer(Gemma4MTPDecoderLayer):
119 """Gemma4 MTP decoder layer using the KV-owning DSpark attention."""
120
121 def __init__(
122 self,
123 config,
124 cache_config: CacheConfig | None,
125 quant_config: QuantizationConfig | None,
126 prefix: str,
127 ) -> None:
128 super().__init__(
129 config, cache_config=cache_config, quant_config=quant_config, prefix=prefix
130 )
131 get_current_vllm_config().compilation_config.static_forward_context.pop(
132 f"{prefix}.self_attn.attn", None
133 )
134 self.self_attn = Gemma4DSparkAttention(
135 config, cache_config, quant_config, prefix=f"{prefix}.self_attn"
136 )
137
138
139 @support_torch_compile
140 class Gemma4DSparkModel(DFlashQwen3Model):
141 """Gemma4 DSpark draft backbone (Gemma4 layers + DSpark Markov head)."""
142
143 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
144 nn.Module.__init__(self)
145 assert vllm_config.speculative_config is not None
146 config = vllm_config.speculative_config.draft_model_config.hf_config
147 self.config = config
148 self.use_aux_hidden_state = True
149 self.target_layer_ids = tuple(
150 getattr(config, "dspark_target_layer_ids", None) or config.target_layer_ids
151 )
152 current_vllm_config = get_current_vllm_config()
153 cache_config = current_vllm_config.cache_config
154 quant_config = current_vllm_config.quant_config
155
156 self.embed_tokens = VocabParallelEmbedding(
157 config.vocab_size,
158 config.hidden_size,
159 prefix=maybe_prefix(prefix, "embed_tokens"),
160 )
161 self.register_buffer(
162 "normalizer",
163 torch.tensor(config.hidden_size**0.5, dtype=vllm_config.model_config.dtype),
164 persistent=False,
165 )
166 self.fc = ReplicatedLinear(
167 config.hidden_size * len(self.target_layer_ids),
168 config.hidden_size,
169 bias=False,
170 return_bias=False,
171 quant_config=quant_config,
172 prefix=maybe_prefix(prefix, "fc"),
173 )
174 self.hidden_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
175 self.layers = nn.ModuleList(
176 Gemma4DSparkDecoderLayer(
177 config,
178 cache_config,
179 quant_config,
180 prefix=maybe_prefix(prefix, f"layers.{i}"),
181 )
182 for i in range(config.num_hidden_layers)
183 )
184 self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
185 draft_vocab_size = (
186 getattr(config, "draft_vocab_size", None) or config.vocab_size
187 )
188 self.markov_head = DSparkMarkovHead(
189 config.vocab_size,
190 draft_vocab_size,
191 config.markov_rank,
192 prefix=maybe_prefix(prefix, "markov_head"),
193 )
194 self.confidence_head: DSparkConfidenceHead | None = None
195 if getattr(config, "enable_confidence_head", False):
196 with_markov = getattr(config, "confidence_head_with_markov", False)
197 input_dim = config.hidden_size
198 if with_markov:
199 input_dim += config.markov_rank
200 self.confidence_head = DSparkConfidenceHead(
201 input_dim,
202 prefix=maybe_prefix(prefix, "confidence_head"),
203 bias=True,
204 with_markov=with_markov,
205 )
206
207 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
208 return self.embed_tokens(input_ids) * self.normalizer
209
210 def _build_fused_kv_buffers(self) -> None:
211 layers_attn = [layer.self_attn for layer in self.layers]
212 attn0 = layers_attn[0]
213 assert all(a.use_k_eq_v for a in layers_attn), (
214 "Gemma4 DSpark fused precompute assumes uniform attention_k_eq_v layers"
215 )
216 self._build_context_kv_buffers(layers_attn, attn0.k_proj.bias is not None)
217 self._rope_head_size = attn0.rotary_emb.head_size
218 self._rope_cos_sin_cache = attn0.rotary_emb.cos_sin_cache
219 self._rope_is_neox = attn0.rotary_emb.is_neox_style
220 self._num_attn_layers = len(layers_attn)
221 self._kv_size = attn0.kv_size
222 self._head_dim = attn0.head_dim
223 self._num_kv_heads = attn0.num_kv_heads
224 self._rms_norm_eps = attn0.q_norm.variance_epsilon
225 self._attn_layers = [layer.self_attn.attn for layer in self.layers]
226
227 def _build_context_kv_buffers(
228 self, layers_attn: list[nn.Module], has_bias: bool
229 ) -> None:
230 self._hidden_norm_weight = self.hidden_norm.weight.data
231 self._fused_k_weight = torch.cat([a.k_proj.weight for a in layers_attn], dim=0)
232 self._fused_k_bias: torch.Tensor | None = (
233 torch.cat([a.k_proj.bias for a in layers_attn], dim=0) if has_bias else None
234 )
235 self._k_norm_weights = torch.stack(
236 [a.k_norm.weight.data for a in layers_attn], dim=0
237 ).contiguous()
238 # v_norm has no learnable scale; ones matching the K-norm call shape.
239 self._v_norm_weights = torch.ones(
240 len(layers_attn),
241 layers_attn[0].head_dim,
242 dtype=self._k_norm_weights.dtype,
243 device=self._k_norm_weights.device,
244 )
245
246 def _project_context_kv(
247 self,
248 context_states: torch.Tensor,
249 num_ctx: int,
250 num_layers: int,
251 num_kv_heads: int,
252 head_dim: int,
253 ) -> tuple[torch.Tensor, torch.Tensor]:
254 # Project once via k_proj for all layers. K is raw (the inherited path
255 # applies k_norm + RoPE); V = v_norm(same projection), no RoPE (k_eq_v).
256 normed = torch.empty_like(context_states)
257 ops.rms_norm(
258 normed, context_states, self._hidden_norm_weight, self._rms_norm_eps
259 )
260 all_k_flat = F.linear(normed, self._fused_k_weight, self._fused_k_bias)
261 all_k = (
262 all_k_flat.view(num_ctx, num_layers, num_kv_heads, head_dim)
263 .permute(1, 0, 2, 3)
264 .contiguous()
265 )
266 all_v = torch.empty_like(all_k)
267 ops.rms_norm(all_v, all_k, self._v_norm_weights, self._rms_norm_eps)
268 return all_k, all_v
269
270 def forward(
271 self,
272 input_ids: torch.Tensor,
273 positions: torch.Tensor,
274 input_embeds: torch.Tensor | None = None,
275 ) -> torch.Tensor:
276 hidden_states = (
277 self.embed_input_ids(input_ids) if input_embeds is None else input_embeds
278 )
279 for layer in self.layers:
280 hidden_states, _ = layer(positions, hidden_states, None)
281 return self.norm(hidden_states)
282
283
284 class Gemma4DSparkForCausalLM(Qwen3DSparkForCausalLM):
285 """Gemma4 DSpark speculator over a self-contained draft checkpoint."""
286
287 dspark_shares_target_embeddings = False
288 packed_modules_mapping = {"gate_up_proj": ["gate_proj", "up_proj"]}
289
290 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
291 nn.Module.__init__(self)
292 assert vllm_config.speculative_config is not None
293 self.draft_model_config = vllm_config.speculative_config.draft_model_config
294 self.config = self.draft_model_config.hf_config
295 self.model = Gemma4DSparkModel(
296 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
297 )
298 self.lm_head = ParallelLMHead(
299 self.config.vocab_size,
300 self.config.hidden_size,
301 prefix=maybe_prefix(prefix, "lm_head"),
302 )
303 self.logits_processor = LogitsProcessor(
304 self.config.vocab_size,
305 soft_cap=getattr(self.config, "final_logit_softcapping", None),
306 )
307 self.draft_id_to_target_id = None
308
309 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
310 stacked = [("gate_up_proj", "gate_proj", 0), ("gate_up_proj", "up_proj", 1)]
311 params = dict(self.named_parameters())
312 params.update(dict(self.named_buffers()))
313 loaded: set[str] = set()
314 includes_confidence_head = False
315 for name, w in weights:
316 if "lm_head" not in name:
317 name = "model." + name
318 for pn, wn, shard in stacked:
319 if wn in name and (mapped := name.replace(wn, pn)) in params:
320 params[mapped].weight_loader(params[mapped], w, shard)
321 loaded.add(mapped)
322 break
323 else:
324 if name in params:
325 p = params[name]
326 getattr(p, "weight_loader", default_weight_loader)(p, w)
327 loaded.add(name)
328 if "confidence_head" in name:
329 includes_confidence_head = True
330 if not includes_confidence_head:
331 self.model.confidence_head = None
332 self.model._build_fused_kv_buffers()
333 return loaded