main
py 480 lines 16.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 # Adapted from:
5 # https://github.com/huggingface/transformers/blob/main/src/transformers/models/olmo_hybrid/modeling_olmo_hybrid.py
6 # Copyright 2026 The vLLM team.
7 #
8 # This code combines OLMo2/OLMo3 attention with Gated DeltaNet linear attention
9 # for the OLMo Hybrid architecture.
10 #
11 # Licensed under the Apache License, Version 2.0 (the "License");
12 # you may not use this file except in compliance with the License.
13 # You may obtain a copy of the License at
14 #
15 # http://www.apache.org/licenses/LICENSE-2.0
16 #
17 # Unless required by applicable law or agreed to in writing, software
18 # distributed under the License is distributed on an "AS IS" BASIS,
19 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 # See the License for the specific language governing permissions and
21 # limitations under the License.
22 """Inference-only OLMo Hybrid model compatible with HuggingFace weights."""
23
24 from collections.abc import Iterable
25 from functools import partial
26 from itertools import islice
27
28 import torch
29 from torch import nn
30
31 from vllm.compilation.decorators import support_torch_compile
32 from vllm.config import (
33 VllmConfig,
34 )
35 from vllm.distributed import (
36 get_pp_group,
37 get_tensor_model_parallel_rank,
38 get_tensor_model_parallel_world_size,
39 tensor_model_parallel_all_gather,
40 )
41 from vllm.distributed.utils import split_tensor_along_last_dim
42 from vllm.logger import init_logger
43 from vllm.model_executor.layers.activation import SiluAndMul
44 from vllm.model_executor.layers.attention import Attention
45 from vllm.model_executor.layers.layernorm import RMSNorm
46 from vllm.model_executor.layers.linear import (
47 MergedColumnParallelLinear,
48 QKVParallelLinear,
49 RowParallelLinear,
50 )
51 from vllm.model_executor.layers.logits_processor import LogitsProcessor
52 from vllm.model_executor.layers.mamba.gdn.olmo_gdn_linear_attn import (
53 OlmoHybridGatedDeltaNetAttention,
54 )
55 from vllm.model_executor.layers.mamba.mamba_utils import (
56 MambaStateCopyFunc,
57 MambaStateCopyFuncCalculator,
58 MambaStateDtypeCalculator,
59 MambaStateShapeCalculator,
60 )
61 from vllm.model_executor.layers.rotary_embedding import get_rope
62 from vllm.model_executor.layers.vocab_parallel_embedding import (
63 ParallelLMHead,
64 VocabParallelEmbedding,
65 )
66 from vllm.sequence import IntermediateTensors
67
68 from .interfaces import HasInnerState, IsHybrid, SupportsLoRA, SupportsPP
69 from .utils import (
70 AutoWeightsLoader,
71 WeightsMapper,
72 extract_layer_index,
73 make_empty_intermediate_tensors_factory,
74 make_layers,
75 maybe_prefix,
76 )
77
78 logger = init_logger(__name__)
79
80
81 class OlmoHybridAttention(nn.Module):
82 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
83 super().__init__()
84 self.config = vllm_config.model_config.hf_config
85
86 hidden_size = self.config.hidden_size
87 self.tp_size = get_tensor_model_parallel_world_size()
88 self.total_num_heads = self.config.num_attention_heads
89
90 assert hidden_size % self.total_num_heads == 0
91 assert self.total_num_heads % self.tp_size == 0
92
93 self.num_heads = self.total_num_heads // self.tp_size
94 self.total_num_kv_heads = (
95 self.config.num_key_value_heads or self.total_num_heads
96 )
97 if self.total_num_kv_heads >= self.tp_size:
98 assert self.total_num_kv_heads % self.tp_size == 0
99 else:
100 assert self.tp_size % self.total_num_kv_heads == 0
101
102 self.num_kv_heads = max(1, self.total_num_kv_heads // self.tp_size)
103 self.head_dim = hidden_size // self.total_num_heads
104 self.q_size = self.num_heads * self.head_dim
105 self.kv_size = self.num_kv_heads * self.head_dim
106 self.max_position_embeddings = self.config.max_position_embeddings
107
108 self.qkv_proj = QKVParallelLinear(
109 hidden_size,
110 self.head_dim,
111 self.total_num_heads,
112 self.total_num_kv_heads,
113 bias=False,
114 quant_config=vllm_config.quant_config,
115 prefix=f"{prefix}.qkv_proj",
116 )
117
118 self.tp_rank = get_tensor_model_parallel_rank()
119
120 self.k_norm = RMSNorm(
121 self.total_num_kv_heads * self.head_dim,
122 eps=self.config.rms_norm_eps,
123 )
124 self.q_norm = RMSNorm(
125 self.config.hidden_size,
126 eps=self.config.rms_norm_eps,
127 )
128
129 self.scaling = self.head_dim**-0.5
130
131 self.attn = Attention(
132 self.num_heads,
133 self.head_dim,
134 self.scaling,
135 num_kv_heads=self.num_kv_heads,
136 cache_config=vllm_config.cache_config,
137 quant_config=vllm_config.quant_config,
138 prefix=f"{prefix}.attn",
139 )
140
141 rope_parameters = getattr(self.config, "rope_parameters", None)
142 self._use_rope = (rope_parameters is not None) and (
143 rope_parameters["rope_theta"] is not None
144 )
145
146 self.rotary_emb: nn.Module | None
147 if self._use_rope:
148 self.rotary_emb = get_rope(
149 self.head_dim,
150 max_position=self.max_position_embeddings,
151 rope_parameters=rope_parameters,
152 )
153 else:
154 self.rotary_emb = None
155
156 self.o_proj = RowParallelLinear(
157 self.total_num_heads * self.head_dim,
158 hidden_size,
159 bias=False,
160 quant_config=vllm_config.quant_config,
161 prefix=f"{prefix}.o_proj",
162 )
163
164 def _apply_qk_norm(
165 self, q: torch.Tensor, k: torch.Tensor
166 ) -> tuple[torch.Tensor, torch.Tensor]:
167 if self.tp_size > 1:
168 q = tensor_model_parallel_all_gather(q.contiguous())
169 k = tensor_model_parallel_all_gather(k.contiguous())
170 q = self.q_norm(q)
171 k = self.k_norm(k)
172 if self.tp_size > 1:
173 splitter = partial(split_tensor_along_last_dim, num_partitions=self.tp_size)
174 q = splitter(q)[self.tp_rank]
175 k = splitter(k)[self.tp_rank]
176 return q, k
177
178 def forward(
179 self,
180 positions: torch.Tensor,
181 hidden_states: torch.Tensor,
182 ) -> torch.Tensor:
183 qkv, _ = self.qkv_proj(hidden_states)
184 q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
185 q, k = self._apply_qk_norm(q, k)
186 if self._use_rope:
187 assert self.rotary_emb is not None
188 q, k = self.rotary_emb(positions, q, k)
189 attn_output = self.attn(q, k, v)
190 output, _ = self.o_proj(attn_output)
191 return output
192
193
194 class OlmoHybridMLP(nn.Module):
195 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
196 super().__init__()
197 config = vllm_config.model_config.hf_config
198 hidden_size = config.hidden_size
199 intermediate_size = config.intermediate_size
200
201 self.gate_up_proj = MergedColumnParallelLinear(
202 hidden_size,
203 [intermediate_size] * 2,
204 bias=False,
205 quant_config=vllm_config.quant_config,
206 prefix=f"{prefix}.gate_up_proj",
207 )
208
209 self.act_fn = SiluAndMul()
210
211 self.down_proj = RowParallelLinear(
212 intermediate_size,
213 hidden_size,
214 bias=False,
215 quant_config=vllm_config.quant_config,
216 prefix=f"{prefix}.down_proj",
217 )
218
219 def forward(self, x: torch.Tensor) -> torch.Tensor:
220 gate_up, _ = self.gate_up_proj(x)
221 x = self.act_fn(gate_up)
222 x, _ = self.down_proj(x)
223 return x
224
225
226 class OlmoHybridDecoderLayer(nn.Module):
227 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
228 super().__init__()
229 config = vllm_config.model_config.hf_config
230
231 layer_idx = extract_layer_index(prefix)
232 self.layer_type = config.layer_types[layer_idx]
233 self.layer_idx = layer_idx
234
235 if self.layer_type == "linear_attention":
236 self.linear_attn = OlmoHybridGatedDeltaNetAttention(
237 config,
238 vllm_config,
239 prefix=f"{prefix}.linear_attn",
240 )
241 self.input_layernorm = RMSNorm(
242 config.hidden_size,
243 eps=config.rms_norm_eps,
244 )
245 self.post_attention_layernorm = RMSNorm(
246 config.hidden_size,
247 eps=config.rms_norm_eps,
248 )
249 else:
250 self.self_attn = OlmoHybridAttention(
251 vllm_config=vllm_config,
252 prefix=f"{prefix}.self_attn",
253 )
254 # Attention layers use these norm names
255 self.post_attention_layernorm = RMSNorm(
256 config.hidden_size,
257 eps=config.rms_norm_eps,
258 )
259 self.post_feedforward_layernorm = RMSNorm(
260 config.hidden_size,
261 eps=config.rms_norm_eps,
262 )
263
264 self.mlp = OlmoHybridMLP(
265 vllm_config=vllm_config,
266 prefix=f"{prefix}.mlp",
267 )
268
269 def forward(
270 self,
271 positions: torch.Tensor,
272 hidden_states: torch.Tensor,
273 ) -> torch.Tensor:
274 if self.layer_type == "linear_attention":
275 residual = hidden_states
276 hidden_states = self.input_layernorm(hidden_states)
277
278 attn_output = torch.empty_like(hidden_states)
279 self.linear_attn(
280 hidden_states=hidden_states,
281 output=attn_output,
282 )
283 hidden_states = residual + attn_output
284
285 residual = hidden_states
286 hidden_states = self.post_attention_layernorm(hidden_states)
287 hidden_states = self.mlp(hidden_states)
288 hidden_states = residual + hidden_states
289 else:
290 residual = hidden_states
291 hidden_states = self.self_attn(positions, hidden_states)
292 hidden_states = self.post_attention_layernorm(hidden_states)
293 hidden_states = residual + hidden_states
294
295 residual = hidden_states
296 hidden_states = self.mlp(hidden_states)
297 hidden_states = self.post_feedforward_layernorm(hidden_states)
298 hidden_states = residual + hidden_states
299 return hidden_states
300
301
302 @support_torch_compile
303 class OlmoHybridModel(nn.Module):
304 hf_to_vllm_mapper = WeightsMapper(
305 orig_to_new_stacked={
306 ".self_attn.q_proj": (".self_attn.qkv_proj", "q"),
307 ".self_attn.k_proj": (".self_attn.qkv_proj", "k"),
308 ".self_attn.v_proj": (".self_attn.qkv_proj", "v"),
309 ".linear_attn.q_proj": (".linear_attn.in_proj_qkvg", 0),
310 ".linear_attn.k_proj": (".linear_attn.in_proj_qkvg", 1),
311 ".linear_attn.v_proj": (".linear_attn.in_proj_qkvg", 2),
312 ".linear_attn.g_proj": (".linear_attn.in_proj_qkvg", 3),
313 ".q_conv1d": (".conv1d", 0),
314 ".k_conv1d": (".conv1d", 1),
315 ".v_conv1d": (".conv1d", 2),
316 ".gate_proj": (".gate_up_proj", 0),
317 ".up_proj": (".gate_up_proj", 1),
318 }
319 )
320
321 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
322 super().__init__()
323 self.config = vllm_config.model_config.hf_config
324
325 self.embed_tokens = VocabParallelEmbedding(
326 self.config.vocab_size,
327 self.config.hidden_size,
328 prefix=f"{prefix}.embed_tokens",
329 )
330
331 self.start_layer, self.end_layer, self.layers = make_layers(
332 self.config.num_hidden_layers,
333 lambda prefix: OlmoHybridDecoderLayer(
334 vllm_config=vllm_config, prefix=prefix
335 ),
336 prefix=f"{prefix}.layers",
337 )
338
339 self.norm = RMSNorm(
340 self.config.hidden_size,
341 eps=self.config.rms_norm_eps,
342 )
343
344 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
345 ["hidden_states"], self.config.hidden_size
346 )
347
348 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
349 return self.embed_tokens(input_ids)
350
351 def forward(
352 self,
353 input_ids: torch.Tensor,
354 positions: torch.Tensor,
355 intermediate_tensors: IntermediateTensors | None,
356 inputs_embeds: torch.Tensor | None = None,
357 ) -> torch.Tensor | IntermediateTensors:
358 if get_pp_group().is_first_rank:
359 if inputs_embeds is not None:
360 hidden_states = inputs_embeds
361 else:
362 hidden_states = self.embed_tokens(input_ids)
363 else:
364 assert intermediate_tensors is not None
365 hidden_states = intermediate_tensors["hidden_states"]
366 assert isinstance(hidden_states, torch.Tensor)
367
368 for layer in islice(self.layers, self.start_layer, self.end_layer):
369 hidden_states = layer(positions, hidden_states)
370
371 if not get_pp_group().is_last_rank:
372 return IntermediateTensors({"hidden_states": hidden_states})
373
374 hidden_states = self.norm(hidden_states)
375 return hidden_states
376
377 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
378 loader = AutoWeightsLoader(self)
379 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
380
381
382 class OlmoHybridForCausalLM(
383 nn.Module, HasInnerState, SupportsPP, SupportsLoRA, IsHybrid
384 ):
385 packed_modules_mapping = {
386 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
387 "gate_up_proj": ["gate_proj", "up_proj"],
388 "in_proj_qkvg": ["q_proj", "k_proj", "v_proj", "g_proj"],
389 }
390
391 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
392 super().__init__()
393 config = vllm_config.model_config.hf_config
394 self.config = config
395 self.vllm_config = vllm_config
396 self.model_config = vllm_config.model_config
397
398 self.model = OlmoHybridModel(
399 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
400 )
401
402 self.lm_head = ParallelLMHead(
403 config.vocab_size,
404 config.hidden_size,
405 quant_config=vllm_config.quant_config,
406 prefix=maybe_prefix(prefix, "lm_head"),
407 )
408 if config.tie_word_embeddings:
409 self.lm_head = self.lm_head.tie_weights(self.model.embed_tokens)
410
411 self.logits_processor = LogitsProcessor(config.vocab_size)
412 self.make_empty_intermediate_tensors = (
413 self.model.make_empty_intermediate_tensors
414 )
415
416 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
417 return self.model.embed_input_ids(input_ids)
418
419 def forward(
420 self,
421 input_ids: torch.Tensor,
422 positions: torch.Tensor,
423 intermediate_tensors: IntermediateTensors | None = None,
424 inputs_embeds: torch.Tensor | None = None,
425 ) -> torch.Tensor | IntermediateTensors:
426 hidden_states = self.model(
427 input_ids=input_ids,
428 positions=positions,
429 intermediate_tensors=intermediate_tensors,
430 inputs_embeds=inputs_embeds,
431 )
432 return hidden_states
433
434 def compute_logits(
435 self,
436 hidden_states: torch.Tensor,
437 ) -> torch.Tensor | None:
438 logits = self.logits_processor(self.lm_head, hidden_states)
439 return logits
440
441 @classmethod
442 def get_mamba_state_dtype_from_config(
443 cls,
444 vllm_config: "VllmConfig",
445 ) -> tuple[torch.dtype, torch.dtype]:
446 return MambaStateDtypeCalculator.gated_delta_net_state_dtype(
447 vllm_config.model_config.dtype,
448 vllm_config.cache_config.mamba_cache_dtype,
449 vllm_config.cache_config.mamba_ssm_cache_dtype,
450 )
451
452 @classmethod
453 def get_mamba_state_shape_from_config(
454 cls, vllm_config: "VllmConfig"
455 ) -> tuple[tuple[int, int], tuple[int, int]]:
456 parallel_config = vllm_config.parallel_config
457 hf_config = vllm_config.model_config.hf_config
458 tp_size = parallel_config.tensor_parallel_size
459 num_spec = (
460 vllm_config.speculative_config.num_speculative_tokens
461 if vllm_config.speculative_config
462 else 0
463 )
464 return MambaStateShapeCalculator.gated_delta_net_state_shape(
465 tp_size,
466 hf_config.linear_num_key_heads,
467 hf_config.linear_num_value_heads,
468 hf_config.linear_key_head_dim,
469 hf_config.linear_value_head_dim,
470 hf_config.linear_conv_kernel_dim,
471 num_spec,
472 )
473
474 @classmethod
475 def get_mamba_state_copy_func(cls) -> tuple[MambaStateCopyFunc, MambaStateCopyFunc]:
476 return MambaStateCopyFuncCalculator.gated_delta_net_state_copy_func()
477
478 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
479 loader = AutoWeightsLoader(self)
480 return loader.load_weights(weights)