main
py 187 lines 7.61 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Gemma4 MTP (Multi-Token Prediction) speculator for speculative decoding.
4
5 The Gemma4 assistant model runs all decoder layers per draft step
6 (producing one token), and all its attention layers share KV cache
7 with the target model via cross-model KV sharing.
8 """
9
10 from collections import defaultdict
11
12 import torch.nn as nn
13
14 from vllm.compilation.backends import set_model_tag
15 from vllm.config import VllmConfig, replace
16 from vllm.distributed.parallel_state import get_pp_group
17 from vllm.logger import init_logger
18 from vllm.model_executor.model_loader import get_model
19 from vllm.model_executor.model_loader.utils import get_draft_load_config
20 from vllm.v1.worker.gpu.spec_decode.autoregressive.speculator import (
21 AutoRegressiveSpeculator,
22 )
23
24 logger = init_logger(__name__)
25
26
27 def _copy_target_kv_scales(attn: nn.Module, target_attn: nn.Module) -> None:
28 """Copy target KV scales while preserving their tensor representation.
29
30 Default attention scales are scalar buffers, while some quantization
31 methods replace them with length-one or per-head parameters. Preserve the
32 draft layer's registration type so the shared KV cache is interpreted with
33 the target's values and shapes without aliasing target parameters.
34 """
35 for scale_name in ("_k_scale", "_v_scale"):
36 draft_scale = getattr(attn, scale_name)
37 target_scale = getattr(target_attn, scale_name)
38 scale = target_scale.detach().clone()
39 if isinstance(draft_scale, nn.Parameter):
40 scale = nn.Parameter(scale, requires_grad=False)
41 setattr(attn, scale_name, scale)
42 for scale_name in ("_k_scale_float", "_v_scale_float"):
43 setattr(attn, scale_name, getattr(target_attn, scale_name))
44 for scale_name in ("_k_scale_cpu", "_v_scale_cpu"):
45 getattr(attn, scale_name).copy_(getattr(target_attn, scale_name))
46
47
48 class Gemma4Speculator(AutoRegressiveSpeculator):
49 @property
50 def advance_draft_positions(self) -> bool:
51 # Gemma4 MTP is Q-only and reads K/V from the target's existing cache.
52 # No new KV slots are written, so positions and seq_lens stay fixed.
53 return False
54
55 def load_draft_model(
56 self,
57 target_model: nn.Module,
58 target_attn_layer_names: set[str],
59 ) -> nn.Module:
60 draft_vllm_config = self._create_draft_vllm_config()
61 with set_model_tag("eagle_head"):
62 draft_model = get_model(
63 vllm_config=draft_vllm_config,
64 model_config=self.speculative_config.draft_model_config,
65 load_config=get_draft_load_config(draft_vllm_config),
66 )
67 self._setup_gemma4_kv_sharing(draft_model, target_attn_layer_names)
68 self._share_embeddings(draft_model, target_model)
69 return draft_model
70
71 def _create_draft_vllm_config(self) -> VllmConfig:
72 """Preserve the target's forced TRITON_ATTN backend for draft layers.
73
74 Gemma4 forces TRITON_ATTN due to heterogeneous head dimensions
75 (head_dim=256 sliding, global_head_dim=512 full). The base class
76 resets attention_config.backend to None for draft models, causing
77 sliding layers to fall back to FLASH_ATTN which cannot handle
78 KV-shared cache. Override to carry the target's backend through.
79 """
80 draft_model_config = self.speculative_config.draft_model_config
81 draft_vllm_config = replace(
82 self.vllm_config,
83 model_config=draft_model_config,
84 )
85 target_backend = self.vllm_config.attention_config.backend
86 if target_backend is not None:
87 draft_vllm_config = replace(
88 draft_vllm_config,
89 attention_config=replace(
90 draft_vllm_config.attention_config,
91 backend=target_backend,
92 ),
93 )
94 return draft_vllm_config
95
96 def _setup_gemma4_kv_sharing(
97 self,
98 model: nn.Module,
99 target_attn_layer_names: set[str],
100 ) -> None:
101 """Wire draft layers to share KV and KV scales with the target model.
102
103 Each draft decoder layer is mapped to the last non-KV-shared
104 target layer of the same attention type (sliding or full).
105 """
106 draft_config = self.speculative_config.draft_model_config.hf_config
107 draft_text_config = draft_config.get_text_config()
108 target_config = self.vllm_config.model_config.hf_config
109 target_text_config = target_config.get_text_config()
110 target_layer_types = getattr(target_text_config, "layer_types", [])
111
112 if not (hasattr(model, "model") and hasattr(model.model, "layers")):
113 return
114
115 target_num_kv_shared = getattr(target_text_config, "num_kv_shared_layers", 0)
116 target_num_layers = getattr(
117 target_text_config, "num_hidden_layers", len(target_layer_types)
118 )
119 num_non_shared = target_num_layers - target_num_kv_shared
120 type_to_target_indices: dict[str, list[int]] = defaultdict(list)
121 for idx, lt in enumerate(target_layer_types[:num_non_shared]):
122 type_to_target_indices[lt].append(idx)
123
124 target_prefix = "model.layers"
125 for name in target_attn_layer_names:
126 if ".layers." in name:
127 target_prefix = name.split(".layers.")[0] + ".layers"
128 break
129
130 draft_layer_types = getattr(draft_text_config, "layer_types", [])
131 for draft_idx, layer in enumerate(model.model.layers):
132 if not hasattr(layer, "self_attn"):
133 continue
134 attn = getattr(layer.self_attn, "attn", None)
135 if attn is None:
136 continue
137
138 draft_layer_type = (
139 draft_layer_types[draft_idx]
140 if draft_idx < len(draft_layer_types)
141 else "full_attention"
142 )
143 candidates = type_to_target_indices.get(draft_layer_type, [])
144 if not candidates:
145 logger.warning(
146 "No target layer of type '%s' for draft layer %d",
147 draft_layer_type,
148 draft_idx,
149 )
150 continue
151
152 target_idx = candidates[-1]
153 target_layer_name = f"{target_prefix}.{target_idx}.self_attn.attn"
154 attn.kv_sharing_target_layer_name = target_layer_name
155
156 # KV-cache sharing aliases the cache tensor during allocation, but
157 # the quantization scales live on the Attention modules themselves.
158 # The BF16 draft model has no quantization config, so its K/V scales
159 # otherwise remain at the default 1.0 while it reads the target's
160 # calibrated FP8 cache.
161 target_attn = self.vllm_config.compilation_config.static_forward_context[
162 target_layer_name
163 ]
164 _copy_target_kv_scales(attn, target_attn)
165
166 logger.info(
167 "Gemma4 MTP: draft layer %d (%s) -> %s",
168 draft_idx,
169 draft_layer_type,
170 target_layer_name,
171 )
172
173 def _share_embeddings(
174 self,
175 draft_model: nn.Module,
176 target_model: nn.Module,
177 ) -> None:
178 target_language_model = (
179 target_model.get_language_model()
180 if hasattr(target_model, "get_language_model")
181 else target_model
182 )
183 if get_pp_group().world_size == 1:
184 target_embed = getattr(target_language_model.model, "embed_tokens", None)
185 if target_embed is not None:
186 del draft_model.model.embed_tokens
187 draft_model.model.embed_tokens = target_embed