Skip to content

7.1 从 GPT 到 Llama:渐进式架构转换

GPT 系列与 Llama 系列同属 Transformer 解码器(decoder-only)架构,但在归一化方式、激活函数、位置编码、注意力机制和词表设计等方面,Llama 对 GPT 进行了系统性的改进。这些改进并非一蹴而就,而是在 Llama 1、Llama 2、Llama 3 三代模型中逐步演化,每一代解决不同层面的瓶颈。本节将沿着 GPT → Llama 1 → Llama 2 → Llama 3/3.2 的时间线,逐一剖析每一步的架构变化,并通过代码展示关键改动的实现细节。

Transformer 架构演进示意图

图 7-1:Transformer 解码器架构。GPT 和 Llama 系列共享这一基本框架,但在归一化、激活函数、位置编码等细节上逐步演化。

7.1.1 GPT → Llama 1:四项架构改进

Llama 1 的论文题为 LLaMA: Open and Efficient Foundation Language Models,强调两个核心理念:开放性(完全使用公开数据训练)和高效性(用更小的模型 + 更多数据达到大模型的性能)。Meta 的核心观察是:相比扩大模型参数,增加训练数据规模更能提升性能。例如,7B 参数的模型在 1.4 万亿 token 上训练后,性能仍在持续提升,远超传统经验建议的 2000 亿 token。

在模型结构上,Llama 1 对标准 GPT 架构做了四项关键改进:

改进一:Pre-Norm + RMSNorm 替代 Post-Norm + LayerNorm。 GPT-2 采用 Pre-Norm 结构(归一化放在子层输入前),但归一化层使用 LayerNorm。Llama 1 保留 Pre-Norm 的位置,但将 LayerNorm 替换为 RMSNorm(Root Mean Square Layer Normalization)。RMSNorm 的核心思想是去除 LayerNorm 中的减均值步骤和偏置项,仅通过均方根进行缩放:

RMSNorm(x)=xMean(x2)+ϵγ

对比 LayerNorm 的公式:

LayerNorm(x)=xE[x]Var[x]+ϵγ+β

RMSNorm 省去了均值计算和偏置参数 β,在保持归一化效果的同时降低了计算开销。代码实现如下:

python
# GPT 使用 LayerNorm
class LayerNorm(nn.Module):
    def __init__(self, emb_dim):
        super().__init__()
        self.eps = 1e-5
        self.scale = nn.Parameter(torch.ones(emb_dim))
        self.shift = nn.Parameter(torch.zeros(emb_dim))

    def forward(self, x):
        mean = x.mean(dim=-1, keepdim=True)
        var = x.var(dim=-1, keepdim=True, unbiased=False)
        norm_x = (x - mean) / torch.sqrt(var + self.eps)
        return self.scale * norm_x + self.shift

# Llama 使用 RMSNorm
class RMSNorm(nn.Module):
    def __init__(self, emb_dim, eps=1e-5):
        super().__init__()
        self.eps = eps
        self.weight = nn.Parameter(torch.ones(emb_dim)).float()

    def forward(self, x):
        means = x.pow(2).mean(dim=-1, keepdim=True)
        x_normed = x * torch.rsqrt(means + self.eps)
        return (x_normed * self.weight).to(dtype=x.dtype)

两处关键差异:RMSNorm 没有 shift 参数(无偏置),forward 中只计算 x.pow(2).mean() 而非先算均值再算方差。

改进二:SiLU/SwiGLU 替代 GELU。 GPT-2 的前馈网络使用 GELU 激活函数,结构为两层线性变换夹一个 GELU。Llama 1 将其替换为 SwiGLU——一种结合了 SiLU 激活和门控线性单元(GLU)的结构。SiLU(又名 Swish)定义为:

SiLU(x)=xσ(x)=x1+ex

SiLU 在 0 附近平滑过渡,远离 0 时近似 ReLU,实验表明它在精度上优于 GELU。在此基础上,SwiGLU 引入门控机制,将原来的两层线性变换扩展为三层:

SwiGLU(x)=SiLU(Linear1(x))Linear2(x)

其中 表示逐元素相乘,最终再经过 Linear3 映射回原维度。代码对比如下:

python
# GPT 的 FeedForward:两层线性 + GELU
class FeedForward_GPT(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
            GELU(),
            nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
        )

    def forward(self, x):
        return self.layers(x)

# Llama 的 FeedForward:三层线性 + SwiGLU 门控
class FeedForward(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.fc1 = nn.Linear(cfg["emb_dim"], cfg["hidden_dim"], bias=False)
        self.fc2 = nn.Linear(cfg["emb_dim"], cfg["hidden_dim"], bias=False)
        self.fc3 = nn.Linear(cfg["hidden_dim"], cfg["emb_dim"], bias=False)
        self.silu = nn.SiLU()

    def forward(self, x):
        x_fc1 = self.fc1(x)
        x_fc2 = self.fc2(x)
        x = self.silu(x_fc1) * x_fc2  # 门控:SiLU 输出与另一路线性输出逐元素相乘
        return self.fc3(x)

注意两个变化:(1)Llama 的所有线性层都设 bias=False;(2)中间维度不再是固定的 4×demb,而由独立的 hidden_dim 参数控制。

改进三:旋转位置编码(RoPE)替代绝对位置嵌入。 GPT 使用可学习的绝对位置嵌入:

python
self.pos_emb = nn.Embedding(context_length, emb_dim)

这种方式为每个绝对位置分配一个固定的嵌入向量,无法泛化到训练时未见过的位置。Llama 1 采用旋转位置编码(Rotary Position Embedding, RoPE),通过对 Query 和 Key 向量施加旋转变换来注入位置信息:

RoPE(x,m)=xcos(mθ)+rotate(x)sin(mθ)

其中 m 是 token 位置,θ 是按维度递减的频率参数。RoPE 的核心优势在于:两个 token 的注意力分数仅取决于它们之间的相对距离,而非绝对位置。实现代码如下:

python
def precompute_rope_params(head_dim, theta_base=10_000, context_length=4096):
    assert head_dim % 2 == 0
    inv_freq = 1.0 / (theta_base ** (torch.arange(0, head_dim, 2).float() / head_dim))
    positions = torch.arange(context_length)
    angles = positions.unsqueeze(1) * inv_freq.unsqueeze(0)
    angles = torch.cat([angles, angles], dim=1)
    return torch.cos(angles), torch.sin(angles)

def compute_rope(x, cos, sin):
    batch_size, num_heads, seq_len, head_dim = x.shape
    x1 = x[..., : head_dim // 2]
    x2 = x[..., head_dim // 2 :]
    cos = cos[:seq_len, :].unsqueeze(0).unsqueeze(0)
    sin = sin[:seq_len, :].unsqueeze(0).unsqueeze(0)
    rotated = torch.cat((-x2, x1), dim=-1)
    return (x * cos + rotated * sin).to(dtype=x.dtype)

与 GPT 将位置嵌入加到输入 embedding 上不同,Llama 在注意力模块内部对 Q、K 向量施加旋转——V 向量不参与旋转,因为它不参与注意力分数计算。

改进四:移除 Dropout 和所有偏置。 Llama 1 去掉了 GPT 中的 Embedding Dropout、Attention Dropout 和残差连接中的 Dropout,同时将所有线性层的偏置设为 False。这些简化在大规模训练中被证明不会影响收敛,反而减少了参数量和计算量。

综合以上四项改进,Llama 1 的 TransformerBlock 与 GPT 的对比如下:

python
# Llama 的 TransformerBlock
class TransformerBlock(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.att = MultiHeadAttention(
            d_in=cfg["emb_dim"], d_out=cfg["emb_dim"],
            context_length=cfg["context_length"],
            num_heads=cfg["n_heads"], dtype=cfg["dtype"]
        )
        self.ff = FeedForward(cfg)
        self.norm1 = RMSNorm(cfg["emb_dim"])  # LayerNorm → RMSNorm
        self.norm2 = RMSNorm(cfg["emb_dim"])

    def forward(self, x):
        shortcut = x
        x = self.norm1(x)       # Pre-Norm
        x = self.att(x)         # 内部使用 RoPE,无 Dropout
        x = x + shortcut
        shortcut = x
        x = self.norm2(x)
        x = self.ff(x)          # SwiGLU,无 Dropout
        x = x + shortcut
        return x

Llama 1 在顶层模型类中也移除了绝对位置嵌入和 Dropout:

python
class Llama1Model(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"], dtype=cfg["dtype"])
        # 不再有 pos_emb 和 drop_emb
        self.trf_blocks = nn.Sequential(
            *[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])
        self.final_norm = RMSNorm(cfg["emb_dim"])
        self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False, dtype=cfg["dtype"])

    def forward(self, in_idx):
        x = self.tok_emb(in_idx)  # 不再加位置嵌入
        x = self.trf_blocks(x)
        x = self.final_norm(x)
        return self.out_head(x)

Llama 1 使用 2048 块 A100 80GB GPU 训练 21 天,上下文长度 2048 token,总训练数据 1.4 万亿 token,全部来自公开数据源(CommonCrawl、维基百科、书籍等),不涉及任何私有数据。

7.1.2 Llama 1 → Llama 2:数据规模与对齐

LLM Transformer 块的内部结构

图 7-2:现代 LLM 的 Transformer 块结构。输入经 RMSNorm 归一化后分别送入多头注意力和 SwiGLU FFN,通过残差连接与原始输入相加。

Llama 2 的论文标题为 Llama 2: Open Foundation and Fine-Tuned Chat Models,核心升级集中在三个方面:

开放商用许可。 Llama 1 仅限非商业研究使用,Llama 2 正式开放商业许可,极大扩展了实际应用范围。

训练数据量增长 40%。 Llama 2 将训练数据从 1.4T token 增加到 2 万亿 token,继续践行 Meta 的"数据优先"理念。训练困惑度在 2T token 时仍在持续下降,说明模型远未达到数据饱和。

引入 SFT + RLHF 对齐流程。 Llama 2 首次推出对话微调版本 Llama 2-Chat,完整训练流程包括四个阶段:

  1. 预训练(Pretraining):在 2T token 上进行自回归语言建模。
  2. 监督微调(SFT):使用 10 万条高质量问答对进行标注训练。
  3. 奖励模型训练(Reward Modeling):使用 100 万条人类偏好数据,训练安全性和有用性两个奖励模型。
  4. 强化学习对齐(RLHF):基于奖励模型的信号优化模型输出。

在架构层面,Llama 2 的改动相对保守:

  • 上下文长度从 2048 翻倍至 4096 token
  • 在 70B 模型中引入分组查询注意力(GQA)。GQA 是多头注意力(MHA)和多查询注意力(MQA)的折中方案。在标准 MHA 中,每个头都有独立的 Q、K、V 投影;在 MQA 中,所有头共享一组 K、V;而 GQA 将多个 Query 头分组,每组共享一组 K、V。

以 Llama 2 70B 为例,32 个 Query 头被分为 8 组,每组 4 个 Query 头共享 1 组 K、V。这使得 K、V 的投影矩阵维度缩小为 MHA 的 14,显著降低参数量和推理时的 KV Cache 内存占用。

需要注意的是,GQA 在 Llama 2 中仅用于 70B 模型,7B 和 13B 仍使用标准 MHA。

7.1.3 Llama 2 → Llama 3/3.1/3.2:全面升级

Llama 3 系列(包括 3.0、3.1 和 3.2)在数据规模、架构细节和模态支持上进行了全面升级。

词表扩展:32K → 128K。 Llama 2 使用 SentencePiece 分词器,词表大小 32,000;Llama 3 切换到 Tiktoken 分词器,词表扩展到 128,256。更大的词表显著提升了非英语文本(尤其是中文)的编码效率——同样的文本用更少的 token 表示,既节省计算又提升生成质量。

GQA 全系标配。 从 Llama 3 开始,所有规模的模型(包括 8B)都采用 GQA,不再限于最大模型。以 Llama 3 8B 为例,32 个 Query 头分为 8 个 KV 组,每组 4 个 Query 头共享 1 组 K、V。GroupedQueryAttention 的核心实现如下:

python
class GroupedQueryAttention(nn.Module):
    def __init__(self, d_in, d_out, num_heads, num_kv_groups, dtype=None):
        super().__init__()
        assert d_out % num_heads == 0
        assert num_heads % num_kv_groups == 0

        self.num_heads = num_heads
        self.head_dim = d_out // num_heads
        self.num_kv_groups = num_kv_groups
        self.group_size = num_heads // num_kv_groups

        self.W_query = nn.Linear(d_in, d_out, bias=False, dtype=dtype)
        # K、V 的投影维度从 d_out 缩减为 num_kv_groups * head_dim
        self.W_key = nn.Linear(d_in, num_kv_groups * self.head_dim, bias=False, dtype=dtype)
        self.W_value = nn.Linear(d_in, num_kv_groups * self.head_dim, bias=False, dtype=dtype)
        self.out_proj = nn.Linear(d_out, d_out, bias=False, dtype=dtype)

    def forward(self, x, mask=None, cos=None, sin=None):
        b, num_tokens, d_in = x.shape
        queries = self.W_query(x)                                           # (b, T, d_out)
        keys = self.W_key(x)                                                # (b, T, n_kv * head_dim)
        values = self.W_value(x)

        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)

        # RoPE 仅作用于 Q 和 K
        keys = compute_rope(keys, cos, sin)
        queries = compute_rope(queries, cos, sin)

        # 将 K、V 沿头维度重复,使其与 Q 的头数对齐
        keys = keys.repeat_interleave(self.group_size, dim=1)     # (b, n_heads, T, head_dim)
        values = values.repeat_interleave(self.group_size, dim=1)

        attn_scores = queries @ keys.transpose(2, 3)
        if mask is not None:
            attn_scores.masked_fill_(mask, -torch.inf)
        attn_weights = torch.softmax(attn_scores / self.head_dim**0.5, dim=-1)
        context_vec = (attn_weights @ values).transpose(1, 2).reshape(b, num_tokens, -1)
        return self.out_proj(context_vec)

GQA 相比 MHA 的参数节省量取决于 KV 组数。以 emb_dim=4096, num_heads=32, num_kv_groups=8 为例,K 和 V 的投影矩阵从 4096×4096 缩减为 4096×1024,整个注意力模块参数量从约 6700 万降至约 4200 万,减少约 37%。

训练数据达到 15 万亿 token。 Llama 3 的训练数据是 Llama 2 的 7 倍以上,其中代码数据增加了 4 倍,还引入了 5% 的多语言数据覆盖超过 30 种语言。数据质量通过 Llama 2 自身生成的分类器进行筛选。

RoPE 基频提升。 Llama 3 将 RoPE 的 θbase 从 10,000 提升到 500,000,以更好地支持长上下文:

python
# Llama 2
llama_2_theta_base = 10_000

# Llama 3
llama_3_theta_base = 500_000

更大的 θbase 使低频维度的旋转更缓慢,有效延长了 RoPE 能区分的最大相对距离。

Llama 3.1:128K 上下文与 RoPE 频率缩放。 Llama 3.1 将上下文长度从 8192 扩展到 128K token,为此引入了 RoPE 频率缩放机制(rope_freq 配置),对不同频段的旋转频率进行差异化调整:

python
LLAMA31_CONFIG_8B = {
    "vocab_size": 128_256,
    "context_length": 131_072,     # 128K
    "emb_dim": 4096,
    "n_heads": 32,
    "n_layers": 32,
    "hidden_dim": 14_336,
    "n_kv_groups": 8,
    "rope_base": 500_000.0,
    "rope_freq": {                 # RoPE 频率缩放
        "factor": 8.0,
        "low_freq_factor": 1.0,
        "high_freq_factor": 4.0,
        "original_context_length": 8192,
    },
    "dtype": torch.bfloat16,
}

预训练采用三阶段机制:初始预训练(标准序列长度,120 万步)→ 长上下文训练(分 6 阶段逐步提升至 128K,约 800B token)→ 退火训练(高质量数据,线性降学习率至 0)。

Llama 3.2:轻量化与多模态。 Llama 3.2 引入了 1B 和 3B 两个轻量文本模型,以及 11B 和 90B 两个多模态视觉模型。轻量模型的关键变化包括缩减 emb_dimn_layershidden_dim,并重新启用权重绑定(weight tying)——输入 Embedding 层和输出投影层共享同一组权重参数,这是 GPT-2 使用过但在 Llama 3 8B 中被弃用的技巧。以 Llama 3.2 1B 为例:

python
LLAMA32_CONFIG_1B = {
    "vocab_size": 128_256,
    "context_length": 131_072,
    "emb_dim": 2048,           # 仅为 8B 的一半
    "n_heads": 32,
    "n_layers": 16,            # 仅为 8B 的一半
    "hidden_dim": 8192,
    "n_kv_groups": 8,
    "rope_base": 500_000.0,
    "rope_freq": {
        "factor": 32.0,        # 更大的缩放因子
        "low_freq_factor": 1.0,
        "high_freq_factor": 4.0,
        "original_context_length": 8192,
    },
    "dtype": torch.bfloat16,
}

该配置下模型总参数约 15 亿(其中因 weight tying 实际独立参数约 12.4 亿),在 Nvidia A100 GPU 上使用 torch.compile 编译后,推理速度可达 170 tokens/sec,显存占用仅约 3.1 GB。

多模态模型则通过图像编码器 + 权重适配器将视觉信息映射到语言模型空间,训练时冻结语言模型全部参数,仅训练图像相关模块,使模型在保留语言能力的同时获得图像理解能力。

7.1.4 版本对比总览

现代 LLM 的核心组件标准配置

图 7-3:从 GPT 到 Llama 的组件演进。RMSNorm、SwiGLU、RoPE、GQA 已成为现代 LLM 的标准配置,Qwen3 等模型在此基础上进一步扩展。

下表从多个维度对比 GPT-2、Llama 1、Llama 2 和 Llama 3 系列的关键差异:

特性GPT-2Llama 1Llama 2Llama 3/3.1/3.2
归一化Pre-Norm + LayerNormPre-Norm + RMSNormRMSNormRMSNorm
激活函数GELUSiLU / SwiGLUSwiGLUSwiGLU
位置编码可学习绝对位置嵌入RoPE (θ=10K)RoPE (θ=10K)RoPE (θ=500K) + 频率缩放
注意力机制MHAMHAMHA(7B/13B)、GQA(70B)全系 GQA
词表大小50,257 (BPE)32,000 (SentencePiece)32,000 (SentencePiece)128,256 (Tiktoken)
上下文长度1024204840968192 → 128K (3.1+)
训练数据~40B tokens(WebText)1.4T tokens(公开数据)2T tokens15T+ tokens
线性层偏置
Dropout
许可证MIT(GPT-2 权重)仅研究用途商用许可商用许可
对齐方法SFT + RLHFSFT + 拒绝采样 + DPO
多模态视觉模型(3.2 11B/90B)

下表对比 Llama 3 系列在 MMLU 等基准上的性能提升:

基准测试Llama 2 7BLlama 2 70BLlama 3 8BLlama 3 70BGPT-3.5GPT-4
MMLU34.152.968.482.070.086.4
GPQA21.721.034.239.528.135.7
HumanEval7.925.662.281.748.167.0
GSM8K25.757.579.693.057.192.0
MATH3.811.630.050.434.152.9

Llama 3 8B 在所有基准上全面超越 Llama 2 70B——一个 8B 参数的模型击败了 70B 模型,充分验证了"数据规模比模型规模更重要"的设计哲学。

7.1.5 推理性能与 torch.compile

Llama 3.2 1B 模型的轻量化设计使其在不同硬件和优化策略下展现出显著的推理效率差异。以下是 Nvidia A100 GPU 上的性能对比:

优化方式推理速度显存占用
原始推理42 tokens/sec2.91 GB
FlashAttention54 tokens/sec2.91 GB
torch.compile170 tokens/sec3.12 GB
torch.compile + FlashAttention177 tokens/sec3.61 GB

torch.compile 通过图级别的编译优化(算子融合、内存规划等),在仅增加约 0.2 GB 显存的代价下实现了 4 倍加速。使用方式只需在模型加载后加一行:

python
model = torch.compile(model)

需要注意的是,编译过程有数分钟的前置开销,且加速效果在第一次 generate 调用后才会体现。

GPT 到 Llama 的架构演进路线

图 7-4:从 GPT 到 Llama 的渐进式架构改进。每一代 Llama 在前代基础上引入新的优化(RoPE、GQA、扩展词表等),逐步形成现代 LLM 的标准配置。

7.1.6 小结

从 GPT 到 Llama 的演化路径清晰地展示了大语言模型架构优化的三条主线:

  1. 计算效率:RMSNorm 简化归一化、GQA 压缩 KV 投影、移除偏置和 Dropout——每一步都在减少冗余计算和参数。
  2. 表达能力:SwiGLU 门控提升前馈网络表达力、RoPE 编码相对位置信息、扩大词表提升编码效率——每一步都在增强模型对输入信息的利用能力。
  3. 规模法则(Scaling Laws):Llama 系列始终坚持"数据优先于参数"的理念,从 1.4T 到 2T 再到 15T token,用更多数据而非更大模型来推高性能上限。Llama 3 8B 超越 Llama 2 70B 的事实,是这一理念最有力的验证。

这些改进都不是革命性的架构创新,而是对已有组件的系统性替换和工程优化。它们的成功说明:在大模型时代,细粒度的架构打磨与大规模的数据工程,往往比从零设计新架构更有效