缩放定律
假设你有 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 paper、microsoft/mup、Cerebras µ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)
每次范式转变都推翻了一个「常识」——这就是科学和工程交织推进的方式。
小结
确认你已经搞懂了这些(按顺序检查):
- ✅ 幂律:Loss ∝ N^(-b),收益递减——每次翻倍的提升越来越小
- ✅ Kaplan: 优先增大模型,D 增长慢于 N
- ✅ Chinchilla: 模型和数据同等重要,D/N ≈ 20
- ✅ 过度训练: 推理成本 > 训练成本 → 小模型多喂数据更划算
- ✅ µP: 让不同大小模型的超参可共享,调参成本降低
- ✅ FLOPs 公式: C ≈ 6PD(forward 2P + backward 4P per token)
- ✅ GPU-hours = 总 FLOPs ÷ (有效算力 × 3600);GPU-hours 与卡数无关
- ✅ 显存估算: 20 bytes/param 是 AdamW 全量训练的粗略起点,不是固定公式
- ✅ 真实显存还取决于 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。")