跳到主要内容

缩放定律

假设你有 10 万美元的算力预算,模型该做多深、多宽?数据该用多少?这个问题不靠直觉回答——答案藏在一组幂律关系里。

这一节从幂律分布的基本概念出发,理解 Scaling Laws 三次范式转变,最后学会估算训练所需的算力和显存。

缩放定律(Scaling Laws)研究的是模型规模、数据量和训练计算量之间的定量关系。它的核心发现是一个经验规律:增大模型或增加数据,loss 都会下降,但下降服从幂律——每次翻倍带来的提升越来越小。这个规律决定了给定算力预算下,模型和数据的最优配比。

从 2020 年的 Kaplan 到 2022 年的 Chinchilla,再到 2023 年之后的过度训练范式,最优配比的结论被推翻了两次。这一节用具体数字追踪这两次转变背后的数学依据。

上半场:缩放定律

1. 幂律分布

在进入缩放定律之前,必须先理解「幂律」这个概念。

1.1 线性递减 vs 幂律递减

假设你每个月存的钱翻倍:

  • 线性递减:每次翻倍,收益减固定数值

    • 存 100 → 收益 10
    • 存 200 → 收益 20(+10)
    • 存 400 → 收益 30(+10)← 每次翻倍多赚一样多
  • 幂律递减:每次翻倍,收益减固定比例

    • 存 100 → 收益 10
    • 存 200 → 收益 18(+80%)
    • 存 400 → 收益 32.4(+80%)← 每次翻倍多赚一样比例

LLM 的训练 loss 遵循的是幂律递减:模型大小翻倍,loss 降低一个固定比例。

用公式写:Loss(N) ≈ a × N^(-b) + c

其中:

  • N = 模型参数数量
  • a, b, c = 常数(通过实验拟合出来的)
  • N^(-b) 表示「N 越大,这一项越小」
  • c 表示「loss 的终极下限」——无论模型多大,loss 不可能低于 c
# === 幂律递减的手算演示 ===
print("=== 幂律递减:Loss 随模型大小变化 ===")
print()

# 模拟 loss = a * N^(-b) + c
# 用很小的数字来演示
a, b_pow, c = 5.0, 0.05, 2.0

# 模型从 1M 参数到 100B 参数
sizes = [1, 10, 100, 1000, 10000, 100000] # 单位:百万参数
labels = ["1M", "10M", "100M", "1B", "10B", "100B"]

print(f"公式: Loss = {a} × N^(-{b_pow}) + {c}")
print()
print(f"{'模型大小':>10s} {'Loss':>8s} {'相比上一级':>12s}")
print("-" * 32)

prev_loss = None
for size, label in zip(sizes, labels):
N = size * 1e6 # 转成参数数量
loss = a * N**(-b_pow) + c

if prev_loss:
improvement = (prev_loss - loss) / prev_loss * 100
print(f"{label:>10s} {loss:>6.2f} 降低 {improvement:>5.1f}%")
else:
print(f"{label:>10s} {loss:>6.2f} —")
prev_loss = loss

print()
print("关键观察:")
print(" 1. 每次模型扩大 10 倍,loss 确实在降")
print(" 2. 但降低的幅度越来越小 → 边际收益递减")
print(" 3. 从 1B → 10B 降低的幅度 < 从 1M → 10M")
print()
print("这就是幂律的核心结论:越大越好,但越来越不值。")
print("你需要一个公式来告诉你:到哪一步就不值得再大了。")
=== 幂律递减:Loss 随模型大小变化 ===

公式: Loss = 5.0 × N^(-0.05) + 2.0

模型大小 Loss 相比上一级
--------------------------------
1M 4.51 —
10M 4.23 降低 6.0%
100M 3.99 降低 5.7%
1B 3.77 降低 5.4%
10B 3.58 降低 5.1%
100B 3.41 降低 4.8%

关键观察:
1. 每次模型扩大 10 倍,loss 确实在降
2. 但降低的幅度越来越小 → 边际收益递减
3. 从 1B → 10B 降低的幅度 < 从 1M → 10M

这就是幂律的核心结论:越大越好,但越来越不值。
你需要一个公式来告诉你:到哪一步就不值得再大了。

2. Kaplan 缩放定律(OpenAI, 2020)— 「优先增大模型」

2020 年,OpenAI 做了大量实验,想知道:给定固定的计算预算 C(比如 1e20 FLOPs),模型大小 N 和数据量 D 怎么配比最好?

2.1 核心发现

他们发现,最优的配比是这样的:

N_opt ∝ C^0.73    ← 模型大小随计算预算增长快
D_opt ∝ C^0.27 ← 数据量随计算预算增长慢

指数 0.73 > 0.27 → 模型增长的速度比数据快得多。

用人话:如果你的计算预算翻倍了,应该把大部分多出来的预算用来增大模型,少部分用来增加数据。

2.2 用具体数字感受一下

# === Kaplan 定律手算 ===
print("=== Kaplan 缩放定律:给定计算预算,最优配置是什么? ===")
print()

# 参照点:假设 C=1e20 FLOPs 时,N=1B, D=20B tokens
N_ref = 1e9 # 10 亿参数
D_ref = 20e9 # 200 亿 token
C_ref = 1e20 # 计算预算

print(f"参照点: C={C_ref/1e20:.0f}×10²⁰ FLOPs → N={N_ref/1e9:.0f}B, D={D_ref/1e9:.0f}B tokens")
print()

# 计算预算不断翻倍
budgets = [1e20, 2e20, 4e20, 8e20, 1e21, 1e22]
budget_labels = ["1×", "2×", "4×", "8×", "10×", "100×"]

print(f"{'计算预算':>12s} {'模型大小(Kaplan)':>18s} {'数据量(Kaplan)':>18s} {'D/N比':>10s}")
print("-" * 64)

for C, label in zip(budgets, budget_labels):
ratio = C / C_ref
N_opt = N_ref * (ratio ** 0.73)
D_opt = D_ref * (ratio ** 0.27)
dn_ratio = D_opt / N_opt

if N_opt >= 1e9:
n_str = f"{N_opt/1e9:.1f}B"
else:
n_str = f"{N_opt/1e6:.0f}M"

d_str = f"{D_opt/1e9:.1f}B tokens"
print(f"{label:>12s} {n_str:>18s} {d_str:>18s} {dn_ratio:>8.1f}x")

print()
print("Kaplan 的判断:")
print(" • 计算预算越大,模型越大(远超数据增长)")
print(" • D/N 比越来越小 → 大模型可以「省着用」数据")
print(" • 比如 10B 模型只需要 40B tokens(而不是直觉上的越多越好)")
=== Kaplan 缩放定律:给定计算预算,最优配置是什么? ===

参照点: C=1×10²⁰ FLOPs → N=1B, D=20B tokens

计算预算 模型大小(Kaplan) 数据量(Kaplan) D/N比
----------------------------------------------------------------
1× 1.0B 20.0B tokens 20.0x
2× 1.7B 24.1B tokens 14.5x
4× 2.8B 29.1B tokens 10.6x
8× 4.6B 35.1B tokens 7.7x
10× 5.4B 37.2B tokens 6.9x
100× 28.8B 69.3B tokens 2.4x

Kaplan 的判断:
• 计算预算越大,模型越大(远超数据增长)
• D/N 比越来越小 → 大模型可以「省着用」数据
• 比如 10B 模型只需要 40B tokens(而不是直觉上的越多越好)

3. Chinchilla 缩放定律(DeepMind, 2022)— 「模型和数据同样重要」

3.1 为什么 Kaplan 错了?

DeepMind 重新做了实验,发现 OpenAI 之前做实验时,计算预算增大后,没有充分增加数据量——因为当时没有那么多高质量数据可用。

Kaplan 说的「优先增大模型」其实是在「数据不够」的前提下得出的结论。

3.2 Chinchilla 的新结论

DeepMind 控制了变量:对每个计算预算,尝试了几十种不同的 (N, D) 组合,找到真正最优的那个。结论:

N_opt ∝ C^0.5
D_opt ∝ C^0.5
→ 模型和数据同等重要!
→ D_opt ≈ 20 × N_opt (每个参数配约 20 个 token)

3.3 用同一个计算预算,Kaplan vs Chinchilla 会做出不同的选择

# === Kaplan vs Chinchilla 对决 ===
print("=== Kaplan vs Chinchilla:同一预算,不同方案 ===")
print()

# 一个具体的预算
C = 1e23 # 约 GPT-3 的训练量级
ratio = C / C_ref

N_k = N_ref * (ratio ** 0.73)
D_k = D_ref * (ratio ** 0.27)

N_c = N_ref * (ratio ** 0.5)
D_c = D_ref * (ratio ** 0.5)

print(f"计算预算: {C/1e23:.0f} × 10²³ FLOPs")
print()
print(f"{'':>20s} {'模型大小':>12s} {'数据量':>14s} {'D/N比':>10s}")
print("-" * 60)
print(f"{'Kaplan 说':>20s} {N_k/1e9:>8.1f}B {D_k/1e9:>9.1f}B tokens {D_k/N_k:>8.1f}x")
print(f"{'Chinchilla 说':>20s} {N_c/1e9:>8.1f}B {D_c/1e9:>9.1f}B tokens {D_c/N_c:>8.1f}x")
print()

print(f"对比:")
print(f" Kaplan 方案: 模型大 {N_k/N_c:.1f}×,但数据只有 Chinchilla 的 {D_k/D_c:.1f}×")
print(f" Chinchilla 方案: 模型更小,但数据更多")
print()
print(f"实验结果: Chinchilla 方案 loss 更低 → 推翻了 Kaplan")
print(f" → 数据少的大模型 < 数据多的中等模型")
=== Kaplan vs Chinchilla:同一预算,不同方案 ===

计算预算: 1 × 10²³ FLOPs

模型大小 数据量 D/N比
------------------------------------------------------------
Kaplan 说 154.9B 129.1B tokens 0.8x
Chinchilla 说 31.6B 632.5B tokens 20.0x

对比:
Kaplan 方案: 模型大 4.9×,但数据只有 Chinchilla 的 0.2×
Chinchilla 方案: 模型更小,但数据更多

实验结果: Chinchilla 方案 loss 更低 → 推翻了 Kaplan
→ 数据少的大模型 < 数据多的中等模型

3.4 Chinchilla 的震撼案例

DeepMind 用这个发现训了两个模型:

Gopher:     280B 参数,300B tokens 训练 — 按 Kaplan 配方
Chinchilla: 70B 参数,1.4T tokens 训练 — 按 Chinchilla 配方
训练计算量相同!

结果:Chinchilla (70B) >> Gopher (280B)
→ 小了 4 倍的模型,因为数据多了 4.7 倍,效果反而更好

这就是 Chinchilla 带来的冲击——很多当时的大模型可能都「数据喂少了」。

4. 后 Chinchilla 时代:过度训练反而更好

Chinchilla 说 D/N ≈ 20 最优。但 2023-2024 年的实际做法完全突破了这个建议。

4.1 为什么?因为 Chinchilla 最优化的是「训练成本」

Chinchilla 的「最优」是指:在固定训练预算下,让 loss 最低。

但实际部署时,推理成本才是大头——模型上线后要服务百万用户,推理一次花的钱远超过训练时的钱。

所以最优策略变成了:训一个小模型,狂喂数据,让它超越预期能力。

LLaMA 3 8B:  Chinchilla 说它只需要 8B×20 = 160B tokens
实际喂了 15T tokens → 是 Chinchilla 最优的 94 倍!
效果 → 远超 Chinchilla 最优下的 8B 模型应有的水平

这叫「推理-训练权衡」:多花训练成本,换取推理时用小模型省大钱。

# === 实际模型的 D/N 比 ===
print("=== 各大模型的 D/N 比 ===")
print()

real_models = [
("Chinchilla (最优)", 70, 1400, "20x"),
("LLaMA 7B", 7, 1000, "143x"),
("LLaMA 2 7B", 7, 2000, "286x"),
("LLaMA 3 8B", 8, 15000, "1875x"),
("DeepSeek-V2", 236, 8100, "34x (active: ~386x)"),
]

print(f"{'模型':<20s} {'参数':>8s} {'数据量':>12s} {'D/N':>10s}")
print("-" * 54)
for name, params_b, tokens_b, dn in real_models:
print(f"{name:<20s} {params_b:>5}B {tokens_b:>6}B tokens {dn:>10s}")

print()
print("观察: 2023 年后的模型 D/N 比远超 20x")
print(" → 行业共识已变成「小模型 + 海量数据」")
print(" → 因为推理时用小模型便宜得多,训练时多花点数据成本也值得")
=== 各大模型的 D/N 比 ===

模型 参数 数据量 D/N
------------------------------------------------------
Chinchilla (最优) 70B 1400B tokens 20x
LLaMA 7B 7B 1000B tokens 143x
LLaMA 2 7B 7B 2000B tokens 286x
LLaMA 3 8B 8B 15000B tokens 1875x
DeepSeek-V2 236B 8100B tokens 34x (active: ~386x)

观察: 2023 年后的模型 D/N 比远超 20x
→ 行业共识已变成「小模型 + 海量数据」
→ 因为推理时用小模型便宜得多,训练时多花点数据成本也值得

5. µP:最大更新参数化

训练一个 70B 模型之前,需要先确定超参:学习率、初始化标准差、权重衰减系数……但在 70B 上做网格搜索不现实——跑一次就要几十万。

通常做法:在 10M 的小模型上调好参数,然后原样搬到 70B 上。

但这个方法经常失灵。 10M 上的最优学习率,搬到 70B 上可能训练不稳定——梯度爆炸或 loss 不下降。原因在于,不同大小的模型在训练初期的激活值和梯度量级不一样。一个对 10M 模型合适的步长,对 70B 模型来说可能太大或太小。

µP 解决的就是这个问题。它对每一层的初始化方差和学习率做了特殊的缩放规则,使得不同大小的模型在训练初期的行为保持一致——10M 模型和 70B 模型的「手感」相同。

具体来说,µP 控制了每层权重矩阵的初始化方差和对应的学习率缩放。核心规则是:对于 hidden 维度的权重矩阵 W(d_out × d_in),初始化方差按 1/d_in 缩放,学习率按 1/d_in 缩放(对于输入权重)或 1/d_out 缩放(对于输出权重)。这样一来,无论模型宽度怎么变,每层的输出量级和梯度量级保持恒定。

不用 µP:  10M 上调出 lr=0.01 → 搬到 70B → 可能失稳,还要重新调
用了 µP: 10M 上调出 lr=0.01 → 搬到 70B → 更有机会迁移,但仍要验证

这节省的是巨大的人力和算力成本:小模型调出的超参更有机会迁移到大模型,但真实训练仍需要验证和少量调整。µP 是大模型超参迁移研究中的重要方法,也被不少训练系统和研究工作采用。参考:µP papermicrosoft/mupCerebras µP docs

下半场:资源估算

6. FLOPs 估算

6.1 核心公式

C ≈ 6 × P × D

其中:

  • C = 总计算量(FLOPs)
  • P = 模型参数数量
  • D = 训练 token 数

6.2 为什么是 6?手算一遍

每次训练一个 token,需要做 forward + backward:

Forward(前向传播)

  • 主要是矩阵乘法。
  • 一个 m×k 的矩阵乘 k×n 的矩阵,需要 m×n×k 次乘加 = 2×m×n×k FLOPs
  • 对于每个 token,总计算量 ≈ 2P FLOPs

Backward(反向传播)

  • 计算梯度需要的计算量 ≈ 2 × Forward = 4P FLOPs per token

总计:2P + 4P = 6P FLOPs per token。

训 D 个 token → 总 FLOPs = 6PD。

6.3 用具体模型算一遍

# === FLOPs 估算:一步一步算 ===
print("=== FLOPs 估算:手算 ===")
print()

# 问题:训一个 LLaMA 7B,用了 1T tokens,需要多少 FLOPs?
P = 7e9 # 7B 参数
D = 1e12 # 1T tokens

print(f"模型参数 P = {P/1e9:.0f}B")
print(f"训练数据 D = {D/1e12:.0f}T tokens")
print()

# Step 1: 每个 token 的 FLOPs
flops_per_token = 6 * P
print(f"Step 1 — 每个 token 的 FLOPs = 6 × {P/1e9:.0f}B")
print(f" = 6 × {P:.1e}")
print(f" = {flops_per_token:.1e} FLOPs/token")
print()

# Step 2: 总 FLOPs
total_flops = flops_per_token * D
print(f"Step 2 — 总 FLOPs = {flops_per_token:.1e} × {D/1e12:.0f}T")
print(f" = {flops_per_token:.1e} × {D:.1e}")
print(f" = {total_flops:.1e} FLOPs")
print()

# Step 3: 转成人类可读
def format_flops(flops):
if flops >= 1e24:
return f"{flops/1e24:.1f} YFLOPs (1e24)"
elif flops >= 1e21:
return f"{flops/1e21:.1f} ZFLOPs (1e21)"
elif flops >= 1e18:
return f"{flops/1e18:.1f} EFLOPs (1e18)"
else:
return f"{flops/1e15:.1f} PFLOPs (1e15)"

print(f"Step 3 — 结果: {format_flops(total_flops)}")
print()
print("下一节把这个数字换算成实际的 GPU 时间和成本。")

6.4 从 FLOPs 到 GPU-hours

FLOPs 告诉我们「总共要做多少次运算」,但这个数字太大了(动辄 10²²),很难直观感受「到底需要多少张卡、跑多久」。

工程上更常用的度量是 GPU-hours

GPU-hours = 总 FLOPs ÷ (单卡有效算力 × 3600)

其中「单卡有效算力」不等于硬件标称的峰值。实际训练中,通信开销、数据加载、显存碎片等因素让 GPU 不可能 100% 满载,通常有效利用率在 40%-60% 之间。不同 GPU 的峰值、利用率和租金都不同,选对硬件对成本影响很大。下面用具体数字算一遍。

# === GPU 算力对比 ===
print("=== 常见训练 GPU 对比(示例估算)===")
print()

# 价格变化很快。这里的 price 不是报价承诺,只是为了演示计算方法。
# 写作日期:2026-06-09。真实预算要按当时云厂商/租卡平台报价重算。
gpus = [
("A100 80GB", 312, 0.50, 2.0),
("H100 80GB", 990, 0.50, 3.5),
("H200 141GB", 990, 0.50, 4.5),
("RTX 4090", 165, 0.40, 0.5),
]

print(f"{'GPU':<18s} {'峰值':>8s} {'利用率':>6s} {'有效算力':>12s} {'示例时租':>10s}")
print("-" * 58)
for name, peak, util, price in gpus:
eff = peak * util
print(f"{name:<18s} {peak:>5.0f} TF {util*100:>4.0f}% "
f"{eff:>8.0f} TFLOPS ${price:.1f}/h")

print()
print("关键观察:")
print(" • 峰值算力只说明硬件上限,真实速度还取决于利用率和通信开销")
print(" • 示例时租只是演示计算方法,真实价格会随地区、供需和租期变化")
print(" • 不要只看单卡价格;要一起看有效算力、显存容量、网络和稳定性")
print()

# --- 用 LLaMA 7B + 1T tokens 手算 GPU-hours ---
P = 7e9
D = 1e12
total_flops = 6 * P * D

print("=== GPU-hours 手算:LLaMA 7B + 1T tokens ===")
print()
print(f"总 FLOPs = {total_flops:.1e}")
print()
print("公式: GPU-hours = 总 FLOPs ÷ (有效算力 × 3600)")
print()

print(f"{'GPU':<18s} {'GPU-hours':>12s} {'单卡天数':>10s} "
f"{'256卡天数':>10s} {'256卡示例成本':>14s}")
print("-" * 70)

for name, peak, util, price in gpus:
eff = peak * util
gpu_hours = total_flops / (eff * 1e12 * 3600)
days_1 = gpu_hours / 24
days_256 = gpu_hours / 256 / 24
cost_256 = gpu_hours * price
print(f"{name:<18s} {gpu_hours:>10,.0f} h {days_1:>8,.0f} 天 "
f"{days_256:>8.1f} 天 ${cost_256:>10,.0f}")

print()
print("注意:GPU-hours 是总计算量的度量,与用几张卡无关。")
print(" 256 卡跑 1 天 ≈ 1 卡跑 256 天 ≈ 同一个 GPU-hours 数字。")
print(" 多卡只缩短挂钟时间;真实总成本还会受通信效率和排队时间影响。")

# === 各大模型训练 FLOPs 与 GPU-hours 对比 ===
print("=== 主流模型训练成本一览(估算)===")
print()

models = [
("GPT-3 175B", 175e9, 300e9),
("LLaMA 7B", 7e9, 1e12),
("LLaMA 65B", 65e9, 1.4e12),
("Chinchilla 70B", 70e9, 1.4e12),
("LLaMA 2 70B", 70e9, 2e12),
("LLaMA 3 8B", 8e9, 15e12),
("LLaMA 3 70B", 70e9, 15e12),
("DeepSeek-V3", 671e9, 14.8e12),
]

# 用 A100 作为参照
a100_eff = 312 * 0.5 # A100 有效算力: 156 TFLOPS
a100_price = 2.0 # A100 参考时租: $2/h

print(f"{'模型':<18s} {'参数':>6s} {'数据':>9s} {'总FLOPs':>12s} "
f"{'GPU-hours':>12s} {'256卡·天':>9s} {'成本':>8s}")
print("-" * 80)

for name, params, tokens in models:
flops = 6 * params * tokens
gpu_hours = flops / (a100_eff * 1e12 * 3600)
days_256 = gpu_hours / 256 / 24
cost_m = gpu_hours * a100_price / 1e6

print(f"{name:<18s} {params/1e9:>4.0f}B {tokens/1e9:>6.0f}B tok "
f"{format_flops(flops):>12s} {gpu_hours:>10,.0f} h "
f"{days_256:>7.1f} 天 ${cost_m:>5.1f}M")

print()
print("计算方法:")
print(" GPU-hours = 总 FLOPs ÷ (A100 有效算力 156 TFLOPS × 3600)")
print(" 成本 = GPU-hours × $2/h(A100 参考价)")
print()
print("注意:")
print(" • 以上为单次训练的下限估算,实际还有通信、重训等开销")
print(" • 不同团队的 GPU 类型和利用率不同,数字会有差异")

7. 显存估算

7.1 显存被谁吃了?

训练时,GPU 显存里住着四个「租客」:

┌─────────────────────────────────────────┐
│ GPU 显存 (VRAM) │
├─────────────────────────────────────────┤
│ ① 模型参数 (FP16) 2P bytes │
│ ② 梯度 (FP16) 2P bytes │
│ ③ AdamW 优化器状态 (FP32) 8P bytes │
│ ├ m (momentum) 4P bytes │
│ └ v (variance) 4P bytes │
│ ④ 模型参数备份 (FP32) 4P bytes │
│ ⑤ 激活值 约 2-10P │
├─────────────────────────────────────────┤
│ 总计 ≈ 18-26P │
└─────────────────────────────────────────┘

经验法则:每个参数大约需要 20 bytes 显存。

7.2 用 LLaMA 7B 算一遍

# === 显存估算:手算 ===
print("=== 显存估算:LLaMA 7B 全量训练需要多少显存? ===")
print()

P = 7e9 # 7B 参数

# 这是 AdamW + 混合精度 + 全量训练的粗略起点。
# 不同框架会因为 ZeRO/FSDP、activation checkpointing、8-bit optimizer 等改变显存。
param_fp16 = 2 * P
grad_fp16 = 2 * P
adam_m = 4 * P
adam_v = 4 * P
param_fp32 = 4 * P

model_optimizer = param_fp16 + grad_fp16 + adam_m + adam_v + param_fp32

print("模型参数 + 梯度 + AdamW 状态:")
print(f" ① 模型参数 (FP16/BF16): {param_fp16/1e9:.1f} GB")
print(f" ② 梯度 (FP16/BF16): {grad_fp16/1e9:.1f} GB")
print(f" ③ Adam m (FP32): {adam_m/1e9:.1f} GB")
print(f" ④ Adam v (FP32): {adam_v/1e9:.1f} GB")
print(f" ⑤ FP32 master weights: {param_fp32/1e9:.1f} GB")
print(f" 小计: {model_optimizer/1e9:.1f} GB")
print(f" = {model_optimizer/P:.0f} bytes/param,不含激活值")
print()

# 激活值不是固定的 bytes/param,它主要由 batch_size、seq_len、层数和 checkpoint 策略决定。
batch_size = 4
seq_len = 4096

print(f"激活值(取决于 batch={batch_size}, seq_len={seq_len} 和是否重计算):")
print(f" 粗略占位: 约 2-10P = {2*P/1e9:.0f}-{10*P/1e9:.0f} GB")
print()

activation_low = 2 * P
activation_high = 10 * P
total_low = (model_optimizer + activation_low) / 1e9
total_high = (model_optimizer + activation_high) / 1e9

print(f"粗略总显存: {total_low:.0f} - {total_high:.0f} GB")
print(f"A100 (80G) 需要: {total_low/80:.0f} - {total_high/80:.0f} 张")
print()

print("=== 各模型训练显存需求(粗略起点)===")
print()
print(f"{'模型':<18s} {'状态+梯度':>12s} {'含激活粗估':>14s} {'A100(80G)':>12s}")
print("-" * 62)

for name, params, _ in [
("1.5B 小模型", 1.5e9, None),
("LLaMA 7B", 7e9, None),
("LLaMA 13B", 13e9, None),
("LLaMA 65B", 65e9, None),
("GPT-3 175B", 175e9, None),
]:
states = 16 * params / 1e9
total = 20 * params / 1e9
gpus = total / 80
print(f"{name:<18s} {states:>8.0f} GB ~{total:>10.0f} GB {gpus:>8.0f} 张")

print()
print("经验法则: 20 GB / 1B 参数只是 AdamW 全量训练的粗略起点。")
print("ZeRO/FSDP 会切分状态,checkpointing 会省激活,8-bit optimizer 会省优化器状态。")
print("推理显存主要是权重 + KV Cache,不能直接套训练显存公式。")

8. 缩放定律三次范式转变总结

2020 Kaplan (OpenAI):
「优先增大模型,数据可以少点」
N ∝ C^0.73, D ∝ C^0.27
→ GPT-3 175B + 300B tokens (D/N=1.7x)

2022 Chinchilla (DeepMind):
「模型和数据同等重要!」
N ∝ C^0.5, D ∝ C^0.5, D/N ≈ 20
→ Chinchilla 70B + 1.4T tokens (D/N=20x) 打败 Gopher 280B

2023+ 过度训练 (LLaMA 3 等):
「小模型 + 海量数据,推理更便宜」
D/N >> 20, 甚至 > 1000
→ LLaMA 3 8B + 15T tokens (D/N=1875x)

每次范式转变都推翻了一个「常识」——这就是科学和工程交织推进的方式。

小结

确认你已经搞懂了这些(按顺序检查):

  1. ✅ 幂律:Loss ∝ N^(-b),收益递减——每次翻倍的提升越来越小
  2. ✅ Kaplan: 优先增大模型,D 增长慢于 N
  3. ✅ Chinchilla: 模型和数据同等重要,D/N ≈ 20
  4. ✅ 过度训练: 推理成本 > 训练成本 → 小模型多喂数据更划算
  5. ✅ µP: 让不同大小模型的超参可共享,调参成本降低
  6. ✅ FLOPs 公式: C ≈ 6PD(forward 2P + backward 4P per token)
  7. ✅ GPU-hours = 总 FLOPs ÷ (有效算力 × 3600);GPU-hours 与卡数无关
  8. ✅ 显存估算: 20 bytes/param 是 AdamW 全量训练的粗略起点,不是固定公式
  9. ✅ 真实显存还取决于 ZeRO/FSDP、activation checkpointing、optimizer、batch 和 seq_len

一句话总结:缩放定律告诉你最优配比(理论),FLOPs/显存/GPU-hours 估算告诉你能不能实现、要花多少钱(工程)。2024 年的共识是——小模型 + 海量数据,以推理成本为优化目标。

作业> 可以让 AI 帮忙解释思路,但不建议直接让 AI "做完这道题"。

作业 1:FLOPs 估算训练 LLaMA 7B 用了 1T()tokens。使用公式 计算总 FLOPs,并换算成 PFLOPs-days(1 PFLOPs-day = FLOPs)。小提示:

# 作业 1:FLOPs 估算P = 7e9    # 7B 参数D = 1e12   # 1T tokens# TODO: 计算总 FLOPstotal_flops = None  # 在这里计算# TODO: 换算成 PFLOPs-days# 1 PFLOPs-day = 1e15 * 86400 FLOPspflops_days = None  # 在这里计算assert total_flops is not None, "请先计算总 FLOPs"assert pflops_days is not None, "请先计算 PFLOPs-days"expected_flops = 6 * P * Dexpected_days = expected_flops / (1e15 * 86400)assert total_flops == expected_flops, f"总 FLOPs 应为 {expected_flops:.2e}"assert abs(pflops_days - expected_days) < 0.01, f"PFLOPs-days 应为 {expected_days:.1f}"print(f"✅ 作业 1 通过:")print(f"   总 FLOPs: {total_flops:.2e}")print(f"   等价于 {pflops_days:.0f} PFLOPs-days")print("   这大约需要在 2048 张 A100 上训练约 21 天。")

作业 2:Kaplan vs Chinchilla 资源分配给定计算预算 ,Kaplan 建议把 73% 的预算增长分配给模型参数(),Chinchilla 建议模型和数据各 50%()。如果预算翻倍( 变为 ),分别计算 Kaplan 和 Chinchilla 方案下模型参数 增长到原来的多少倍。小提示:,Kaplan 用 0.73,Chinchilla 用 0.5。

# 作业 2:Kaplan vs Chinchilla 资源分配import math# 预算翻倍:ratio = 2ratio = 2# TODO: 计算 Kaplan 方案下模型参数增长倍数# N_new / N_old = ratio ^ 0.73kaplan_ratio = None  # 在这里计算# TODO: 计算 Chinchilla 方案下模型参数增长倍数# N_new / N_old = ratio ^ 0.5chinchilla_ratio = None  # 在这里计算assert kaplan_ratio is not None, "请先计算 Kaplan 倍数"assert chinchilla_ratio is not None, "请先计算 Chinchilla 倍数"expected_kaplan = 2 ** 0.73expected_chinchilla = 2 ** 0.5assert abs(kaplan_ratio - expected_kaplan) < 0.01, f"Kaplan 倍数应为 {expected_kaplan:.3f}"assert abs(chinchilla_ratio - expected_chinchilla) < 0.01, f"Chinchilla 倍数应为 {expected_chinchilla:.3f}"print(f"✅ 作业 2 通过:")print(f"   Kaplan: 模型参数增长 {kaplan_ratio:.2f}×(优先增大模型)")print(f"   Chinchilla: 模型参数增长 {chinchilla_ratio:.2f}×(模型数据均衡增长)")print("   Kaplan 的模型增长更快,Chinchilla 更平衡。")

作业 3:显存估算LLaMA 7B 全量训练(AdamW + 混合精度)的显存组成:- 模型参数(FP16):- 梯度(FP16):- AdamW 优化器状态(FP32 的 m 和 v):- 模型参数主副本(FP32):计算总显存需求(单位 GB,),并说明为什么 7B 模型全量训练需要远超 14 GB 显存。小提示:总显存 = bytes = bytes,再换算成 GB。

# 作业 3:显存估算P = 7e9  # 7B 参数# TODO: 计算各部分显存(单位 bytes)param_fp16 = None     # 2 bytes per paramgrad_fp16 = None      # 2 bytes per paramadam_m = None          # 4 bytes per param (FP32)adam_v = None          # 4 bytes per param (FP32)param_fp32 = None      # 4 bytes per param (主副本)# TODO: 计算总显存(单位 GB)total_memory_gb = None  # 在这里计算assert all(v is not None for v in [param_fp16, grad_fp16, adam_m, adam_v, param_fp32]), "请先计算各部分"assert total_memory_gb is not None, "请先计算总显存"expected = (2*P + 2*P + 4*P + 4*P + 4*P) / 1e9assert abs(total_memory_gb - expected) < 0.1, f"总显存应为 {expected:.1f} GB"print(f"✅ 作业 3 通过:")print(f"   参数 FP16: {param_fp16/1e9:.1f} GB")print(f"   梯度 FP16: {grad_fp16/1e9:.1f} GB")print(f"   Adam m:    {adam_m/1e9:.1f} GB")print(f"   Adam v:    {adam_v/1e9:.1f} GB")print(f"   参数 FP32: {param_fp32/1e9:.1f} GB")print(f"   总计: {total_memory_gb:.1f} GB(不含激活值)")print("   7B 模型全量训练需要 ~112 GB,远超参数本身的 14 GB。")