从 GPT-1 到 ChatGPT,再到现代 LLM:一文吃透大语言模型 7 年进化史

近两年大模型热度不减,但很多读者反馈:学 LLM 最痛苦的不是数学公式,而是**「概念满天飞、关系一团乱」**。GPT-1/2/3/ChatGPT 到底是什么关系?RoPE 跟传统位置编码的本质区别在哪?GQA 凭什么比 MHA 更好?看着博客一篇比一篇长,能把这些问题串起来讲清楚的不多。

这篇博客打算用两条主线带你一次吃透 LLM 的 7 年进化史:

  • 纵向主线:5 代 GPT 怎么从 1.17 亿参数长成 1750 亿,又怎么"开窍"听懂人话(InstructGPT/ChatGPT)
  • 横向主线:现代 LLM 的 5 大核心模块(位置编码、Attention、FFN、归一化)经历了哪些"组件级"迭代

通读全文,你既能讲清 GPT 家族代际关系,也能对 Llama/Qwen 源码里的"这一行是 GQA、这一行是 RoPE"对答如流。


一、核心概念:先把这 7 个词看明白

在进入正题之前,先用一张表过一遍贯穿全文的核心名词。这一节不堆公式,只用比喻

概念 一句话解释 生活比喻
Decoder-only 只用 Transformer 的解码器堆叠 一个"自说自话"的人,根据前文猜后文
Next-Token Prediction 给前文猜下一个字 玩"成语接龙"
Pre-training + Fine-tuning 先读万卷书,再精修一技 高考刷题 + 报志愿精专
In-Context Learning 不用训练,看几个例子就会 学霸看一眼例题就能举一反三
SFT / RM / RLHF 师傅带 → 评委改 → 学生改 三步驯服野马
Self-Attention 句子内部 token 互相"对眼神" 开会时大家互相观察谁跟谁相关
KV Cache 把算过的 K/V 存下来不重算 推算术题时把中间结果写在草稿纸上

一句话总结 LLM:在大规模无标注文本上预训练的、用 Transformer Decoder 堆起来的、以"猜下一个字"为核心任务的大型神经网络。

它的"大"主要体现在三方面:参数量大(数十亿到上万亿)、训练数据量大(覆盖多语言多领域)、能力强大(跨任务泛化,能问答、写代码、做推理)。


二、主线一:纵向看 5 代 GPT 的"族谱"演进

下面用 GPT 系列串起 LLM 的发展史。每一代解决的问题不一样,这才是进化的本质

2.1 GPT-1(2018):开山祖师,定调两阶段范式

GPT-1(Generative Pre-trained Transformer-1)是 OpenAI 在 2018 年 6 月发布的首个生成式预训练语言模型,参数量仅 1.17 亿(别笑,当时已经很大了)。

它的核心贡献是系统化验证并推广了"预训练 + 微调"两阶段范式

  • 预训练:在海量无标注文本上以"Next-Token Prediction"为目标训练
  • 微调:在少量标注数据上加任务头,迁移到分类、问答等具体任务

一句话:GPT-1 告诉你"大模型 + 任务微调"是条路

2.2 GPT-2(2019):堆料玩家,用规模说话

GPT-2 延续了 GPT-1 的 Decoder-only 架构,但参数规模直接提升 10 倍到 15.42 亿。同时引入了 Pre-LayerNorm 结构,把 LayerNorm 从残差后挪到残差前(这点后面会详细讲),缓解了深层网络的梯度传播问题。

一句话:GPT-2 告诉你"无监督多任务学习"是可行的,规模就是力量

2.3 GPT-3(2020):量变引发质变,In-Context Learning 出圈

GPT-3 是真正的"分水岭"。参数规模达到 1750 亿,相比 GPT-2 提升约百倍。

它最重要的创新不是"大",而是首次系统提出并验证了"上下文学习(In-Context Learning, ICL)"

  • 模型使用阶段无需任何额外训练
  • 仅靠输入文本中的任务描述 + 少量示例就能完成任务

一句话:GPT-3 告诉你"模型大到一定程度,会涌现出小模型没有的能力"

架构上,GPT-3 在 Transformer Block 中交替使用稠密注意力(Dense Attention)和局部带状稀疏注意力(Locally Banded Sparse Attention),把推理成本控制在可接受范围。

2.4 InstructGPT(2022):把"书呆子"教会说话

GPT-3 文本生成很强,但有个致命问题:它是"next token prediction"训练出来的,所以经常听不懂人话。你问它"写一首关于秋天的诗",它可能给你续写一段跟秋天无关的文字。

InstructGPT 引入指令对齐训练,采用三阶段范式:

  1. SFT(Supervised Fine-Tuning):用人工撰写的"指令-回答"对微调模型

  1. RM(Reward Model):让模型针对同一指令生成多个回答,人工排序,训练一个"评分器"
  2. RLHF(Reinforcement Learning from Human Feedback):以 RM 为奖励信号,用 PPO 算法继续优化 SFT 模型

一句话:InstructGPT 告诉你"光会续写还不够,还得学会按人类意图回答"

2.5 ChatGPT(2022.11):RLHF 路线的集大成者

ChatGPT 与 InstructGPT 同一技术路线,核心区别是数据集:它使用了特别构建的多轮对话格式数据,并支持连续交互、承认错误、拒绝不当请求。

一句话:ChatGPT 告诉你"RLHF + 对话数据 = 真正可用的产品"

2.6 5 代 GPT 一张表看懂

模型 年份 参数量 核心创新 范式突破
GPT-1 2018 1.17 亿 Decoder-only 预训练 + 微调
GPT-2 2019 15 亿 Pre-LayerNorm 无监督多任务
GPT-3 2020 1750 亿 In-Context Learning 涌现能力
InstructGPT 2022 13 亿 RLHF 三阶段 指令对齐
ChatGPT 2022.11 - 多轮对话 RLHF 产品化

关键洞见:LLM 这 5 年其实只做了两件事——

  • 一是**“变大”**(GPT-1 → GPT-3)
  • 二是**“变听话”**(InstructGPT → ChatGPT)

记住这两条主线,再看现在所有的 LLM 进展,无非是这两条主线的延伸。


三、主线二:横向看 5 大架构模块的"组件级"演进

纵向看完 5 代 GPT,我们换条线:横向看现代 LLM 内部到底用了哪些技术

可以这样理解:如果说 GPT 家族是"几代人",那这一节讲的是"这代人里每个器官长什么样"。

3.1 位置编码:RoPE 一统江湖

Transformer 的 Self-Attention 是置换不变的——把句子打乱,它算出来的注意力分数一样。这意味着模型天生不知道"谁在前谁在后",必须显式注入位置信息。

位置编码经历了三代演进

第一代:正余弦位置编码(原始 Transformer)

用固定函数为每个位置生成唯一向量:

PE(pos,2i)=sin(pos/10000(2i/d))PE(pos,2i+1)=cos(pos/10000(2i/d)) PE(pos, 2i) = sin(pos / 10000^(2i/d)) \\ PE(pos, 2i+1) = cos(pos / 10000^(2i/d)) PE(pos,2i)=sin(pos/10000(2i/d))PE(pos,2i+1)=cos(pos/10000(2i/d))

优点:无需训练、隐含相对位置信息(通过三角恒等式可以证明 PEm⋅PEn=cos⁡(ω(m−n))PE_m \cdot PE_n = \cos(\omega(m-n))PEmPEn=cos(ω(mn))

致命缺陷:注意力分数 score=qmknTscore = q_m k_n^Tscore=qmknT 展开后,相对位置信息项 PEm⋅A⋅PEnTPE_m \cdot A \cdot PE_n^TPEmAPEnT 里的矩阵 A=WQWKTA = W_Q W_K^TA=WQWKT 是可学习的,会在训练中破坏原本的相对位置结构。

第二代:可学习位置编码(BERT、GPT-1/2)

为每个位置分配一个可训练的向量

优点:灵活、训练简单。

致命缺陷:只能在训练时见过的序列长度内工作,完全没有长度外推能力。要支持更长文本?对不起,得重新训练或微调。

第三代:RoPE 旋转位置编码(当前主流 ⭐)

RoPE(Rotary Position Embedding) 由苏剑林在 RoFormer 论文中提出,Llama、Qwen、DeepSeek 等所有主流 LLM 都在用

它的核心思想很巧妙:不对词向量加位置编码,而是直接对 qqqkkk 向量按位置做"旋转"

具体来说,把 qqqkkk 拆成二维子向量 (a,b)(a, b)(a,b),位置 mmm 的 token 旋转 m⋅θim \cdot \theta_imθi 角度:

qm′=qm⋅R(m⋅θi) q'_m = q_m · R(m · θ_i) qm=qmR(mθi)

其中 R(θ)=[[cosθ,sinθ],[−sinθ,cosθ]]R(θ) = [[cos θ, sin θ], [-sin θ, cos θ]]R(θ)=[[cosθ,sinθ],[sinθ,cosθ]]

神奇的事情发生了——经过旋转换算,注意力分数只依赖相对位置差 m−nm-nmn

score(m,n)=func(qm,kn,m−n) score(m, n) = func(q_m, k_n, m-n) score(m,n)=func(qm,kn,mn)

生活比喻:想象一群人在房间里开会,每个人都按"自己的位置"原地转了几圈。神奇的是,两人之间的"对视强度"只跟他们坐的位置差有关,跟绝对位置无关。这就是 RoPE 的精髓。

RoPE 还有一个工程级优势:支持长度外推。数学结构不依赖训练长度,理论上可以处理比训练时更长的序列(当然,太长也会有衰减,业界通常配合 YaRN 等插值方法进一步外推)。

3.2 Attention:MHA → MQA → GQA 的"减负之路"

Attention 是 LLM 最核心的算力消耗大户。所有 Attention 变体的核心矛盾都是:KV Cache 显存 vs 表达力

MHA(Multi-Head Attention):原版

每个注意力头都独立生成并存储一份 K、V。表达力最强,但序列长、层数多时,KV Cache 显存爆炸。

比喻:每个学生都自己记一套笔记,纸张浪费严重。

MQA(Multi-Query Attention):极端共享

Google 在 2019 年提出 MQA:所有注意力头共享同一套 K、V。KV Cache 从 HHH 套降到 111 套,推理速度大幅提升。

代价:表达力略降,精度略逊于 MHA。

GQA(Grouped-Query Attention):折中方案 ⭐

GQA 是 Llama 2/3、Qwen、DeepSeek 等现代 LLM 的默认选择。把 Q 头分组,组内共享 K、V

  • MHA:每头独立 K/V → KV Cache = HHH
  • MQA:所有头共享 → KV Cache = 111
  • GQA:分组共享 → KV Cache = GGG 套(GGG 介于 1 和 H 之间)
方案 K/V 共享方式 KV Cache 量级 表达能力 典型模型
MHA 每头独立 HHH 最高 原始 Transformer
MQA 所有头共享 111 略降 PaLM
GQA 分组内共享 GGG 接近 MHA Llama 2/3, Qwen, Mistral

比喻:班委分组记笔记,每组一个"记录员"负责本组同学的笔记——既省纸,表达力也没怎么掉。

关于 KV Cache:这是推理优化的核心。它的原理可以一句话讲清楚——自回归生成时,每生成一个新 token 都要跟历史所有 token 算注意力,但历史 K/V 是不变的,存下来直接复用就行

每生成 1 个 token:
  - 不开 KV Cache:重算所有历史 K/V(O(N²))
  - 开启 KV Cache:只算当前 token 的 K/V,拼接到缓存(O(N))

实测下来,开启 KV Cache 后推理速度能提升 5~10 倍,长序列场景甚至更多。

3.3 FFN:从 ReLU 到 SwiGLU,再到 MoE

FFN(Feed-Forward Network)是 Transformer Block 里继 Attention 之后的第二大组件。它的演进集中在激活函数和结构两个方向

激活函数演进
ReLU(死亡神经元) → GELU(平滑) → SiLU/Swish → SwiGLU(当前主流)
  • ReLU:简单粗暴,但负区间梯度为 0,神经元会"死掉"
  • GELU:用正态分布 CDF 软化门控,GPT-2/3 时代的主流
  • SwiGLULlama、Qwen、DeepSeek、Mistral 全部在用。它是 GLU(Gated Linear Unit)的一种变体,采用"主分支 × 门控分支"双路设计:

FFNSwiGLU(x)=(SiLU(x⋅Wgate)⊙x⋅Wup)⋅Wdown FFN_{SwiGLU(x)} = (SiLU(x · W_{gate}) ⊙ x · W_{up}) · W_{down} FFNSwiGLU(x)=(SiLU(xWgate)xWup)Wdown

比喻:把 FFN 想象成"做菜"。SwiGLU 不是简单地把食材扔进锅里翻炒(ReLU/GELU),而是主厨先做一份半成品,门控师傅把关"哪些食材能上桌",最后合并出锅——味道自然更好。

MoE:把 FFN 升级为"专家团"

MoE(Mixture of Experts) 是当前大模型扩展的标配。它用多个并行的 FFN 专家替代单一 FFN 层,通过 Router 选择 top-k 个专家参与计算:

输入 token → Router 选 top-2 专家 → 专家各自计算 → 加权合并输出

核心优势

  • 高容量、低计算:总参数量大(专家多),但每次前向只激活 kkk 个专家
  • 专家分工:不同专家擅长不同模式(写代码、写诗、做数学)
  • 天然分布式:专家可分布到不同设备并行计算

典型代表:Mixtral 8x7B、DeepSeek-V2、Qwen2-MoE、GPT-4(传闻也是 MoE)。

比喻:医院里不分"全科医生"了,改成"专家门诊"。每个病人(token)挂号时由分诊台(Router)推荐最对口的 2 个专家——比一个全科医生效率高得多。

3.4 归一化与残差:RMSNorm + Pre-Norm 成标配

归一化和残差连接是 Transformer 能堆到上百层的"地基"。这块的演进主要发生在两个维度。

RMSNorm:去掉均值的简化版 LayerNorm

LayerNorm 要算均值和方差,RMSNorm 只算均方根(RMS)

RMSNorm(x)=(x/RMS(x))⋅γ RMSNorm(x) = (x / RMS(x)) · γ RMSNorm(x)=(x/RMS(x))γ

实践发现,均值项在深层 Transformer 里对稳定性贡献有限,去掉它不仅不损害性能,还更高效。Llama、Qwen、DeepSeek 全部采用 RMSNorm。

Pre Norm vs Post Norm

归一化放残差前还是后,对深层网络训练稳定性影响巨大。

  • Post Norm(原始 Transformer):y=Norm(x+F(x))y = \text{Norm}(x + F(x))y=Norm(x+F(x))
    • 问题:归一化操作破坏了恒等梯度路径,深层网络训练不稳定
  • Pre Norm(现代主流 ⭐):y=x+F(Norm(x))y = x + F(\text{Norm}(x))y=x+F(Norm(x))
    • 优势:归一化在子层内部,残差路径上的恒等梯度(∂y/∂x=1\partial y / \partial x = 1y/x=1)完整保留

比喻:把残差连接比作"高速公路",Pre Norm 是主路全程不收费(梯度直达),Post Norm 则是每过一个城市都收一次过路费(梯度被归一化扰动),跑长途(深层网络)当然前者更稳。

方案 结构 恒等梯度路径 训练稳定性 典型模型
Post Norm y=Norm(x+F(x))y = \text{Norm}(x + F(x))y=Norm(x+F(x)) 被扰动 ❌ 不稳定 原始 Transformer
Pre Norm y=x+F(Norm(x))y = x + F(\text{Norm}(x))y=x+F(Norm(x)) 完整保留 ✅ 稳定 GPT-3, Llama, Qwen

3.5 整合示范:用 Qwen3 源码把上面 4 个模块"串起来"

光讲理论太抽象,我们拿 Qwen3 源码(现代 LLM 的典型代表)逐行标注释,看上面讲的 5 大模块是怎么被整合到一段真实代码里的。

先看 Qwen3Model 整体结构

class Qwen3Model(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        # 输入层:token embedding (B, T) → (B, T, D)
        self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])

        # Transformer Block 堆叠
        self.trf_blocks = nn.ModuleList(
            [TransformerBlock(cfg) for _ in range(cfg["n_layers"])]
        )

        # 输出前的 RMSNorm(注意是 Pre-Norm 风格)
        self.final_norm = RMSNorm(cfg["emb_dim"])

        # 输出层:投影回词表大小
        self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)

        # 预计算 RoPE 所需的 cos/sin(避免每个 step 重算)
        cos, sin = compute_rope_params(
            head_dim=cfg["head_dim"],
            theta_base=cfg["rope_base"],
            context_length=cfg["context_length"]
        )
        self.register_buffer("cos", cos, persistent=False)
        self.register_buffer("sin", sin, persistent=False)

        # KV Cache 位置追踪
        self.current_pos = 0

    def forward(self, in_idx, cache=None):
        # 1) token embedding
        x = self.tok_emb(in_idx)   # Shape: (B, T, D)

        # 2) 因果掩码(带 KV Cache 的切片处理)
        if cache is not None:
            pos_start = self.current_pos
            pos_end = pos_start + x.shape[1]
            self.current_pos = pos_end
        else:
            pos_start = 0
        # ... mask 构造略

        # 3) 逐层 Transformer Block
        for i, block in enumerate(self.trf_blocks):
            blk_cache = cache.get(i) if cache else None
            x, new_blk_cache = block(x, mask, self.cos, self.sin,
                                     start_pos=pos_start, cache=blk_cache)
            if cache is not None:
                cache.update(i, new_blk_cache)

        # 4) 输出前归一化
        x = self.final_norm(x)
        # 5) 投影回词表
        logits = self.out_head(x)
        return logits

再看 TransformerBlock,短短几行就集齐了 Pre-Norm + GQA + SwiGLU 三大件:

class TransformerBlock(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        # ① GQA 注意力层
        self.att = GroupedQueryAttention(
            d_in=cfg["emb_dim"],
            num_heads=cfg["n_heads"],
            num_kv_groups=cfg["n_kv_groups"],   # ← 这就是 GQA!
            qk_norm=cfg["qk_norm"],
        )
        # ② SwiGLU FFN
        self.ff = FeedForward(cfg)
        # ③ Pre-Norm:两个 RMSNorm
        self.norm1 = RMSNorm(cfg["emb_dim"])
        self.norm2 = RMSNorm(cfg["emb_dim"])

    def forward(self, x, mask, cos, sin, start_pos=0, cache=None):
        # 【Pre-Norm + 残差】第一子层:Attention
        shortcut = x
        x = self.norm1(x)              # ← Pre-Norm:先归一化
        x, next_cache = self.att(x, mask, cos, sin,
                                  start_pos=start_pos, cache=cache)
        x = x + shortcut               # ← 残差连接

        # 【Pre-Norm + 残差】第二子层:FFN
        shortcut = x
        x = self.norm2(x)              # ← Pre-Norm
        x = self.ff(x)                 # ← SwiGLU FFN
        x = x + shortcut               # ← 残差连接
        return x, next_cache

再看 SwiGLU FFN 的实现:

class FeedForward(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        # 三个线性层:gate、up、down(无 bias 是现代 LLM 标配)
        self.fc1 = nn.Linear(cfg["emb_dim"], cfg["hidden_dim"], bias=False)  # gate
        self.fc2 = nn.Linear(cfg["emb_dim"], cfg["hidden_dim"], bias=False)  # up
        self.fc3 = nn.Linear(cfg["hidden_dim"], cfg["emb_dim"], bias=False)  # down

    def forward(self, x):
        # SwiGLU 激活:SiLU(gate(x)) * up(x)
        x = F.silu(self.fc1(x)) * self.fc2(x)
        return self.fc3(x)

最后看 GQA 的核心代码,重点关注 KV 重复这一步:

class GroupedQueryAttention(nn.Module):
    def forward(self, x, mask, cos, sin, start_pos=0, cache=None):
        b, num_tokens, _ = x.shape

        # 1) 线性投影得到 Q、K、V
        queries = self.W_query(x)  # (B, T, n_heads * head_dim)
        keys    = self.W_key(x)    # (B, T, n_kv_groups * head_dim)  ← GQA 关键
        values  = self.W_value(x)  # (B, T, n_kv_groups * head_dim)  ← GQA 关键

        # 2) 重塑维度
        queries = queries.view(b, num_tokens, self.num_heads, self.head_dim).transpose(1, 2)
        keys    = keys.view(b, num_tokens, self.num_kv_groups, self.head_dim).transpose(1, 2)
        values  = values.view(b, num_tokens, self.num_kv_groups, self.head_dim).transpose(1, 2)

        # 3) QK 归一化(Qwen3 特有,提升训练稳定性)
        if self.q_norm:
            queries = self.q_norm(queries)
            keys = self.k_norm(keys)

        # 4) RoPE 位置编码
        queries = apply_rope(queries, cos, sin, offset=start_pos)
        keys    = apply_rope(keys, cos, sin, offset=start_pos)

        # 5) KV Cache:拼接历史 K/V
        if cache is not None:
            keys = torch.cat([cache[0], keys], dim=2)
            values = torch.cat([cache[1], values], dim=2)
        next_cache = (keys, values)

        # 6) 【GQA 核心】把 G 份 KV 重复扩展到 H 份
        keys   = keys.repeat_interleave(self.group_size, dim=1)
        values = values.repeat_interleave(self.group_size, dim=1)

        # 7) 注意力计算 + 因果掩码
        attn_scores = (queries @ keys.transpose(2, 3)) / (self.head_dim ** 0.5)
        attn_scores = attn_scores.masked_fill(mask, -torch.inf)
        attn_weights = F.softmax(attn_scores, dim=-1)
        context = (attn_weights @ values).transpose(1, 2).reshape(b, num_tokens, -1)
        return self.out_proj(context), next_cache

实战要点:上面这段代码里,register_buffer 注册的 cos/sin 会跟着模型 .to(device) 自动迁移,但不会出现在 state_dict() 里被优化器更新。这正是 RoPE 该有的样子。


四、实战代码:手把手跑一遍

理论讲完了,上手写两个最小可运行 demo,把上面的概念"摸"一遍。

Demo 1:5 分钟跑通 GPT-2 + In-Context Learning

这个 demo 演示两件事:(1) Hugging Face 一行代码加载 GPT-2;(2) 用 Few-shot Prompting 直观感受 GPT-3 提出的 In-Context Learning。

"""
Demo 1: 跑通 GPT-2,演示 In-Context Learning
依赖:pip install transformers torch
"""
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

# 1) 加载 tokenizer 和模型(gpt2 约 500MB,国内可用镜像)
model_name = "gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)

# 【踩坑提醒】GPT-2 没有 pad_token,必须手动设置,否则 batch 生成会报错
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
model.eval()  # 推理模式

# 2) 演示 1:让 GPT-2 "续写"(它只会续写,不会按指令回答)
prompt = "The capital of France is"
inputs = tokenizer(prompt, return_tensors="pt").to(device)
# Shape: input_ids -> (1, T)
with torch.no_grad():
    output = model.generate(
        **inputs,
        max_new_tokens=10,
        do_sample=False,        # 贪心解码,结果可复现
        pad_token_id=tokenizer.eos_token_id,
    )
print("【续写结果】:", tokenizer.decode(output[0], skip_special_tokens=True))
# 期望输出:The capital of France is Paris.(或者类似续写)

# 3) 演示 2:In-Context Learning(Few-shot Prompting)
# 给 3 个示例,让模型"看例题学会任务"
few_shot_prompt = """Translate English to French.
English: Hello
French: Bonjour
English: Thank you
French: Merci
English: Good morning
French:"""
inputs = tokenizer(few_shot_prompt, return_tensors="pt").to(device)
with torch.no_grad():
    output = model.generate(
        **inputs,
        max_new_tokens=5,
        do_sample=False,
        pad_token_id=tokenizer.eos_token_id,
    )
print("【ICL 结果】:", tokenizer.decode(output[0], skip_special_tokens=True))
# 期望输出:...French: Bonjour(GPT-2 通过看例题"学会"了翻译模式)

运行结果说明:GPT-2 是 base model,能力有限。它能勉强完成一些简单 ICL,但要稳定做任务还是得用 InstructGPT/ChatGPT 这种经过 SFT+RLHF 的版本。这就是为什么 1750 亿参数的 GPT-3 也干不过 13 亿参数的 InstructGPT

Demo 2:手写一个最小"现代 LLM Block"(GQA + RoPE + SwiGLU)

这个 demo 把上面讲的 5 大模块里的核心组件浓缩到一个文件里,让你看到现代 LLM 的"最小可运行单元"长什么样

"""
Demo 2: 手写一个最小 GQA + RoPE + SwiGLU Block
展示现代 LLM 的核心组件是如何拼装的
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import math


# ============ 1. RMSNorm ============
class RMSNorm(nn.Module):
    def __init__(self, dim, eps=1e-6):
        super().__init__()
        self.eps = eps
        self.scale = nn.Parameter(torch.ones(dim))

    def forward(self, x):
        # Shape: (B, T, D) -> 保持不变
        norm = x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
        return norm * self.scale


# ============ 2. RoPE(旋转位置编码)===========
def precompute_rope(head_dim, max_len=2048, theta_base=10000.0):
    """预计算 RoPE 的 cos/sin 表"""
    # Shape: (head_dim // 2,)
    inv_freq = 1.0 / (theta_base ** (torch.arange(0, head_dim, 2).float() / head_dim))
    # Shape: (max_len, head_dim // 2)
    positions = torch.arange(max_len).float()
    angles = positions.unsqueeze(1) * inv_freq.unsqueeze(0)
    # Shape: (max_len, head_dim)
    angles = torch.cat([angles, angles], dim=1)
    return torch.cos(angles), torch.sin(angles)


def apply_rope(x, cos, sin):
    """对 q/k 应用旋转位置编码
    x: (B, H, T, D) -> 输出同 shape
    """
    B, H, T, D = x.shape
    # 前后半配对(Qwen3 风格)
    x1, x2 = x[..., :D//2], x[..., D//2:]
    # 扩展维度以匹配输入
    cos = cos[:T].unsqueeze(0).unsqueeze(0)  # (1, 1, T, D)
    sin = sin[:T].unsqueeze(0).unsqueeze(0)
    # 旋转公式:x * cos + (-x2, x1) * sin
    rotated = torch.cat((-x2, x1), dim=-1)
    return (x * cos) + (rotated * sin)


# ============ 3. GQA(分组查询注意力)============
class GQA(nn.Module):
    def __init__(self, d_model, n_heads, n_kv_groups):
        super().__init__()
        assert n_heads % n_kv_groups == 0, "n_heads 必须能被 n_kv_groups 整除"
        self.n_heads = n_heads
        self.n_kv_groups = n_kv_groups
        self.group_size = n_heads // n_kv_groups
        self.head_dim = d_model // n_heads

        # Q 输出维度 = n_heads * head_dim
        # K/V 输出维度 = n_kv_groups * head_dim  ← GQA 关键
        self.W_q = nn.Linear(d_model, n_heads * self.head_dim, bias=False)
        self.W_k = nn.Linear(d_model, n_kv_groups * self.head_dim, bias=False)
        self.W_v = nn.Linear(d_model, n_kv_groups * self.head_dim, bias=False)
        self.W_o = nn.Linear(n_heads * self.head_dim, d_model, bias=False)

    def forward(self, x, cos, sin, mask=None):
        B, T, _ = x.shape
        # 1) 投影:Q 完整,K/V 缩减为 n_kv_groups 份
        q = self.W_q(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
        k = self.W_k(x).view(B, T, self.n_kv_groups, self.head_dim).transpose(1, 2)
        v = self.W_v(x).view(B, T, self.n_kv_groups, self.head_dim).transpose(1, 2)
        # Shape: q -> (B, H, T, D_h), k/v -> (B, G, T, D_h)

        # 2) RoPE 注入位置信息
        q = apply_rope(q, cos, sin)
        k = apply_rope(k, cos, sin)

        # 3) GQA 核心:把 K/V 在 head 维度重复扩展到 n_heads 份
        k = k.repeat_interleave(self.group_size, dim=1)  # (B, H, T, D_h)
        v = v.repeat_interleave(self.group_size, dim=1)

        # 4) 注意力计算
        scores = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)  # (B, H, T, T)
        if mask is not None:
            scores = scores.masked_fill(mask, -torch.inf)
        weights = F.softmax(scores, dim=-1)
        context = weights @ v  # (B, H, T, D_h)
        context = context.transpose(1, 2).reshape(B, T, -1)
        return self.W_o(context)


# ============ 4. SwiGLU FFN ============
class SwiGLU(nn.Module):
    def __init__(self, d_model, hidden_dim):
        super().__init__()
        self.W_gate = nn.Linear(d_model, hidden_dim, bias=False)
        self.W_up   = nn.Linear(d_model, hidden_dim, bias=False)
        self.W_down = nn.Linear(hidden_dim, d_model, bias=False)

    def forward(self, x):
        # SwiGLU: SiLU(gate(x)) * up(x)
        return self.W_down(F.silu(self.W_gate(x)) * self.W_up(x))


# ============ 5. 完整 Block:Pre-Norm + GQA + SwiGLU ============
class ModernBlock(nn.Module):
    """一个完整的"现代 LLM Block",集成 Pre-Norm + GQA + RoPE + SwiGLU"""
    def __init__(self, d_model, n_heads, n_kv_groups, ffn_hidden):
        super().__init__()
        self.norm1 = RMSNorm(d_model)
        self.norm2 = RMSNorm(d_model)
        self.att = GQA(d_model, n_heads, n_kv_groups)
        self.ffn = SwiGLU(d_model, ffn_hidden)

    def forward(self, x, cos, sin, mask=None):
        # Pre-Norm + 残差
        x = x + self.att(self.norm1(x), cos, sin, mask)
        x = x + self.ffn(self.norm2(x))
        return x


# ============ 6. 跑起来 ============
if __name__ == "__main__":
    # 超参数:模拟一个"迷你 LLaMA"
    d_model = 512        # 隐藏维度
    n_heads = 8          # 注意力头数
    n_kv_groups = 2      # KV 分组数(GQA:4 个 Q 头共享 1 份 KV)
    ffn_hidden = 2048    # FFN 隐藏维度
    max_len = 1024       # 上下文长度
    batch_size = 2
    seq_len = 64

    block = ModernBlock(d_model, n_heads, n_kv_groups, ffn_hidden)
    cos, sin = precompute_rope(d_model // n_heads, max_len)

    # 模拟输入
    x = torch.randn(batch_size, seq_len, d_model)
    # 因果掩码
    mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool()
    mask = mask.unsqueeze(0).unsqueeze(0)  # (1, 1, T, T)

    # 前向
    out = block(x, cos, sin, mask)
    print(f"输入 shape: {x.shape}")
    print(f"输出 shape: {out.shape}")
    print(f"GQA 参数量 vs MHA 参数量: KV 部分仅 {n_kv_groups/n_heads*100:.0f}%")
    # 期望输出:
    # 输入 shape: torch.Size([2, 64, 512])
    # 输出 shape: torch.Size([2, 64, 512])
    # GQA 参数量 vs MHA 参数量: KV 部分仅 25%

代码关键注释

  • repeat_interleave 是 GQA 的灵魂:KV Cache 实际只存 GGG,前向时再"逻辑扩展"到 HHH
  • register_buffer 不写的 RoPE 可以用 precompute_rope 提前算好,避免每个 step 重复计算
  • 训练时记得 model.train() + 梯度清零 + device 一致性

五、避坑指南:实际工程中的 5 个常见坑

最后分享 5 个新手最常踩的坑,都是我自己在带项目和面试候选人时反复遇到过的:

现象 解决
Base Model ≠ Instruct Model gpt2 跑对话 prompt,输出驴唇不对马嘴 选 Instruct 版(如 Qwen2.5-7B-InstructLlama-3-8B-Instruct
Tokenizer 不匹配 中文输入到 gpt2 里全是乱码或 <unk> 中文场景用 Qwen/ChatGLM/Baichua 等中文 tokenizer
GPT-2 没有 pad_token DataCollator 或 batch generate 报 ValueError 显式设置 tokenizer.pad_token = tokenizer.eos_token
推理忘开 KV Cache 长文本生成慢得离谱,1 个 token 跑 1 秒 model.generate(use_cache=True)(默认就是 True)
Position Interpolation 缺失 长文本超训练长度后效果断崖式下降 用 YaRN / NTK-aware 插值外推,或直接选支持长上下文的模型

训练侧的额外建议

  • 显存吃紧:选 GQA / MQA 模型(Llama 3 8B),优先用 bfloat16 而不是 float16(避免数值溢出)
  • 微调起点:优先 Instruct Model,不是 Base Model(除非你有强领域数据 + 充足算力)
  • RLHF 替代方案:PPO 不稳定,普通玩家用 DPO、KTO 替代,效果接近但训练简单 10 倍

六、总结:一张表看懂 LLM 7 年

最后用 2 张表收尾。

表 1:5 代 GPT 代际对比

模型 年份 参数量 核心创新 范式突破
GPT-1 2018 1.17 亿 Decoder-only 预训练 + 微调
GPT-2 2019 15 亿 Pre-LayerNorm 无监督多任务
GPT-3 2020 1750 亿 In-Context Learning 涌现能力
InstructGPT 2022 13 亿 RLHF 三阶段 指令对齐
ChatGPT 2022.11 - 多轮对话 RLHF 产品化

表 2:现代 LLM 5 大模块主流选型

模块 现代主流方案 代表模型
位置编码 RoPE Llama, Qwen, DeepSeek
Attention GQA Llama 2/3, Mistral, Qwen
FFN SwiGLU Llama, Qwen, DeepSeek
归一化 RMSNorm + Pre-Norm Llama, Qwen, DeepSeek
模型扩展 MoE 稀疏激活 Mixtral, DeepSeek-V2, Qwen2-MoE

最后一句话送给你

LLM 这 7 年就做了一件事——把 Transformer 的每个模块都换了一遍

GPT-1 给你看骨架,GPT-3 给你看规模,InstructGPT 给你看对齐,Llama/Qwen 给你看现代架构。

理解了这 5 大模块的演进 + 5 代 GPT 的代际使命,你就看懂了 90% 的现代大模型。剩下的 10%,留给未来的 GPT-5 / Claude 4 / Gemini 3 去颠覆。

如果这篇博客帮你理清了 LLM 的进化脉络,点赞、收藏、关注走一波~ 下一篇我会写 LLM 微调实战(LoRA / QLoRA / DPO 全流程),敬请期待。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐