Download model.py from dbdx1923/dbdx: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/dbdx1923/dbdx/resolve/main/model.py
- Command line
-
hf download hf://dbdx1923/dbdx/model.py
-
curl -L -o model.py https://huggingface.co/dbdx1923/dbdx/resolve/main/model.py
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("接口检查通过。") | |