跳到主要内容

内容阅读

AI 底层工程阿新聊ai

大模型的缩放定律:Scaling Law、Chinchilla、MoE,为什么模型越大越好

本文迁移自 mindcarver/91ai · 原始位置 docs/machine learning/stage5 transformer/05 scaling laws.md · 由 @阿新聊ai 整理。 <! 调研来源: 1. Kaplan et al., "Scaling Laws for Neural Lan...

本文迁移自 mindcarver/91ai · 原始位置 docs/machine-learning/stage5-transformer/05-scaling-laws.md · 由 @阿新聊ai 整理。

大模型的缩放定律:Scaling Law、Chinchilla、MoE,为什么模型越大越好

TL;DR: Scaling Law 描述了语言模型损失如何随参数量(N)、数据量(D)和计算量(C)的幂律关系变化。OpenAI 在 2020 年发现 $L(N) \propto N^{-0.076}$——参数量每增加 10 倍,损失下降约 15%。但 Chinchilla 在 2022 年修正了这个结论:最优缩放是 N 和 D 同时增加,每个参数需要约 20 个 token,这意味着 GPT-3 175B 实际上训练不足。MoE(Mixture of Experts)通过条件计算让模型有更多参数但不增加每次推理的计算量。Scaling Law 的核心洞察是:在当前技术路线下,模型越大、数据越多,效果越好,而且这个趋势还没有饱和的迹象。

为什么这很重要

Scaling Law 不是一个理论发现,而是一个工程指南。它回答了一个极其实际的问题:我有 1000 万美元的 GPU 预算,应该训练多大的模型、用多少数据?

在 Scaling Law 之前,这个问题只能通过试错来回答。很多团队把大部分预算花在了训练更大的模型上,但数据量没有同步增加。Chinchilla 证明这种做法是低效的——一个用更多数据训练的较小模型(70B),可以超越用较少数据训练的大模型(175B)。

这个发现直接影响了 LLaMA 的设计决策:Meta 用了 1.4T tokens 训练 65B 的模型(每个参数约 20 个 token),而不是用更少的数据训练一个更大的模型。结果 LLaMA-65B 在很多任务上超越了 GPT-3 175B——参数量只有后者的 1/3。

Scaling Law 还告诉我们一件重要的事:在当前的技术路线下,性能提升还没有遇到天花板。 模型越大、数据越多,效果越好。这是一个经验事实,不需要新的理论突破——只需要更多的计算和数据。

核心概念

幂律关系(Power Law)

Scaling Law 的核心是幂律关系:

$$L(x) = \left(\frac{x_c}{x}\right)^{\alpha} + L_{\infty}$$

其中:

  • $L(x)$ 是损失
  • $x$ 是某个缩放变量(N、D 或 C)
  • $x_c$ 是一个常数
  • $\alpha$ 是缩放指数(决定"多快变好")
  • $L_{\infty}$ 是不可约损失(理论下限)

幂律关系在对数坐标系中是一条直线:

$$\log(L(x) - L_{\infty}) = -\alpha \log(x) + \text{const}$$

这意味着损失随缩放变量的增加而可预测地下降。更重要的是,这种可预测性允许你从小模型的实验结果外推到大模型的性能

三个缩放变量

变量 符号 含义 缩放关系
参数量 $N$ 模型的可训练参数数量 $L(N) \propto N^{-0.076}$
数据量 $D$ 训练 token 数量 $L(D) \propto D^{-0.095}$
计算量 $C$ 总 FLOPs(= 6ND 近似) $L(C) \propto C^{-0.050}$

这些指数看起来很小,但它们在对数空间中是有意义的:

  • 参数量从 1B 增加到 100B(100 倍):损失下降 $100^{0.076} \approx 1.42$ 倍
  • 这在交叉熵损失上可能只是从 3.0 降到 2.1,但在实际任务中意味着质的差异

工作原理(简化的心智模型)

给 12 岁孩子的解释

Scaling Law 就像研究"练习时间和考试成绩的关系"。你发现了一个规律:练习时间每增加 10 倍,错误率下降约 15%。

这个规律告诉你:

  • 从 10 小时增加到 100 小时,效果提升很大
  • 从 1000 小时增加到 10000 小时,效果提升有但变小了(递减但不停滞)
  • 这个规律让你可以预测:"如果我练习 5000 小时,错误率大概是多少"

Chinchilla 的修正:原来的研究说"练习越久越好"。Chinchilla 发现,重要的是"练习题的数量"要和"你的脑容量"匹配。如果你的脑容量很大(大模型)但练习题太少(数据不足),你的潜力没有被发挥。最优的配比是:每个脑细胞需要做约 20 道练习题。

MoE 就像一个公司有很多专家(参数多),但每次只咨询最相关的几位专家(计算量少)。公司有 100 个专家,但每次只需要咨询 2 个——参数量大,但计算量不大。

工作原理(详细机制)

1. OpenAI 的原始 Scaling Law(2020)

Kaplan 等人在从 768 到 1.5B 参数的模型上做实验,发现了以下幂律关系:

参数量缩放(当数据充足时):

$$L(N) = \left(\frac{N_c}{N}\right)^{\alpha_N}, \quad \alpha_N \approx 0.076, \quad N_c \approx 8.8 \times 10^{13}$$

数据量缩放(当模型足够大时):

$$L(D) = \left(\frac{D_c}{D}\right)^{\alpha_D}, \quad \alpha_D \approx 0.095, \quad D_c \approx 5.4 \times 10^{13}$$

计算量缩放

$$L(C) = \left(\frac{C_c}{C}\right)^{\alpha_C}, \quad \alpha_C \approx 0.050, \quad C_c \approx 1.6 \times 10^{10}$$

关键发现 1:平滑的幂律

损失随 N、D、C 的变化是平滑的幂律关系。没有"突然变好"或"饱和"的迹象(至少在他们实验的范围内)。

关键发现 2:计算量的最优分配

给定固定计算量 $C$,存在最优的模型大小 $N^$ 和数据量 $D^$:

$$N^* \propto C^{0.73}, \quad D^* \propto C^{0.27}$$

这意味着:当计算量增加时,应该把大部分增加的预算投入到更大的模型上(0.73),较少投入到更多数据上(0.27)。

但 Chinchilla 修正了这个结论。

关键发现 3:过拟合的边界

当 $N$ 和 $D$ 不匹配时,会出现过拟合。经验法则:

$$D \geq 10 \times N$$

即数据量至少应该是参数量的 10 倍,才能避免严重过拟合。

2. Chinchilla:计算最优的缩放(2022)

DeepMind 的 Hoffmann 等人重新审视了 Scaling Law,发现了一个重要问题:OpenAI 的原始实验没有在所有模型大小上训练到收敛。

当他们确保所有模型都训练到收敛时,得到了不同的最优分配:

$$N^* \propto C^{0.50}, \quad D^* \propto C^{0.50}$$

计算量增加时,模型大小和数据量应该等比例增加

具体数值:每个参数约需要 20 个 token。

$$D^* \approx 20 \times N^*$$

Chinchilla 的实验验证

DeepMind 训练了一系列模型,从 70M 到 16B 参数,都在约 300B tokens 上训练。然后训练了一个 70B 的 Chinchilla 模型,在 1.4T tokens 上训练。

结果:Chinchilla 70B 在几乎所有任务上超越了 GPT-3 175B(参数量只有 40%)。

模型 参数量 训练 Tokens Tokens/参数 性能
GPT-3 175B 175B 300B 1.7 基准
Chinchilla 70B 70B 1.4T 20 更好

GPT-3 每个参数只有 1.7 个 token——严重训练不足。Chinchilla 每个参数有 20 个 token——接近最优。

Chinchilla 对行业的影响

Chinchilla 的发现直接导致了以下变化:

  • LLaMA 系列:严格遵循 20 tokens/参数 的配比。LLaMA-65B 用了 1.4T tokens
  • GPT-4:据推测也大幅增加了训练数据量
  • Mistral 7B:用大量数据训练小模型,效果出奇地好

3. MoE(Mixture of Experts):参数多但计算少

MoE 的核心思想:不是所有参数都在每次推理中被使用。

基本结构

在 MoE 的 Transformer 中,FFN 层被替换为多个"专家"FFN。每次推理时,一个路由(router)决定每个 token 由哪个专家处理:

标准 Transformer FFN:
  输入 x → FFN(x) → 输出
  所有参数每次都参与计算

MoE Transformer FFN:
  输入 x → Router(x) → 选择 Top-K 个专家
          → Expert_1(x), Expert_3(x), Expert_7(x)
          → 加权求和 → 输出
  只有 K 个专家的参数参与计算(K << 总专家数)

路由机制

路由器是一个简单的线性层 + Softmax:

$$\text{router}(x) = \text{softmax}(W_r x)$$

$$\text{selected experts} = \text{Top-K}(\text{router}(x))$$

通常 $K = 1$ 或 $K = 2$。如果有 $E = 8$ 个专家,每次只有 2 个被激活——计算量是标准 FFN 的 2/8 = 25%。

MoE 的数学分析

对于一个 $E$ 个专家、Top-$K$ 路由的 MoE 层:

  • 参数量:$E \times d_{model} \times d_{ff}$(所有专家的参数总和)
  • 每次推理的计算量:$K \times d_{model} \times d_{ff}$(只有 K 个专家参与)
  • 计算效率比:$K / E$

例如,Switch Transformer 的配置:$E = 128, K = 1$,计算效率比 = $1/128 = 0.8%$。参数量是标准模型的 128 倍,但计算量几乎不变。

负载均衡问题

MoE 的一个实际问题:路由器可能把大部分 token 都分配给少数几个专家,导致其他专家闲置。

解决方案:辅助损失(Auxiliary Loss)

$$\mathcal{L}{aux} = \alpha \cdot \sum{i=1}^{E} f_i \cdot P_i$$

其中 $f_i$ 是分配给专家 $i$ 的 token 比例,$P_i$ 是路由器分配给专家 $i$ 的平均概率。这个损失鼓励负载均匀分布。

知名 MoE 模型

模型 总参数 活跃参数 专家数 Top-K
GShard 600B ~10B 2048 2
Switch-C 1.6T ~10B 2048 1
Mixtral 8×7B 46.7B 12.9B 8 2
DeepSeek-V2 236B 21B 160 6

Mixtral 8×7B 是最成功的开源 MoE 模型之一。它有 8 个专家,每次选择 2 个。总参数 46.7B,但每次推理只使用约 12.9B 参数。它的性能接近 LLaMA-70B,但推理速度快约 3 倍。

4. Scaling Law 的局限和修正

饱和问题

幂律关系不会永远成立。当 $L \to L_{\infty}$ 时,增长会放缓。Caballero et al. (2023) 提出"Broken Neural Scaling Laws",用分段幂律来描述这种饱和:

$$L(x) = \sum_{i} a_i \left(\frac{x}{x_i}\right)^{b_i}$$

实际上,目前的语言模型损失还没有到达明显的饱和点。GPT-4 的训练损失仍然遵循幂律趋势。但数据质量可能成为比数量更重要的瓶颈。

数据墙问题

高质量文本数据的总量是有限的。估计互联网上的高质量英文文本约 10T tokens。如果训练一个 10T 参数的 Chinchilla 最优模型,需要约 200T tokens——远超可用数据。

解决方案:

  • 多语言数据:扩展到非英文文本
  • 合成数据:用强模型生成训练数据(但存在"模型坍缩"风险)
  • 数据质量:用更少但更高质量的数据(如 Phi 系列模型)
  • 课程学习:按难度排序训练数据

Emergent Abilities(涌现能力)

Wei et al. (2022) 观察到某些能力在模型达到一定规模前几乎为零,然后突然出现:

  • 少样本学习:在 ~10B 参数时开始有效
  • 链式思维推理:在 ~100B 参数时开始出现
  • 低资源语言理解:在更大规模时出现

这些"涌现"是否是真实的认知能力,还是评估指标的伪影(如非线性指标的阈值效应),目前仍有争议。但无论如何,大模型能做到小模型做不到的事,这是一个事实。

代码示例(完整可运行的 Python)

Scaling Law 拟合和 MoE 实现

"""
Scaling Law 拟合 + MoE 实现演示
运行要求: pip install torch numpy matplotlib
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np


# ============================================================
# 1. Scaling Law 拟合
# ============================================================
class ScalingLaw:
    """Scaling Law 拟合器"""
    def __init__(self):
        self.alpha = None
        self.x_c = None
        self.L_inf = None

    def fit(self, x_data, loss_data, L_inf=1.0):
        """用最小二乘法拟合幂律关系"""
        x = np.array(x_data)
        y = np.maximum(np.array(loss_data) - L_inf, 1e-10)

        # 对数空间的线性回归: log(y) = -alpha * log(x) + const
        log_x = np.log(x)
        log_y = np.log(y)

        # 最小二乘
        A = np.vstack([log_x, np.ones(len(log_x))]).T
        slope, intercept = np.linalg.lstsq(A, log_y, rcond=None)[0]

        self.alpha = -slope
        self.L_inf = L_inf
        self.x_c = np.exp(intercept) ** (1 / self.alpha)

        return self

    def predict(self, x):
        """预测给定 x 的损失"""
        return self.L_inf + self.x_c / (np.array(x) ** self.alpha)

    def __repr__(self):
        return (f"ScalingLaw(alpha={self.alpha:.4f}, "
                f"x_c={self.x_c:.2e}, L_inf={self.L_inf:.4f})")


def chinchilla_optimal(compute_budget_flops):
    """
    根据 Chinchilla Scaling Law 计算最优模型大小和数据量
    N* ∝ C^0.5, D* ∝ C^0.5, D* ≈ 20 * N*
    """
    # 近似: C ≈ 6 * N * D (训练一个 token 的计算量约为 6N)
    # 最优: D = 20N, C = 6 * N * 20N = 120 * N^2
    N_star = (compute_budget_flops / 120) ** 0.5
    D_star = 20 * N_star
    return N_star, D_star


# ============================================================
# 2. MoE (Mixture of Experts) 实现
# ============================================================
class Expert(nn.Module):
    """单个专家 FFN"""
    def __init__(self, d_model, d_ff):
        super().__init__()
        self.w1 = nn.Linear(d_model, d_ff)
        self.w2 = nn.Linear(d_ff, d_model)

    def forward(self, x):
        return self.w2(F.relu(self.w1(x)))


class MoELayer(nn.Module):
    """MoE 层:多个专家 + Top-K 路由"""
    def __init__(self, d_model, d_ff, num_experts=8, top_k=2):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = top_k

        self.experts = nn.ModuleList([
            Expert(d_model, d_ff) for _ in range(num_experts)
        ])
        self.router = nn.Linear(d_model, num_experts)

    def forward(self, x):
        batch_size, seq_len, d_model = x.shape
        x_flat = x.view(-1, d_model)  # (B*T, D)

        # 路由分数
        router_logits = self.router(x_flat)  # (B*T, E)
        router_probs = F.softmax(router_logits, dim=-1)

        # Top-K 选择
        top_k_probs, top_k_indices = router_probs.topk(self.top_k, dim=-1)
        top_k_probs = top_k_probs / top_k_probs.sum(dim=-1, keepdim=True)  # 归一化

        # 计算专家输出
        output = torch.zeros_like(x_flat)

        for i in range(self.top_k):
            for b in range(x_flat.size(0)):
                expert_idx = top_k_indices[b, i].item()
                expert_output = self.experts[expert_idx](https://raw.githubusercontent.com/mindcarver/91ai/main/docs/machine-learning/stage5-transformer/x_flat[b:b+1])
                output[b] += top_k_probs[b, i] * expert_output.squeeze(0)

        # 辅助损失(负载均衡)
        if self.training:
            # 每个专家被选中的频率
            expert_counts = torch.zeros(self.num_experts, device=x.device)
            for idx in top_k_indices.flatten():
                expert_counts[idx] += 1
            expert_freq = expert_counts / expert_counts.sum()

            # 路由器给每个专家的平均概率
            router_avg = router_probs.mean(dim=0)

            aux_loss = (expert_freq * router_avg).sum() * self.num_experts
        else:
            aux_loss = 0

        return output.view(batch_size, seq_len, d_model), aux_loss


class MoEEfficient(nn.Module):
    """高效的 MoE 实现(向量化)"""
    def __init__(self, d_model, d_ff, num_experts=8, top_k=2):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = top_k

        # 所有专家参数合并为大矩阵(更高效)
        self.w1 = nn.Parameter(torch.randn(num_experts, d_model, d_ff) * 0.01)
        self.w2 = nn.Parameter(torch.randn(num_experts, d_ff, d_model) * 0.01)
        self.router = nn.Linear(d_model, num_experts)

    def forward(self, x):
        B, T, D = x.shape
        x_flat = x.view(-1, D)  # (N, D), N = B*T

        # 路由
        router_logits = self.router(x_flat)
        top_k_probs, top_k_indices = router_logits.topk(self.top_k, dim=-1)
        top_k_probs = F.softmax(top_k_probs, dim=-1)

        # 计算输出
        output = torch.zeros_like(x_flat)
        for k in range(self.top_k):
            for e in range(self.num_experts):
                mask = (top_k_indices[:, k] == e)
                if mask.any():
                    expert_input = x_flat[mask]
                    # 批量矩阵乘法
                    h = F.relu(expert_input @ self.w1[e])
                    expert_output = h @ self.w2[e]
                    output[mask] += top_k_probs[mask, k].unsqueeze(-1) * expert_output

        return output.view(B, T, D)


# ============================================================
# 验证代码
# ============================================================
if __name__ == "__main__":
    np.random.seed(42)
    torch.manual_seed(42)

    print("=" * 60)
    print("Scaling Law 拟合")
    print("=" * 60)

    # 模拟不同模型大小的实验结果
    model_sizes = np.array([10e6, 50e6, 100e6, 500e6, 1e9, 5e9, 10e9])
    # 模拟损失(带一些噪声)
    true_alpha = 0.076
    losses = 1.8 + 5.0 / (model_sizes ** true_alpha) + np.random.randn(len(model_sizes)) * 0.01

    # 拟合 Scaling Law
    law = ScalingLaw().fit(model_sizes, losses, L_inf=1.8)
    print(f"\n拟合结果: {law}")

    # 预测更大模型的性能
    predict_sizes = [50e9, 100e9, 175e9, 500e9, 1e12]
    print(f"\n性能预测:")
    print(f"  {'参数量':>15} {'预测损失':>12} {'相对提升':>12}")
    ref_loss = law.predict([10e9])[0]
    for size in predict_sizes:
        pred = law.predict([size])[0]
        improvement = (ref_loss - pred) / ref_loss * 100
        print(f"  {size/1e9:>12.0f}B {pred:>12.4f} {improvement:>11.1f}%")

    # Chinchilla 最优分配
    print(f"\n" + "=" * 60)
    print("Chinchilla 最优分配")
    print("=" * 60)

    budgets = [
        ("1 GPU-month (A100)", 1e21),
        ("10 GPU-months", 1e22),
        ("100 GPU-months", 1e23),
        ("1 GPU-year", 1e22 * 12),
        ("GPT-3 级别", 3.1e23),
        ("GPT-4 级别 (估计)", 2e25),
    ]

    print(f"\n  {'预算':>25} {'最优模型':>15} {'最优数据':>15} {'Tokens/参数':>15}")
    for name, budget in budgets:
        N, D = chinchilla_optimal(budget)
        print(f"  {name:>25} {N/1e9:>12.1f}B {D/1e9:>12.1f}B {D/N:>15.1f}")

    # MoE 测试
    print(f"\n" + "=" * 60)
    print("MoE (Mixture of Experts) 测试")
    print("=" * 60)

    d_model = 64
    d_ff = 256
    num_experts = 8

    # 标准 FFN
    standard_ffn = nn.Sequential(
        nn.Linear(d_model, d_ff),
        nn.ReLU(),
        nn.Linear(d_ff, d_model),
    )

    # MoE FFN
    moe = MoELayer(d_model, d_ff, num_experts=num_experts, top_k=2)

    # 参数量对比
    standard_params = sum(p.numel() for p in standard_ffn.parameters())
    moe_params = sum(p.numel() for p in moe.parameters())
    moe_active = standard_params * 2  # Top-2: 每次 2 个专家参与

    print(f"\n参数量对比:")
    print(f"  标准 FFN: {standard_params:,}")
    print(f"  MoE (8 专家): {moe_params:,} ({moe_params/standard_params:.1f}x)")
    print(f"  MoE 活跃参数 (Top-2): {moe_active:,} ({moe_active/standard_params:.1f}x)")

    # 前向传播测试
    x = torch.randn(2, 10, d_model)

    std_out = standard_ffn(x)
    moe_out, aux_loss = moe(x)

    print(f"\n前向传播:")
    print(f"  输入: {list(x.shape)}")
    print(f"  标准 FFN 输出: {list(std_out.shape)}")
    print(f"  MoE 输出: {list(moe_out.shape)}")
    print(f"  MoE 辅助损失: {aux_loss:.4f}")

    # 不同 MoE 配置的对比
    print(f"\nMoE 配置对比 (d_model=4096, d_ff=16384):")
    print(f"  {'专家数':>8} {'总参数':>15} {'Top-1 活跃':>15} {'Top-2 活跃':>15} {'Top-1 效率':>12}")

    d, d_ff_large = 4096, 16384
    base_ffn_params = d * d_ff_large * 2
    for E in [4, 8, 16, 32, 64, 128]:
        total = base_ffn_params * E + d * E  # 专家参数 + 路由参数
        top1_active = base_ffn_params + d * E  # 1 个专家 + 路由
        top2_active = base_ffn_params * 2 + d * E  # 2 个专家 + 路由
        efficiency = top1_active / total * 100
        print(f"  {E:>8} {total/1e6:>12.0f}M {top1_active/1e6:>12.0f}M "
              f"{top2_active/1e6:>12.0f}M {efficiency:>11.1f}%")

    # Scaling Law 实用计算器
    print(f"\n" + "=" * 60)
    print("Scaling Law 实用计算器")
    print("=" * 60)

    # 给定模型大小,计算需要的最优数据量
    print(f"\n给定模型大小,Chinchilla 最优数据量:")
    for N_b in [7, 13, 30, 65, 175]:
        D_optimal = 20 * N_b * 1e9  # tokens
        C_total = 6 * N_b * 1e9 * D_optimal  # FLOPs
        gpu_hours = C_total / (312e12 * 3600)  # A100 = 312 TFLOPs
        print(f"  {N_b:>4}B 参数 → {D_optimal/1e12:.1f}T tokens, "
              f"~{C_total/1e23:.1f} × 10^23 FLOPs, "
              f"~{gpu_hours/1000:.0f}k A100-hours")

    # GPT 系列的缩放轨迹
    print(f"\nGPT 系列缩放轨迹:")
    models = [
        ("GPT-1", 0.117, 5, "~5GB"),
        ("GPT-2", 1.5, 40, "~40GB"),
        ("GPT-3", 175, 300, "~570GB"),
        ("GPT-4 (估计)", 1800, 13000, "~13TB"),
    ]
    for name, n_b, d_b, raw in models:
        ratio = d_b / n_b
        optimal = "✓ 接近最优" if 15 &#x3C; ratio &#x3C; 30 else "✗ 训练不足" if ratio &#x3C; 10 else "偏多"
        print(f"  {name:>15}: {n_b:>7.1f}B 参数, {d_b:>6.0f}B tokens, "
              f"tokens/param={ratio:.1f}, {optimal}")

评论

0
登录后可以参与评论和讨论。
💬

还没有评论

欢迎留下第一条评论,帮助这篇内容更快形成讨论。