# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from __future__ import annotations import math from collections.abc import Iterable import torch import torch.nn as nn import torch.nn.functional as F from transformers import T5Config from vllm.distributed import get_tensor_model_parallel_rank, get_tensor_model_parallel_world_size from vllm.model_executor.layers.activation import get_act_fn from vllm.model_executor.layers.layernorm import RMSNorm from vllm.model_executor.layers.linear import ( ColumnParallelLinear, MergedColumnParallelLinear, QKVParallelLinear, RowParallelLinear, ) from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding from vllm.model_executor.model_loader.weight_utils import default_weight_loader class T5SelfAttention(nn.Module): def __init__( self, config: T5Config, has_relative_attention_bias: bool = False, prefix: str = "", ): super().__init__() self.d_model = config.d_model self.d_kv = config.d_kv self.n_heads = config.num_heads self.inner_dim = self.n_heads * self.d_kv self.has_relative_attention_bias = has_relative_attention_bias self.relative_attention_num_buckets = config.relative_attention_num_buckets self.relative_attention_max_distance = config.relative_attention_max_distance tp_size = get_tensor_model_parallel_world_size() assert self.n_heads % tp_size == 0, f"num_heads ({self.n_heads}) must be divisible by tp_size ({tp_size})" self.n_heads_per_partition = self.n_heads // tp_size # Fused Q/K/V projection, sharded across heads. # HF has separate q, k, v — we fuse into qkv_proj and use # stacked_params_mapping in load_weights() to load each shard. self.qkv_proj = QKVParallelLinear( hidden_size=self.d_model, head_size=self.d_kv, total_num_heads=self.n_heads, total_num_kv_heads=self.n_heads, # T5 uses MHA bias=False, ) # Output projection: all-reduce back to full d_model # Named ``o`` to match HF's ``SelfAttention.o`` self.o = RowParallelLinear( self.inner_dim, self.d_model, bias=False, input_is_parallel=True, return_bias=False, ) if has_relative_attention_bias: # Store full embedding; slice heads per rank in forward self.relative_attention_bias = nn.Embedding(self.relative_attention_num_buckets, self.n_heads) @staticmethod def _relative_position_bucket( relative_position: torch.Tensor, bidirectional: bool = True, num_buckets: int = 32, max_distance: int = 128, ) -> torch.Tensor: relative_buckets = 0 if bidirectional: num_buckets //= 2 relative_buckets += (relative_position > 0).to(torch.long) * num_buckets relative_position = torch.abs(relative_position) else: relative_position = -torch.min(relative_position, torch.zeros_like(relative_position)) max_exact = num_buckets // 2 is_small = relative_position < max_exact relative_position_if_large = max_exact + ( torch.log(relative_position.float() / max_exact) / math.log(max_distance / max_exact) * (num_buckets - max_exact) ).to(torch.long) relative_position_if_large = torch.min( relative_position_if_large, torch.full_like(relative_position_if_large, num_buckets - 1), ) relative_buckets += torch.where(is_small, relative_position, relative_position_if_large) return relative_buckets def compute_bias(self, query_length: int, key_length: int, device: torch.device) -> torch.Tensor: """Compute relative position bias, returning only the local head shard.""" context_position = torch.arange(query_length, dtype=torch.long, device=device)[:, None] memory_position = torch.arange(key_length, dtype=torch.long, device=device)[None, :] relative_position = memory_position - context_position relative_position_bucket = self._relative_position_bucket( relative_position, bidirectional=True, num_buckets=self.relative_attention_num_buckets, max_distance=self.relative_attention_max_distance, ) # values: (query_length, key_length, num_heads) values = self.relative_attention_bias(relative_position_bucket) # Slice to local heads for this TP rank tp_rank = get_tensor_model_parallel_rank() head_start = tp_rank * self.n_heads_per_partition head_end = head_start + self.n_heads_per_partition values = values[:, :, head_start:head_end] # (1, local_heads, query_length, key_length) values = values.permute(2, 0, 1).unsqueeze(0) return values def forward( self, hidden_states: torch.Tensor, mask: torch.Tensor | None = None, position_bias: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: batch_size, seq_length = hidden_states.shape[:2] # Fused QKV projection qkv, _ = self.qkv_proj(hidden_states) q_size = self.n_heads_per_partition * self.d_kv kv_size = self.n_heads_per_partition * self.d_kv query_states, key_states, value_states = qkv.split([q_size, kv_size, kv_size], dim=-1) # Reshape: (batch, seq, local_heads, d_kv) -> (batch, local_heads, seq, d_kv) query_states = query_states.view(batch_size, seq_length, self.n_heads_per_partition, self.d_kv).transpose(1, 2) key_states = key_states.view(batch_size, seq_length, self.n_heads_per_partition, self.d_kv).transpose(1, 2) value_states = value_states.view(batch_size, seq_length, self.n_heads_per_partition, self.d_kv).transpose(1, 2) # Attention scores: (batch, local_heads, seq, seq) scores = torch.matmul(query_states, key_states.transpose(3, 2)) if position_bias is None: if self.has_relative_attention_bias: position_bias = self.compute_bias(seq_length, seq_length, device=scores.device) else: position_bias = torch.zeros( (1, self.n_heads_per_partition, seq_length, seq_length), device=scores.device, dtype=scores.dtype, ) if mask is not None: position_bias = position_bias + mask scores += position_bias attn_weights = F.softmax(scores.float(), dim=-1).type_as(scores) attn_output = torch.matmul(attn_weights, value_states) # (batch, local_heads, seq, d_kv) -> (batch, seq, local_heads * d_kv) attn_output = attn_output.transpose(1, 2).contiguous() attn_output = attn_output.view(batch_size, seq_length, -1) attn_output = self.o(attn_output) return attn_output, position_bias class T5DenseGatedActDense(nn.Module): def __init__(self, config: T5Config, prefix: str = ""): super().__init__() self.wi = MergedColumnParallelLinear( config.d_model, [config.d_ff, config.d_ff], bias=False, gather_output=False, ) self.wo = RowParallelLinear( config.d_ff, config.d_model, bias=False, input_is_parallel=True, return_bias=False, ) self.act = get_act_fn(config.dense_act_fn) def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: # MergedColumnParallelLinear outputs concatenated [gate, up] gate_up, _ = self.wi(hidden_states) gate, up = gate_up.chunk(2, dim=-1) hidden_states = self.act(gate) * up hidden_states = self.wo(hidden_states) return hidden_states class T5DenseActDense(nn.Module): def __init__(self, config: T5Config, prefix: str = ""): super().__init__() self.wi = ColumnParallelLinear( config.d_model, config.d_ff, bias=False, gather_output=False, return_bias=False, ) self.wo = RowParallelLinear( config.d_ff, config.d_model, bias=False, input_is_parallel=True, return_bias=False, ) self.act = get_act_fn(config.dense_act_fn) def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: hidden_states = self.wi(hidden_states) hidden_states = self.act(hidden_states) hidden_states = self.wo(hidden_states) return hidden_states class T5LayerSelfAttention(nn.Module): def __init__(self, config: T5Config, has_relative_attention_bias: bool = False, prefix: str = ""): super().__init__() self.SelfAttention = T5SelfAttention(config, has_relative_attention_bias, prefix=f"{prefix}.SelfAttention") self.layer_norm = RMSNorm(config.d_model, eps=config.layer_norm_epsilon) def forward( self, hidden_states: torch.Tensor, mask: torch.Tensor | None = None, position_bias: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: normed = self.layer_norm(hidden_states) attn_output, position_bias = self.SelfAttention(normed, mask=mask, position_bias=position_bias) hidden_states = hidden_states + attn_output # Clamp for fp16 stability (matching HF T5) if hidden_states.dtype == torch.float16: clamp_value = torch.finfo(hidden_states.dtype).max - 1000 hidden_states = torch.clamp(hidden_states, min=-clamp_value, max=clamp_value) return hidden_states, position_bias class T5LayerFF(nn.Module): def __init__(self, config: T5Config, prefix: str = ""): super().__init__() if config.is_gated_act: self.DenseReluDense = T5DenseGatedActDense(config, prefix=f"{prefix}.DenseReluDense") else: self.DenseReluDense = T5DenseActDense(config, prefix=f"{prefix}.DenseReluDense") self.layer_norm = RMSNorm(config.d_model, eps=config.layer_norm_epsilon) def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: normed = self.layer_norm(hidden_states) ff_output = self.DenseReluDense(normed) hidden_states = hidden_states + ff_output if hidden_states.dtype == torch.float16: clamp_value = torch.finfo(hidden_states.dtype).max - 1000 hidden_states = torch.clamp(hidden_states, min=-clamp_value, max=clamp_value) return hidden_states class T5Block(nn.Module): def __init__(self, config: T5Config, has_relative_attention_bias: bool = False, prefix: str = ""): super().__init__() self.layer = nn.ModuleList( [ T5LayerSelfAttention(config, has_relative_attention_bias, prefix=f"{prefix}.layer.0"), T5LayerFF(config, prefix=f"{prefix}.layer.1"), ] ) def forward( self, hidden_states: torch.Tensor, mask: torch.Tensor | None = None, position_bias: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: hidden_states, position_bias = self.layer[0](hidden_states, mask=mask, position_bias=position_bias) hidden_states = self.layer[1](hidden_states) return hidden_states, position_bias class T5Stack(nn.Module): def __init__(self, config: T5Config, shared: nn.Embedding, prefix: str = ""): super().__init__() self.embed_tokens = shared self.block = nn.ModuleList( [ T5Block(config, has_relative_attention_bias=(i == 0), prefix=f"{prefix}.block.{i}") for i in range(config.num_layers) ] ) self.final_layer_norm = RMSNorm(config.d_model, eps=config.layer_norm_epsilon) def forward( self, input_ids: torch.Tensor, attention_mask: torch.Tensor | None = None, ) -> torch.Tensor: hidden_states = self.embed_tokens(input_ids) # Build attention mask: (batch, seq) -> (batch, 1, 1, seq) if attention_mask is not None: extended_mask = attention_mask[:, None, None, :].to(dtype=hidden_states.dtype) extended_mask = (1.0 - extended_mask) * torch.finfo(hidden_states.dtype).min else: extended_mask = None position_bias = None for block in self.block: hidden_states, position_bias = block( hidden_states, mask=extended_mask, position_bias=position_bias, ) hidden_states = self.final_layer_norm(hidden_states) return hidden_states class T5EncoderModel(nn.Module): """T5 encoder model applying upstream vLLM layers""" def __init__(self, config: T5Config, prefix: str = ""): super().__init__() self.config = config self.prefix = prefix self.shared = VocabParallelEmbedding(config.vocab_size, config.d_model) self.encoder = T5Stack(config, self.shared, prefix=f"{prefix}.encoder") @property def dtype(self) -> torch.dtype: return next(self.parameters()).dtype @property def device(self) -> torch.device: return next(self.parameters()).device def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor: return self.shared(input_ids) def forward( self, input_ids: torch.Tensor, attention_mask: torch.Tensor | None = None, ) -> tuple[torch.Tensor, ...]: hidden_states = self.encoder(input_ids, attention_mask=attention_mask) return (hidden_states,) def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: stacked_params_mapping = [ ("qkv_proj", "q", "q"), ("qkv_proj", "k", "k"), ("qkv_proj", "v", "v"), ("wi", "wi_0", 0), ("wi", "wi_1", 1), ] model_prefix = self.prefix params_dict = dict(self.named_parameters()) loaded_params: set[str] = set() for name, loaded_weight in weights: original_name = name if model_prefix and name.startswith(model_prefix + "."): name = name[len(model_prefix) + 1 :] lookup_name = name matched = False for param_name, weight_name, shard_id in stacked_params_mapping: if f".{weight_name}." not in name: continue lookup_name = name.replace(f".{weight_name}.", f".{param_name}.", 1) if lookup_name in params_dict: param = params_dict[lookup_name] weight_loader = param.weight_loader weight_loader(param, loaded_weight, shard_id) matched = True break if not matched: if name in params_dict: param = params_dict[name] weight_loader = getattr(param, "weight_loader", default_weight_loader) weight_loader(param, loaded_weight) matched = True is_embed = "encoder.embed_tokens" in lookup_name is_shared = lookup_name.startswith("shared.") or ".shared." in lookup_name target_name = None if is_embed or is_shared: if is_embed: target_name = lookup_name.replace("encoder.embed_tokens", "shared") else: target_name = lookup_name.replace("shared.", "encoder.embed_tokens.", 1) if not matched and target_name in params_dict: weight_loader = getattr(params_dict[target_name], "weight_loader", default_weight_loader) weight_loader(params_dict[target_name], loaded_weight) loaded_params.add(target_name) matched = True if not matched: continue if target_name is not None and target_name in params_dict and target_name not in loaded_params: if target_name != lookup_name: weight_loader = getattr(params_dict[target_name], "weight_loader", default_weight_loader) weight_loader(params_dict[target_name], loaded_weight) loaded_params.add(target_name) loaded_params.add(original_name) loaded_params.add(lookup_name) return loaded_params