243 lines
14 KiB
Python
243 lines
14 KiB
Python
"""
|
||
MiniMind with Multi-head Latent Attention (MLA)
|
||
基于 DeepSeek-V2 的低秩 KV 压缩注意力机制(解耦 RoPE 架构)
|
||
将 GQA 中独立的 K/V 投影替换为"压缩→缓存→还原"的低秩路径
|
||
位置信息通过独立的低维投影承载(decoupled rope),K 的 content 部分不施 RoPE
|
||
KV-Cache 从 GQA 的 768 floats/token/layer 压缩到 kv_lora_rank + rope_dim floats(默认 128+48=176,压缩约 4.4 倍)
|
||
"""
|
||
import math, torch, torch.nn.functional as F
|
||
from torch import nn
|
||
from transformers import PreTrainedModel, GenerationMixin
|
||
from transformers.modeling_outputs import MoeCausalLMOutputWithPast
|
||
from model.model_minimind import (
|
||
MiniMindConfig, RMSNorm, precompute_freqs_cis, repeat_kv,
|
||
FeedForward, MOEFeedForward
|
||
)
|
||
|
||
|
||
class MiniMindMLAConfig(MiniMindConfig):
|
||
model_type = "minimind_mla"
|
||
|
||
def __init__(self, kv_lora_rank=128, q_lora_rank=256, rope_dim=None, **kwargs):
|
||
kwargs["attention_type"] = "mla"
|
||
super().__init__(**kwargs)
|
||
self.kv_lora_rank = kv_lora_rank
|
||
self.q_lora_rank = q_lora_rank
|
||
# hidden_size=768 → head_dim=96 → rope_dim=48
|
||
self.rope_dim = rope_dim if rope_dim is not None else self.hidden_size // self.num_attention_heads // 2
|
||
|
||
|
||
def _apply_rope(x, cos, sin, unsqueeze_dim=1):
|
||
"""单张量 RoPE,MLA 中 Q 和 K 序列长度不同,需要分别应用"""
|
||
def rotate_half(x): return torch.cat((-x[..., x.shape[-1] // 2:], x[..., : x.shape[-1] // 2]), dim=-1)
|
||
return ((x * cos.unsqueeze(unsqueeze_dim)) + (rotate_half(x) * sin.unsqueeze(unsqueeze_dim))).to(x.dtype)
|
||
|
||
|
||
class MLAAttention(nn.Module):
|
||
def __init__(self, config: MiniMindMLAConfig):
|
||
super().__init__()
|
||
self.num_key_value_heads = config.num_attention_heads if config.num_key_value_heads is None else config.num_key_value_heads
|
||
self.n_local_heads = config.num_attention_heads
|
||
self.n_local_kv_heads = self.num_key_value_heads
|
||
self.n_rep = self.n_local_heads // self.n_local_kv_heads
|
||
self.head_dim = config.head_dim
|
||
self.kv_lora_rank = config.kv_lora_rank
|
||
self.q_lora_rank = config.q_lora_rank
|
||
self.rope_dim = config.rope_dim
|
||
self.is_causal = True
|
||
|
||
# Q 低秩压缩路径 (DeepSeek-V2 Eq.12-14)
|
||
self.q_compress = nn.Linear(config.hidden_size, config.q_lora_rank, bias=False)
|
||
self.q_up = nn.Linear(config.q_lora_rank, config.num_attention_heads * self.head_dim, bias=False)
|
||
self.q_rope_proj = nn.Linear(config.q_lora_rank, config.num_attention_heads * self.rope_dim, bias=False)
|
||
|
||
# KV 低秩压缩路径 (DeepSeek-V2 Eq.9-11)
|
||
self.kv_compress = nn.Linear(config.hidden_size, config.kv_lora_rank, bias=False)
|
||
self.kv_up_k = nn.Linear(config.kv_lora_rank, self.num_key_value_heads * self.head_dim, bias=False)
|
||
self.kv_up_v = nn.Linear(config.kv_lora_rank, self.num_key_value_heads * self.head_dim, bias=False)
|
||
|
||
# K 的解耦 RoPE 投影 (DeepSeek-V2 Eq.15)
|
||
self.k_rope_proj = nn.Linear(config.hidden_size, config.rope_dim, bias=False)
|
||
|
||
# 输出投影
|
||
self.o_proj = nn.Linear(config.num_attention_heads * self.head_dim, config.hidden_size, bias=False)
|
||
|
||
# 归一化
|
||
self.q_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||
self.kv_norm = RMSNorm(config.kv_lora_rank, eps=config.rms_norm_eps)
|
||
self.attn_dropout = nn.Dropout(config.dropout)
|
||
self.resid_dropout = nn.Dropout(config.dropout)
|
||
self.dropout = config.dropout
|
||
self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention') and config.flash_attn
|
||
|
||
def forward(self, x, position_embeddings, past_key_value=None, use_cache=False, attention_mask=None):
|
||
bsz, seq_len, _ = x.shape
|
||
cos, sin = position_embeddings # [seq_len, rope_dim]
|
||
|
||
# ── Q Path ──
|
||
q_latent = self.q_compress(x) # [bsz, seq, q_lora_rank]
|
||
# Q content: q_compress → q_up → q_norm (NO RoPE)
|
||
q_content = self.q_up(q_latent).view(bsz, seq_len, self.n_local_heads, self.head_dim)
|
||
q_content = self.q_norm(q_content) # [bsz, seq, 8, 96]
|
||
# Q rope: q_compress → q_rope_proj → RoPE
|
||
q_rope = self.q_rope_proj(q_latent).view(bsz, seq_len, self.n_local_heads, self.rope_dim)
|
||
q_rope = _apply_rope(q_rope, cos, sin) # [bsz, seq, 8, 48]
|
||
# 拼接 content + rope
|
||
xq = torch.cat([q_content, q_rope], dim=-1) # [bsz, seq, 8, 144]
|
||
|
||
# ── K Path ──
|
||
# Content: kv_compress → norm → cache → kv_up_k (NO RoPE!)
|
||
kv_latent = self.kv_norm(self.kv_compress(x)) # [bsz, seq, kv_lora_rank]
|
||
# Rope: k_rope_proj → RoPE (共享跨所有 head)
|
||
k_rope = self.k_rope_proj(x).unsqueeze(2) # [bsz, seq, 1, rope_dim]
|
||
k_rope = _apply_rope(k_rope, cos, sin).squeeze(2) # [bsz, seq, rope_dim]
|
||
|
||
# 拼接缓存
|
||
if past_key_value is not None:
|
||
past_kv_latent, past_k_rope = past_key_value
|
||
kv_latent = torch.cat([past_kv_latent, kv_latent], dim=1)
|
||
k_rope = torch.cat([past_k_rope, k_rope], dim=1)
|
||
past_kv = (kv_latent, k_rope) if use_cache else None
|
||
|
||
total_len = kv_latent.shape[1]
|
||
# 从 latent 还原 K content (NO RoPE!)
|
||
k_content = self.kv_up_k(kv_latent).view(bsz, total_len, self.n_local_kv_heads, self.head_dim) # [bsz, total, 4, 96]
|
||
# K rope 扩展 head 维度后拼接
|
||
k_rope_expanded = k_rope.unsqueeze(2).expand(-1, -1, self.n_local_kv_heads, -1) # [bsz, total, 4, rope_dim]
|
||
xk = torch.cat([k_content, k_rope_expanded], dim=-1) # [bsz, total, 4, 144]
|
||
|
||
# ── V Path ──
|
||
xv = self.kv_up_v(kv_latent).view(bsz, total_len, self.n_local_kv_heads, self.head_dim) # [bsz, total, 4, 96]
|
||
|
||
# ── Attention ──
|
||
scale = math.sqrt(self.head_dim + self.rope_dim)
|
||
xq, xk, xv = (xq.transpose(1, 2), repeat_kv(xk, self.n_rep).transpose(1, 2), repeat_kv(xv, self.n_rep).transpose(1, 2))
|
||
if self.flash and (seq_len > 1) and (not self.is_causal or past_key_value is None) and (attention_mask is None or torch.all(attention_mask == 1)):
|
||
output = F.scaled_dot_product_attention(xq, xk, xv, dropout_p=self.dropout if self.training else 0.0, is_causal=self.is_causal)
|
||
else:
|
||
scores = (xq @ xk.transpose(-2, -1)) / scale
|
||
if self.is_causal: scores[:, :, :, -seq_len:] += torch.full((seq_len, seq_len), float("-inf"), device=scores.device).triu(1)
|
||
if attention_mask is not None: scores += (1.0 - attention_mask.unsqueeze(1).unsqueeze(2)) * -1e9
|
||
output = self.attn_dropout(F.softmax(scores.float(), dim=-1).type_as(xq)) @ xv
|
||
output = output.transpose(1, 2).reshape(bsz, seq_len, -1)
|
||
output = self.resid_dropout(self.o_proj(output))
|
||
return output, past_kv
|
||
|
||
|
||
class MiniMindMLABlock(nn.Module):
|
||
def __init__(self, layer_id: int, config: MiniMindMLAConfig):
|
||
super().__init__()
|
||
self.self_attn = MLAAttention(config)
|
||
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||
self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||
self.mlp = FeedForward(config) if not config.use_moe else MOEFeedForward(config)
|
||
|
||
def forward(self, hidden_states, position_embeddings, past_key_value=None, use_cache=False, attention_mask=None):
|
||
residual = hidden_states
|
||
hidden_states, present_key_value = self.self_attn(
|
||
self.input_layernorm(hidden_states), position_embeddings,
|
||
past_key_value, use_cache, attention_mask
|
||
)
|
||
hidden_states += residual
|
||
hidden_states = hidden_states + self.mlp(self.post_attention_layernorm(hidden_states))
|
||
return hidden_states, present_key_value
|
||
|
||
|
||
class MiniMindMLAModel(nn.Module):
|
||
def __init__(self, config: MiniMindMLAConfig):
|
||
super().__init__()
|
||
self.config = config
|
||
self.vocab_size, self.num_hidden_layers = config.vocab_size, config.num_hidden_layers
|
||
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
||
self.dropout = nn.Dropout(config.dropout)
|
||
self.layers = nn.ModuleList([MiniMindMLABlock(l, config) for l in range(self.num_hidden_layers)])
|
||
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||
freqs_cos, freqs_sin = precompute_freqs_cis(dim=config.rope_dim, end=config.max_position_embeddings, rope_base=config.rope_theta, rope_scaling=config.rope_scaling)
|
||
self.register_buffer("freqs_cos", freqs_cos, persistent=False)
|
||
self.register_buffer("freqs_sin", freqs_sin, persistent=False)
|
||
|
||
def forward(self, input_ids, attention_mask=None, past_key_values=None, use_cache=False, **kwargs):
|
||
batch_size, seq_length = input_ids.shape
|
||
if hasattr(past_key_values, 'layers'): past_key_values = None
|
||
past_key_values = past_key_values or [None] * len(self.layers)
|
||
# MLA 的 cache 是 tuple (kv_latent, k_rope),取 kv_latent 的长度
|
||
start_pos = past_key_values[0][0].shape[1] if past_key_values[0] is not None else 0
|
||
hidden_states = self.dropout(self.embed_tokens(input_ids))
|
||
# Recompute RoPE buffers lost during meta-device init (transformers>=5.x)
|
||
if self.freqs_cos[0, 0] == 0:
|
||
freqs_cos, freqs_sin = precompute_freqs_cis(dim=self.config.rope_dim, end=self.config.max_position_embeddings, rope_base=self.config.rope_theta, rope_scaling=self.config.rope_scaling)
|
||
self.freqs_cos, self.freqs_sin = freqs_cos.to(hidden_states.device), freqs_sin.to(hidden_states.device)
|
||
# MLA: RoPE 只作用于当前 token 的 rope 投影(Q rope 和 K rope 使用相同位置范围)
|
||
position_embeddings = (self.freqs_cos[start_pos:start_pos + seq_length], self.freqs_sin[start_pos:start_pos + seq_length])
|
||
presents = []
|
||
for layer, past_key_value in zip(self.layers, past_key_values):
|
||
hidden_states, present = layer(
|
||
hidden_states,
|
||
position_embeddings,
|
||
past_key_value=past_key_value,
|
||
use_cache=use_cache,
|
||
attention_mask=attention_mask
|
||
)
|
||
presents.append(present)
|
||
hidden_states = self.norm(hidden_states)
|
||
aux_loss = sum([l.mlp.aux_loss for l in self.layers if isinstance(l.mlp, MOEFeedForward)], hidden_states.new_zeros(1).squeeze())
|
||
return hidden_states, presents, aux_loss
|
||
|
||
|
||
class MiniMindMLAForCausalLM(PreTrainedModel, GenerationMixin):
|
||
config_class = MiniMindMLAConfig
|
||
_tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
|
||
|
||
def __init__(self, config: MiniMindMLAConfig = None):
|
||
self.config = config or MiniMindMLAConfig()
|
||
super().__init__(self.config)
|
||
self.model = MiniMindMLAModel(self.config)
|
||
self.lm_head = nn.Linear(self.config.hidden_size, self.config.vocab_size, bias=False)
|
||
if self.config.tie_word_embeddings: self.model.embed_tokens.weight = self.lm_head.weight
|
||
self.post_init()
|
||
|
||
def forward(self, input_ids, attention_mask=None, past_key_values=None, use_cache=False, logits_to_keep=0, labels=None, **kwargs):
|
||
hidden_states, past_key_values, aux_loss = self.model(input_ids, attention_mask, past_key_values, use_cache, **kwargs)
|
||
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
|
||
logits = self.lm_head(hidden_states[:, slice_indices, :])
|
||
loss = None
|
||
if labels is not None:
|
||
x, y = logits[..., :-1, :].contiguous(), labels[..., 1:].contiguous()
|
||
loss = F.cross_entropy(x.view(-1, x.size(-1)), y.view(-1), ignore_index=-100)
|
||
return MoeCausalLMOutputWithPast(loss=loss, aux_loss=aux_loss, logits=logits, past_key_values=past_key_values, hidden_states=hidden_states)
|
||
|
||
@torch.inference_mode()
|
||
def generate(self, inputs=None, attention_mask=None, max_new_tokens=8192, temperature=0.85, top_p=0.85, top_k=50, eos_token_id=2, streamer=None, use_cache=True, num_return_sequences=1, do_sample=True, repetition_penalty=1.0, **kwargs):
|
||
input_ids = kwargs.pop("input_ids", inputs).repeat(num_return_sequences, 1)
|
||
attention_mask = attention_mask.repeat(num_return_sequences, 1) if attention_mask is not None else None
|
||
past_key_values = kwargs.pop("past_key_values", None)
|
||
finished = torch.zeros(input_ids.shape[0], dtype=torch.bool, device=input_ids.device)
|
||
if streamer: streamer.put(input_ids.cpu())
|
||
for _ in range(max_new_tokens):
|
||
# MLA: past_key_values[0] 是 tuple (kv_latent, k_rope),取 kv_latent 的长度
|
||
past_len = past_key_values[0][0].shape[1] if past_key_values else 0
|
||
outputs = self.forward(input_ids[:, past_len:], attention_mask, past_key_values, use_cache=use_cache, **kwargs)
|
||
attention_mask = torch.cat([attention_mask, attention_mask.new_ones(attention_mask.shape[0], 1)], -1) if attention_mask is not None else None
|
||
logits = outputs.logits[:, -1, :] / temperature
|
||
if repetition_penalty != 1.0:
|
||
for i in range(input_ids.shape[0]):
|
||
seen = torch.unique(input_ids[i]); score = logits[i, seen]; logits[i, seen] = torch.where(score > 0, score / repetition_penalty, score * repetition_penalty)
|
||
if top_k > 0:
|
||
logits[logits < torch.topk(logits, top_k)[0][..., -1, None]] = -float('inf')
|
||
if top_p < 1.0:
|
||
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
|
||
mask = torch.cumsum(torch.softmax(sorted_logits, dim=-1), dim=-1) > top_p
|
||
mask[..., 1:], mask[..., 0] = mask[..., :-1].clone(), 0
|
||
logits[mask.scatter(1, sorted_indices, mask)] = -float('inf')
|
||
next_token = torch.multinomial(torch.softmax(logits, dim=-1), num_samples=1) if do_sample else torch.argmax(logits, dim=-1, keepdim=True)
|
||
if eos_token_id is not None: next_token = torch.where(finished.unsqueeze(-1), next_token.new_full((next_token.shape[0], 1), eos_token_id), next_token)
|
||
input_ids = torch.cat([input_ids, next_token], dim=-1)
|
||
past_key_values = outputs.past_key_values if use_cache else None
|
||
if streamer: streamer.put(next_token.cpu())
|
||
if eos_token_id is not None:
|
||
finished |= next_token.squeeze(-1).eq(eos_token_id)
|
||
if finished.all(): break
|
||
if streamer: streamer.end()
|
||
if kwargs.get("return_kv"): return {'generated_ids': input_ids, 'past_kv': past_key_values}
|
||
return input_ids
|