main
py 372 lines 14.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Qwen3 DSpark draft model for semi-autoregressive drafting.
4
5 DSpark drafts a whole block in one parallel pass (DFlash-style: context-KV
6 precompute + a non-causal query-block forward) and then injects intra-block
7 dependency with a lightweight sequential Markov head.
8
9 The parallel backbone is a standard Qwen3 decoder stack reused from the
10 DFlash Qwen3 draft (see qwen3_dflash.py). DSpark adds:
11 * ``markov_head``: low-rank V x r / r x V transition bias added to the base
12 logits, sampled left-to-right by the speculator (the sequential stage).
13 * ``confidence_head``: per-position acceptance-probability estimate.
14
15 DSparkMarkovHead and DSparkConfidenceHead are shared with the DSV4-style DSpark model.
16 """
17
18 from collections.abc import Iterable
19
20 import torch
21 import torch.nn as nn
22
23 from vllm.config import VllmConfig
24 from vllm.logger import init_logger
25 from vllm.model_executor.layers.linear import ReplicatedLinear
26 from vllm.model_executor.layers.logits_processor import LogitsProcessor
27 from vllm.model_executor.layers.quantization import QuantizationConfig
28 from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import (
29 dequantize_to_dtype,
30 nvfp4_gathered_bias,
31 )
32 from vllm.model_executor.layers.vocab_parallel_embedding import (
33 ParallelLMHead,
34 )
35 from vllm.platforms import current_platform
36
37 from .qwen3_dflash import DFlashQwen3ForCausalLM, DFlashQwen3Model
38 from .utils import (
39 AutoWeightsLoader,
40 WeightsMapper,
41 maybe_prefix,
42 process_eagle_weight,
43 )
44
45 logger = init_logger(__name__)
46
47
48 class DSparkMarkovHead(nn.Module):
49 """Sequential transition-bias head (low-rank V x r, r x V).
50
51 ``markov_w1[token]`` embeds the previously sampled token (target vocab,
52 ``vocab_size``); ``markov_w2`` projects it to a draft-vocab bias
53 (``draft_vocab_size``) added to the base draft logits. The two sizes
54 coincide for full-vocab drafts.
55
56 Both weights are replicated because the head runs sequentially for every
57 draft position. Sharding them would add an all-reduce and a full-vocab
58 gather to each position.
59 """
60
61 def __init__(
62 self,
63 vocab_size: int,
64 draft_vocab_size: int,
65 markov_rank: int,
66 prefix: str,
67 quant_config: QuantizationConfig | None = None,
68 retain_weight_for_gather: bool = False,
69 ) -> None:
70 super().__init__()
71 self.markov_w1 = nn.Embedding(vocab_size, markov_rank)
72 self.markov_w2 = ParallelLMHead(
73 draft_vocab_size,
74 markov_rank,
75 bias=False,
76 quant_config=quant_config,
77 prefix=maybe_prefix(prefix, "markov_w2"),
78 disable_tp=True,
79 )
80 self.markov_w2._retain_weight_for_gather = retain_weight_for_gather
81 self.markov_w2.is_w4a16_nvfp4 = False
82
83 def embed(self, token_ids: torch.Tensor) -> torch.Tensor:
84 """r-dim Markov embedding of ``token_ids`` ([B] -> [B, r])."""
85 return self.markov_w1(token_ids)
86
87 def bias(
88 self,
89 markov_embed: torch.Tensor,
90 logits_processor: LogitsProcessor,
91 ) -> torch.Tensor:
92 """Vocab-size transition bias from a Markov embedding ([B, r] -> [B, V])."""
93 return logits_processor(self.markov_w2, markov_embed)
94
95 def apply_bias_gathered(
96 self,
97 markov_embed: torch.Tensor,
98 logits: torch.Tensor,
99 values: torch.Tensor,
100 index: torch.Tensor,
101 scale: float = 1.0,
102 ) -> torch.Tensor:
103 """Apply the Markov bias only to selected rows of ``logits``.
104
105 The caller initializes ``logits`` to ``-inf`` once for all draft
106 positions. This method scatters the corrected candidate values into
107 that dense buffer so the normal sampler sees the truncated proposal.
108 """
109 if self.markov_w2._retain_weight_for_gather and self.markov_w2.is_w4a16_nvfp4:
110 # Use the fast Triton kernel for W4A16 NVFP4 weights with BF16
111 # activations on CUDA. FP16 activations, non-CUDA accelerators, and
112 # CPU use the slower dequantize/scatter fallback below.
113 if (
114 current_platform.is_cuda()
115 and markov_embed.is_cuda
116 and markov_embed.dtype == torch.bfloat16
117 and values.dtype == torch.bfloat16
118 and logits.dtype == torch.bfloat16
119 ):
120 nvfp4_gathered_bias(
121 markov_embed,
122 self.markov_w2._nvfp4_weight_for_gather,
123 self.markov_w2._nvfp4_weight_scale_for_gather,
124 self.markov_w2._nvfp4_weight_global_scale_for_gather,
125 values,
126 index,
127 logits,
128 scale,
129 )
130 return logits
131
132 flat_index = index.reshape(-1)
133 packed_weight = self.markov_w2._nvfp4_weight_for_gather.index_select(
134 0, flat_index
135 )
136 weight_scale = self.markov_w2._nvfp4_weight_scale_for_gather.index_select(
137 0, flat_index
138 )
139 weight = dequantize_to_dtype(
140 packed_weight,
141 weight_scale,
142 self.markov_w2._nvfp4_weight_global_scale_for_gather,
143 dtype=markov_embed.dtype,
144 block_size=self.markov_w2._nvfp4_group_size_for_gather,
145 swizzle=False,
146 ).view(*index.shape, -1)
147 else:
148 weight = self.markov_w2.weight[index]
149 if weight.dtype != markov_embed.dtype:
150 raise RuntimeError(
151 "Gathered Markov projection requires unquantized weights or "
152 "retained ModelOpt W4A16 NVFP4 weights."
153 )
154 corrected = values.unsqueeze(-1).clone()
155 corrected.baddbmm_(
156 weight,
157 markov_embed.unsqueeze(-1),
158 beta=1.0,
159 alpha=scale,
160 )
161 return logits.scatter_(1, index, corrected.squeeze(-1))
162
163
164 class DSparkConfidenceHead(nn.Module):
165 """DSpark acceptance-confidence head."""
166
167 def __init__(
168 self,
169 input_dim: int,
170 prefix: str,
171 bias: bool = False,
172 with_markov: bool = True,
173 ) -> None:
174 super().__init__()
175 self.with_markov = with_markov
176 self.proj = ReplicatedLinear(
177 input_dim,
178 1,
179 bias=bias,
180 return_bias=False,
181 params_dtype=torch.float32,
182 prefix=maybe_prefix(prefix, "proj"),
183 )
184
185 def forward(self, hidden: torch.Tensor, markov_embed: torch.Tensor) -> torch.Tensor:
186 x = (
187 torch.cat([hidden, markov_embed], dim=-1) if self.with_markov else hidden
188 ).float()
189 return self.proj(x).squeeze(-1)
190
191
192 class Qwen3DSparkModel(DFlashQwen3Model):
193 """DFlash Qwen3 backbone + DSpark Markov / confidence heads."""
194
195 def __init__(
196 self,
197 *,
198 vllm_config: VllmConfig,
199 start_layer_id: int = 0,
200 prefix: str = "",
201 ) -> None:
202 super().__init__(
203 vllm_config=vllm_config, start_layer_id=start_layer_id, prefix=prefix
204 )
205 config = self.config
206 draft_vocab_size = (
207 getattr(config, "draft_vocab_size", None) or config.vocab_size
208 )
209 self.markov_head = DSparkMarkovHead(
210 config.vocab_size,
211 draft_vocab_size,
212 config.markov_rank,
213 prefix=maybe_prefix(prefix, "markov_head"),
214 quant_config=self.quant_config,
215 retain_weight_for_gather=(
216 getattr(config, "dspark_draft_topk", None) is not None
217 ),
218 )
219 self.confidence_head: DSparkConfidenceHead | None = None
220 if getattr(config, "enable_confidence_head", False):
221 with_markov = getattr(config, "confidence_head_with_markov", False)
222 input_dim = config.hidden_size
223 if with_markov:
224 input_dim += config.markov_rank
225 self.confidence_head = DSparkConfidenceHead(
226 input_dim,
227 prefix=maybe_prefix(prefix, "confidence_head"),
228 bias=True,
229 with_markov=with_markov,
230 )
231
232
233 class Qwen3DSparkForCausalLM(DFlashQwen3ForCausalLM):
234 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
235 nn.Module.__init__(self)
236 speculative_config = vllm_config.speculative_config
237 assert speculative_config is not None
238 self.draft_model_config = speculative_config.draft_model_config
239 self.config = self.draft_model_config.hf_config
240 if getattr(self.config, "draft_vocab_size", None) is None:
241 self.config.draft_vocab_size = getattr(self.config, "vocab_size", None)
242 target_layer_num = vllm_config.model_config.get_total_num_hidden_layers()
243 self.model = Qwen3DSparkModel(
244 vllm_config=vllm_config,
245 prefix=maybe_prefix(prefix, "model"),
246 start_layer_id=target_layer_num,
247 )
248
249 logit_scale = getattr(self.config, "logit_scale", 1.0)
250 self.lm_head = ParallelLMHead(
251 self.config.draft_vocab_size,
252 self.config.hidden_size,
253 prefix=maybe_prefix(prefix, "lm_head"),
254 )
255 self.logits_processor = LogitsProcessor(
256 self.config.draft_vocab_size, scale=logit_scale
257 )
258 self.target_vocab_size = vllm_config.model_config.get_vocab_size()
259 # Only a pruned draft (subset of the target vocab) needs a d2t mapping to
260 # scatter ids up. A draft >= the target vocab (e.g. padded to the target's
261 # physical embedding size) maps identically, so no d2t is needed.
262 if self.config.draft_vocab_size < self.target_vocab_size:
263 self.draft_id_to_target_id = nn.Parameter(
264 torch.zeros(self.config.draft_vocab_size, dtype=torch.long),
265 requires_grad=False,
266 )
267 else:
268 self.draft_id_to_target_id = None
269
270 def get_draft_kv_cache_layer_names(self) -> list[str]:
271 return [layer.self_attn.attn.layer_name for layer in self.model.layers]
272
273 def compute_draft_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
274 # Draft-vocab logits without the d2t scatter: the speculator adds the
275 # Markov bias in draft space, then remaps via map_draft_to_target.
276 return self.logits_processor(self.lm_head, hidden_states)
277
278 def map_draft_to_target(self, draft_ids: torch.Tensor) -> torch.Tensor:
279 # Map draft-vocab ids to target ids (identity for full-vocab drafts).
280 if self.draft_id_to_target_id is None:
281 return draft_ids
282 return draft_ids + self.draft_id_to_target_id[draft_ids]
283
284 def markov_embed(self, token_ids: torch.Tensor) -> torch.Tensor:
285 return self.model.markov_head.embed(token_ids)
286
287 def markov_bias(self, markov_embed: torch.Tensor) -> torch.Tensor:
288 return self.model.markov_head.bias(markov_embed, self.logits_processor)
289
290 def apply_markov_bias_gathered(
291 self,
292 markov_embed: torch.Tensor,
293 logits: torch.Tensor,
294 values: torch.Tensor,
295 index: torch.Tensor,
296 ) -> torch.Tensor:
297 return self.model.markov_head.apply_bias_gathered(
298 markov_embed,
299 logits,
300 values,
301 index,
302 self.logits_processor.scale,
303 )
304
305 def compute_confidence(
306 self, head_hidden: torch.Tensor, markov_embed: torch.Tensor
307 ) -> torch.Tensor:
308 """Per-position acceptance probability for each drafted token."""
309 assert self.model.confidence_head is not None
310 return torch.sigmoid(self.model.confidence_head(head_hidden, markov_embed))
311
312 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
313 model_weights = {}
314 includes_embed_tokens = False
315 includes_lm_head = False
316 includes_draft_id_mapping = False
317 includes_confidence_head = False
318 for name, loaded_weight in weights:
319 # t2d is training-only; the draft remaps via d2t at sampling time.
320 if "t2d" in name:
321 continue
322 if "d2t" in name:
323 name = name.replace("d2t", "draft_id_to_target_id")
324 includes_draft_id_mapping = True
325 elif "lm_head" not in name:
326 name = "model." + name
327 if "embed_tokens" in name:
328 includes_embed_tokens = True
329 if "lm_head" in name:
330 includes_lm_head = True
331 if "confidence_head" in name:
332 includes_confidence_head = True
333 model_weights[name] = loaded_weight
334 # Sets has_own_embed_tokens / has_own_lm_head so load_dspark_model
335 # knows whether to keep these or alias the target's.
336 process_eagle_weight(self, name)
337
338 # mask_embedding is an unused placeholder param; DSpark masks via the vocab row.
339 # embed_tokens / lm_head are optional; when omitted they are shared from
340 # the target by load_dspark_model, so skip the unloaded params here.
341 uses_expanded_input_vocab = self.config.vocab_size > self.target_vocab_size
342 uses_reduced_vocab = self.config.draft_vocab_size < self.target_vocab_size
343 if uses_expanded_input_vocab and not includes_embed_tokens:
344 raise ValueError(
345 "Qwen3 DSpark checkpoints whose input vocab_size is larger than "
346 "the target vocabulary must include embed_tokens weights."
347 )
348 if uses_reduced_vocab and not includes_lm_head:
349 raise ValueError(
350 "Reduced-vocabulary Qwen3 DSpark checkpoints must include "
351 "lm_head weights; the full target lm_head cannot be shared."
352 )
353 if uses_reduced_vocab and not includes_draft_id_mapping:
354 raise ValueError(
355 "Reduced-vocabulary Qwen3 DSpark checkpoints must include a "
356 "d2t mapping so sampled draft ids can be converted to target ids."
357 )
358
359 orig_to_new_substr = {"mask_embedding": None}
360 if not includes_embed_tokens:
361 orig_to_new_substr["embed_tokens"] = None
362 if not includes_lm_head:
363 orig_to_new_substr["lm_head"] = None
364 if not includes_draft_id_mapping:
365 orig_to_new_substr["draft_id_to_target_id"] = None
366 if self.model.confidence_head is None or not includes_confidence_head:
367 self.model.confidence_head = None
368 orig_to_new_substr["confidence_head"] = None
369 mapper = WeightsMapper(orig_to_new_substr=orig_to_new_substr)
370 loader = AutoWeightsLoader(self)
371 loader.load_weights(model_weights.items(), mapper=mapper)
372 self.model._build_fused_kv_buffers()