dbdx / model.py
MouseBill's picture
Upload model.py with huggingface_hub
d640b79 verified
Raw
History Blame Contribute Delete
10.4 kB
"""
SmolGPT — 参考 HuggingFace SmolLM2-135M 的 "深而窄" 设计
设计理念(与同仓库 deep-gpt 对比):
- 深:n_layer 12 → 30(参考 SmolLM2 的 30 层)
- 窄:n_embd 768 → 512,head_dim 保持 64
- GQA:分组查询注意力 8 Q heads / 2 KV heads(4:1)
类比 SmolLM2 的 9 Q / 3 KV,减小 KV cache 占用
- ReLU² 前馈(Primer 论文),hidden_dim=1728;
相比 SwiGLU 少 1 个矩阵,同参数预算下更宽
- RMSNorm + QK-norm,RoPE theta=100000(参考 SmolLM2)
- 残差出口零初始化(c_proj.weight = 0,起步即恒等映射)、tied embeddings
总参数:约 98.5M(满足 ≤100M 约束)
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
def precompute_freqs(head_dim: int, seq_len: int, theta: float = 100000.0):
"""预计算 RoPE 旋转频率(cos/sin),形状 (seq_len, head_dim)。"""
inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim))
t = torch.arange(seq_len, dtype=torch.float32)
freqs = torch.outer(t, inv_freq) # (seq_len, head_dim//2)
cos = torch.cat([freqs.cos(), freqs.cos()], dim=-1)
sin = torch.cat([freqs.sin(), freqs.sin()], dim=-1)
return cos, sin
def rotate_half(x: torch.Tensor) -> torch.Tensor:
x1, x2 = x[..., : x.shape[-1] // 2], x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
def apply_rope(q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor):
cos = cos.unsqueeze(0).unsqueeze(0) # (1, 1, T, head_dim)
sin = sin.unsqueeze(0).unsqueeze(0)
q = q * cos + rotate_half(q) * sin
k = k * cos + rotate_half(k) * sin
return q, k
class RMSNorm(nn.Module):
"""RMS 层归一化:相比 LayerNorm 无均值偏移,更快且数值稳定。"""
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:
norm = x.float().pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt()
return (x.float() * norm * self.weight).type_as(x)
class GroupedQueryAttention(nn.Module):
"""分组查询注意力(GQA)+ QK-norm + RoPE + Flash SDPA。
Q heads 数量 > KV heads 数量,K/V 在 head 维度上被多个 Q 共享。
显著降低注意力的参数量与推理 KV cache 占用。
"""
def __init__(self, n_embd: int, n_head: int, n_kv_head: int):
super().__init__()
assert n_embd % n_head == 0, f"n_embd={n_embd} 必须能被 n_head={n_head} 整除"
assert n_head % n_kv_head == 0, f"n_head={n_head} 必须能被 n_kv_head={n_kv_head} 整除"
self.n_head = n_head
self.n_kv_head = n_kv_head
self.head_dim = n_embd // n_head
self.n_rep = n_head // n_kv_head # 每个 KV head 服务多少个 Q head
# 分开三组投影:Q 全宽,K/V 收窄到 n_kv_head * head_dim
self.q_proj = nn.Linear(n_embd, n_head * self.head_dim, bias=False)
self.k_proj = nn.Linear(n_embd, n_kv_head * self.head_dim, bias=False)
self.v_proj = nn.Linear(n_embd, n_kv_head * self.head_dim, bias=False)
self.o_proj = nn.Linear(n_embd, n_embd, bias=False)
# QK-norm:每个 head 独立归一化,稳定大 batch / 长训练时的 attention logits
self.q_norm = RMSNorm(self.head_dim)
self.k_norm = RMSNorm(self.head_dim)
def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
B, T, C = x.shape
q = self.q_proj(x).view(B, T, self.n_head, self.head_dim).transpose(1, 2)
k = self.k_proj(x).view(B, T, self.n_kv_head, self.head_dim).transpose(1, 2)
v = self.v_proj(x).view(B, T, self.n_kv_head, self.head_dim).transpose(1, 2)
# QK-norm(在 RoPE 之前)
q = self.q_norm(q)
k = self.k_norm(k)
q, k = apply_rope(q, k, cos[:T], sin[:T])
# GQA:把 K/V 在 head 维度上重复 n_rep 次以匹配 Q
# 比 SDPA 的 enable_gqa 参数更通用(兼容旧版 PyTorch)
if self.n_rep > 1:
k = k.repeat_interleave(self.n_rep, dim=1)
v = v.repeat_interleave(self.n_rep, dim=1)
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
return self.o_proj(y.transpose(1, 2).contiguous().view(B, T, C))
class ReLU2MLP(nn.Module):
"""ReLU² 前馈:c_proj(ReLU(c_fc(x))²)。
来自 Primer 论文 (https://arxiv.org/abs/2109.08668v2)。相比 SwiGLU:
- 只需 2 个矩阵(vs 3),同参数预算下 hidden_dim 可放大 1.5×
- 没有门控分支,前向/反向都更快
- LM 任务上质量与 SwiGLU 持平或略优(modded-nanogpt 验证)
"""
def __init__(self, n_embd: int, hidden_dim: int):
super().__init__()
self.c_fc = nn.Linear(n_embd, hidden_dim, bias=False)
self.c_proj = nn.Linear(hidden_dim, n_embd, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.c_proj(F.relu(self.c_fc(x)).square())
class Block(nn.Module):
"""Pre-norm 块:RMSNorm → GQA → 残差 → RMSNorm → ReLU² → 残差。"""
def __init__(self, n_embd: int, n_head: int, n_kv_head: int, hidden_dim: int):
super().__init__()
self.norm1 = RMSNorm(n_embd)
self.attn = GroupedQueryAttention(n_embd, n_head, n_kv_head)
self.norm2 = RMSNorm(n_embd)
self.ffn = ReLU2MLP(n_embd, hidden_dim)
def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
x = x + self.attn(self.norm1(x), cos, sin)
x = x + self.ffn(self.norm2(x))
return x
class SmolGPT(nn.Module):
"""
深而窄的 GPT 模型,约 98.5M 参数。
默认超参数:
n_embd=512, n_layer=30, n_head=8, n_kv_head=2 (head_dim=64), hidden_dim=1728
(hidden_dim 从原 SwiGLU 的 1152 调到 ReLU² 的 1728,保持 MLP 总参数
不变:SwiGLU 用 3×512×1152 = ReLU² 用 2×512×1728)
"""
cos: torch.Tensor
sin: torch.Tensor
def __init__(
self,
vocab_size: int = 50257,
n_embd: int = 512,
n_head: int = 8,
n_kv_head: int = 2,
n_layer: int = 30,
block_size: int = 1024,
hidden_dim: int = 1728,
rope_theta: float = 100000.0,
):
super().__init__()
self.block_size = block_size
self.token_emb = nn.Embedding(vocab_size, n_embd)
head_dim = n_embd // n_head
cos, sin = precompute_freqs(head_dim, block_size, theta=rope_theta)
self.register_buffer("cos", cos, persistent=False)
self.register_buffer("sin", sin, persistent=False)
self.blocks = nn.ModuleList(
[Block(n_embd, n_head, n_kv_head, hidden_dim) for _ in range(n_layer)]
)
self.norm_f = RMSNorm(n_embd)
# 权重绑定:lm_head 与 token_emb 共享,节省 ~26M 参数
self.lm_head = nn.Linear(n_embd, vocab_size, bias=False)
self.lm_head.weight = self.token_emb.weight
self._init_weights()
def _init_weights(self):
"""GPT-2 风格初始化 + 残差出口零初始化。
残差出口(GQA 的 o_proj、ReLU2MLP 的 c_proj)置零意味着训练起步时
每个 Block 都是恒等映射 `x ← x + 0`,模型等价于 Embed → Norm → LM_Head
的浅层模型。残差贡献从第一步开始由训练过程精确学出,loss 曲线起步
更平滑(modded-nanogpt / Primer 等多个实验验证)。
"""
for module in self.modules():
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
elif isinstance(module, nn.Embedding):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
# 残差流出口:attention 的 o_proj、MLP 的 c_proj
for name, p in self.named_parameters():
if name.endswith(("o_proj.weight", "c_proj.weight")):
nn.init.zeros_(p)
def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
"""
Args:
input_ids: LongTensor (B, T)
Returns:
logits: FloatTensor (B, T, vocab_size)
"""
B, T = input_ids.shape
assert T <= self.block_size, f"序列长度 {T} 超过 block_size {self.block_size}"
x = self.token_emb(input_ids)
cos = self.cos[:T]
sin = self.sin[:T]
for block in self.blocks:
x = block(x, cos, sin)
x = self.norm_f(x)
return self.lm_head(x)
# --- 必须实现的接口 ---
def load_model(checkpoint_path: str, device: str = "cuda") -> torch.nn.Module:
"""从 checkpoint 加载 SmolGPT。支持纯 state_dict 或带 config 的字典格式。"""
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=True)
if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint:
state_dict = checkpoint["model_state_dict"]
config = checkpoint.get("config", {})
model = SmolGPT(**config)
else:
state_dict = checkpoint
model = SmolGPT()
# 去除 torch.compile 添加的 '_orig_mod.' 前缀
unwanted_prefix = "_orig_mod."
for k in list(state_dict.keys()):
if k.startswith(unwanted_prefix):
state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k)
model.load_state_dict(state_dict)
model.to(device)
model.eval()
return model
# --- 快速完整性检查 ---
if __name__ == "__main__":
model = SmolGPT()
unique_params = sum(p.numel() for p in set(model.parameters()))
all_params = sum(p.numel() for p in model.parameters())
print(f"唯一参数量: {unique_params:,} ({unique_params/1e6:.2f}M)")
print(f"全部参数量: {all_params:,} ({all_params/1e6:.2f}M) ← model.parameters() 统计")
assert unique_params <= 100_000_000, f"超过 100M 限制!实际 {unique_params/1e6:.2f}M"
dummy = torch.randint(0, 50257, (2, 1024))
logits = model(dummy)
print(f"输入 shape: {dummy.shape}")
print(f"输出 shape: {logits.shape}")
assert logits.shape == (2, 1024, 50257), "输出形状错误!"
print("接口检查通过。")