main
py 524 lines 20.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """HRM-Text: Hierarchical Reasoning Model — Text variant.
4
5 Reference Hugging Face implementation:
6 src/transformers/models/hrm_text/modeling_hrm_text.py
7
8 The model performs a hierarchical recurrent forward over two transformer
9 stacks (``H`` slow, ``L`` fast) inside nested loops. Each recurrence step
10 gets its own KV cache slot via a unique vLLM-visible layer index. The
11 PrefixLM attention pattern (prompt bidirectional, response causal) is
12 realized by reusing ``EncoderOnlyAttention`` (which sets ``causal=False``
13 unconditionally on every metadata build) but with ``attn_type=DECODER``
14 so the KV cache is allocated; see ``HrmTextAttention`` for usage.
15
16 The on-disk ``attn.gqkv_proj.weight`` (rows concatenated as
17 ``[gate | q | k | v]``) is loaded by a single
18 ``MergedColumnParallelLinear`` with four equal-sized output partitions;
19 its weight loader auto-splits the fused tensor along the output dim by
20 ``output_sizes`` (the same path used by Phi-3's fused gate_up_proj).
21 """
22
23 from collections.abc import Iterable
24 from typing import Literal
25
26 import torch
27 from torch import nn
28 from transformers import PreTrainedConfig
29
30 from vllm.compilation.decorators import support_torch_compile
31 from vllm.config import CacheConfig, VllmConfig
32 from vllm.distributed import get_tensor_model_parallel_world_size
33 from vllm.model_executor.layers.activation import SiluAndMul
34 from vllm.model_executor.layers.attention import PrefillPrefixLMAttention
35 from vllm.model_executor.layers.layernorm import RMSNorm
36 from vllm.model_executor.layers.linear import (
37 MergedColumnParallelLinear,
38 RowParallelLinear,
39 )
40 from vllm.model_executor.layers.logits_processor import LogitsProcessor
41 from vllm.model_executor.layers.quantization import QuantizationConfig
42 from vllm.model_executor.layers.rotary_embedding import get_rope
43 from vllm.model_executor.layers.vocab_parallel_embedding import (
44 ParallelLMHead,
45 VocabParallelEmbedding,
46 )
47 from vllm.sequence import IntermediateTensors
48
49 from .utils import AutoWeightsLoader, WeightsMapper, maybe_prefix
50
51
52 class HrmTextMLP(nn.Module):
53 def __init__(
54 self,
55 hidden_size: int,
56 intermediate_size: int,
57 hidden_act: str,
58 bias: bool = False,
59 quant_config: QuantizationConfig | None = None,
60 prefix: str = "",
61 ) -> None:
62 super().__init__()
63 if hidden_act != "silu":
64 raise ValueError(
65 f"HrmTextMLP only supports hidden_act='silu', got {hidden_act!r}"
66 )
67 self.gate_up_proj = MergedColumnParallelLinear(
68 hidden_size,
69 [intermediate_size] * 2,
70 bias=bias,
71 quant_config=quant_config,
72 prefix=f"{prefix}.gate_up_proj",
73 )
74 self.down_proj = RowParallelLinear(
75 intermediate_size,
76 hidden_size,
77 bias=bias,
78 quant_config=quant_config,
79 prefix=f"{prefix}.down_proj",
80 )
81 self.act_fn = SiluAndMul()
82
83 def forward(self, x: torch.Tensor) -> torch.Tensor:
84 gate_up, _ = self.gate_up_proj(x)
85 x = self.act_fn(gate_up)
86 x, _ = self.down_proj(x)
87 return x
88
89
90 class HrmTextAttention(nn.Module):
91 """One self-attention block; weights shared across recurrence steps.
92
93 HF transformers writes a single fused ``attn.gqkv_proj.weight`` on
94 disk (per ``transformers/conversion_mapping.py`` ``"hrm_text"``
95 mapping; rows are concatenated as ``[gate | q | k | v]`` along
96 ``dim=0``). We mirror that on the model side with a single
97 ``MergedColumnParallelLinear`` whose four equal output partitions
98 are sharded along the head axis under TP; its weight loader
99 auto-splits the fused tensor (same path used by Phi-3's fused
100 gate_up_proj). HF's runtime config currently hardcodes MHA
101 (``num_key_value_groups=1``); GQA would require ``QKVParallelLinear``
102 semantics for q/k/v shard replication and is left for a follow-up
103 if/when HF adds it.
104
105 Holds:
106 - parameters: gqkv_proj, o_proj, rotary_emb (shared across cycles).
107 - ``attn_per_step``: a ``nn.ModuleDict`` keyed by recurrence step
108 (as a string), each value an ``EncoderOnlyAttention`` (with
109 ``attn_type=DECODER`` so the KV cache is allocated; the
110 ``EncoderOnlyAttention`` wrapper sets ``causal=False`` on every
111 metadata build). The L stack steps are
112 ``[high_cycle_idx*(L_cycles+1)+low_cycle_idx]`` and the H stack
113 steps are ``[high_cycle_idx*(L_cycles+1)+L_cycles]``; the two
114 ranges are disjoint so each instance registers a unique vLLM
115 ``layer_name``
116 (``model.{H,L}_module.layers.{global_idx}.self_attn``) and gets
117 its own KV cache slot. The global layer index per recurrence step
118 is ``step * num_layers_per_stack + layer_idx_in_stack``, matching
119 the HF transformers ``cycle_offset`` formula in
120 ``modeling_hrm_text.py``.
121 """
122
123 def __init__(
124 self,
125 config: PreTrainedConfig,
126 layer_idx_in_stack: int,
127 stack_kind: Literal["L", "H"],
128 cache_config: CacheConfig | None = None,
129 quant_config: QuantizationConfig | None = None,
130 prefix: str = "",
131 ) -> None:
132 super().__init__()
133 self.config = config
134 self.hidden_size = config.hidden_size
135 tp_size = get_tensor_model_parallel_world_size()
136
137 self.total_num_heads = config.num_attention_heads
138 assert self.total_num_heads % tp_size == 0, (
139 f"num_attention_heads={self.total_num_heads} must be divisible "
140 f"by tp_size={tp_size}"
141 )
142 # HF main hardcodes MHA (num_key_value_groups=1). We follow.
143 self.total_num_kv_heads = config.num_attention_heads
144 self.num_heads = self.total_num_heads // tp_size
145 self.num_kv_heads = self.total_num_kv_heads // tp_size
146 self.head_dim = getattr(
147 config, "head_dim", self.hidden_size // self.total_num_heads
148 )
149 self.q_size = self.num_heads * self.head_dim
150 self.kv_size = self.num_kv_heads * self.head_dim
151 self.scaling = self.head_dim**-0.5
152
153 bias = getattr(config, "attention_bias", False)
154
155 # gqkv_proj: 4-way fused [gate | q | k | v] matching the on-disk
156 # `attn.gqkv_proj.weight` row layout. MergedColumnParallelLinear's
157 # weight_loader auto-splits the fused disk tensor along the output
158 # dim by `output_sizes` (Phi-3's fused gate_up_proj path). MHA
159 # only: GQA (num_kv_heads != num_heads) would need
160 # QKVParallelLinear semantics for q/k/v shard replication.
161 per_head_size = self.total_num_heads * self.head_dim
162 self.gqkv_proj = MergedColumnParallelLinear(
163 input_size=self.hidden_size,
164 output_sizes=[per_head_size] * 4,
165 bias=bias,
166 quant_config=quant_config,
167 prefix=f"{prefix}.gqkv_proj",
168 )
169 self.o_proj = RowParallelLinear(
170 input_size=self.total_num_heads * self.head_dim,
171 output_size=self.hidden_size,
172 bias=bias,
173 quant_config=quant_config,
174 prefix=f"{prefix}.o_proj",
175 )
176
177 # vllm get_rope accepts ``rope_parameters`` directly, matching
178 # the dict-shaped HF config field.
179 self.rotary_emb = get_rope(
180 head_size=self.head_dim,
181 max_position=config.max_position_embeddings,
182 rope_parameters=config.rope_parameters,
183 )
184
185 # Create one Attention instance per recurrence step actually used
186 # by this stack. L runs at steps {h*(L+1)+l : 0 <= l < L_cycles},
187 # H at steps {h*(L+1)+L : 0 <= h < H_cycles}; the sets are
188 # disjoint, so one global index per (step, layer_in_stack) gives
189 # each Attention its own ``layer_name`` and KV cache slot.
190 H_cycles = config.H_cycles
191 L_cycles = config.L_cycles
192 num_layers_per_stack = config.num_layers_per_stack
193 if stack_kind == "L":
194 steps_used = [
195 high_cycle_idx * (L_cycles + 1) + low_cycle_idx
196 for high_cycle_idx in range(H_cycles)
197 for low_cycle_idx in range(L_cycles)
198 ]
199 else: # "H"
200 steps_used = [
201 high_cycle_idx * (L_cycles + 1) + L_cycles
202 for high_cycle_idx in range(H_cycles)
203 ]
204
205 # `PrefillPrefixLMAttention` forces `causal=False` on every metadata
206 # build, so the prompt attends bidirectionally during prefill (matching
207 # the HRM-Text training distribution), while `attn_type=DECODER` keeps
208 # the KV cache allocation needed by the recurrent forward. At
209 # single-token decode `causal=False` is a no-op. See
210 # `PrefillPrefixLMAttention`.
211 self.attn_per_step = nn.ModuleDict()
212 for step in steps_used:
213 global_idx = step * num_layers_per_stack + layer_idx_in_stack
214 unique_prefix = prefix.replace(
215 f"layers.{layer_idx_in_stack}", f"layers.{global_idx}"
216 )
217 self.attn_per_step[str(step)] = PrefillPrefixLMAttention(
218 num_heads=self.num_heads,
219 head_size=self.head_dim,
220 scale=self.scaling,
221 num_kv_heads=self.num_kv_heads,
222 cache_config=cache_config,
223 quant_config=quant_config,
224 prefix=f"{unique_prefix}.attn",
225 )
226
227 def forward(
228 self,
229 positions: torch.Tensor,
230 hidden_states: torch.Tensor,
231 current_step: int,
232 ) -> torch.Tensor:
233 gqkv, _ = self.gqkv_proj(hidden_states)
234 g, q, k, v = gqkv.split(
235 [self.q_size, self.q_size, self.kv_size, self.kv_size], dim=-1
236 )
237 q, k = self.rotary_emb(positions, q, k)
238 attn_out = self.attn_per_step[str(current_step)](q, k, v)
239 # Sigmoid gate. Shapes: attn_out is (..., q_size); g is (..., q_size).
240 attn_out = torch.sigmoid(g) * attn_out
241 out, _ = self.o_proj(attn_out)
242 return out
243
244
245 class HrmTextDecoderLayer(nn.Module):
246 def __init__(
247 self,
248 config: PreTrainedConfig,
249 layer_idx_in_stack: int,
250 stack_kind: Literal["L", "H"],
251 cache_config: CacheConfig | None = None,
252 quant_config: QuantizationConfig | None = None,
253 prefix: str = "",
254 ) -> None:
255 super().__init__()
256 self.hidden_size = config.hidden_size
257 # Attribute name `self_attn` matches HF's model class. The on-disk
258 # `attn.{gqkv_proj,o_proj}.weight` keys are renamed to
259 # `self_attn.{gqkv_proj,o_proj}.weight` by the `WeightsMapper` in
260 # `HrmTextForCausalLM` so vLLM's standard `AutoWeightsLoader`
261 # handles the rest.
262 self.self_attn = HrmTextAttention(
263 config=config,
264 layer_idx_in_stack=layer_idx_in_stack,
265 stack_kind=stack_kind,
266 cache_config=cache_config,
267 quant_config=quant_config,
268 prefix=f"{prefix}.self_attn",
269 )
270 self.mlp = HrmTextMLP(
271 hidden_size=config.hidden_size,
272 intermediate_size=config.intermediate_size,
273 hidden_act=config.hidden_act,
274 bias=getattr(config, "mlp_bias", False),
275 quant_config=quant_config,
276 prefix=f"{prefix}.mlp",
277 )
278 # Parameterless RMSNorm (HF main: HrmTextRMSNorm has no weight).
279 self.input_layernorm = RMSNorm(
280 config.hidden_size, eps=config.rms_norm_eps, has_weight=False
281 )
282 self.post_attention_layernorm = RMSNorm(
283 config.hidden_size, eps=config.rms_norm_eps, has_weight=False
284 )
285
286 def forward(
287 self,
288 positions: torch.Tensor,
289 hidden_states: torch.Tensor,
290 current_step: int,
291 ) -> torch.Tensor:
292 residual = hidden_states
293 hidden_states = self.input_layernorm(hidden_states)
294 hidden_states = self.self_attn(
295 positions=positions,
296 hidden_states=hidden_states,
297 current_step=current_step,
298 )
299 hidden_states = residual + hidden_states
300
301 residual = hidden_states
302 hidden_states = self.post_attention_layernorm(hidden_states)
303 hidden_states = self.mlp(hidden_states)
304 hidden_states = residual + hidden_states
305 return hidden_states
306
307
308 class HrmTextStack(nn.Module):
309 """A single transformer stack — used twice (H and L)."""
310
311 def __init__(
312 self,
313 config: PreTrainedConfig,
314 stack_kind: Literal["L", "H"],
315 cache_config: CacheConfig | None = None,
316 quant_config: QuantizationConfig | None = None,
317 prefix: str = "",
318 ) -> None:
319 super().__init__()
320 self.layers = nn.ModuleList(
321 [
322 HrmTextDecoderLayer(
323 config=config,
324 layer_idx_in_stack=i,
325 stack_kind=stack_kind,
326 cache_config=cache_config,
327 quant_config=quant_config,
328 prefix=f"{prefix}.layers.{i}",
329 )
330 for i in range(config.num_layers_per_stack)
331 ]
332 )
333 self.final_norm = RMSNorm(
334 config.hidden_size, eps=config.rms_norm_eps, has_weight=False
335 )
336
337 def forward(
338 self,
339 positions: torch.Tensor,
340 hidden_states: torch.Tensor,
341 current_step_base: int,
342 ) -> torch.Tensor:
343 for layer in self.layers:
344 hidden_states = layer(
345 positions=positions,
346 hidden_states=hidden_states,
347 current_step=current_step_base,
348 )
349 return self.final_norm(hidden_states)
350
351
352 @support_torch_compile
353 class HrmTextModel(nn.Module):
354 """Hierarchical recurrent transformer body.
355
356 Forward (matches HF main exactly,
357 src/transformers/models/hrm_text/modeling_hrm_text.py:495-547):
358
359 hidden_states_high_cycle = embed(input_ids) * embedding_scale
360 hidden_states_low_cycle = z_L_init.expand_as(hidden_states_high_cycle)
361 for high_cycle_idx in range(H_cycles):
362 for low_cycle_idx in range(L_cycles):
363 step = high_cycle_idx * (L_cycles + 1) + low_cycle_idx
364 hidden_states_low_cycle = L_module(
365 hidden_states_low_cycle + hidden_states_high_cycle,
366 current_step=step,
367 )
368 step = high_cycle_idx * (L_cycles + 1) + L_cycles
369 hidden_states_high_cycle = H_module(
370 hidden_states_high_cycle + hidden_states_low_cycle,
371 current_step=step,
372 )
373 return hidden_states_high_cycle
374 """
375
376 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
377 super().__init__()
378 config = vllm_config.model_config.hf_config
379 cache_config = vllm_config.cache_config
380 quant_config = vllm_config.quant_config
381 self.config = config
382
383 self.embed_tokens = VocabParallelEmbedding(
384 config.vocab_size,
385 config.hidden_size,
386 quant_config=quant_config,
387 prefix=f"{prefix}.embed_tokens",
388 )
389 self.L_module = HrmTextStack(
390 config=config,
391 stack_kind="L",
392 cache_config=cache_config,
393 quant_config=quant_config,
394 prefix=f"{prefix}.L_module",
395 )
396 self.H_module = HrmTextStack(
397 config=config,
398 stack_kind="H",
399 cache_config=cache_config,
400 quant_config=quant_config,
401 prefix=f"{prefix}.H_module",
402 )
403 # Frozen learned initial L state. HF inits to zeros and sets
404 # requires_grad_(False); for inference we just load the tensor.
405 self.z_L_init = nn.Parameter(
406 torch.zeros(config.hidden_size), requires_grad=False
407 )
408
409 # Embedding scale: HF uses config.embedding_scale (default
410 # 1 / initializer_range = 50.0 when initializer_range=0.02). NOT
411 # sqrt(hidden_size) like Gemma.
412 self.embedding_scale = getattr(config, "embedding_scale", None)
413 if self.embedding_scale is None:
414 init_range = getattr(config, "initializer_range", 0.02)
415 self.embedding_scale = 1.0 / init_range
416
417 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
418 return self.embed_tokens(input_ids) * self.embedding_scale
419
420 def forward(
421 self,
422 input_ids: torch.Tensor | None,
423 positions: torch.Tensor,
424 intermediate_tensors: IntermediateTensors | None = None,
425 inputs_embeds: torch.Tensor | None = None,
426 ) -> torch.Tensor:
427 if inputs_embeds is None:
428 assert input_ids is not None
429 inputs_embeds = self.embed_input_ids(input_ids)
430
431 hidden_states_high_cycle = inputs_embeds
432 hidden_states_low_cycle = self.z_L_init.to(
433 dtype=hidden_states_high_cycle.dtype,
434 device=hidden_states_high_cycle.device,
435 ).expand_as(hidden_states_high_cycle)
436
437 H_cycles = self.config.H_cycles
438 L_cycles = self.config.L_cycles
439 for high_cycle_idx in range(H_cycles):
440 for low_cycle_idx in range(L_cycles):
441 step = high_cycle_idx * (L_cycles + 1) + low_cycle_idx
442 hidden_states_low_cycle = self.L_module(
443 positions=positions,
444 hidden_states=hidden_states_low_cycle + hidden_states_high_cycle,
445 current_step_base=step,
446 )
447 step = high_cycle_idx * (L_cycles + 1) + L_cycles
448 hidden_states_high_cycle = self.H_module(
449 positions=positions,
450 hidden_states=hidden_states_high_cycle + hidden_states_low_cycle,
451 current_step_base=step,
452 )
453
454 return hidden_states_high_cycle
455
456
457 class HrmTextForCausalLM(nn.Module):
458 """Hierarchical Reasoning Model — Text variant, causal LM.
459
460 Reference: src/transformers/models/hrm_text/modeling_hrm_text.py
461 """
462
463 # On-disk weight key remap: HF stores attention weights as
464 # `attn.{gqkv_proj,o_proj}.weight`; our model uses `self_attn.*`
465 # (matching HF's runtime model class). Both `gqkv_proj` (4-way fused
466 # gate/q/k/v) and `mlp.gate_up_proj` (2-way fused gate/up) are loaded
467 # directly via MergedColumnParallelLinear's fused-on-disk path; no
468 # packed_modules_mapping entries are needed.
469 hf_to_vllm_mapper = WeightsMapper(
470 orig_to_new_substr={".attn.": ".self_attn."},
471 )
472
473 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
474 super().__init__()
475 config = vllm_config.model_config.hf_config
476 quant_config = vllm_config.quant_config
477 self.config = config
478 self.quant_config = quant_config
479
480 if vllm_config.parallel_config.pipeline_parallel_size > 1:
481 raise ValueError(
482 "HrmTextForCausalLM does not support pipeline parallelism."
483 )
484
485 self.model = HrmTextModel(
486 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
487 )
488
489 self.lm_head = ParallelLMHead(
490 config.vocab_size,
491 config.hidden_size,
492 quant_config=quant_config,
493 prefix=maybe_prefix(prefix, "lm_head"),
494 )
495 if config.tie_word_embeddings:
496 self.lm_head = self.lm_head.tie_weights(self.model.embed_tokens)
497 self.logits_processor = LogitsProcessor(config.vocab_size)
498
499 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
500 return self.model.embed_input_ids(input_ids)
501
502 def forward(
503 self,
504 input_ids: torch.Tensor | None,
505 positions: torch.Tensor,
506 intermediate_tensors: IntermediateTensors | None = None,
507 inputs_embeds: torch.Tensor | None = None,
508 ) -> torch.Tensor:
509 return self.model(
510 input_ids=input_ids,
511 positions=positions,
512 intermediate_tensors=intermediate_tensors,
513 inputs_embeds=inputs_embeds,
514 )
515
516 def compute_logits(
517 self,
518 hidden_states: torch.Tensor,
519 ) -> torch.Tensor | None:
520 return self.logits_processor(self.lm_head, hidden_states)
521
522 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
523 loader = AutoWeightsLoader(self)
524 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)