main
py 947 lines 37.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import io
5 from collections.abc import Iterable
6
7 import regex as re
8 import torch
9 import torch.nn.functional as F
10 from torch import nn
11 from transformers import Qwen3Config
12
13 from vllm import _custom_ops as ops
14 from vllm.compilation.decorators import support_torch_compile
15 from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
16 from vllm.distributed import (
17 get_tensor_model_parallel_rank,
18 get_tensor_model_parallel_world_size,
19 )
20 from vllm.logger import init_logger
21 from vllm.model_executor.layers.attention import Attention
22 from vllm.model_executor.layers.layernorm import RMSNorm
23 from vllm.model_executor.layers.linear import (
24 QKVParallelLinear,
25 ReplicatedLinear,
26 RowParallelLinear,
27 UnquantizedLinearMethod,
28 )
29 from vllm.model_executor.layers.logits_processor import LogitsProcessor
30 from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
31 from vllm.model_executor.layers.rotary_embedding import get_rope
32 from vllm.model_executor.layers.vocab_parallel_embedding import (
33 ParallelLMHead,
34 VocabParallelEmbedding,
35 )
36 from vllm.multimodal.inputs import NestedTensors
37 from vllm.transformers_utils.config import set_default_rope_theta
38 from vllm.transformers_utils.repo_utils import get_hf_file_bytes
39 from vllm.v1.attention.backend import AttentionType
40 from vllm.v1.worker.gpu.spec_decode.eagle.eagle3_utils import (
41 get_eagle3_aux_layers_from_config,
42 )
43
44 from .qwen2 import Qwen2MLP as Qwen3MLP
45 from .qwen3 import Qwen3ForCausalLM
46 from .utils import (
47 AutoWeightsLoader,
48 WeightsMapper,
49 get_draft_quant_config,
50 maybe_prefix,
51 process_eagle_weight,
52 )
53
54 logger = init_logger(__name__)
55
56
57 _SLIDING_ATTENTION = "sliding_attention"
58 _DRAFT_LAYER_PATTERN = re.compile(r"(?<![A-Za-z0-9_.])layers\.(\d+)")
59
60
61 def _add_global_draft_layer_exclusions(
62 quant_config: QuantizationConfig | None,
63 start_layer_id: int,
64 num_hidden_layers: int,
65 ) -> None:
66 """Add runtime layer aliases for checkpoint-local quant exclusions."""
67 if quant_config is None or start_layer_id == 0:
68 return
69 exclusions = getattr(quant_config, "exclude_modules", None)
70 if not isinstance(exclusions, list):
71 return
72
73 def offset_local_layer(match: re.Match[str]) -> str:
74 layer_idx = int(match.group(1))
75 if layer_idx >= num_hidden_layers:
76 return match.group(0)
77 return f"layers.{layer_idx + start_layer_id}"
78
79 for exclusion in tuple(exclusions):
80 global_exclusion = _DRAFT_LAYER_PATTERN.sub(offset_local_layer, exclusion)
81 if global_exclusion != exclusion and global_exclusion not in exclusions:
82 exclusions.append(global_exclusion)
83
84
85 def _dflash_layer_causal(config: Qwen3Config, layer_idx: int) -> bool:
86 """Resolve explicit causality before falling back to legacy layer defaults."""
87 is_causal = getattr(config, "is_causal", None)
88 if is_causal is not None:
89 return bool(is_causal)
90 override = (getattr(config, "dflash_config", None) or {}).get("causal")
91 if override is not None:
92 return bool(override)
93 layer_types = getattr(config, "layer_types", None)
94 if not layer_types:
95 return False
96 return layer_types[layer_idx] == _SLIDING_ATTENTION
97
98
99 def dflash_has_any_non_causal(config: Qwen3Config) -> bool:
100 """Whether the draft needs a non-causal-capable backend, resolved from config
101 (config mirror of the model's ``get_draft_attn_causal``, usable pre-build)."""
102 return not all(
103 _dflash_layer_causal(config, i) for i in range(config.num_hidden_layers)
104 )
105
106
107 def _get_dflash_fc_input_size(vllm_config: VllmConfig) -> int:
108 spec_config = vllm_config.speculative_config
109 assert spec_config is not None
110 config = spec_config.draft_model_config.hf_config
111 aux_layers = get_eagle3_aux_layers_from_config(spec_config)
112 num_features_to_use = len(aux_layers) if aux_layers else config.num_hidden_layers
113 target_hidden_size = (
114 getattr(config, "target_hidden_size", None) or config.hidden_size
115 )
116 return target_hidden_size * num_features_to_use
117
118
119 def _resolve_layer_attention(
120 config: Qwen3Config, layer_idx: int
121 ) -> tuple[int | None, bool]:
122 """Resolve ``(sliding_window, causal)`` for one DFlash draft layer.
123
124 +----------------------+-------------------------+--------------------------------+
125 | Config | ``layer_type`` | *``causal`` |
126 +======================+=========================+================================+
127 | ``layer_types`` | SWA if ``use_swa`` | True if ``layer_types[i]=SWA`` |
128 | | else ``layer_types[i]`` | else False |
129 +----------------------+-------------------------+--------------------------------+
130 | ``layer_types=None`` | SWA | False |
131 | + ``use_swa=True`` | | |
132 +----------------------+-------------------------+--------------------------------+
133 | ``layer_types=None`` | Full | False |
134 | + ``use_swa=False`` | | |
135 +----------------------+-------------------------+--------------------------------+
136 * If ``dflash_config.causal`` is set, its value overrides ``causal`` for all layers.
137
138 This is to support a varied ecosystem of checkpoints, including:
139 - XiaomiMiMo/MiMo-V2.5-Pro-FP4-DFlash (sets "use_swa", assumes non-causal)
140 - z-lab/gemma-4-31B-it-DFlash (has mixed layer types, assumes causal only for SWA)
141 - z-lab/Qwen3.5-9B-DFlash ("standard" DFlash, all full attn, assumes non-causal)
142 """
143 dflash_config = getattr(config, "dflash_config", None) or {}
144 layer_types = getattr(config, "layer_types", None)
145 use_swa = dflash_config.get("use_swa", False)
146
147 any_sliding = False
148 if layer_types is not None:
149 num_sliding = sum(lt == _SLIDING_ATTENTION for lt in layer_types)
150 any_sliding = num_sliding > 0
151 # Mixed sliding/full attention needs multiple KV groups (V2 runner only).
152 if (
153 0 < num_sliding < len(layer_types)
154 and not get_current_vllm_config().use_v2_model_runner
155 ):
156 raise NotImplementedError(
157 "DFlash drafters with mixed sliding/full attention require "
158 "the V2 model runner; relaunch with "
159 "VLLM_USE_V2_MODEL_RUNNER=1."
160 )
161
162 # ``use_swa`` forces SWA on every layer, even an all-full ``layer_types``.
163 if layer_types is None or (use_swa and not any_sliding):
164 is_sliding = use_swa
165 else:
166 is_sliding = layer_types[layer_idx] == _SLIDING_ATTENTION
167
168 sliding_window = None
169 if is_sliding:
170 sliding_window = dflash_config.get(
171 "swa_window_size", getattr(config, "sliding_window", None)
172 )
173 if sliding_window is None:
174 raise ValueError(
175 "DFlash sliding attention requires a window size configured in "
176 "dflash_config.swa_window_size or the top-level sliding_window."
177 )
178
179 return sliding_window, _dflash_layer_causal(config, layer_idx)
180
181
182 class DFlashQwen3Attention(nn.Module):
183 """Attention for DFlash speculative decoding.
184
185 Context KVs are pre-inserted into the KV cache before the forward pass.
186 This layer handles only query tokens via standard attention.
187 Adapted from Qwen3Attention."""
188
189 def __init__(
190 self,
191 hidden_size: int,
192 num_heads: int,
193 num_kv_heads: int,
194 rope_parameters: dict,
195 max_position: int = 4096 * 32,
196 head_dim: int | None = None,
197 rms_norm_eps: float = 1e-06,
198 attention_bias: bool = False,
199 add_swa_attention_sink_bias: bool = False,
200 v_scale: float | None = None,
201 sliding_window: int | None = None,
202 causal: bool = False,
203 is_neox_style: bool = True,
204 cache_config: CacheConfig | None = None,
205 quant_config: QuantizationConfig | None = None,
206 prefix: str = "",
207 attn_type: str = AttentionType.DECODER,
208 ) -> None:
209 super().__init__()
210 self.layer_name = prefix
211 self.hidden_size = hidden_size
212 tp_size = get_tensor_model_parallel_world_size()
213 self.total_num_heads = num_heads
214 assert self.total_num_heads % tp_size == 0
215 self.num_heads = self.total_num_heads // tp_size
216 self.total_num_kv_heads = num_kv_heads
217 if self.total_num_kv_heads >= tp_size:
218 assert self.total_num_kv_heads % tp_size == 0
219 else:
220 assert tp_size % self.total_num_kv_heads == 0
221 self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
222 self.head_dim = head_dim or hidden_size // self.total_num_heads
223 self.q_size = self.num_heads * self.head_dim
224 self.kv_size = self.num_kv_heads * self.head_dim
225 self.scaling = self.head_dim**-0.5
226 self.v_scale = v_scale
227
228 self.qkv_proj = QKVParallelLinear(
229 hidden_size,
230 self.head_dim,
231 self.total_num_heads,
232 self.total_num_kv_heads,
233 bias=attention_bias,
234 quant_config=quant_config,
235 prefix=f"{prefix}.qkv_proj",
236 )
237 self.o_proj = RowParallelLinear(
238 self.total_num_heads * self.head_dim,
239 hidden_size,
240 bias=attention_bias, # DFlash has o_proj bias when using attention bias
241 quant_config=quant_config,
242 prefix=f"{prefix}.o_proj",
243 )
244
245 self.rotary_emb = get_rope(
246 self.head_dim,
247 max_position=max_position,
248 is_neox_style=is_neox_style,
249 rope_parameters=rope_parameters,
250 )
251
252 self.attention_sink_bias = (
253 torch.nn.Parameter(torch.empty(self.num_heads), requires_grad=False)
254 if add_swa_attention_sink_bias
255 else None
256 )
257
258 self.sliding_window = sliding_window
259 self.attn = Attention(
260 self.num_heads,
261 self.head_dim,
262 self.scaling,
263 num_kv_heads=self.num_kv_heads,
264 cache_config=cache_config,
265 quant_config=quant_config,
266 per_layer_sliding_window=sliding_window,
267 prefix=f"{prefix}.attn",
268 attn_type=attn_type,
269 sinks=self.attention_sink_bias,
270 )
271 self.causal = causal
272 self.q_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
273 self.k_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
274
275 def forward(
276 self,
277 positions: torch.Tensor,
278 hidden_states: torch.Tensor,
279 ) -> torch.Tensor:
280 """DFlash attention assumes that the KV cache is already populated
281 with the context K/V from the target model's hidden states. This forward op
282 computes attention for the query tokens only.
283 See also: precompute_and_store_context_kv"""
284 qkv, _ = self.qkv_proj(hidden_states)
285 q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
286
287 # Per-head RMSNorm
288 q_shape, k_shape = q.shape, k.shape
289 q = self.q_norm(
290 q.view(*q_shape[:-1], q_shape[-1] // self.head_dim, self.head_dim)
291 ).view(q_shape)
292 k = self.k_norm(
293 k.view(*k_shape[:-1], k_shape[-1] // self.head_dim, self.head_dim)
294 ).view(k_shape)
295
296 q, k = self.rotary_emb(positions, q, k)
297
298 if self.v_scale is not None:
299 v = v * self.v_scale
300 attn_output = self.attn(q, k, v)
301 output, _ = self.o_proj(attn_output)
302 return output
303
304
305 class DFlashQwen3DecoderLayer(nn.Module):
306 def __init__(
307 self,
308 vllm_config: VllmConfig,
309 *,
310 config: Qwen3Config,
311 layer_idx: int,
312 cache_config: CacheConfig | None = None,
313 quant_config: QuantizationConfig | None = None,
314 prefix: str = "",
315 ) -> None:
316 super().__init__()
317 self.hidden_size = config.hidden_size
318 set_default_rope_theta(config, default_theta=1000000)
319 attn_type = AttentionType.DECODER
320
321 # DFlash drafts store the sink-bias flag inside dflash_config; fall back
322 # to the top-level attribute used by other (e.g. MiMo) configs.
323 dflash_config = getattr(config, "dflash_config", None) or {}
324 add_swa_attention_sink_bias = dflash_config.get(
325 "attention_sink_bias",
326 getattr(config, "add_swa_attention_sink_bias", False),
327 )
328
329 # Resolve this layer's attention mode (full vs sliding window, causal vs
330 # non-causal) from the draft config.
331 sliding_window, causal = _resolve_layer_attention(config, layer_idx)
332
333 # RoPE layout. The rotation applies to the draft's own Q/K, so this is
334 # fixed by how the head was distilled, not by the target: a neox-trained
335 # head on an interleaved target still needs neox. A mismatch is silent --
336 # acceptance collapses and nothing errors -- so a checkpoint that was
337 # distilled the other way has to say so here.
338 is_neox_style = getattr(config, "is_neox_style", True)
339
340 self.self_attn = DFlashQwen3Attention(
341 hidden_size=self.hidden_size,
342 num_heads=config.num_attention_heads,
343 max_position=config.max_position_embeddings,
344 num_kv_heads=config.num_key_value_heads,
345 rms_norm_eps=config.rms_norm_eps,
346 attention_bias=getattr(config, "attention_bias", False),
347 add_swa_attention_sink_bias=add_swa_attention_sink_bias,
348 v_scale=dflash_config.get("attention_value_scale"),
349 sliding_window=sliding_window,
350 causal=causal,
351 is_neox_style=is_neox_style,
352 head_dim=getattr(config, "head_dim", None),
353 cache_config=cache_config,
354 quant_config=quant_config,
355 rope_parameters=config.rope_parameters,
356 prefix=f"{prefix}.self_attn",
357 attn_type=attn_type,
358 )
359 self.mlp = Qwen3MLP(
360 hidden_size=self.hidden_size,
361 intermediate_size=config.intermediate_size,
362 hidden_act=config.hidden_act,
363 quant_config=quant_config,
364 prefix=f"{prefix}.mlp",
365 )
366 self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
367 self.post_attention_layernorm = RMSNorm(
368 config.hidden_size, eps=config.rms_norm_eps
369 )
370
371 def forward(
372 self,
373 positions: torch.Tensor,
374 hidden_states: torch.Tensor,
375 residual: torch.Tensor | None,
376 ) -> tuple[torch.Tensor, torch.Tensor]:
377 if residual is not None:
378 hidden_states, residual = self.input_layernorm(hidden_states, residual)
379 else:
380 residual = hidden_states
381 hidden_states = self.input_layernorm(hidden_states)
382
383 hidden_states = self.self_attn(
384 positions=positions,
385 hidden_states=hidden_states,
386 )
387
388 hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
389 hidden_states = self.mlp(hidden_states)
390 return hidden_states, residual
391
392
393 @support_torch_compile
394 class DFlashQwen3Model(nn.Module):
395 decoder_layer_cls = DFlashQwen3DecoderLayer
396
397 hf_to_vllm_mapper = WeightsMapper(
398 orig_to_new_substr={
399 "midlayer.": "layers.0.",
400 # Muse-Glimmer-30B-assistant names the aux-hidden-state encoder
401 # `encoder.fc` / `encoder.output_norm_enc`; this head calls them
402 # `fc` / `hidden_norm`. Same tensors and shapes, different names.
403 "encoder.output_norm_enc.": "hidden_norm.",
404 "encoder.fc.": "fc.",
405 },
406 orig_to_new_stacked={
407 ".q_proj": (".qkv_proj", "q"),
408 ".k_proj": (".qkv_proj", "k"),
409 ".v_proj": (".qkv_proj", "v"),
410 ".gate_proj": (".gate_up_proj", 0),
411 ".up_proj": (".gate_up_proj", 1),
412 },
413 )
414
415 def __init__(
416 self,
417 *,
418 vllm_config: VllmConfig,
419 start_layer_id: int = 0,
420 prefix: str = "",
421 ) -> None:
422 super().__init__()
423 speculative_config = vllm_config.speculative_config
424 assert speculative_config is not None
425 self.config = speculative_config.draft_model_config.hf_config
426 self.vocab_size = self.config.vocab_size
427 self.quant_config = get_draft_quant_config(vllm_config)
428 _add_global_draft_layer_exclusions(
429 self.quant_config, start_layer_id, self.config.num_hidden_layers
430 )
431
432 drafter_config = getattr(self.config, "eagle_config", {})
433 drafter_config.update(getattr(self.config, "dflash_config", {}))
434
435 self.use_aux_hidden_state = drafter_config.get(
436 "use_aux_hidden_state",
437 getattr(self.config, "use_aux_hidden_state", True),
438 )
439
440 current_vllm_config = get_current_vllm_config()
441
442 self.embed_tokens = VocabParallelEmbedding(
443 self.config.vocab_size,
444 self.config.hidden_size,
445 prefix=maybe_prefix(prefix, "embed_tokens"),
446 )
447
448 # Masked query slots are fed to the draft as `mask_token_id`. Most DFlash
449 # checkpoints will have the mask embedding in the vocabulary embedding table
450 # at that slot id. Some checkpoints (XiaomiMiMo/MiMo-V2.5-Pro-FP4-DFlash) ship
451 # with a separate mask embedding tensor to use instead. When present, we load it
452 # and substitute it for embed_tokens[mask_token_id] when computing embeddings.
453 self.mask_token_id = drafter_config.get(
454 "mask_token_id", getattr(self.config, "mask_token_id", None)
455 )
456 self.mask_embedding = nn.Parameter(
457 torch.zeros(self.config.hidden_size, dtype=vllm_config.model_config.dtype),
458 requires_grad=False,
459 )
460 self.has_separate_mask_embedding = False
461
462 self.layers = nn.ModuleList(
463 [
464 self.decoder_layer_cls(
465 current_vllm_config,
466 config=self.config,
467 layer_idx=layer_idx,
468 cache_config=current_vllm_config.cache_config,
469 quant_config=self.quant_config,
470 prefix=maybe_prefix(prefix, f"layers.{layer_idx + start_layer_id}"),
471 )
472 for layer_idx in range(self.config.num_hidden_layers)
473 ]
474 )
475 if self.use_aux_hidden_state:
476 self.fc = ReplicatedLinear(
477 input_size=_get_dflash_fc_input_size(
478 vllm_config,
479 ),
480 output_size=self.config.hidden_size,
481 bias=False,
482 params_dtype=vllm_config.model_config.dtype,
483 quant_config=self.quant_config,
484 prefix=maybe_prefix(prefix, "fc"),
485 return_bias=False,
486 )
487 self.hidden_norm = RMSNorm(
488 self.config.hidden_size,
489 eps=self.config.rms_norm_eps,
490 )
491 self.norm = RMSNorm(
492 self.config.hidden_size,
493 eps=self.config.rms_norm_eps,
494 )
495
496 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
497 embeds = self.embed_tokens(input_ids)
498 if self.has_separate_mask_embedding and self.mask_token_id is not None:
499 # Replace masked slots with the dedicated mask embedding.
500 is_mask = (input_ids == self.mask_token_id).unsqueeze(-1)
501 embeds = torch.where(is_mask, self.mask_embedding.to(embeds.dtype), embeds)
502 return embeds
503
504 def _build_context_kv_buffers(
505 self,
506 layers_attn: list[nn.Module],
507 has_bias: bool,
508 ) -> None:
509 self._hidden_norm_weight = self.hidden_norm.weight.data
510 self._context_qkv_projs = [a.qkv_proj for a in layers_attn]
511 self._context_q_sizes = [a.q_size for a in layers_attn]
512
513 if all(
514 isinstance(proj.quant_method, UnquantizedLinearMethod)
515 for proj in self._context_qkv_projs
516 ):
517 # KV projection weights: [num_layers * 2 * kv_size, hidden_size]
518 kv_weights = [a.qkv_proj.weight[a.q_size :] for a in layers_attn]
519 self._fused_kv_weight: torch.Tensor | None = torch.cat(kv_weights, dim=0)
520 if has_bias:
521 kv_biases = [a.qkv_proj.bias[a.q_size :] for a in layers_attn]
522 self._fused_kv_bias: torch.Tensor | None = torch.cat(kv_biases, dim=0)
523 else:
524 self._fused_kv_bias = None
525 else:
526 # Quantized linear weights may use packed storage that cannot be
527 # consumed by F.linear. Run each projection through its quant method.
528 self._fused_kv_weight = None
529 self._fused_kv_bias = None
530
531 # K-norm weights stacked into one contiguous [num_layers, head_dim]
532 # tensor so the per-layer K-norm runs as a single grouped kernel.
533 self._k_norm_weights = torch.stack(
534 [a.k_norm.weight.data for a in layers_attn], dim=0
535 ).contiguous()
536
537 def _build_fused_kv_buffers(self) -> None:
538 """Build fused weight buffers for precompute_and_store_context_kv.
539
540 Must be called after weights are loaded. Stacks the KV-projection
541 weights, K-norm weights, and RoPE parameters from every attention
542 layer so that precompute_and_store_context_kv can run one fused
543 GEMM for all layers at once. Also aliases the weight of the hidden_norm.
544 """
545 layers_attn = [layer.self_attn for layer in self.layers]
546 attn0 = layers_attn[0]
547 has_bias = attn0.qkv_proj.bias is not None
548
549 self._build_context_kv_buffers(layers_attn, has_bias)
550
551 # RoPE parameters
552 self._rope_head_size = attn0.rotary_emb.head_size
553 self._rope_cos_sin_cache = attn0.rotary_emb.cos_sin_cache
554 self._rope_is_neox = attn0.rotary_emb.is_neox_style
555 # Validation that RoPE params are the same across all layers
556 for attn in layers_attn[1:]:
557 assert (
558 attn.rotary_emb.head_size == self._rope_head_size
559 and attn.rotary_emb.is_neox_style == self._rope_is_neox
560 ), "All layers must have the same RoPE parameters for DFlash precomputation"
561
562 # Layer metadata
563 self._num_attn_layers = len(layers_attn)
564 self._kv_size = attn0.kv_size
565 self._head_dim = attn0.head_dim
566 self._num_kv_heads = attn0.num_kv_heads
567 self._rms_norm_eps = attn0.q_norm.variance_epsilon
568 # Validation that all layers have the same attention config
569 for attn in layers_attn[1:]:
570 assert (
571 attn.kv_size == self._kv_size
572 and attn.head_dim == self._head_dim
573 and attn.num_kv_heads == self._num_kv_heads
574 and attn.q_norm.variance_epsilon == self._rms_norm_eps
575 ), "All layers must have the same attn config for DFlash precomputation"
576
577 # References to inner Attention layers for direct cache writes
578 self._attn_layers = [layer.self_attn.attn for layer in self.layers]
579
580 def _project_context_kv(
581 self,
582 context_states: torch.Tensor,
583 num_ctx: int,
584 num_layers: int,
585 num_kv_heads: int,
586 head_dim: int,
587 ) -> tuple[torch.Tensor, torch.Tensor]:
588 # --- Fused KV projection (one GEMM for all layers) ---
589 normed_context_states = torch.empty_like(context_states)
590 ops.rms_norm(
591 normed_context_states,
592 context_states,
593 self._hidden_norm_weight,
594 self._rms_norm_eps,
595 )
596 if self._fused_kv_weight is not None:
597 all_kv_flat = F.linear(
598 normed_context_states, self._fused_kv_weight, self._fused_kv_bias
599 )
600 all_kv = all_kv_flat.view(num_ctx, num_layers, 2, num_kv_heads, head_dim)
601 else:
602 layer_kv = []
603 for proj, q_size in zip(
604 self._context_qkv_projs, self._context_q_sizes, strict=True
605 ):
606 qkv, _ = proj(normed_context_states)
607 layer_kv.append(
608 qkv[:, q_size:].view(num_ctx, 2, num_kv_heads, head_dim)
609 )
610 all_kv = torch.stack(layer_kv, dim=1)
611
612 # Single contiguous copy that separates K/V and transposes to
613 # layer-major layout. Result: [2, L, num_ctx, nkv, hd] contiguous.
614 # Indexing dim-0 gives contiguous [L, num_ctx, nkv, hd] for K and V.
615 all_kv = all_kv.permute(2, 1, 0, 3, 4).contiguous()
616 all_k = all_kv[0] # [L, num_ctx, nkv, hd], contiguous
617 all_v = all_kv[1] # [L, num_ctx, nkv, hd], contiguous
618 return all_k, all_v
619
620 def _normalize_context_k(self, all_k: torch.Tensor) -> torch.Tensor:
621 # --- Grouped RMSNorm K across all layers ([L, num_ctx, nkv, hd]) ---
622 # The weight is selected per layer by the outermost (layer) index.
623 all_k_normed = torch.empty_like(all_k)
624 ops.rms_norm(
625 all_k_normed,
626 all_k,
627 self._k_norm_weights,
628 self._rms_norm_eps,
629 )
630 return all_k_normed
631
632 def precompute_and_store_context_kv(
633 self,
634 context_states: torch.Tensor,
635 context_positions: torch.Tensor,
636 context_slot_mapping: torch.Tensor | list[torch.Tensor | None] | None = None,
637 ) -> None:
638 """Precompute K/V for context states write them into each layer's KV cache.
639
640 Input context states are projected to K/V, normed, and have RoPE applied.
641 Since the context shape is different than the query shape, we can't rely on the
642 regular forward pass to apply torch.compile and CUDA graphs to this section.
643 As such, this function is optimized to minimize the number of torch ops present:
644 we use fused vLLM kernels for RMSNorm and RoPE, fuse the GEMM into one
645 large projection, and avoid cloning buffers (with .contiguous()) where possible.
646
647 When context_slot_mapping is None (e.g. during dummy_run) only
648 the computation runs, and no K/V is written to cache.
649 """
650 if not hasattr(self, "_num_attn_layers"):
651 logger.warning_once(
652 "DFlash buffer initialization was skipped. If dummy weights are not "
653 "in use, this may indicate an error in weight loading."
654 )
655 self._build_fused_kv_buffers()
656
657 num_ctx = context_states.shape[0]
658 L = self._num_attn_layers
659 kv = self._kv_size
660 hd = self._head_dim
661 nkv = self._num_kv_heads
662
663 all_k, all_v = self._project_context_kv(context_states, num_ctx, L, nkv, hd)
664 all_k_normed = self._normalize_context_k(all_k)
665
666 # --- Fused RoPE across all layers ---
667 # View as [L * num_ctx, kv] so RoPE sees one big batch (no copy).
668 # In-place RoPE: pass K as the "query" arg with key=None.
669 all_k_flat = all_k_normed.view(L * num_ctx, kv)
670 positions_repeated = context_positions.repeat(L)
671 cos_sin_cache = self._rope_cos_sin_cache
672 if cos_sin_cache.dtype != all_k_flat.dtype:
673 cos_sin_cache = cos_sin_cache.to(dtype=all_k_flat.dtype)
674 ops.rotary_embedding(
675 positions_repeated,
676 all_k_flat,
677 None,
678 self._rope_head_size,
679 cos_sin_cache,
680 self._rope_is_neox,
681 )
682
683 if context_slot_mapping is None:
684 return
685
686 v_scale = getattr(self.layers[0].self_attn, "v_scale", None)
687 if v_scale is not None:
688 all_v.mul_(v_scale)
689
690 # --- Per-layer cache insert ---
691 all_k_final = all_k_flat.view(L, num_ctx, nkv, hd)
692 per_layer = isinstance(context_slot_mapping, (list, tuple))
693 for i in range(L):
694 slot_mapping = (
695 context_slot_mapping[i] if per_layer else context_slot_mapping
696 )
697 if slot_mapping is None:
698 continue # dummy run: skip cache ops
699 attn = self._attn_layers[i]
700 kv_cache = attn.kv_cache
701 attn.impl.do_kv_cache_update(
702 attn,
703 all_k_final[i],
704 all_v[i],
705 kv_cache,
706 slot_mapping,
707 )
708
709 def forward(
710 self,
711 input_ids: torch.Tensor,
712 positions: torch.Tensor,
713 input_embeds: torch.Tensor | None = None,
714 ) -> torch.Tensor:
715 if input_embeds is None:
716 input_embeds = self.embed_input_ids(input_ids)
717
718 hidden_states = input_embeds
719
720 residual = None
721 for layer in self.layers:
722 hidden_states, residual = layer(
723 positions=positions,
724 hidden_states=hidden_states,
725 residual=residual,
726 )
727 hidden_states, _ = self.norm(hidden_states, residual)
728 return hidden_states
729
730 def _preprocess(
731 self, weights: Iterable[tuple[str, torch.Tensor]]
732 ) -> Iterable[tuple[str, torch.Tensor]]:
733 tp_size = get_tensor_model_parallel_world_size()
734 tp_rank = get_tensor_model_parallel_rank()
735 for name, loaded_weight in weights:
736 if "attention_sink_bias" in name:
737 # Sink bias is per-head; shard it across TP ranks like the
738 # attention heads themselves.
739 heads_per_rank = loaded_weight.shape[0] // tp_size
740 loaded_weight = loaded_weight.narrow(
741 0, tp_rank * heads_per_rank, heads_per_rank
742 )
743 yield name, loaded_weight
744
745 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
746 loader = AutoWeightsLoader(self)
747 return loader.load_weights(
748 self._preprocess(weights), mapper=self.hf_to_vllm_mapper
749 )
750
751
752 class DFlashQwen3ForCausalLM(Qwen3ForCausalLM):
753 model_cls = DFlashQwen3Model
754
755 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
756 nn.Module.__init__(self)
757 speculative_config = vllm_config.speculative_config
758 assert speculative_config is not None
759 self.draft_model_config = speculative_config.draft_model_config
760 self.config = self.draft_model_config.hf_config
761 if getattr(self.config, "draft_vocab_size", None) is None:
762 self.config.draft_vocab_size = getattr(self.config, "vocab_size", None)
763 target_layer_num = vllm_config.model_config.get_total_num_hidden_layers()
764 self.model = self.model_cls(
765 vllm_config=vllm_config,
766 prefix=maybe_prefix(prefix, "model"),
767 start_layer_id=target_layer_num,
768 )
769
770 logit_scale = getattr(self.config, "logit_scale", 1.0)
771 self.lm_head = ParallelLMHead(
772 self.config.draft_vocab_size,
773 self.config.hidden_size,
774 quant_config=(
775 get_draft_quant_config(vllm_config)
776 if getattr(self.config, "has_own_lm_head", False)
777 else None
778 ),
779 prefix=maybe_prefix(prefix, "lm_head"),
780 )
781 self.logits_processor = LogitsProcessor(
782 self.config.draft_vocab_size, scale=logit_scale
783 )
784 target_vocab_size = vllm_config.model_config.get_vocab_size()
785 if self.config.draft_vocab_size != target_vocab_size:
786 self.draft_id_to_target_id = nn.Parameter(
787 torch.zeros(self.config.draft_vocab_size, dtype=torch.long),
788 requires_grad=False,
789 )
790 else:
791 self.draft_id_to_target_id = None
792
793 def embed_input_ids(
794 self,
795 input_ids: torch.Tensor,
796 multimodal_embeddings: NestedTensors | None = None,
797 is_multimodal: torch.Tensor | None = None,
798 ) -> torch.Tensor:
799 return self.model.embed_input_ids(input_ids)
800
801 def forward( # type: ignore[override]
802 self,
803 input_ids: torch.Tensor,
804 positions: torch.Tensor,
805 inputs_embeds: torch.Tensor | None = None,
806 ) -> torch.Tensor:
807 return self.model(input_ids, positions, inputs_embeds)
808
809 def get_draft_kv_cache_layer_names(self) -> list[str]:
810 return [layer.self_attn.attn.layer_name for layer in self.model.layers]
811
812 def get_draft_attn_causal(self) -> list[bool]:
813 """Per-layer attention causality, aligned with
814 get_draft_kv_cache_layer_names."""
815 return [layer.self_attn.causal for layer in self.model.layers]
816
817 def compute_logits(
818 self,
819 hidden_states: torch.Tensor,
820 ) -> torch.Tensor | None:
821 logits = self.logits_processor(self.lm_head, hidden_states)
822 if self.draft_id_to_target_id is None:
823 return logits
824
825 base = torch.arange(self.config.draft_vocab_size, device=logits.device)
826 targets = base + self.draft_id_to_target_id
827 logits_new = logits.new_full(
828 (logits.shape[0], self.config.vocab_size),
829 float("-inf"),
830 )
831 logits_new[:, targets] = logits
832 return logits_new
833
834 def precompute_and_store_context_kv(
835 self,
836 context_states: torch.Tensor,
837 context_positions: torch.Tensor,
838 context_slot_mapping: torch.Tensor | list[torch.Tensor | None] | None = None,
839 ) -> None:
840 """Precompute projected + RoPE'd K/V and write to cache."""
841 self.model.precompute_and_store_context_kv(
842 context_states, context_positions, context_slot_mapping
843 )
844
845 def combine_hidden_states(
846 self,
847 hidden_states: torch.Tensor,
848 ) -> torch.Tensor:
849 if not self.model.use_aux_hidden_state:
850 return hidden_states
851 needs_squeeze = hidden_states.dim() == 1
852 if needs_squeeze:
853 hidden_states = hidden_states.unsqueeze(0)
854 expected = self.model.fc.input_size
855 if hidden_states.shape[-1] != expected:
856 raise ValueError(
857 f"DFlash drafter expects {expected} concatenated aux hidden "
858 f"features but received {hidden_states.shape[-1]}. This usually "
859 "means the draft model's target_layer_ids reference layers that "
860 "do not exist in the target model (incompatible draft/target pair)."
861 )
862 result = self.model.fc(hidden_states)
863 if needs_squeeze:
864 result = result.squeeze(0)
865 return result
866
867 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
868 model_weights = {}
869 includes_draft_id_mapping = False
870 includes_embed_tokens = False
871 for name, loaded_weight in weights:
872 assert "mask_hidden" not in name, (
873 "DFlash embeds masked slots via mask_token_id (optionally "
874 "overridden by a mask_embedding.pt file); it should not ship a "
875 "mask_hidden weight."
876 )
877 if "t2d" in name:
878 continue
879 if "d2t" in name:
880 name = name.replace("d2t", "draft_id_to_target_id")
881 includes_draft_id_mapping = True
882 elif "lm_head" not in name:
883 name = "model." + name
884 if "embed_tokens" in name:
885 includes_embed_tokens = True
886 model_weights[name] = loaded_weight
887 process_eagle_weight(self, name)
888
889 # Route the separately-trained mask embedding (if shipped) through the
890 # standard weight loader alongside the rest of the draft weights.
891 mask_embedding = self._read_mask_embedding()
892 if mask_embedding is not None:
893 model_weights["model.mask_embedding"] = mask_embedding
894 self.model.has_separate_mask_embedding = True
895
896 orig_to_new_substr: dict[str, None] = {}
897 if not includes_draft_id_mapping:
898 orig_to_new_substr["draft_id_to_target_id"] = None
899 if not includes_embed_tokens:
900 orig_to_new_substr["embed_tokens"] = None
901 if not self.model.use_aux_hidden_state:
902 orig_to_new_substr["fc."] = None
903 if not self.model.has_separate_mask_embedding:
904 orig_to_new_substr["mask_embedding"] = None
905 mapper = WeightsMapper(orig_to_new_substr=orig_to_new_substr)
906 loader = AutoWeightsLoader(self)
907 loader.load_weights(model_weights.items(), mapper=mapper)
908 self.model._build_fused_kv_buffers()
909
910 def _read_mask_embedding(self) -> torch.Tensor | None:
911 """Checks for an override mask embedding in `mask_embedding.pt` and returns it.
912
913 Some checkpoints ship a separately-trained mask embedding for the mask token,
914 which we use to overwrite the embedding for `mask_token_id`. This helper
915 checks for the file, loads the pytorch tensor, and returns the embedding to use.
916
917 Returns None if the override file is not present.
918 """
919 mask_token_id = self.model.mask_token_id
920 if mask_token_id is None:
921 return None
922
923 MASK_EMBEDDING_FILENAME = "mask_embedding.pt"
924 data = get_hf_file_bytes(
925 MASK_EMBEDDING_FILENAME,
926 self.draft_model_config.model,
927 self.draft_model_config.revision,
928 )
929 if data is None:
930 return None
931
932 state = torch.load(io.BytesIO(data), weights_only=True)
933 if isinstance(state, dict):
934 if state.get("mask_token_id", mask_token_id) != mask_token_id:
935 raise ValueError(
936 f"{MASK_EMBEDDING_FILENAME} mask_token_id does not match "
937 f"dflash_config.mask_token_id ({mask_token_id}). "
938 f"Got {state.get('mask_token_id')}."
939 )
940 state = state["embedding"]
941
942 logger.info(
943 "Loaded DFlash mask embedding for mask_token_id %s from %s",
944 mask_token_id,
945 MASK_EMBEDDING_FILENAME,
946 )
947 return state.reshape(-1)