Files
xiaoli-llm-chat/xiaoli_next.py
T

422 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
XiaLi-Next 统一架构 v1.0
=========================
同一套架构,从 20M 到 7B+ 通用
核心设计:RoPE + SwiGLU + Pre-RMSNorm + GQA + Weight Tying
用法:
from xiaoli_next import XiaLiConfig, XiaLiModel
# 20M 超小模型
cfg = XiaLiConfig.preset('20m')
model = XiaLiModel(cfg)
# 1B 中等模型
cfg = XiaLiConfig.preset('1b')
model = XiaLiModel(cfg)
# 7B 大模型
cfg = XiaLiConfig.preset('7b')
model = XiaLiModel(cfg)
"""
import math
import json
from typing import Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
# ════════════════════════════════════════════════════════════
# 1. 配置系统
# ════════════════════════════════════════════════════════════
class XiaLiConfig:
"""统一配置 - 通过 preset 快速切换规模"""
def __init__(self):
# 架构
self.d_model: int = 320
self.n_layers: int = 8
self.n_heads: int = 8
self.n_kv_heads: int = 4 # GQA: KV头数(=n_heads 就是MHA,=1 就是MQA)
self.d_ff: int = 0 # 0=自动 = int(d_model * 8/3 * 2) SwiGLU需要额外gate
self.max_seq_len: int = 2048 # 最大序列长度
self.dropout: float = 0.0 # 大模型dropout=0
self.rope_theta: float = 10000.0 # RoPE base frequency
self.tie_weights: bool = True # Weight Tying
self.vocab_size: int = 20007
# 训练
self.norm_eps: float = 1e-5
@classmethod
def preset(cls, name: str) -> 'XiaLiConfig':
"""按规模取配置"""
cfgs = {
# ── 超小模型 ──
'20m': dict(d_model=288, n_layers=6, n_heads=6, n_kv_heads=3, max_seq_len=1024),
'26m': dict(d_model=320, n_layers=8, n_heads=8, n_kv_heads=4, max_seq_len=2048),
# ── 百M级 ──
'100m': dict(d_model=512, n_layers=12, n_heads=8, n_kv_heads=4, max_seq_len=2048),
'300m': dict(d_model=768, n_layers=16, n_heads=12, n_kv_heads=4, max_seq_len=4096),
# ── 十亿级 ──
'500m': dict(d_model=1024, n_layers=24, n_heads=16, n_kv_heads=8, max_seq_len=8192),
'1b': dict(d_model=1536, n_layers=24, n_heads=16, n_kv_heads=8, max_seq_len=8192),
'3b': dict(d_model=2560, n_layers=32, n_heads=24, n_kv_heads=8, max_seq_len=8192),
# ── 十亿级+ ──
'7b': dict(d_model=4096, n_layers=32, n_heads=32, n_kv_heads=8, max_seq_len=8192, rope_theta=500000.0),
'13b': dict(d_model=5120, n_layers=40, n_heads=40, n_kv_heads=10, max_seq_len=8192, rope_theta=500000.0),
'70b': dict(d_model=8192, n_layers=80, n_heads=64, n_kv_heads=8, max_seq_len=8192, rope_theta=500000.0),
}
cfg = cls()
if name in cfgs:
for k, v in cfgs[name].items():
setattr(cfg, k, v)
if cfg.d_ff == 0:
# SwiGLU 标准: 8/3 * d_model(参数量等价于 4x 标准 FFN)
cfg.d_ff = int(cfg.d_model * 8 / 3)
return cfg
# ════════════════════════════════════════════════════════════
# 2. RMSNorm
# ════════════════════════════════════════════════════════════
class XiaLiRMSNorm(nn.Module):
"""RMS Layer Normalization"""
def __init__(self, dim: int, eps: float = 1e-5):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x: torch.Tensor) -> torch.Tensor:
rms = torch.sqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
return x / rms * self.weight
# ════════════════════════════════════════════════════════════
# 3. RoPE(旋转位置编码)
# ════════════════════════════════════════════════════════════
def precompute_rope_freqs(dim: int, max_len: int, theta: float = 10000.0) -> Tuple[torch.Tensor, torch.Tensor]:
"""预计算 RoPE 频率"""
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
t = torch.arange(max_len, dtype=torch.float32)
angles = torch.outer(t, freqs) # [max_len, dim//2]
cos = angles.cos().unsqueeze(0).unsqueeze(2) # [1, max_len, 1, dim//2]
sin = angles.sin().unsqueeze(0).unsqueeze(2)
return cos, sin
def apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
"""对 x 应用 RoPE。x: [batch, seq, n_heads, head_dim]"""
head_dim = x.size(-1)
# cos/sin 是每对维度一个值 (head_dim//2),需要扩张到 head_dim
cos = torch.repeat_interleave(cos, 2, dim=-1) # [..., d//2] -> [..., d]
sin = torch.repeat_interleave(sin, 2, dim=-1)
x1 = x[..., :head_dim // 2] # [..., d//2]
x2 = x[..., head_dim // 2:] # [..., d//2]
rotated = torch.cat([-x2, x1], dim=-1) # [..., d]
return x * cos + rotated * sin
# ════════════════════════════════════════════════════════════
# 4. GQA 注意力(分组查询注意力)
# ════════════════════════════════════════════════════════════
class GroupedQueryAttention(nn.Module):
"""分组查询注意力 - 小模型=MHA,大模型节省KV Cache"""
def __init__(self, config: XiaLiConfig):
super().__init__()
self.n_heads = config.n_heads
self.n_kv_heads = config.n_kv_heads # GQA: KV头数
self.head_dim = config.d_model // config.n_heads
self.n_rep = self.n_heads // self.n_kv_heads # 每个KV头被几个Q头共享
# 投影(无bias)
self.wq = nn.Linear(config.d_model, config.n_heads * self.head_dim, bias=False)
self.wk = nn.Linear(config.d_model, self.n_kv_heads * self.head_dim, bias=False)
self.wv = nn.Linear(config.d_model, self.n_kv_heads * self.head_dim, bias=False)
self.wo = nn.Linear(config.n_heads * self.head_dim, config.d_model, bias=False)
self.attn_dropout = nn.Dropout(config.dropout)
def forward(
self,
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
mask: Optional[torch.Tensor] = None,
) -> torch.Tensor:
batch, seq_len, _ = x.shape
# QKV 投影
q = self.wq(x).view(batch, seq_len, self.n_heads, self.head_dim)
k = self.wk(x).view(batch, seq_len, self.n_kv_heads, self.head_dim)
v = self.wv(x).view(batch, seq_len, self.n_kv_heads, self.head_dim)
# RoPE
cos_ = cos[:, :seq_len]
sin_ = sin[:, :seq_len]
q = apply_rope(q, cos_, sin_)
k = apply_rope(k, cos_, sin_)
# GQA: KV 头扩展到 Q 头数
if self.n_rep > 1:
k = k.repeat_interleave(self.n_rep, dim=2)
v = v.repeat_interleave(self.n_rep, dim=2)
# 转置为 [batch, n_heads, seq, head_dim]
q = q.transpose(1, 2)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
# 注意力计算
scale = self.head_dim ** -0.5
attn = torch.matmul(q, k.transpose(-2, -1)) * scale
if mask is not None:
attn = attn + mask[:, :, :seq_len, :seq_len]
attn = F.softmax(attn, dim=-1, dtype=torch.float32).to(q.dtype)
attn = self.attn_dropout(attn)
out = torch.matmul(attn, v) # [batch, n_heads, seq, head_dim]
out = out.transpose(1, 2).contiguous().view(batch, seq_len, -1)
return self.wo(out)
# ════════════════════════════════════════════════════════════
# 5. SwiGLU 前馈网络
# ════════════════════════════════════════════════════════════
class SwiGLU(nn.Module):
"""SwiGLU 前馈网络 - 比 ReLU FFN 质量更高"""
def __init__(self, config: XiaLiConfig):
super().__init__()
# d_ff 已按 8/3*d_model 计算。SwiGLU: w1和w3同维度,w2降回
hidden = config.d_ff
self.w1 = nn.Linear(config.d_model, hidden, bias=False)
self.w2 = nn.Linear(hidden, config.d_model, bias=False)
self.w3 = nn.Linear(config.d_model, hidden, bias=False)
self.dropout = nn.Dropout(config.dropout)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# SwiGLU(x) = (SiLU(x @ W1) * (x @ W3)) @ W2
return self.w2(self.dropout(F.silu(self.w1(x)) * self.w3(x)))
# ════════════════════════════════════════════════════════════
# 6. Transformer 块
# ════════════════════════════════════════════════════════════
class XiaLiBlock(nn.Module):
"""Transformer 块 - Pre-RMSNorm + GQA + SwiGLU"""
def __init__(self, config: XiaLiConfig, layer_idx: int):
super().__init__()
self.layer_idx = layer_idx
self.attention = GroupedQueryAttention(config)
self.feed_forward = SwiGLU(config)
self.attn_norm = XiaLiRMSNorm(config.d_model, config.norm_eps)
self.ff_norm = XiaLiRMSNorm(config.d_model, config.norm_eps)
def forward(
self,
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
mask: Optional[torch.Tensor] = None,
) -> torch.Tensor:
h = x + self.attention(self.attn_norm(x), cos, sin, mask)
out = h + self.feed_forward(self.ff_norm(h))
return out
# ════════════════════════════════════════════════════════════
# 7. 主模型
# ════════════════════════════════════════════════════════════
class XiaLiModel(nn.Module):
"""
XiaLi-Next 统一架构
核心设计:
- RoPE 位置编码(无学习的 position embedding)
- Pre-RMSNorm(训练稳定)
- SwiGLU FFN(同等参数量更高质量)
- GQA 注意力(大模型省 KV Cache)
- Weight Tying(embedding 与输出层共享权重)
- 所有 Linear 无 bias(更干净)
- 从 20M 到 7B+ 统一架构
"""
def __init__(self, config: XiaLiConfig):
super().__init__()
self.config = config
# 词嵌入
self.embedding = nn.Embedding(config.vocab_size, config.d_model)
# 预计算 RoPE
cos, sin = precompute_rope_freqs(
config.d_model // config.n_heads,
config.max_seq_len * 2,
config.rope_theta,
)
self.register_buffer('rope_cos', cos, persistent=False)
self.register_buffer('rope_sin', sin, persistent=False)
# Transformer 块
self.blocks = nn.ModuleList([
XiaLiBlock(config, i) for i in range(config.n_layers)
])
# 最终归一化
self.norm = XiaLiRMSNorm(config.d_model, config.norm_eps)
# 输出层(与 embedding 共享权重)
self.fc_out = nn.Linear(config.d_model, config.vocab_size, bias=False)
if config.tie_weights:
self.fc_out.weight = self.embedding.weight # Weight Tying
self._init_weights()
def _init_weights(self):
"""初始化权重"""
for name, param in self.named_parameters():
if param.dim() >= 2:
nn.init.normal_(param, mean=0.0, std=0.02)
else:
nn.init.zeros_(param)
def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
batch, seq_len = x.shape
# 词嵌入
h = self.embedding(x)
# RoPE
cos = self.rope_cos[:, :seq_len]
sin = self.rope_sin[:, :seq_len]
# Transformer 层
for block in self.blocks:
h = block(h, cos, sin, mask)
# 最终归一化
h = self.norm(h)
# 输出
return self.fc_out(h)
def generate_mask(self, seq_len: int) -> torch.Tensor:
"""生成因果掩码 [1, 1, seq, seq]"""
mask = torch.full((seq_len, seq_len), float('-inf'))
mask = torch.triu(mask, diagonal=1)
return mask.unsqueeze(0).unsqueeze(0)
@torch.no_grad()
def generate(
self,
input_ids: torch.Tensor,
max_new_tokens: int = 100,
temperature: float = 0.7,
top_k: int = 40,
eos_id: int = 3,
) -> torch.Tensor:
"""自回归生成"""
self.eval()
for _ in range(max_new_tokens):
# 截断到 max_seq_len
seq = input_ids if input_ids.size(1) <= self.config.max_seq_len else input_ids[:, -self.config.max_seq_len:]
mask = self.generate_mask(seq.size(1)).to(seq.device)
logits = self.forward(seq, mask)
logits = logits[:, -1, :] / temperature
# Top-k 采样
if top_k > 0:
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, -1:]] = float('-inf')
probs = F.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, 1)
input_ids = torch.cat([input_ids, next_token], dim=-1)
if next_token.item() == eos_id:
break
return input_ids
def count_params(self) -> dict:
"""统计参数量"""
total = sum(p.numel() for p in self.parameters())
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
# 各模块
emb = self.embedding.weight.numel()
attn = sum(p.numel() for n, p in self.named_parameters() if 'attention' in n)
ffn = sum(p.numel() for n, p in self.named_parameters() if 'feed_forward' in n)
norm = sum(p.numel() for n, p in self.named_parameters() if 'norm' in n)
return {
'total': total,
'trainable': trainable,
'embedding': emb,
'attention': attn,
'ffn': ffn,
'norm': norm,
'total_m': total / 1e6,
}
# ════════════════════════════════════════════════════════════
# 8. 与原有系统兼容的训练适配
# ════════════════════════════════════════════════════════════
# 导出兼容名(方便从原系统切换)
XiaLiNextModel = XiaLiModel
XiaLiNextConfig = XiaLiConfig
# ════════════════════════════════════════════════════════════
# 9. 快速测试
# ════════════════════════════════════════════════════════════
if __name__ == "__main__":
print("=" * 65)
print(" XiaLi-Next 架构测试")
print("=" * 65)
for name in ['20m', '26m', '100m']:
cfg = XiaLiConfig.preset(name)
try:
model = XiaLiModel(cfg)
stats = model.count_params()
print(f"\n [{name}] {cfg.d_model}d × {cfg.n_layers}L × {cfg.n_heads}H (KV={cfg.n_kv_heads})")
print(f" 参数量: {stats['total_m']:.2f}M")
print(f" 其中: emb={stats['embedding']/1e6:.2f}M, attn={stats['attention']/1e6:.2f}M, "
f"ffn={stats['ffn']/1e6:.2f}M")
# 前向测试
x = torch.randint(0, cfg.vocab_size - 1, (1, 64))
try:
out = model(x)
print(f" 前向: ✅ {out.shape}")
except Exception as e:
print(f" 前向: ❌ {e}")
except Exception as e:
print(f"\n [{name}] ❌ {e}")
print(f"\n{'='*65}")
print(f" ✅ XiaLi-Next 就绪!")
print(f" 从 20M 到 7B+ 统一架构")
print(f"{'='*65}")