main
py 202 lines 7.58 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import Iterable
5
6 import torch
7 import torch.nn as nn
8
9 from vllm.config import VllmConfig
10 from vllm.model_executor.layers.logits_processor import LogitsProcessor
11 from vllm.model_executor.layers.vocab_parallel_embedding import (
12 ParallelLMHead,
13 )
14 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
15
16 from .utils import maybe_prefix
17
18
19 class ResidualBlock(nn.Module):
20 def __init__(self, config: VllmConfig, hidden_size: int, num_layers: int) -> None:
21 super().__init__()
22
23 self.layers = nn.ModuleList(
24 [
25 nn.Linear(
26 hidden_size,
27 hidden_size,
28 bias=getattr(config, "medusa_fc_bias", False),
29 )
30 for _ in range(num_layers)
31 ]
32 )
33 self.act = nn.SiLU()
34
35 def forward(self, x: torch.Tensor) -> torch.Tensor:
36 for layer in self.layers:
37 x = x + self.act(layer(x))
38 return x
39
40
41 class Medusa(nn.Module):
42 """This class implements the Medusa draft model from the paper: https://arxiv.org/abs/2401.10774
43 Reference implementation: https://github.com/FasterDecoding/Medusa
44
45 Differences from reference implementation:
46 1. Currently this only supports generating proposals from top-1 tokens.
47 2. We have an optional token_map which reduces draft vocab to most
48 frequently used tokens to give some additional speed-up by reducing
49 sampling overhead. This is disabled unless the checkpoint file has
50 explicit token_map tensor and config has an optional attribute
51 truncated_vocab_size < vocab_size. To use this technique, one has to find
52 the top-k most frequent tokens in target dataset and add that as a tensor
53 in the draft checkpoint (using key token_map). Also, the draft config
54 needs to have truncated_vocab_size (=k) as an attribute."""
55
56 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
57 speculative_config = vllm_config.speculative_config
58 assert speculative_config is not None
59 config = speculative_config.draft_model_config.hf_config
60 super().__init__()
61 self.config = config
62 self.blocks = nn.ModuleList(
63 [
64 ResidualBlock(
65 config=config,
66 hidden_size=self.config.hidden_size,
67 num_layers=self.config.num_hidden_layers,
68 )
69 for _ in range(self.config.num_heads)
70 ]
71 )
72 self.orig_vocab_size = config.vocab_size
73 self.truncated_vocab_size = config.truncated_vocab_size
74
75 if getattr(config, "original_lm_head", False):
76 self.lm_head = ParallelLMHead(
77 self.truncated_vocab_size,
78 config.hidden_size,
79 prefix=maybe_prefix(prefix, "lm_head"),
80 )
81 self.lm_heads = [self.lm_head for _ in range(self.config.num_heads)]
82 else:
83 self.lm_heads = nn.ModuleList(
84 [
85 ParallelLMHead(
86 config.vocab_size,
87 config.hidden_size,
88 prefix=maybe_prefix(prefix, f"lm_heads.{i}"),
89 )
90 for i in range(self.config.num_heads)
91 ]
92 )
93
94 logit_scale = getattr(config, "logit_scale", 1.0)
95 self.logits_processor = LogitsProcessor(
96 config.vocab_size, self.truncated_vocab_size, logit_scale
97 )
98
99 # Token map is a idx to token mapping to reduce the vocab size for
100 # the draft model. Using smaller vocab size for draft, containing
101 # only most frequent tokens reduces the speculation overhead. This
102 # doesn't affect the acceptance rate much and thus gives more speed
103 # -up. By default, this is disabled and is only used if the EAGLE
104 # checkpoint file has token_map tensor.
105 self.token_map = None
106
107 def forward(self, hidden_states: torch.Tensor) -> list[torch.Tensor]:
108 return [block(hidden_states) for block in self.blocks]
109
110 def compute_logits(
111 self,
112 hidden_states: list[torch.Tensor],
113 ) -> list[torch.Tensor]:
114 logits_lst: list[torch.Tensor] = []
115
116 for hs, lm_head in zip(hidden_states, self.lm_heads):
117 _logits = self.logits_processor(lm_head, hs)
118
119 if _logits is None:
120 # _logits should only be None on rank > 0, in which case
121 # it should remain true for every lm_head
122 assert len(logits_lst) == 0
123 continue
124
125 if self.token_map is None:
126 logits_lst.append(_logits)
127 else:
128 logits_lst.append(
129 -torch.inf
130 * torch.ones(
131 size=(*_logits.shape[:-1], self.orig_vocab_size),
132 device=_logits.device,
133 dtype=_logits.dtype,
134 )
135 )
136
137 logits_lst[-1][..., self.token_map] = _logits
138
139 return logits_lst
140
141 @staticmethod
142 def _remap_old_checkpoint_key(name: str) -> str:
143 """Map old FasterDecoding checkpoint keys to vLLM parameter names.
144
145 Old format uses bare numeric prefixes:
146 '{head}.{layer}.linear.weight' -> 'blocks.{head}.layers.{layer}.weight'
147 '{head}.{layer}.linear.bias' -> 'blocks.{head}.layers.{layer}.bias'
148 '{head}.{N}.weight' -> 'lm_heads.{head}.weight'
149 """
150 parts = name.split(".")
151 if len(parts) >= 3 and parts[0].isdigit() and parts[1].isdigit():
152 head, layer = parts[0], parts[1]
153 rest = parts[2:]
154 if "linear" in rest:
155 param = ".".join(rest[rest.index("linear") + 1 :])
156 return f"blocks.{head}.layers.{layer}.{param}"
157 else:
158 return f"lm_heads.{head}.{'.'.join(rest)}"
159 return name
160
161 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
162 params_dict = dict(self.named_parameters())
163 loaded_params: set[str] = set()
164
165 weights_map = {}
166
167 for name, loaded_weight in weights:
168 name = name.replace("medusa_heads.", "")
169 name = self._remap_old_checkpoint_key(name)
170
171 if name == "token_map":
172 if self.truncated_vocab_size < self.orig_vocab_size:
173 self.token_map = nn.Parameter(loaded_weight, requires_grad=False)
174 elif name in params_dict:
175 weights_map[name] = loaded_weight
176 elif (
177 getattr(self.config, "original_lm_head", False)
178 and name == "lm_heads.0.weight"
179 ):
180 weights_map["lm_head.weight"] = loaded_weight
181
182 for name, loaded_weight in weights_map.items():
183 if (
184 "lm_head" in name
185 and self.token_map is not None
186 and loaded_weight.shape[0] > self.token_map.shape[0]
187 ):
188 loaded_weight = loaded_weight[self.token_map]
189
190 param = params_dict[name]
191 weight_loader = getattr(param, "weight_loader", default_weight_loader)
192 weight_loader(param, loaded_weight)
193 loaded_params.add(name)
194
195 if self.token_map is not None:
196 self.token_map.to(device=self.lm_heads[0].weight.device)
197
198 assert (self.truncated_vocab_size == self.orig_vocab_size) or (
199 self.token_map is not None
200 )
201
202 return loaded_params