Files
2026-07-13 13:33:03 +08:00

1431 lines
66 KiB
Python

import math
import torch
import torch.nn.functional as F
from typing import Optional, Tuple
from .model_mapper import ModelMapper
from .custom_op import FusedAttention, FusedRoPE, MoE, FusedLinearAttention
class Embedding(torch.nn.Module):
def __init__(self, embed, config):
super().__init__()
self.hidden_size = config.hidden_size
self.embed = embed
self.embed_scale = 1.0
config_embed_scale = getattr(config, 'scale_emb', None)
if config_embed_scale is not None:
self.embed_scale = config_embed_scale
elif config.model_type == 'gemma' or config.model_type == 'gemma2':
self.embed_scale = self.hidden_size**0.5
if hasattr(embed, 'embed_scale'):
self.embed_scale = embed.embed_scale
# Replace ScaledWordEmbedding with plain Embedding to avoid double
# scaling (scale is applied separately via model.scale_emb)
if hasattr(embed, 'scalar_embed_scale'):
plain_embed = torch.nn.Embedding(embed.num_embeddings, embed.embedding_dim, embed.padding_idx)
plain_embed.weight = embed.weight
self.embed = plain_embed
def forward(self, input_ids):
inputs_embeds = self.embed(input_ids).view(-1, 1, self.hidden_size)
return inputs_embeds
class RMSNorm(torch.nn.Module):
def __init__(self, hidden_size, eps=1e-6):
"""
LlamaRMSNorm is equivalent to T5LayerNorm
"""
super().__init__()
self.weight = torch.nn.Parameter(torch.ones(hidden_size))
self.variance_epsilon = eps
def forward(self, hidden_states, gate=None):
input_dtype = hidden_states.dtype
hidden_states = hidden_states.to(torch.float32)
variance = hidden_states.pow(2).mean(-1, keepdim=True)
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
hidden_states = self.weight * hidden_states.to(input_dtype)
if gate is not None:
hidden_states = hidden_states * F.silu(gate.to(torch.float32))
return hidden_states.to(input_dtype)
def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
batch, num_key_value_heads, slen, head_dim = hidden_states.shape
if n_rep == 1:
return hidden_states
hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim)
return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
class Attention(torch.nn.Module):
def __init__(self, attn, layer_id, config, rotary, mapper):
super().__init__()
self.export_fused_attn = False
if config is None: return
self.config = config
self.kv_cache = True
self.layer_id = layer_id
self.rotary = rotary
export_args = getattr(config, 'export_args', None)
self.export_fused_rope = getattr(export_args, 'transformer_c4', False)
self.hidden_size = config.hidden_size
self.head_dim = config.head_dim
if isinstance(config.num_attention_heads, list):
self.num_heads = config.num_attention_heads[layer_id]
self.num_key_value_heads = config.num_key_value_heads[layer_id]
else:
self.head_dim = config.head_dim
self.num_heads = config.num_attention_heads
self.num_key_value_heads = config.num_key_value_heads
self.num_key_value_groups = self.num_heads // self.num_key_value_heads
ModelMapper.do_map(self, attn, mapper['attention'])
self.qk_norm_after_rope = getattr(config, 'qk_norm_after_rope', False)
if not self.qk_norm_after_rope:
self.qk_norm_after_rope = (
hasattr(attn, 'query_layernorm') and hasattr(attn, 'key_layernorm')
)
# Read attention scaling from the original HF attention module
if hasattr(attn, 'scaling'):
self.attn_scaling = attn.scaling
# k_eq_v / KV sharing detection (gemma4 and similar models)
# Mapper key 'k_eq_v' acts as sentinel: its presence means per-layer detection is needed.
# Detection is structural (works across HF versions):
# - k_proj exists, v_proj missing → k_eq_v (K serves as both K and V)
# - both missing + is_kv_shared_layer → pure KV sharing (no local K/V computation)
if getattr(self, 'k_eq_v', None) is not None:
has_k_proj = hasattr(self, 'k_proj') and self.k_proj is not None
has_v_proj = hasattr(self, 'v_proj') and self.v_proj is not None
self.k_eq_v = has_k_proj and not has_v_proj
# per-layer head_dim auto-detection (gemma4 has varying head_dim)
if hasattr(self, 'q_proj') and self.q_proj is not None:
actual_head_dim = self.q_proj.out_features // self.num_heads
if actual_head_dim != self.head_dim:
self.head_dim = actual_head_dim
if has_k_proj:
actual_kv_heads = self.k_proj.out_features // self.head_dim
if actual_kv_heads != self.num_key_value_heads:
self.num_key_value_heads = actual_kv_heads
self.num_key_value_groups = self.num_heads // self.num_key_value_heads
else:
self.k_eq_v = False
# KV sharing (gemma4): track which layers share KV
self.is_kv_shared_layer = getattr(attn, 'is_kv_shared_layer', False)
self.kv_shared_layer_index = getattr(attn, 'kv_shared_layer_index', None)
self.store_full_length_kv = getattr(attn, 'store_full_length_kv', False)
# Create FusedAttention with KV sharing info
kv_shared_idx = self.kv_shared_layer_index if self.is_kv_shared_layer else -1
self.fused_attn = FusedAttention(self.num_heads * self.head_dim, self.kv_cache, f'/layers.{layer_id}/self_attn/FusedAttention', layer_id, kv_shared_idx)
self.fused_rope = FusedRoPE(self.head_dim, f'/layers.{layer_id}/self_attn/FusedRoPE')
if hasattr(self, 'qkv_proj') and self.qkv_proj is not None:
# split qkv linear to q, k, v
split_sizes = [self.hidden_size] * 3
if self.qkv_proj.weight.shape[0] != self.hidden_size * 3:
# M/GQA
split_sizes = [
self.num_heads * self.head_dim, # q_size
self.num_key_value_heads * self.head_dim, # k_size
self.num_key_value_heads * self.head_dim # v_size
]
self.q_proj = torch.nn.Linear(self.hidden_size, split_sizes[0])
self.k_proj = torch.nn.Linear(self.hidden_size, split_sizes[1])
self.v_proj = torch.nn.Linear(self.hidden_size, split_sizes[2])
if config.model_type == 'chatglm':
# chatglm-6b
qkv_weight = self.qkv_proj.weight.data.view(self.num_heads, 3, self.head_dim, self.hidden_size)
self.q_proj.weight.data = qkv_weight[:, 0, :, :].reshape(self.hidden_size, self.hidden_size)
self.k_proj.weight.data = qkv_weight[:, 1, :, :].reshape(self.hidden_size, self.hidden_size)
self.v_proj.weight.data = qkv_weight[:, 2, :, :].reshape(self.hidden_size, self.hidden_size)
qkv_bias = self.qkv_proj.bias.data.view(self.num_heads, 3, self.head_dim)
self.q_proj.bias.data = qkv_bias[:, 0, :].reshape(self.hidden_size)
self.k_proj.bias.data = qkv_bias[:, 1, :].reshape(self.hidden_size)
self.v_proj.bias.data = qkv_bias[:, 2, :].reshape(self.hidden_size)
else:
# other
qw, kw, vw = torch.split(self.qkv_proj.weight, split_sizes)
self.q_proj.weight.data = qw
self.k_proj.weight.data = kw
self.v_proj.weight.data = vw
if self.qkv_proj.bias is not None:
qb, kb, vb = torch.split(self.qkv_proj.bias, split_sizes)
self.q_proj.bias.data = qb
self.k_proj.bias.data = kb
self.v_proj.bias.data = vb
else:
data_type = self.q_proj.weight.dtype
self.q_proj.bias.data = torch.zeros(split_sizes[0], dtype=data_type)
self.k_proj.bias.data = torch.zeros(split_sizes[1], dtype=data_type)
self.v_proj.bias.data = torch.zeros(split_sizes[2], dtype=data_type)
self.q_proj.weight.requires_grad = False
self.k_proj.weight.requires_grad = False
self.v_proj.weight.requires_grad = False
self.q_proj.bias.requires_grad = False
self.k_proj.bias.requires_grad = False
self.v_proj.bias.requires_grad = False
self.past_key_value = None
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
rotary_pos_emb: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
bsz, q_len, _ = hidden_states.size()
query_states = self.q_proj(hidden_states)
key_states = None
value_states = None
if self.q_proj.out_features == 2 * self.num_heads * self.head_dim:
reshaped = query_states.view(bsz, q_len, self.num_heads, self.head_dim * 2)
query_states, gate = torch.split(reshaped, self.head_dim, dim=-1)
gate = gate.reshape(bsz, q_len, -1)
else:
gate = None
qk_norm_after_rope = getattr(self, 'qk_norm_after_rope', getattr(self.config, 'qk_norm_after_rope', False))
query_states = query_states.view(bsz, q_len, self.num_heads, self.head_dim)
q_norm_before_rope = not qk_norm_after_rope and hasattr(self, 'q_norm') and self.q_norm is not None
# KV sharing: for shared layers, reuse KV from source layer (test mode only)
shared_kv_cache = getattr(self, '_shared_kv_cache', None)
use_shared_kv = (self.is_kv_shared_layer and shared_kv_cache is not None
and self.kv_shared_layer_index in shared_kv_cache
and not torch.onnx.is_in_onnx_export())
k_norm_before_rope = False
if use_shared_kv:
key_states, value_states = shared_kv_cache[self.kv_shared_layer_index]
elif self.k_proj is not None:
key_states = self.k_proj(hidden_states)
if self.k_eq_v:
value_states = key_states.clone()
else:
value_states = self.v_proj(hidden_states)
key_states = key_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim)
value_states = value_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim)
k_norm_before_rope = not qk_norm_after_rope and hasattr(self, 'k_norm') and self.k_norm is not None
# gemma4 has v_norm (RMSNorm without scale)
if hasattr(self, 'v_norm') and self.v_norm is not None:
value_states = self.v_norm(value_states)
else:
# Pure KV sharing layer: no local K/V projections (e.g. gemma4 in HF>=5.5.4)
# Dummy K/V for ONNX tracing; FusedAttention handles sharing via kv_shared_layer_index
key_states = query_states.new_zeros(bsz, q_len, self.num_key_value_heads, self.head_dim)
value_states = key_states
k_norm_before_rope = False
kv_seq_len = key_states.shape[1]
if self.past_key_value is not None:
kv_seq_len += self.past_key_value[0].shape[1]
# rope
if self.rotary is not None:
cos, sin = rotary_pos_emb[0], rotary_pos_emb[1]
use_fused_rope = (
self.export_fused_attn and torch.onnx.is_in_onnx_export()
and self.export_fused_rope
and not qk_norm_after_rope
and not use_shared_kv
and self.k_proj is not None
and self.rotary.model_type not in ['chatglm', 'chatglm2', 'ernie4_5', 'glm_ocr']
and cos.shape[-1] == self.head_dim
and sin.shape[-1] == self.head_dim
)
fuse_qk_norm = use_fused_rope and q_norm_before_rope and k_norm_before_rope
if use_fused_rope:
if not fuse_qk_norm:
if q_norm_before_rope:
query_states = self.q_norm(query_states)
if k_norm_before_rope:
key_states = self.k_norm(key_states)
query_states, key_states = self.fused_rope(
query_states,
key_states,
cos,
sin,
self.q_norm if fuse_qk_norm else None,
self.k_norm if fuse_qk_norm else None,
)
else:
# Most models apply q/k norm before rotary, but HunYuan applies it after rotary.
if q_norm_before_rope:
query_states = self.q_norm(query_states)
if k_norm_before_rope:
key_states = self.k_norm(key_states)
query_states = self.rotary.apply_rotary_pos(query_states, cos, sin)
if not use_shared_kv and self.k_proj is not None:
key_states = self.rotary.apply_rotary_pos(key_states, cos, sin)
elif q_norm_before_rope or k_norm_before_rope:
if q_norm_before_rope:
query_states = self.q_norm(query_states)
if k_norm_before_rope:
key_states = self.k_norm(key_states)
if qk_norm_after_rope:
if hasattr(self, 'q_norm') and self.q_norm is not None:
query_states = self.q_norm(query_states)
if not use_shared_kv and self.k_proj is not None and hasattr(self, 'k_norm') and self.k_norm is not None:
key_states = self.k_norm(key_states)
# MobileLLM model llama4_text has qk_norm after rotary
if hasattr(self, 'qk_norm') and self.qk_norm is not None :
query_states = self.qk_norm(query_states)
key_states = self.qk_norm(key_states)
if self.export_fused_attn and torch.onnx.is_in_onnx_export():
attn_output = self.fused_attn(query_states, key_states, value_states, attention_mask)
if gate is not None:
attn_output = attn_output * torch.sigmoid(gate)
attn_output = self.o_proj(attn_output)
return attn_output
# kv cache
if self.past_key_value is not None:
past_key, past_value = self.past_key_value[0], self.past_key_value[1]
key_states = torch.cat((past_key, key_states), dim=1)
value_states = torch.cat((past_value, value_states), dim=1)
if not use_shared_kv:
self.past_key_value = torch.stack((key_states, value_states))
query_states = query_states.transpose(1, 2)
if use_shared_kv:
# Shared KV is already in transposed format [B, heads, head_dim, seq] / [B, heads, seq, head_dim]
pass
else:
key_states = key_states.permute([0, 2, 3, 1])
value_states = value_states.transpose(1, 2)
# Store KV for sharing (source layers that other layers will read from)
if self.store_full_length_kv and shared_kv_cache is not None:
shared_kv_cache[self.layer_id] = (key_states.clone(), value_states.clone())
# repeat k/v heads if n_kv_heads < n_heads
key_states = repeat_kv(key_states, self.num_key_value_groups)
value_states = repeat_kv(value_states, self.num_key_value_groups)
#------- attention ----------
# query_states @ key_states
attn_scaling = getattr(self, 'attn_scaling', 1.0 / math.sqrt(self.head_dim))
attn_weights = torch.matmul(query_states, key_states) * attn_scaling
# attention_mask
if attention_mask.dtype in (torch.bool, torch.int32):
# chatglm
attn_weights.masked_fill_(attention_mask, -10000.0)
else:
attn_weights = attn_weights + attention_mask
if hasattr(self, 'sinks'):
sinks = self.sinks.reshape(1, -1, 1, 1).to(torch.float32).expand(query_states.shape[0], -1, query_states.shape[-2], -1)
combined_logits = torch.cat([attn_weights, sinks], dim=-1)
combined_logits = combined_logits - combined_logits.max(dim=-1, keepdim=True).values
probs = F.softmax(combined_logits, dim=-1, dtype=torch.float32).to(query_states.dtype)
attn_weights = probs[..., :-1] # we drop the sink here
else:
# upcast softmax to fp32
attn_weights = F.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype)
# attn_weights @ value_states
attn_output = torch.matmul(attn_weights, value_states)
attn_output = attn_output.transpose(1, 2).contiguous()
attn_output = attn_output.reshape(bsz, q_len, -1)
if gate is not None:
attn_output = attn_output * torch.sigmoid(gate)
attn_output = self.o_proj(attn_output)
return attn_output
def l2norm(x: torch.FloatTensor, dim: int = -1, eps: float = 1e-6):
"""This function is intended to align with the l2norm implementation in the FLA library."""
inv_norm = torch.rsqrt((x * x).sum(dim=dim, keepdim=True) + eps)
return x * inv_norm
def torch_chunk_gated_delta_rule(
query,
key,
value,
g,
beta,
chunk_size=64,
initial_state=None,
output_final_state=False,
use_qk_l2norm_in_kernel=False,
):
initial_dtype = query.dtype
if use_qk_l2norm_in_kernel:
query = l2norm(query, dim=-1, eps=1e-6)
key = l2norm(key, dim=-1, eps=1e-6)
query, key, value, beta, g = [
x.transpose(1, 2).contiguous().to(torch.float32) for x in (query, key, value, beta, g)
]
batch_size, num_heads, sequence_length, k_head_dim = key.shape
v_head_dim = value.shape[-1]
pad_size = (chunk_size - sequence_length % chunk_size) % chunk_size
query = F.pad(query, (0, 0, 0, pad_size))
key = F.pad(key, (0, 0, 0, pad_size))
value = F.pad(value, (0, 0, 0, pad_size))
beta = F.pad(beta, (0, pad_size))
g = F.pad(g, (0, pad_size))
total_sequence_length = sequence_length + pad_size
scale = 1 / (query.shape[-1] ** 0.5)
query = query * scale
v_beta = value * beta.unsqueeze(-1)
k_beta = key * beta.unsqueeze(-1)
# reshape to chunks
query, key, value, k_beta, v_beta = [
x.reshape(x.shape[0], x.shape[1], -1, chunk_size, x.shape[-1]) for x in (query, key, value, k_beta, v_beta)
]
g = g.reshape(g.shape[0], g.shape[1], -1, chunk_size)
mask = torch.triu(torch.ones(chunk_size, chunk_size, dtype=torch.bool, device=query.device), diagonal=0)
# chunk decay
g = g.cumsum(dim=-1)
decay_mask = ((g.unsqueeze(-1) - g.unsqueeze(-2)).tril().exp().float()).tril()
attn = -((k_beta @ key.transpose(-1, -2)) * decay_mask).masked_fill(mask, 0)
for i in range(1, chunk_size):
row = attn[..., i, :i].clone()
sub = attn[..., :i, :i].clone()
attn[..., i, :i] = row + (row.unsqueeze(-1) * sub).sum(-2)
attn = attn + torch.eye(chunk_size, dtype=attn.dtype, device=attn.device)
value = attn @ v_beta
k_cumdecay = attn @ (k_beta * g.exp().unsqueeze(-1))
last_recurrent_state = (
torch.zeros(batch_size, num_heads, k_head_dim, v_head_dim).to(value)
if initial_state is None
else initial_state.to(value)
)
core_attn_out = torch.zeros_like(value)
mask = torch.triu(torch.ones(chunk_size, chunk_size, dtype=torch.bool, device=query.device), diagonal=1)
# for each chunk
for i in range(0, total_sequence_length // chunk_size):
q_i, k_i, v_i = query[:, :, i], key[:, :, i], value[:, :, i]
attn = (q_i @ k_i.transpose(-1, -2) * decay_mask[:, :, i]).masked_fill_(mask, 0)
v_prime = (k_cumdecay[:, :, i]) @ last_recurrent_state
v_new = v_i - v_prime
attn_inter = (q_i * g[:, :, i, :, None].exp()) @ last_recurrent_state
core_attn_out[:, :, i] = attn_inter + attn @ v_new
last_recurrent_state = (
last_recurrent_state * g[:, :, i, -1, None, None].exp()
+ (k_i * (g[:, :, i, -1, None] - g[:, :, i]).exp()[..., None]).transpose(-1, -2) @ v_new
)
if not output_final_state:
last_recurrent_state = None
core_attn_out = core_attn_out.reshape(core_attn_out.shape[0], core_attn_out.shape[1], -1, core_attn_out.shape[-1])
core_attn_out = core_attn_out[:, :, :sequence_length]
core_attn_out = core_attn_out.transpose(1, 2).contiguous().to(initial_dtype)
return core_attn_out, last_recurrent_state
def torch_recurrent_gated_delta_rule(
query, key, value, g, beta, initial_state, output_final_state, use_qk_l2norm_in_kernel=False
):
initial_dtype = query.dtype
if use_qk_l2norm_in_kernel:
query = l2norm(query, dim=-1, eps=1e-6)
key = l2norm(key, dim=-1, eps=1e-6)
query, key, value, beta, g = [
x.transpose(1, 2).contiguous().to(torch.float32) for x in (query, key, value, beta, g)
]
batch_size, num_heads, sequence_length, k_head_dim = key.shape
v_head_dim = value.shape[-1]
scale = 1 / (query.shape[-1] ** 0.5)
query = query * scale
core_attn_out = torch.zeros(batch_size, num_heads, sequence_length, v_head_dim).to(value)
last_recurrent_state = (
torch.zeros(batch_size, num_heads, k_head_dim, v_head_dim).to(value)
if initial_state is None
else initial_state.to(value)
)
for i in range(sequence_length):
q_t = query[:, :, i]
k_t = key[:, :, i]
v_t = value[:, :, i]
g_t = g[:, :, i].exp().unsqueeze(-1).unsqueeze(-1)
beta_t = beta[:, :, i].unsqueeze(-1)
last_recurrent_state = last_recurrent_state * g_t
kv_mem = (last_recurrent_state * k_t.unsqueeze(-1)).sum(dim=-2)
delta = (v_t - kv_mem) * beta_t
last_recurrent_state = last_recurrent_state + k_t.unsqueeze(-1) * delta.unsqueeze(-2)
core_attn_out[:, :, i] = (last_recurrent_state * q_t.unsqueeze(-1)).sum(dim=-2)
if not output_final_state:
last_recurrent_state = None
core_attn_out = core_attn_out.transpose(1, 2).contiguous().to(initial_dtype)
return core_attn_out, last_recurrent_state
def torch_gated_delta_rule(
query, # [B, L, H, K] query vectors
key, # [B, L, H, K] key vectors
value, # [B, L, H, V] value vectors
g, # [B, L, H] log-space decay (negative values)
beta, # [B, L, H] learning rate for delta update
initial_state=None, # [B, H, K, V] initial recurrent state
output_final_state=False,
use_qk_l2norm_in_kernel=False,
):
"""
Non-chunk implementation of Gated Delta Rule (Linear Attention).
Mathematically equivalent to torch_chunk_gated_delta_rule.
Maintains a key-value memory (state S) of shape [K, V] per head,
updated at each timestep using the Delta Learning Rule with gating.
Per-step formula (for each head independently):
─────────────────────────────────────────────────
S_t = S_{t-1} * exp(g_t) # 1. decay old memory
v_pred = S_t^T @ k_t # 2. predict value for current key
delta = beta_t * (v_t - v_pred) # 3. prediction error * learning rate
S_t = S_t + k_t @ delta^T # 4. update memory (outer product)
o_t = S_t^T @ (q_t / sqrt(d_k)) # 5. query the memory
─────────────────────────────────────────────────
Shapes:
S: [B, H, K, V] recurrent state (key-value memory)
k_t: [B, H, K] key at timestep t
v_t: [B, H, V] value at timestep t
q_t: [B, H, K] query at timestep t
g_t: [B, H] log-decay scalar per head
beta_t:[B, H] learning rate scalar per head
o_t: [B, H, V] output at timestep t
"""
initial_dtype = query.dtype
# Optional: L2 normalize Q, K before computation
if use_qk_l2norm_in_kernel:
query = l2norm(query, dim=-1, eps=1e-6)
key = l2norm(key, dim=-1, eps=1e-6)
# [B, L, H, D] -> [B, H, L, D], all in float32
query, key, value, beta, g = [
x.transpose(1, 2).contiguous().to(torch.float32)
for x in (query, key, value, beta, g)
]
B, H, L, K = key.shape
V = value.shape[-1]
# Scale query: q = q / sqrt(d_k)
query = query * (K ** -0.5)
# Initialize recurrent state S: [B, H, K, V]
if initial_state is None:
S = torch.zeros(B, H, K, V, device=query.device, dtype=torch.float32)
else:
S = initial_state.to(torch.float32)
outputs = []
for t in range(L):
q_t = query[:, :, t] # [B, H, K]
k_t = key[:, :, t] # [B, H, K]
v_t = value[:, :, t] # [B, H, V]
g_t = g[:, :, t] # [B, H]
beta_t = beta[:, :, t] # [B, H]
# ── Step 1: Decay ──
# S = S * exp(g_t), g_t < 0 so this shrinks old memory
S = S * g_t[:, :, None, None].exp()
# ── Step 2: Read (predict value for current key) ──
# v_pred = S^T @ k_t : [B, H, K, V]^T @ [B, H, K, 1] -> [B, H, V]
v_pred = (S.transpose(-1, -2) @ k_t.unsqueeze(-1)).squeeze(-1)
# ── Step 3: Delta (prediction error * learning rate) ──
# delta = beta_t * (v_t - v_pred) : [B, H, V]
delta = beta_t[:, :, None] * (v_t - v_pred)
# ── Step 4: Write (update memory with outer product) ──
# S += k_t @ delta^T : [B, H, K, 1] @ [B, H, 1, V] -> [B, H, K, V]
S = S + k_t.unsqueeze(-1) @ delta.unsqueeze(-2)
# ── Step 5: Query (read output from updated memory) ──
# o_t = S^T @ q_t : [B, H, K, V]^T @ [B, H, K, 1] -> [B, H, V]
o_t = (S.transpose(-1, -2) @ q_t.unsqueeze(-1)).squeeze(-1)
outputs.append(o_t)
# Stack: list of [B, H, V] -> [B, H, L, V]
core_attn_out = torch.stack(outputs, dim=2)
if not output_final_state:
S = None
# [B, H, L, V] -> [B, L, H, V], restore original dtype
core_attn_out = core_attn_out.transpose(1, 2).contiguous().to(initial_dtype)
return core_attn_out, S
class ShortConvAttention(torch.nn.Module):
def __init__(self, attn, layer_id, config, mapper):
super().__init__()
self.layer_id = layer_id
self.hidden_size = config.hidden_size
self.conv_kernel_size = config.conv_L_cache
ModelMapper.do_map(self, attn, mapper['linear_attention'])
self.fused_attn = FusedLinearAttention(
name=f'/layers.{layer_id}/self_attn/FusedLinearAttention',
attn_type="short_conv",
num_k_heads=1,
num_v_heads=1,
head_k_dim=self.hidden_size,
head_v_dim=self.hidden_size,
use_qk_l2norm=False
)
self.conv_state = None
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
) -> torch.Tensor:
# Note: ShortConvAttention is mask-free; `attention_mask` is accepted
# only to keep the call signature uniform with `Attention.forward` and
# is intentionally unused.
batch_size, seq_len, _ = hidden_states.shape
# in_proj: [B, L, H] -> [B, L, 3H]
bcx = self.in_proj(hidden_states)
if torch.onnx.is_in_onnx_export():
# ONNX path: pass through FusedLinearAttention custom op
bcx_t = bcx.transpose(1, 2) # [B, 3H, L]
gate = torch.zeros(batch_size, seq_len, 1, dtype=bcx.dtype, device=bcx.device)
beta = torch.zeros(batch_size, seq_len, 1, dtype=bcx.dtype, device=bcx.device)
attn_out = self.fused_attn(bcx_t, gate, beta, self.conv.weight.data.detach())
# attn_out: [B, L, 1, H] -> [B, L, H]
attn_out = attn_out.view(batch_size, seq_len, -1)
output = self.out_proj(attn_out)
return output
# Test path: manual computation
# Split into B_, C_, x_ each [B, L, H]
B_, C_, x_ = bcx.chunk(3, dim=-1)
# Bx = B_ * x_
Bx = B_ * x_
# Transpose for conv: [B, H, L]
Bx = Bx.transpose(1, 2)
conv_state_size = self.conv_kernel_size - 1
if self.conv_state is not None:
conv_input = torch.cat([self.conv_state, Bx], dim=-1)
conv_out = F.conv1d(conv_input, self.conv.weight, padding=0, groups=self.hidden_size)
new_conv_state = conv_input[:, :, -conv_state_size:]
else:
new_conv_state = F.pad(Bx, (conv_state_size - Bx.shape[-1], 0))
conv_out = self.conv(Bx)[:, :, :seq_len]
# No SiLU for short_conv (unlike gated_delta_rule)
# Transpose back: [B, H, L] -> [B, L, H]
conv_out = conv_out.transpose(1, 2)
# y = C_ * conv_out
y = C_ * conv_out
output = self.out_proj(y)
self.conv_state = new_conv_state
return output
class LinearAttention(torch.nn.Module):
def __init__(self, attn, layer_id, config, rotary, mapper):
super().__init__()
self.layer_id = layer_id
self.config = config
self.hidden_size = config.hidden_size
self.head_dim = config.head_dim
self.conv_kernel_size = config.linear_conv_kernel_dim
self.conv_state_size = self.conv_kernel_size - 1
self.head_k_dim = config.linear_key_head_dim
self.head_v_dim = config.linear_value_head_dim
self.num_k_heads = config.linear_num_key_heads
self.num_v_heads = config.linear_num_value_heads
self.key_dim = self.head_k_dim * self.num_k_heads
self.value_dim = self.head_v_dim * self.num_v_heads
self.conv_dim = self.key_dim * 2 + self.value_dim
ModelMapper.do_map(self, attn, mapper['linear_attention'])
original_norm = self.norm
self.norm = RMSNorm(self.head_v_dim, eps=config.rms_norm_eps)
self.norm.weight.data = original_norm.weight.data
self.fused_attn = FusedLinearAttention(
name=f'/layers.{layer_id}/self_attn/FusedLinearAttention',
attn_type="gated_delta_rule",
num_k_heads=self.num_k_heads,
num_v_heads=self.num_v_heads,
head_k_dim=self.head_k_dim,
head_v_dim=self.head_v_dim,
use_qk_l2norm=True
)
self.conv_state = None
self.rnn_state = None
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
# Note: LinearAttention is mask-free; `attention_mask` is accepted
# only to keep the call signature uniform with `Attention.forward` and
# is intentionally unused.
batch_size, seq_len, _ = hidden_states.shape
# 1. Linear Projections
# mixed_qkv: [B, L, 2*key_dim + value_dim]
mixed_qkv = self.in_proj_qkv(hidden_states)
# Transpose for Conv1d: [B, Dim, L]
mixed_qkv = mixed_qkv.transpose(1, 2)
# Gate, Beta, Z projections
z = self.in_proj_z(hidden_states) # [B, L, value_dim]
b = self.in_proj_b(hidden_states) # [B, L, num_v_heads]
a = self.in_proj_a(hidden_states) # [B, L, num_v_heads]
# 2. Pre-compute gates
beta = torch.sigmoid(b)
gate = -self.A_log.float().exp() * F.softplus(a.float() + self.dt_bias)
if torch.onnx.is_in_onnx_export():
attn_out = self.fused_attn(mixed_qkv, gate, beta, self.conv1d.weight.data.detach())
attn_out = attn_out.reshape(-1, self.head_v_dim)
z = z.reshape(-1, self.head_v_dim)
attn_out = self.norm(attn_out, z)
attn_out = attn_out.view(batch_size, seq_len, -1)
output = self.out_proj(attn_out)
return output
# === Normal path: full computation for testing ===
# 3. State Management (Conv State & Recurrent State)
if self.conv_state is not None:
conv_state = self.conv_state
conv_input = torch.cat([conv_state, mixed_qkv], dim=-1)
mixed_qkv = F.silu(F.conv1d(conv_input, self.conv1d.weight, self.conv1d.bias, padding=0, groups=self.conv_dim))
new_conv_state = conv_input[:, :, -self.conv_state_size:]
else:
new_conv_state = F.pad(mixed_qkv, (self.conv_state_size - mixed_qkv.shape[-1], 0))
mixed_qkv = F.silu(self.conv1d(mixed_qkv)[:, :, :seq_len])
# 4. Split Q, K, V
mixed_qkv = mixed_qkv.transpose(1, 2)
query, key, value = torch.split(
mixed_qkv,
[self.key_dim, self.key_dim, self.value_dim],
dim=-1
)
query = query.view(batch_size, seq_len, self.num_k_heads, self.head_k_dim)
key = key.view(batch_size, seq_len, self.num_k_heads, self.head_k_dim)
value = value.view(batch_size, seq_len, self.num_v_heads, self.head_v_dim)
# 5. GQA Expansion
if self.num_v_heads > self.num_k_heads:
factor = self.num_v_heads // self.num_k_heads
query = query.repeat_interleave(factor, dim=2)
key = key.repeat_interleave(factor, dim=2)
# 6. Gated Delta Rule
if self.rnn_state is None:
attn_out, last_recurrent_state = torch_gated_delta_rule(
query, key, value,
g=gate, beta=beta,
initial_state=None,
output_final_state=True,
use_qk_l2norm_in_kernel=True,
)
else:
recurrent_state = self.rnn_state
attn_out, last_recurrent_state = torch_recurrent_gated_delta_rule(
query, key, value,
g=gate, beta=beta,
initial_state=recurrent_state,
output_final_state=True,
use_qk_l2norm_in_kernel=True,
)
# 7. Post-process
attn_out = attn_out.reshape(-1, self.head_v_dim)
z = z.reshape(-1, self.head_v_dim)
attn_out = self.norm(attn_out, z)
attn_out = attn_out.view(batch_size, seq_len, -1)
output = self.out_proj(attn_out)
# Update internal state
self.conv_state = new_conv_state
self.rnn_state = last_recurrent_state
return output
def create_linear_attention(attn, layer_id, config, rotary, mapper):
"""Factory function for creating LinearAttention variants based on config."""
if hasattr(config, 'conv_L_cache') and config.conv_L_cache > 0:
return ShortConvAttention(attn, layer_id, config, mapper)
return LinearAttention(attn, layer_id, config, rotary, mapper)
def rotate_half(x):
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
def _compute_yarn_parameters(rotary_dim, base_theta, scaling_config, max_position_embeddings):
"""
计算 YaRN (Yet another RoPE extensioN method) 的参数。
此函数等价于 Hugging Face Transformers 中的 YaRN 实现。
Args:
rotary_dim (int): RoPE 的维度。
base_theta (float): RoPE 的基础 theta 值。
scaling_config (dict): 包含 YaRN 特定配置的字典。
max_position_embeddings (int): 模型的最大位置编码。
Returns:
tuple[torch.Tensor, float]:
- inv_freq (torch.Tensor): 计算好的、用于 RoPE 的逆频率 (即 theta)。
- attention_scaling (float): 应用于 Query 向量的缩放因子。
"""
def get_mscale(scale, m_scale):
if scale <= 1:
return 1.0
return 0.1 * m_scale * math.log(scale) + 1.0
def find_correction_dim(num_rotations, d, b, max_pos):
return (d * math.log(max_pos / (num_rotations * 2 * math.pi))) / (2 * math.log(b))
def find_correction_range(low_rot, high_rot, d, b, max_pos):
low = find_correction_dim(low_rot, d, b, max_pos)
high = find_correction_dim(high_rot, d, b, max_pos)
return max(0, math.floor(low)), min(d - 1, math.ceil(high))
def linear_ramp_factor(mn, mx, d):
if mn == mx:
mx += 0.001
linear_func = (torch.arange(d, dtype=torch.float32) - mn) / (mx - mn)
return torch.clamp(linear_func, 0, 1)
# 1. 提取 YaRN 参数
factor = scaling_config['factor']
beta_fast = scaling_config.get("beta_fast", 32)
beta_slow = scaling_config.get("beta_slow", 1)
original_max_pos = scaling_config.get("original_max_position_embeddings", max_position_embeddings)
mscale = scaling_config.get("mscale", 1.0)
# 2. 计算 attention_scaling (即 attention_factor)
attention_scaling = get_mscale(factor, mscale)
# 3. 计算 inv_freq (即 theta)
dim = rotary_dim
# 计算插值和外推的频率
pos_freqs = base_theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim)
inv_freq_extrapolation = 1.0 / pos_freqs
inv_freq_interpolation = 1.0 / (factor * pos_freqs)
# 找到需要修正的维度范围
low, high = find_correction_range(beta_fast, beta_slow, dim, base_theta, original_max_pos)
# 创建维度混合的 ramp (作用于 dim//2 的频率上)
inv_freq_extrapolation_factor = 1 - linear_ramp_factor(low, high, dim // 2)
# 混合插值和外推频率,得到最终的 inv_freq
inv_freq = (
inv_freq_interpolation * (1 - inv_freq_extrapolation_factor)
+ inv_freq_extrapolation * inv_freq_extrapolation_factor
)
return inv_freq, attention_scaling
class Rotary(torch.nn.Module):
def __init__(self, config):
super().__init__()
if config is None: return
self.rope_theta = config.rope_theta
self.rope_ratio = config.rope_ratio
if self.rope_ratio is not None:
self.rope_theta *= self.rope_ratio
self.rotary_dim = config.head_dim
self.model_type = config.model_type
if hasattr(config, 'rotary_dim'):
self.rotary_dim = config.rotary_dim
if self.model_type == 'chatglm':
self.rotary_dim = config.head_dim // 2
# Qwen3.5 / LFM2 style: flat rope_parameters dict
if hasattr(config, 'rope_parameters') and config.rope_parameters is not None:
rp = config.rope_parameters
# Detect gemma4-style per-layer-type rope_parameters (dict of dicts)
is_per_layer_type = any(isinstance(v, dict) for v in rp.values())
if not is_per_layer_type:
if 'rope_theta' in rp:
self.rope_theta = rp['rope_theta']
if 'partial_rotary_factor' in rp:
self.partial_rotary_factor = rp['partial_rotary_factor']
self.rotary_dim = int(self.rotary_dim * self.partial_rotary_factor)
config.rope_scaling = rp
self.mrope_section = None
self.theta_sections = None
self.attention_scaling = 1.0
self.is_scaled = False
self.mrope_interleaved = False
def get_theta():
return 1.0 / (self.rope_theta ** (torch.arange(0, self.rotary_dim, 2, dtype=torch.float32) / self.rotary_dim))
# default rope type's theta
self.theta = get_theta()
# other type
if hasattr(config, 'rope_scaling') and config.rope_scaling is not None:
scaling_config = config.rope_scaling
# get rope_type
rope_type = 'default'
if 'type' in config.rope_scaling:
rope_type = config.rope_scaling['type']
elif 'rope_type' in config.rope_scaling:
rope_type = config.rope_scaling['rope_type']
# gen theta for rope_type
if rope_type == 'dynamic': # NTK
if 'alpha' in config.rope_scaling: # NTKAlpha in Hunyuan
self.rope_theta *= (config.rope_scaling['alpha'] ** (self.rotary_dim / (self.rotary_dim - 2)))
else: # NTKScaling
pass
self.theta = get_theta()
elif rope_type == 'yarn':
self.is_scaled = True
self.theta, self.attention_scaling = _compute_yarn_parameters(
rotary_dim=self.rotary_dim,
base_theta=self.rope_theta,
scaling_config=scaling_config,
max_position_embeddings=config.max_position_embeddings
)
elif rope_type == 'longrope': # longrope in MiniCPM
self.is_scaled = True
original_max_position_embeddings = config.rope_scaling['original_max_position_embeddings']
scale = (config.max_position_embeddings / original_max_position_embeddings)
self.attention_scaling = math.sqrt(1 + math.log(scale) / math.log(original_max_position_embeddings))
# long_factor = config.rope_scaling['long_factor']
short_factor = config.rope_scaling['short_factor']
self.theta = get_theta() / torch.tensor(short_factor, dtype=torch.float32)
# mrope for multimode
if 'mrope_section' in scaling_config:
self.mrope_interleaved = scaling_config.get('mrope_interleaved', False)
self.mrope_section = scaling_config['mrope_section']
self.theta = get_theta().unsqueeze(0)
self.theta_sections = self.theta.split(self.mrope_section, dim=-1)
def apply_interleaved_mrope(freqs, mrope_section):
# mrope apply func from qwen3-vl
freqs_t = freqs[0] # just overwrite the first dimension T
for dim, offset in enumerate((1, 2), start=1): # H, W
length = mrope_section[dim] * 3
idx = slice(offset, length, 3)
freqs_t[..., idx] = freqs[dim, ..., idx]
return freqs_t
if self.mrope_interleaved:
half_rotary = self.rotary_dim // 2
freq_idx = torch.arange(0, 3 * half_rotary).reshape(3, 1, half_rotary)
self.mrope_reindex = apply_interleaved_mrope(freq_idx, self.mrope_section).flatten()
self.is_mrope = self.theta_sections is not None or self.mrope_interleaved
def forward(self, position_ids):
if self.is_mrope:
return self.mrope_forward(position_ids)
position_ids = position_ids.float().reshape(-1, 1)
idx_theta = position_ids * self.theta.to(position_ids.device)
rotary_pos_emb = torch.stack([torch.cos(idx_theta), torch.sin(idx_theta)])
if self.model_type == 'ernie4_5':
rotary_pos_emb = torch.stack((rotary_pos_emb, rotary_pos_emb), dim=-1)
rotary_pos_emb = rotary_pos_emb.reshape(*rotary_pos_emb.shape[:-2], -1)
elif self.model_type != 'chatglm2':
rotary_pos_emb = torch.cat((rotary_pos_emb, rotary_pos_emb), dim=-1)
rotary_pos_emb = rotary_pos_emb.unsqueeze(2).unsqueeze(1)
if self.is_scaled:
rotary_pos_emb *= self.attention_scaling
return rotary_pos_emb
def mrope_forward(self, position_ids):
position_ids = position_ids.float().unsqueeze(-1)
if self.mrope_interleaved:
idx_theta = position_ids * self.theta.to(position_ids.device)
idx_theta = idx_theta.transpose(1, 0).reshape(-1, 3 * self.rotary_dim // 2)
idx_theta = idx_theta[:, self.mrope_reindex]
else:
idx_theta = torch.concat([
position_ids[0] * self.theta_sections[0],
position_ids[1] * self.theta_sections[1],
position_ids[2] * self.theta_sections[2]
], dim=-1)
rotary_pos_emb = torch.stack([torch.cos(idx_theta), torch.sin(idx_theta)])
if self.model_type in ['glm_ocr']:
# interleaved doubling: [c0,c0,c1,c1,...,cn,cn]
rotary_pos_emb = torch.stack((rotary_pos_emb, rotary_pos_emb), dim=-1)
rotary_pos_emb = rotary_pos_emb.reshape(*rotary_pos_emb.shape[:-2], -1)
else:
rotary_pos_emb = torch.cat((rotary_pos_emb, rotary_pos_emb), dim=-1)
rotary_pos_emb = rotary_pos_emb.unsqueeze(2).unsqueeze(1)
return rotary_pos_emb
def apply_rotary_pos(self, x, cos, sin):
if self.model_type == 'chatglm':
return self.chatglm_rotary_pos(x, cos, sin)
if self.model_type == 'chatglm2':
return self.chatglm2_rotary_pos(x, cos, sin)
if self.model_type in ['phi-msft', 'qwen3_5', 'qwen3_5_moe']:
return self.phi_rotary_pos(x, cos, sin)
if self.model_type in ['ernie4_5', 'glm_ocr']:
return self.ernie_rotary_pos(x, cos, sin)
# Auto-detect partial rotary: cos/sin dim < x dim
if cos.shape[-1] < x.shape[-1]:
return self.phi_rotary_pos(x, cos, sin)
return self.llama_rotary_pos(x, cos, sin)
def llama_rotary_pos(self, x, cos, sin):
x = (x * cos) + (rotate_half(x) * sin)
return x
def ernie_rotary_pos(self, x, cos, sin):
rotate_half_x = torch.stack(
[-x[:, :, :, 1::2], x[:, :, :, 0::2]], dim=-1
).reshape(x.shape)
x = (x * cos) + (rotate_half_x * sin)
return x
def phi_rotary_pos(self, x, cos, sin):
# Use cos dim to determine rotary_dim (handles per-layer different rotary_dim)
rotary_dim = cos.shape[-1]
x, x_pass = x[..., :rotary_dim], x[..., rotary_dim:]
x = (x * cos) + (rotate_half(x) * sin)
return torch.cat((x, x_pass), dim=-1)
def chatglm2_rotary_pos(self, x, cos, sin):
x, x_pass = x[..., :self.rotary_dim], x[..., self.rotary_dim:]
b, s, n, h = x.shape
xshaped = x.view(b, s, n, h//2, 2)
x = torch.concat(
[
xshaped[..., 0] * cos - xshaped[..., 1] * sin,
xshaped[..., 1] * cos + xshaped[..., 0] * sin,
],
-1,
)
return torch.cat((x, x_pass), dim=-1)
def chatglm_rotary_pos(self, x, cos, sin):
seq = x.shape[1]
x1, x2 = x[..., :self.rotary_dim], x[..., self.rotary_dim:]
cos1, sin1 = cos[:, :seq, ...], sin[:, :seq, ...]
cos2, sin2 = cos[:, seq:, ...], sin[:, seq:, ...]
x1 = (x1 * cos1) + (rotate_half(x1) * sin1)
x2 = (x2 * cos2) + (rotate_half(x2) * sin2)
return torch.cat((x1, x2), dim=-1)
class VisionRotary(Rotary):
def __init__(self, config):
super().__init__(config)
# support [h_pos, w_pos]
def forward(self, position_ids):
# [2, patch_len, 1]
position_ids = position_ids.float().unsqueeze(-1)
idx_theta = position_ids * self.theta
# [patch_len, rotary_dim]
idx_theta = idx_theta.permute(1, 0, 2).reshape(-1, self.rotary_dim)
rotary_pos_emb = torch.stack([torch.cos(idx_theta), torch.sin(idx_theta)])
rotary_pos_emb = torch.cat((rotary_pos_emb, rotary_pos_emb), dim=-1)
rotary_pos_emb = rotary_pos_emb.unsqueeze(2).unsqueeze(1)
return rotary_pos_emb
def apply_rotary_pos(self, x, cos, sin):
x = (x * cos) + (rotate_half(x) * sin)
return x
class GptOssExpert(torch.nn.Module):
def __init__(self, hidden_size, expert_dim):
super().__init__()
self.expert_dim = expert_dim
self.gate_up_proj_linear = torch.nn.Linear(hidden_size, 2 * expert_dim)
self.down_proj_linear = torch.nn.Linear(expert_dim, hidden_size)
self.alpha = 1.702
self.limit = 7.0
def forward(self, hidden_states: torch.Tensor, debug=False) -> torch.Tensor:
gate_up = self.gate_up_proj_linear(hidden_states)
gate, up = gate_up[..., ::2], gate_up[..., 1::2]
# gate = gate.clamp(min=None, max=self.limit)
limit_tensor = torch.tensor(self.limit, device=gate.device, dtype=gate.dtype)
gate = torch.min(gate, limit_tensor)
up = up.clamp(min=-self.limit, max=self.limit)
glu = gate * torch.sigmoid(gate * self.alpha)
gated_output = (up + 1) * glu
out = self.down_proj_linear(gated_output)
return out
class Qwen3Expert(torch.nn.Module):
def __init__(self, hidden_size, expert_dim, act_fn):
super().__init__()
self.expert_dim = expert_dim
self.gate_up_proj_linear = torch.nn.Linear(hidden_size, 2 * expert_dim, bias=False)
self.down_proj_linear = torch.nn.Linear(expert_dim, hidden_size, bias=False)
self.act_fn = act_fn
def forward(self, hidden_states: torch.Tensor, debug=False) -> torch.Tensor:
gate_up = self.gate_up_proj_linear(hidden_states)
# gate, up = gate_up[..., ::2], gate_up[..., 1::2]
gate, up = gate_up.chunk(2, dim=-1)
out = self.down_proj_linear(up * self.act_fn(gate))
return out
class Mlp(torch.nn.Module):
def __init__(self, mlp, mapper, layer_id):
super().__init__()
self.layer_id = layer_id
ModelMapper.do_map(self, mlp, mapper['mlp'])
self.is_moe = hasattr(self, 'experts')
self.export_moe = False
self.custom_moe = MoE(self.num_experts, self.top_k, layer_id)
if isinstance(self.experts, torch.nn.ModuleList):
self.moe_type = 'qwen3_moe'
else:
self.moe_type = 'qwen3_5_moe'
self.norm_topk_prob = True
# refacte experts to qwen3_experts
original_experts = self.experts
hidden_size = getattr(original_experts, 'hidden_dim', None) or getattr(original_experts, 'hidden_size')
expert_dim = getattr(original_experts, 'intermediate_dim', None) or getattr(original_experts, 'intermediate_size')
act_fn = original_experts.act_fn
new_experts_list = torch.nn.ModuleList()
for i in range(self.num_experts):
expert_mlp = Qwen3Expert(hidden_size, expert_dim, act_fn)
expert_mlp.gate_up_proj_linear.weight.data = original_experts.gate_up_proj.data[i]
expert_mlp.down_proj_linear.weight.data = original_experts.down_proj.data[i]
new_experts_list.append(expert_mlp)
self.experts = new_experts_list
if not isinstance(self.gate, torch.nn.Linear):
gate = torch.nn.Linear(hidden_size, self.num_experts, bias=False)
gate.weight.data = self.gate.weight.data
self.gate = gate
if hasattr(self, 'expert_bias') and self.expert_bias is not None:
self.moe_type = 'lfm2_moe'
if hasattr(self, 'router'):
self.moe_type = 'gpt_oss'
hidden_dim = self.router.weight.shape[1]
self.gate = torch.nn.Linear(hidden_dim, self.num_experts, bias=True)
self.gate.weight.data = self.router.weight.data
self.gate.bias.data = self.router.bias.data
# refacte experts to qwen3_experts
original_experts = self.experts
expert_dim = original_experts.expert_dim
new_experts_list = torch.nn.ModuleList()
for i in range(self.num_experts):
expert_mlp = GptOssExpert(hidden_dim, expert_dim)
expert_mlp.gate_up_proj_linear.weight.data = original_experts.gate_up_proj.data[i].transpose(0, 1)
expert_mlp.gate_up_proj_linear.bias.data = original_experts.gate_up_proj_bias.data[i]
expert_mlp.down_proj_linear.weight.data = original_experts.down_proj.data[i].transpose(0, 1)
expert_mlp.down_proj_linear.bias.data = original_experts.down_proj_bias.data[i]
new_experts_list.append(expert_mlp)
self.experts = new_experts_list
del self.router
def forward(self, hidden_states: torch.Tensor):
if not self.is_moe:
# general Mlp
return self.down_proj(self.act_fn(self.gate_proj(hidden_states)) * self.up_proj(hidden_states))
# MoE Mlp
batch_size, sequence_length, hidden_dim = hidden_states.shape
hidden_states = hidden_states.view(-1, hidden_dim)
if hasattr(self, 'shared_expert'):
shared_expert_output = F.sigmoid(self.shared_expert_gate(hidden_states)) * self.shared_expert(hidden_states)
shared_expert_output = shared_expert_output.reshape(batch_size, sequence_length, hidden_dim)
else:
shared_expert_output = None
if self.moe_type == 'lfm2_moe':
router_logits = self.gate(hidden_states)
routing_weights = router_logits.sigmoid()
scores_for_routing = routing_weights + self.expert_bias
_, selected_experts = torch.topk(scores_for_routing, self.top_k, dim=-1)
routing_weights = torch.gather(routing_weights, dim=-1, index=selected_experts)
if self.norm_topk_prob:
routing_weights = routing_weights / (routing_weights.sum(dim=-1, keepdim=True) + 1e-6)
routing_weights = (routing_weights * self.routed_scaling_factor).to(hidden_states.dtype)
elif self.moe_type == 'gpt_oss':
router_logits = self.gate(hidden_states)
routing_weights, selected_experts = torch.topk(router_logits, self.top_k, dim=-1)
routing_weights = F.softmax(routing_weights, dim=-1, dtype=torch.float).to(hidden_states.dtype)
else:
router_logits = self.gate(hidden_states)
routing_weights = F.softmax(router_logits, dim=-1, dtype=torch.float)
routing_weights, selected_experts = torch.topk(routing_weights, self.top_k, dim=-1)
if self.norm_topk_prob: # only diff with mixtral sparse moe block!
routing_weights /= routing_weights.sum(dim=-1, keepdim=True)
# we cast back to the input dtype
routing_weights = routing_weights.to(hidden_states.dtype)
if self.export_moe:
expert_output = self.custom_moe(hidden_states, routing_weights, selected_experts)
if shared_expert_output is not None:
expert_output = expert_output + shared_expert_output
return expert_output
final_hidden_states = torch.zeros(
(batch_size * sequence_length, hidden_dim), dtype=hidden_states.dtype, device=hidden_states.device
)
if False: # cpp impl
seqlen, topk = selected_experts.shape
if seqlen == 1:
expert_idx = int(selected_experts[0, 0])
scale = float(routing_weights[0, 0])
output = self.experts[expert_idx](hidden_states) * scale
for i in range(1, topk):
expert_idx = int(selected_experts[0, i])
scale = float(routing_weights[0, i])
output += self.experts[expert_idx](hidden_states) * scale
return output
hss = torch.split(hidden_states, 1)
expertWorks = [[] for i in range(self.num_experts)]
for i in range(seqlen):
for j in range(topk):
expert_idx = int(selected_experts[i, j])
scale = float(routing_weights[i, j])
expertWorks[expert_idx].append((i, scale))
for i in range(self.num_experts):
if len(expertWorks[i]) == 0:
continue
input_hs = []
for token_id, scale in expertWorks[i]:
input_hs.append(hss[token_id])
output_hs = self.experts[i](torch.concat(input_hs))
output_hss = torch.split(output_hs, 1)
for j in range(len(expertWorks[i])):
token_id, scale = expertWorks[i][j]
scale_hs = output_hss[j] * scale
final_hidden_states[token_id] += scale_hs.squeeze(0)
return final_hidden_states
# One hot encode the selected experts to create an expert mask
# this will be used to easily index which expert is going to be sollicitated
expert_mask = F.one_hot(selected_experts, num_classes=self.num_experts).permute(2, 1, 0)
expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero()
# Loop over all available experts in the model and perform the computation on each expert
# for expert_idx in range(self.num_experts):
for expert_idx in expert_hit:
expert_idx = expert_idx[0]
idx, top_x = torch.where(expert_mask[expert_idx])
expert_layer = self.experts[expert_idx]
# Index the correct hidden states and compute the expert hidden state for
# the current expert. We need to make sure to multiply the output hidden
# states by `routing_weights` on the corresponding tokens (top-1 and top-2)
current_state = hidden_states[None, top_x].reshape(-1, hidden_dim)
current_hidden_states = expert_layer(current_state) * routing_weights[top_x, idx, None]
# However `index_add_` only support torch tensors for indexing so we'll use
# the `top_x` tensor here.
final_hidden_states.index_add_(0, top_x, current_hidden_states.to(hidden_states.dtype))
final_hidden_states = final_hidden_states.reshape(batch_size, sequence_length, hidden_dim)
if shared_expert_output is not None:
final_hidden_states = final_hidden_states + shared_expert_output
return final_hidden_states
class Decoder(torch.nn.Module):
def __init__(self, decoder, layer_id, config, rotary=None, mapper=None):
super().__init__()
if rotary is None:
rotary = config.rotary
if mapper is None:
mapper = config.model_map
ModelMapper.do_map(self, decoder, mapper['decoder'])
if 'mlp' in mapper and hasattr(self.mlp, 'experts'):
self.mlp = Mlp(self.mlp, mapper, layer_id)
# gemma4 MoE: router and experts are at decoder layer level (parallel to dense MLP)
self.has_gemma4_moe = hasattr(self, 'experts') and self.experts is not None
if self.has_gemma4_moe:
original_experts = self.experts
num_experts = original_experts.num_experts
hidden_size = original_experts.hidden_dim
expert_dim = original_experts.intermediate_dim
act_fn = original_experts.act_fn
# Refactor 3D Parameter experts into ModuleList of Qwen3Expert
new_experts_list = torch.nn.ModuleList()
for i in range(num_experts):
expert_mlp = Qwen3Expert(hidden_size, expert_dim, act_fn)
expert_mlp.gate_up_proj_linear.weight.data = original_experts.gate_up_proj.data[i]
expert_mlp.down_proj_linear.weight.data = original_experts.down_proj.data[i]
new_experts_list.append(expert_mlp)
self.experts = new_experts_list
# Extract gate Linear from router (router has norm+scale+proj+per_expert_scale)
self.moe_gate = torch.nn.Linear(hidden_size, num_experts, bias=False)
self.moe_gate.weight.data = self.router.proj.weight.data
self.moe_router_norm = self.router.norm
self.moe_router_scale = self.router.scale.data
self.moe_router_scalar_root = self.router.scalar_root_size
self.moe_per_expert_scale = self.router.per_expert_scale.data
self.moe_num_experts = num_experts
self.moe_top_k = config.origin_config.text_config.top_k_experts
self.custom_moe = MoE(num_experts, self.moe_top_k, layer_id)
self.export_moe = False
del self.router
self.layer_type = 'full_attention'
if hasattr(self, 'self_attn') and self.self_attn is not None:
self.self_attn = Attention(self.self_attn, layer_id, config, rotary, mapper)
if hasattr(self, 'linear_attn') and self.linear_attn is not None:
self.self_attn = create_linear_attention(self.linear_attn, layer_id, config, rotary, mapper)
self.layer_type = 'linear_attention'
self.hidden_size = config.hidden_size
if hasattr(config, 'num_hidden_layers'):
# minicpm
self.num_hidden_layers = config.num_hidden_layers
# chatglm
self.alpha = (2 * config.num_hidden_layers) ** 0.5 if config.model_type == 'chatglm' else 1.0
def forward(
self,
hidden_states: torch.Tensor,
rotary_pos_emb: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
) -> Tuple[torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]]]:
hidden_states = hidden_states.view(1, -1, self.hidden_size)
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
norm_hidden_states = hidden_states
# Self Attention or Linear Attention
if self.layer_type == 'full_attention':
hidden_states = self.self_attn(
hidden_states=hidden_states,
rotary_pos_emb=rotary_pos_emb,
attention_mask=attention_mask,
)
elif self.layer_type == 'linear_attention':
hidden_states = self.self_attn(
hidden_states=hidden_states,
attention_mask=attention_mask,
)
# Fully Connected
if not hasattr(self, 'post_attention_layernorm'):
# phi
feed_forward_hidden_states = self.mlp(norm_hidden_states)
hidden_states = hidden_states + feed_forward_hidden_states + residual
elif hasattr(self, 'alpha') and self.alpha != 1.0:
# chatglm-6b
hidden_states = norm_hidden_states * self.alpha + hidden_states
mlp_input = self.post_attention_layernorm(hidden_states)
mlp_output = self.mlp(mlp_input)
hidden_states = mlp_input * self.alpha + mlp_output
elif hasattr(self, 'pre_feedforward_layernorm'):
# gemma2 / gemma4
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.pre_feedforward_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
if self.has_gemma4_moe:
# gemma4 MoE: dense MLP + MoE experts in parallel
mlp_output = self.post_feedforward_layernorm_1(hidden_states)
# Router uses residual (pre-MLP hidden states)
router_input = residual.reshape(-1, residual.shape[-1])
# Routing: norm -> scale -> proj -> softmax -> topk -> normalize -> per_expert_scale
normed = self.moe_router_norm(router_input)
normed = normed * self.moe_router_scale * self.moe_router_scalar_root
router_logits = self.moe_gate(normed)
routing_weights = F.softmax(router_logits, dim=-1, dtype=torch.float)
routing_weights, selected_experts = torch.topk(routing_weights, self.moe_top_k, dim=-1)
routing_weights = routing_weights / (routing_weights.sum(dim=-1, keepdim=True) + 1e-6)
routing_weights = (routing_weights * self.moe_per_expert_scale[selected_experts]).to(router_input.dtype)
if self.export_moe:
expert_input = self.pre_feedforward_layernorm_2(router_input)
expert_output = self.custom_moe(expert_input, routing_weights, selected_experts)
else:
# Expert computation
expert_input = self.pre_feedforward_layernorm_2(router_input)
batch_size, sequence_length = residual.shape[0], residual.shape[1]
hidden_dim = residual.shape[-1]
expert_output = torch.zeros_like(router_input)
expert_mask = F.one_hot(selected_experts, num_classes=self.moe_num_experts).permute(2, 1, 0)
expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero()
for expert_idx in expert_hit:
expert_idx = expert_idx[0]
idx, top_x = torch.where(expert_mask[expert_idx])
current_state = expert_input[top_x]
current_hidden = self.experts[expert_idx](current_state) * routing_weights[top_x, idx, None]
expert_output.index_add_(0, top_x, current_hidden.to(expert_output.dtype))
expert_output = expert_output.reshape(residual.shape)
expert_output = self.post_feedforward_layernorm_2(expert_output)
hidden_states = mlp_output + expert_output
hidden_states = self.post_feedforward_layernorm(hidden_states)
hidden_states = residual + hidden_states
elif hasattr(self, 'scale_depth'):
# minicpm
hidden_states = residual + hidden_states * (self.scale_depth / math.sqrt(self.num_hidden_layers))
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + hidden_states * (self.scale_depth / math.sqrt(self.num_hidden_layers))
else:
# general
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + hidden_states
# gemma4 PLE (Per-Layer Embeddings)
if hasattr(self, 'per_layer_input_gate') and self.per_layer_input_gate is not None:
per_layer_input = getattr(self, '_per_layer_input', None)
if per_layer_input is not None:
residual = hidden_states
hidden_states = self.per_layer_input_gate(hidden_states)
hidden_states = self.act_fn(hidden_states)
hidden_states = hidden_states * per_layer_input
hidden_states = self.per_layer_projection(hidden_states)
hidden_states = self.post_per_layer_input_norm(hidden_states)
hidden_states = residual + hidden_states
# gemma4 layer_scalar
if hasattr(self, 'layer_scalar') and self.layer_scalar is not None:
hidden_states = hidden_states * self.layer_scalar
return hidden_states
class Lm(torch.nn.Module):
def __init__(self, lm_, final_logit_softcapping=None):
super().__init__()
self.lm = lm_
self.final_logit_softcapping = final_logit_softcapping
def forward(self, hidden_states):
m_logits = self.lm(hidden_states)
if self.final_logit_softcapping is not None:
m_logits = m_logits / self.final_logit_softcapping
m_logits = torch.tanh(m_logits)
m_logits = m_logits * self.final_logit_softcapping
return m_logits