引言
LLaMA-3 405B、GPT-4、Falcon 180B——这些模型究竟是如何训练出来的?简单地说"多用点 GPU 就行"这种说法,大大简化了现实的复杂性。要真正训练出数百亿参数的 LLM,需要基于缩放定律(scaling laws)设计超参数、精细的分布式训练策略、确保训练稳定性,以及对庞大计算资源的高效管理。
本指南以实战为核心,涵盖大规模 LLM 预训练的所有关键要素。
1. 缩放定律(Scaling Laws)
1.1 Kaplan 等人(OpenAI)的缩放定律
2020 年,Kaplan 等人发表的论文《Scaling Laws for Neural Language Models》证明了语言模型的性能会随三个要素呈幂律(power law)提升。
- N:模型参数数量
- D:训练数据 token 数量
- C:总计算预算(FLOPs)
核心发现:
- 模型规模优先(计算效率视角):在固定的计算预算下,增大模型规模比增加数据更高效
- 数据效率:同一模型规模下训练得再久,收益也会递减
- 幂律:损失 L 遵循 L(N) ≈ (Nc/N)^αN 的形式
根据这一法则,"把大部分计算预算投入到更大的模型上,数据量则低于最优值"才是高效做法。结果就是 GPT-3(175B)这样的巨型模型只用了相对较少的 token(300B)进行训练。
1.2 Chinchilla 最优缩放(Hoffmann et al. 2022)
2022 年,DeepMind 的 Hoffmann 等人在论文《Training Compute-Optimal Large Language Models》中提出了一项推翻 Kaplan 结论的重要发现。
核心主张:现有的大型模型都严重欠训练(undertrained)。
Chinchilla 实验:
- 现有方式:Gopher(280B 参数,300B token)
- Chinchilla:70B 参数,1.4T token
- 结果:Chinchilla 以更小的规模压制了 Gopher
Chinchilla 最优缩放定律:
给定计算预算 C 时,最优模型规模 N 与数据量 D:
N_optimal ≈ 0.1174 × C^0.4999 (参数数量)
D_optimal ≈ 1.6972 × C^0.5001 (token 数量)
也就是说,N 和 D 应该以几乎相同的比例随计算量缩放。经验法则为:
D_optimal ≈ 20 × N
7B 参数模型 → 至少需要 140B token 70B 参数模型 → 至少需要 1.4T token
1.3 实用计算公式
FLOPs 估算:
def estimate_training_flops(
num_params, # 参数数量
num_tokens, # 训练 token 数量
include_backward=True
):
"""
C ≈ 6 × N × D (前向传播 + 反向传播)
反向传播的成本约为前向传播的 2 倍
"""
flops_per_token = 6 * num_params
total_flops = flops_per_token * num_tokens
return total_flops
# 示例: LLaMA-2 7B, 2T token
flops = estimate_training_flops(7e9, 2e12)
print(f"Total FLOPs: {flops:.2e}")
# Total FLOPs: 8.40e+22
# GPU 训练时间估算 (A100 312 TFLOPs, 假设 MFU 50%)
a100_flops = 312e12 # 312 TFLOPs
mfu = 0.5 # Model FLOP Utilization
training_seconds = flops / (a100_flops * mfu)
training_hours = training_seconds / 3600
# 使用 1000 张 A100 时
num_gpus = 1000
gpu_hours_per_gpu = training_hours / num_gpus
print(f"训练时间: {gpu_hours_per_gpu:.1f} GPU-hours per GPU")
print(f"总 GPU 时间: {training_hours:.0f} GPU-hours")
1.4 Chinchilla 之后的趋势:"过度训练(Overtrain)"策略
从实用角度看,存在一种训练量超过 Chinchilla 最优值的倾向。
原因:训练成本与推理成本之间的权衡
- 训练只做一次,但推理要执行数百万次
- 让更小的模型训练得更久 → 降低推理成本
示例:LLaMA-3 8B 用 15T token 训练(约为 Chinchilla 基准的 100 倍)
2. 预训练数据流水线
2.1 数据来源构成
高质量的预训练数据是 LLM 性能的核心。
主要数据来源:
| 来源 | 特点 | 质量 |
|---|---|---|
| Common Crawl | 网页爬取,规模达数 PB | 噪声多,必须清洗 |
| Books3/Gutenberg | 书籍,质量较高 | 多样性有限 |
| Wikipedia | 百科全书,信息经过验证 | 数量有限 |
| GitHub | 代码,提升逻辑推理能力 | 需注意许可证 |
| ArXiv/PubMed | 学术论文 | 专业性高 |
| StackExchange | 问答,实用知识 | 质量较好 |
实际混合比例(估算,以 Llama-2 为准):
data_mixture = {
"Common Crawl (filtered)": 0.67, # 67%
"Books": 0.14, # 14%
"GitHub": 0.045, # 4.5%
"Wikipedia": 0.045, # 4.5%
"Gutenberg": 0.025, # 2.5%
"ArXiv": 0.025, # 2.5%
"StackExchange": 0.02, # 2%
}
2.2 数据清洗流水线
import re
from typing import List, Optional
from dataclasses import dataclass
@dataclass
class DocumentFilter:
min_tokens: int = 50
max_tokens: int = 100000
min_avg_word_length: float = 3.0
max_symbol_ratio: float = 0.1
min_alpha_ratio: float = 0.7
def filter_document(text: str, config: DocumentFilter) -> Optional[str]:
"""基础质量过滤器"""
tokens = text.split()
token_count = len(tokens)
# 长度过滤
if not (config.min_tokens <= token_count <= config.max_tokens):
return None
# 平均单词长度
avg_word_len = sum(len(t) for t in tokens) / token_count
if avg_word_len < config.min_avg_word_length:
return None
# 字母占比
alpha_chars = sum(1 for c in text if c.isalpha())
if alpha_chars / len(text) < config.min_alpha_ratio:
return None
return text
def deduplicate_documents(texts: List[str], n_gram_size: int = 13) -> List[str]:
"""
基于 MinHash LSH 的去重
(实际实现使用 datasketch 库)
"""
from datasketch import MinHash, MinHashLSH
lsh = MinHashLSH(threshold=0.8, num_perm=128)
unique_texts = []
for i, text in enumerate(texts):
minhash = MinHash(num_perm=128)
# 按 n-gram 单位做哈希
words = text.lower().split()
for j in range(len(words) - n_gram_size + 1):
ngram = " ".join(words[j:j+n_gram_size])
minhash.update(ngram.encode("utf-8"))
if not lsh.query(minhash):
lsh.insert(str(i), minhash)
unique_texts.append(text)
return unique_texts
2.3 分词器训练
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import ByteLevel
def train_tokenizer(
corpus_files: List[str],
vocab_size: int = 32000,
output_path: str = "tokenizer.json"
):
"""BPE 分词器训练(SentencePiece 方式)"""
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))
tokenizer.pre_tokenizer = ByteLevel(add_prefix_space=False)
trainer = BpeTrainer(
vocab_size=vocab_size,
min_frequency=2,
special_tokens=["[UNK]", "[BOS]", "[EOS]", "[PAD]"],
show_progress=True
)
tokenizer.train(files=corpus_files, trainer=trainer)
tokenizer.save(output_path)
return tokenizer
# 计算 token 数量
def count_tokens(file_path: str, tokenizer) -> int:
total = 0
with open(file_path, "r") as f:
for line in f:
tokens = tokenizer.encode(line.strip())
total += len(tokens.ids)
return total
3. Megatron-LM
3.1 NVIDIA Megatron 简介
Megatron-LM 是 NVIDIA 开发的大规模语言模型训练框架,为 GPT、BERT、T5 等大型模型的训练提供了专门的并行化技术。
核心特点:
- 张量并行(Tensor Parallelism):在 GPU 之间切分矩阵运算
- 流水线并行(Pipeline Parallelism):将各层依次分散到不同 GPU
- 序列并行(Sequence Parallelism):序列维度也进行分散(Megatron v2)
- 集成 Flash Attention:内存高效的注意力计算
3.2 张量并行的实现原理
Transformer 的核心运算是矩阵乘法。如何切分这一运算,正是张量并行的关键所在。
Multi-Head Attention 的张量切分:
Q、K、V 投影: [d_model, d_head × n_heads]
→ 每张 GPU: [d_model, d_head × (n_heads/tp_size)]
FFN 层的张量切分:
第一层线性层: [d_model, d_ffn] → 按列方向切分
第二层线性层: [d_ffn, d_model] → 按行方向切分
→ 每张 GPU: [d_model, d_ffn/tp_size] + [d_ffn/tp_size, d_model]
# Megatron 的 ColumnParallelLinear 概念(简化版)
class ColumnParallelLinear(nn.Module):
"""将权重矩阵按列方向切分"""
def __init__(self, in_features, out_features, tp_size):
super().__init__()
self.tp_size = tp_size
assert out_features % tp_size == 0
local_out = out_features // tp_size
# 每张 GPU 只持有整体的 1/tp_size
self.weight = nn.Parameter(torch.empty(local_out, in_features))
def forward(self, x):
# 输入是完整大小,输出是切分后的大小
return torch.nn.functional.linear(x, self.weight)
# 之后需要 All-reduce 或 All-gather
class RowParallelLinear(nn.Module):
"""将权重矩阵按行方向切分"""
def __init__(self, in_features, out_features, tp_size):
super().__init__()
self.tp_size = tp_size
assert in_features % tp_size == 0
local_in = in_features // tp_size
# 每张 GPU 处理输入维度的 1/tp_size
self.weight = nn.Parameter(torch.empty(out_features, local_in))
def forward(self, x):
# x: [batch, seq, in_features/tp_size]
local_output = torch.nn.functional.linear(x, self.weight)
# 用 All-reduce 汇总各 GPU 的部分和
dist.all_reduce(local_output)
return local_output
3.3 序列并行
序列并行会把 LayerNorm 和 Dropout 沿序列维度分散开。
Attention 输入: [batch, seq/tp_size, d_model] (每张 GPU)
→ All-gather: [batch, seq, d_model]
→ Self-Attention (按列切分)
→ Reduce-scatter: [batch, seq/tp_size, d_model]
→ FFN (按行切分)
这样一来,LayerNorm 所需的内存也会减少 tp_size 倍。
3.4 Megatron 配置示例
#!/bin/bash
# 用 Megatron-LM 训练 GPT-3 175B (示例)
GPUS_PER_NODE=8
NNODES=64
TP_SIZE=8 # 张量并行
PP_SIZE=16 # 流水线并行
DP_SIZE=$((GPUS_PER_NODE * NNODES / TP_SIZE / PP_SIZE))
# DP_SIZE = 8 * 64 / 8 / 16 = 4
torchrun \
--nproc_per_node $GPUS_PER_NODE \
--nnodes $NNODES \
pretrain_gpt.py \
--num-layers 96 \
--hidden-size 12288 \
--num-attention-heads 96 \
--seq-length 2048 \
--max-position-embeddings 2048 \
--global-batch-size 1536 \
--train-iters 300000 \
--lr 6e-5 \
--lr-decay-style cosine \
--min-lr 6e-6 \
--lr-warmup-fraction 0.001 \
--weight-decay 0.1 \
--tensor-model-parallel-size $TP_SIZE \
--pipeline-model-parallel-size $PP_SIZE \
--micro-batch-size 1 \
--bf16 \
--use-flash-attn \
--sequence-parallel \
--data-path /data/pile_text_document \
--vocab-file /data/gpt2-vocab.json \
--merge-file /data/gpt2-merges.txt \
--save /checkpoints/gpt3-175b \
--load /checkpoints/gpt3-175b
4. 3D 并行化 (DP + TP + PP)
4.1 三种并行化的结合
在大规模 LLM 训练中,通常会同时使用三种并行化方式。
| 并行化类型 | 分散对象 | 通信模式 | 开销 |
|---|---|---|---|
| 数据并行 (DP) | 批次 | 梯度 All-reduce | 低 |
| 张量并行 (TP) | 层内矩阵 | 每层 All-reduce | 中等(对延迟敏感) |
| 流水线并行 (PP) | 层组 | 点对点 | 气泡开销 |
4.2 寻找最优并行配置
def find_optimal_parallelism(
world_size: int,
num_layers: int,
hidden_size: int,
gpu_memory_gb: float = 80.0
) -> dict:
"""
3D 并行化最优配置搜索
一般规则:
- TP: 单节点内(利用 NVLink),通常为 4 或 8
- PP: 跨节点分散,取决于层数
- DP: 剩余的 GPU
"""
configs = []
for tp in [1, 2, 4, 8]:
for pp in range(1, world_size + 1):
dp = world_size // (tp * pp)
if dp < 1:
continue
if world_size != dp * tp * pp:
continue
if num_layers % pp != 0:
continue
# 流水线气泡比率计算
# 微批次数 m、pp 阶段数 p 时: bubble = (p-1)/(m+p-1)
# 这里 m = global_batch / (dp * micro_batch)
micro_batch = 4 # 假设值
global_batch = 2048 # 假设值
m = global_batch // (dp * micro_batch)
if m <= 0:
continue
bubble_rate = (pp - 1) / (m + pp - 1)
configs.append({
"tp": tp, "pp": pp, "dp": dp,
"bubble_rate": bubble_rate,
"layers_per_stage": num_layers // pp
})
# 优先选择气泡比率低、TP 合理的配置
configs.sort(key=lambda x: (x["bubble_rate"], x["tp"]))
return configs[:5]
# 示例: 512 张 GPU, GPT-3 规模 (96 层)
best_configs = find_optimal_parallelism(512, 96, 12288)
for c in best_configs:
print(f"TP={c['tp']}, PP={c['pp']}, DP={c['dp']}, "
f"Bubble={c['bubble_rate']:.2%}, Layers/Stage={c['layers_per_stage']}")
4.3 DeepSpeed 与 Megatron 的结合
# Megatron-DeepSpeed 集成配置
deepspeed_config = {
"train_batch_size": 2048,
"train_micro_batch_size_per_gpu": 1,
"gradient_accumulation_steps": 256, # 2048 / (8 DP * 1 micro)
"bf16": {"enabled": True},
"zero_optimization": {
"stage": 1, # 与 TP+PP 一起使用 ZeRO-1
},
"gradient_clipping": 1.0,
"wall_clock_breakdown": True,
"steps_per_print": 10
}
4.4 通信拓扑优化
# 考虑 NVLink 与 InfiniBand 的带宽差异
# NVLink 3.0: 600 GB/s (双向)
# InfiniBand HDR: 200 Gb/s = 每端口 25 GB/s
# 通信组设计原则:
# TP 组: 同一节点内的 GPU (利用 NVLink)
# PP 组: 跨节点 (利用 InfiniBand)
# DP 组: 跨节点 (用 ZeRO 最小化通信)
def setup_process_groups(tp_size, pp_size, dp_size):
"""设置进程组"""
world_size = tp_size * pp_size * dp_size
rank = torch.distributed.get_rank()
# 张量并行组 (推荐同一节点内)
for dp_rank in range(dp_size):
for pp_rank in range(pp_size):
ranks = [
dp_rank * tp_size * pp_size + pp_rank * tp_size + tp_rank
for tp_rank in range(tp_size)
]
group = torch.distributed.new_group(ranks)
if rank in ranks:
tp_group = group
return tp_group
5. 训练稳定性
5.1 处理损失突增
大规模训练中,损失突然飙升(loss spike)是常见问题。
原因分析:
- 梯度范数过大:某个批次中梯度异常偏大
- 学习率过高:损失曲面地形突变
- 不良数据批次:异常值或处理错误的数据
- 数值不稳定:bf16/fp16 下的上溢/下溢
监控代码:
import torch
import wandb
from collections import deque
class TrainingMonitor:
def __init__(self, spike_threshold: float = 3.0, window_size: int = 100):
self.loss_history = deque(maxlen=window_size)
self.grad_norm_history = deque(maxlen=window_size)
self.spike_threshold = spike_threshold
self.spike_count = 0
def check_loss_spike(self, current_loss: float) -> bool:
if len(self.loss_history) < 10:
self.loss_history.append(current_loss)
return False
mean_loss = sum(self.loss_history) / len(self.loss_history)
if current_loss > mean_loss * self.spike_threshold:
self.spike_count += 1
print(f"SPIKE 检测: current={current_loss:.4f}, mean={mean_loss:.4f}")
return True
self.loss_history.append(current_loss)
return False
def compute_grad_norm(self, model) -> float:
total_norm = 0.0
for p in model.parameters():
if p.grad is not None:
param_norm = p.grad.data.norm(2)
total_norm += param_norm.item() ** 2
return total_norm ** 0.5
def log_step(self, step: int, loss: float, model, lr: float):
grad_norm = self.compute_grad_norm(model)
is_spike = self.check_loss_spike(loss)
wandb.log({
"train/loss": loss,
"train/grad_norm": grad_norm,
"train/lr": lr,
"train/is_spike": int(is_spike),
"train/spike_count": self.spike_count,
}, step=step)
return grad_norm, is_spike
5.2 梯度裁剪与噪声尺度
def adaptive_gradient_clipping(
model,
optimizer,
max_norm: float = 1.0,
clip_coef: float = 0.01
):
"""
自适应梯度裁剪
AGC (Adaptive Gradient Clipping): 按层裁剪
"""
for module in model.modules():
for name, param in module.named_parameters(recurse=False):
if param.grad is None:
continue
# 比较参数范数与梯度范数
param_norm = param.data.norm(2)
grad_norm = param.grad.data.norm(2)
# 裁剪阈值: 参数范数的 clip_coef 倍
max_grad_norm = clip_coef * param_norm
if grad_norm > max_grad_norm and grad_norm > 0:
param.grad.data.mul_(max_grad_norm / grad_norm)
def compute_gradient_noise_scale(model, world_size: int) -> float:
"""
梯度噪声尺度 (Gradient Noise Scale) 计算
GNS = tr(S) / ||g||^2
GNS 越高 → 可以使用越大的批次大小
"""
grads = [p.grad.data for p in model.parameters() if p.grad is not None]
# 整体梯度范数
g_norm_sq = sum(g.norm(2).item() ** 2 for g in grads)
# 简化的方差估计
variance = sum((g - g.mean()).norm(2).item() ** 2 for g in grads)
gns = variance / g_norm_sq if g_norm_sq > 0 else 0
return gns
5.3 学习率调度策略
import math
def cosine_schedule_with_warmup(
current_step: int,
warmup_steps: int,
total_steps: int,
max_lr: float,
min_lr: float
) -> float:
"""
余弦衰减 + 线性预热
绝大多数 LLM 训练中作为标准使用
"""
if current_step < warmup_steps:
# 线性预热
return max_lr * current_step / warmup_steps
else:
# 余弦衰减
progress = (current_step - warmup_steps) / (total_steps - warmup_steps)
cosine_val = 0.5 * (1 + math.cos(math.pi * progress))
return min_lr + (max_lr - min_lr) * cosine_val
# WSD (Warmup-Stable-Decay) 调度 - 近来很受欢迎
def wsd_schedule(
current_step: int,
warmup_steps: int,
stable_steps: int,
decay_steps: int,
max_lr: float,
min_lr: float
) -> float:
"""
Warmup → Stable → Decay 三阶段调度
MiniCPM、LLaMA-3 等采用
对持续学习(continual learning)有利
"""
if current_step < warmup_steps:
return max_lr * current_step / warmup_steps
elif current_step < warmup_steps + stable_steps:
return max_lr
else:
decay_progress = (current_step - warmup_steps - stable_steps) / decay_steps
return max_lr - (max_lr - min_lr) * min(decay_progress, 1.0)
5.4 批次大小调度
# OpenAI GPT-3 论文中提出的批次大小递增策略
def get_batch_size_schedule(
current_tokens: int,
final_batch_tokens: int = 4_000_000, # 最终批次大小 (以 token 为单位)
initial_batch_tokens: int = 32_000, # 初始批次大小
ramp_tokens: int = 1_200_000_000 # 递增区间 (token)
) -> int:
"""
批次大小随 token 处理量线性增加
初期: 用小批次快速收敛
后期: 用大批次稳定训练
"""
if current_tokens < ramp_tokens:
progress = current_tokens / ramp_tokens
batch_tokens = initial_batch_tokens + (
final_batch_tokens - initial_batch_tokens
) * progress
return int(batch_tokens)
return final_batch_tokens
6. 检查点策略
6.1 分布式检查点
import os
import torch
import torch.distributed as dist
from pathlib import Path
class DistributedCheckpointer:
def __init__(self, save_dir: str, max_checkpoints: int = 5):
self.save_dir = Path(save_dir)
self.max_checkpoints = max_checkpoints
self.save_dir.mkdir(parents=True, exist_ok=True)
def save(
self,
model,
optimizer,
scheduler,
step: int,
rank: int,
world_size: int
):
"""保存分布式检查点"""
checkpoint_dir = self.save_dir / f"step_{step:08d}"
checkpoint_dir.mkdir(parents=True, exist_ok=True)
# 各 rank 保存自己的分片
rank_path = checkpoint_dir / f"rank_{rank:04d}_of_{world_size:04d}.pt"
# 模型状态 (ZeRO 分片)
model_state = {
"model": model.state_dict(),
"optimizer": optimizer.state_dict(),
"scheduler": scheduler.state_dict() if scheduler else None,
"step": step,
"rank": rank,
"world_size": world_size,
}
torch.save(model_state, rank_path)
# rank 0 保存元数据
if rank == 0:
meta = {
"step": step,
"world_size": world_size,
"timestamp": __import__("time").time()
}
torch.save(meta, checkpoint_dir / "meta.pt")
# 清理旧检查点
if rank == 0:
self._cleanup_old_checkpoints()
def _cleanup_old_checkpoints(self):
checkpoints = sorted(self.save_dir.glob("step_*"))
while len(checkpoints) > self.max_checkpoints:
oldest = checkpoints.pop(0)
import shutil
shutil.rmtree(oldest)
6.2 异步检查点保存
import threading
from queue import Queue
class AsyncCheckpointer:
"""在独立线程中保存检查点 (不中断训练)"""
def __init__(self, save_dir: str):
self.save_dir = save_dir
self.queue = Queue(maxsize=2)
self.worker = threading.Thread(target=self._worker, daemon=True)
self.worker.start()
def _worker(self):
while True:
item = self.queue.get()
if item is None:
break
state_dict, path = item
torch.save(state_dict, path)
self.queue.task_done()
def save_async(self, model, step: int, rank: int):
"""不阻塞主线程,加入保存队列"""
# 复制到 CPU (释放 GPU 内存)
state_dict = {
k: v.cpu().clone() for k, v in model.state_dict().items()
}
path = os.path.join(self.save_dir, f"step_{step}_rank_{rank}.pt")
if not self.queue.full():
self.queue.put((state_dict, path))
else:
# 队列已满则同步保存
torch.save(state_dict, path)
7. 训练监控
7.1 核心指标追踪
import wandb
import numpy as np
class LLMTrainingTracker:
def __init__(self, project_name: str, run_name: str):
wandb.init(project=project_name, name=run_name)
self.step = 0
self.token_count = 0
def log_training_step(
self,
loss: float,
learning_rate: float,
grad_norm: float,
batch_tokens: int,
elapsed_seconds: float
):
tokens_per_second = batch_tokens / elapsed_seconds
# 用 token 处理量估算 MFU
# MFU = actual_flops / theoretical_peak_flops
self.token_count += batch_tokens
self.step += 1
wandb.log({
# 训练指标
"train/loss": loss,
"train/perplexity": np.exp(min(loss, 20)),
"train/grad_norm": grad_norm,
"train/learning_rate": learning_rate,
# 效率指标
"throughput/tokens_per_second": tokens_per_second,
"throughput/samples_per_second": tokens_per_second / 2048,
# 进度
"progress/total_tokens": self.token_count,
"progress/step": self.step,
}, step=self.step)
def log_evaluation(self, eval_loss: float, perplexities: dict):
wandb.log({
"eval/loss": eval_loss,
"eval/perplexity": np.exp(eval_loss),
**{f"eval/{k}_ppl": v for k, v in perplexities.items()}
}, step=self.step)
7.2 损失曲线解读
正常训练曲线的特征:
- 预热区间:快速下降
- 稳定区间:缓慢但持续下降
- 后期区间:非常平缓的下降
问题信号:
- 完全停滞 (Loss plateau):学习率过低、数据枯竭
- 急剧突增:不良批次、梯度爆炸
- NaN/Inf:数值不稳定、fp16 上溢
def analyze_loss_curve(losses: list, window: int = 100) -> dict:
"""损失曲线自动分析"""
if len(losses) < window * 2:
return {}
recent = losses[-window:]
previous = losses[-2*window:-window]
recent_mean = np.mean(recent)
previous_mean = np.mean(previous)
improvement = (previous_mean - recent_mean) / previous_mean
# 突增检测
global_mean = np.mean(losses)
global_std = np.std(losses)
spikes = [l for l in losses if l > global_mean + 3 * global_std]
return {
"recent_loss": recent_mean,
"improvement_rate": improvement,
"spike_count": len(spikes),
"is_stagnant": improvement < 0.001,
"recommendation": "建议提高学习率" if improvement < 0.001 else "正常"
}
8. 开源 LLM 训练代码库
8.1 GPT-NeoX (EleutherAI)
# 安装并运行 GPT-NeoX
git clone https://github.com/EleutherAI/gpt-neox
cd gpt-neox
pip install -r requirements/requirements.txt
# 配置文件 (configs/20B.yml)
# 开始训练
python deepy.py train configs/20B.yml
GPT-NeoX 结合了基于 Megatron 的流水线并行与 DeepSpeed。EleutherAI 的 Pythia 系列就是用这个代码库训练的。
8.2 OLMo (Allen AI)
# OLMo 训练配置 (简化版)
from olmo import TrainConfig, ModelConfig, OptimizerConfig
train_config = TrainConfig(
model=ModelConfig(
d_model=4096,
n_heads=32,
n_layers=32,
mlp_ratio=8/3,
vocab_size=50280,
max_sequence_length=2048,
attention_type="flash",
),
optimizer=OptimizerConfig(
name="adamw",
learning_rate=3e-4,
weight_decay=0.1,
betas=(0.9, 0.95),
),
max_duration="300000ba", # 300,000 个批次
global_train_batch_size=2048,
device_train_microbatch_size=2,
precision="bf16",
fsdp_config={
"wrapping_strategy": "by_block",
"precision": "bf16",
"sharding_strategy": "FULL_SHARD",
},
)
OLMo 以完全透明为目标,公开训练数据、代码和中间检查点。
8.3 torchtitan
# torchtitan - PyTorch 原生 LLM 训练
# Meta 开发的最新预训练框架
# torchtitan 配置 (toml 格式)
config = """
[model]
name = "llama3"
flavor = "8B"
tokenizer_path = "./original/tokenizer.model"
[optimizer]
name = "AdamW"
lr = 3e-4
[training]
batch_size = 8
seq_len = 8192
max_norm = 1.0
steps = 10000
data_parallel_replicate_degree = 1
data_parallel_shard_degree = -1 # FSDP2
tensor_parallel_degree = 1
compile = true # 启用 torch.compile
[checkpoint]
enable_checkpoint = true
folder = "outputs/checkpoint"
interval_type = "steps"
interval = 500
"""
torchtitan 用 PyTorch 原生方式实现了 FSDP2、Tensor Parallel、Pipeline Parallel。
8.4 FSDP2 (Fully Sharded Data Parallel)
import torch
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp.wrap import transformer_auto_wrap_policy
from transformers.models.llama.modeling_llama import LlamaDecoderLayer
# FSDP 包装策略
auto_wrap_policy = transformer_auto_wrap_policy(
transformer_layer_cls={LlamaDecoderLayer}
)
# FSDP2 (torch 2.x 的新 API)
from torch.distributed._composable.fsdp import fully_shard, MixedPrecisionPolicy
mp_policy = MixedPrecisionPolicy(
param_dtype=torch.bfloat16,
reduce_dtype=torch.float32,
)
for layer in model.model.layers:
fully_shard(
layer,
mp_policy=mp_policy,
reshard_after_forward=True, # 类似 ZeRO-3
)
fully_shard(model, mp_policy=mp_policy)
9. 成本估算与效率化策略
9.1 GPU 时间计算
def estimate_training_cost(
model_params: float, # 参数数量 (例如: 7e9)
training_tokens: float, # 训练 token 数 (例如: 2e12)
num_gpus: int,
gpu_type: str = "A100-80GB",
cloud_provider: str = "AWS"
) -> dict:
"""训练成本估算"""
# GPU 规格 (理论峰值, bf16)
gpu_specs = {
"A100-80GB": {"tflops": 312, "memory_gb": 80, "nvlink": True},
"H100-80GB": {"tflops": 989, "memory_gb": 80, "nvlink": True},
"A10G-24GB": {"tflops": 125, "memory_gb": 24, "nvlink": False},
"V100-32GB": {"tflops": 130, "memory_gb": 32, "nvlink": True},
}
# 云服务价格 (美元/小时, 大致估算)
cloud_prices = {
"AWS": {"A100-80GB": 3.97, "H100-80GB": 8.0, "A10G-24GB": 1.006},
"GCP": {"A100-80GB": 3.67, "H100-80GB": 7.0},
"Azure": {"A100-80GB": 3.40, "H100-80GB": 6.0},
"Lambda": {"A100-80GB": 1.99, "H100-80GB": 3.29},
}
spec = gpu_specs[gpu_type]
price_per_hour = cloud_prices.get(cloud_provider, {}).get(gpu_type, 0)
# FLOPs 计算
total_flops = 6 * model_params * training_tokens
# 实际 MFU 假设 (通常为 35-55%)
mfu = 0.45
effective_tflops = spec["tflops"] * 1e12 * mfu
# 总训练时间
total_seconds = total_flops / (effective_tflops * num_gpus)
total_hours = total_seconds / 3600
total_days = total_hours / 24
# 成本计算
total_cost = price_per_hour * num_gpus * total_hours
return {
"total_flops": f"{total_flops:.2e}",
"training_hours": f"{total_hours:.1f}",
"training_days": f"{total_days:.1f}",
"gpu_hours": f"{total_hours * num_gpus:.0f}",
"estimated_cost_usd": f"{total_cost:,.0f}",
"mfu": mfu,
}
# 示例
result = estimate_training_cost(
model_params=7e9,
training_tokens=2e12,
num_gpus=256,
gpu_type="A100-80GB",
cloud_provider="Lambda"
)
for k, v in result.items():
print(f"{k}: {v}")
9.2 MFU (Model FLOP Utilization) 优化
def measure_mfu(
model,
batch_tokens: int,
elapsed_seconds: float,
theoretical_peak_tflops: float
) -> float:
"""
实测 MFU
MFU = actual_FLOP_rate / theoretical_peak_FLOP_rate
"""
# 估算模型的实际 FLOPs
num_params = sum(p.numel() for p in model.parameters())
actual_flops = 6 * num_params * batch_tokens # 前向(2) + 反向(4)
actual_tflops = actual_flops / elapsed_seconds / 1e12
mfu = actual_tflops / theoretical_peak_tflops
return mfu
# MFU 提升策略:
# 1. 使用 Flash Attention (最小化内存 I/O)
# 2. 启用 torch.compile (内核融合)
# 3. 谨慎使用激活检查点 (会降低速度)
# 4. 选择最优批次大小 (最大化 GPU 占用率)
# 5. 让通信与计算重叠 (FSDP/DeepSpeed 设置)
9.3 效率化策略总结
| 策略 | 内存节省 | 速度影响 | 实现难度 |
|---|---|---|---|
| ZeRO-2 | 中等 | -5% | 简单 |
| ZeRO-3 | 高 | -15% | 中等 |
| Flash Attention 2 | 高 | +20% | 简单 |
| torch.compile | 无 | +15-30% | 简单 |
| Activation Checkpointing | 中等 | -30% | 简单 |
| bf16 (相较 fp16) | 无 | 稳定性提升 | 简单 |
| 序列打包 (sequence packing) | 无 | +20% | 中等 |
10. 完整的预训练启动脚本
#!/usr/bin/env python3
"""
大规模 LLM 预训练完整示例
GPT 风格模型, DeepSpeed ZeRO-2 + Flash Attention
"""
import os
import time
import math
import torch
import torch.nn as nn
import deepspeed
from torch.utils.data import DataLoader, IterableDataset
from transformers import (
AutoConfig,
AutoModelForCausalLM,
AutoTokenizer,
get_cosine_schedule_with_warmup,
)
# 配置
TRAINING_CONFIG = {
"model_name": "meta-llama/Llama-2-7b",
"total_tokens": 1_000_000_000, # 1B token (演示用)
"max_seq_len": 2048,
"global_batch_size": 1024, # 全局批次 (以 token 计: 1024 * 2048 = 2M)
"micro_batch_per_gpu": 4,
"learning_rate": 3e-4,
"min_lr": 3e-5,
"warmup_tokens": 20_000_000, # 2% 预热
"weight_decay": 0.1,
"grad_clip": 1.0,
"save_every_steps": 1000,
"eval_every_steps": 500,
"log_every_steps": 10,
}
class StreamingTokenDataset(IterableDataset):
"""流式 token 数据集 (处理大容量数据)"""
def __init__(self, data_path: str, seq_len: int, rank: int, world_size: int):
self.data_path = data_path
self.seq_len = seq_len
self.rank = rank
self.world_size = world_size
def __iter__(self):
# 各 rank 处理不同的数据区间
with open(self.data_path, "rb") as f:
f.seek(self.rank * self.seq_len * 2) # 以 uint16 为基准
while True:
chunk = f.read(self.seq_len * 2 * self.world_size)
if not chunk:
break
tokens = torch.frombuffer(chunk, dtype=torch.uint16).long()
if len(tokens) < self.seq_len + 1:
break
input_ids = tokens[:self.seq_len]
labels = tokens[1:self.seq_len + 1]
yield {"input_ids": input_ids, "labels": labels}
def train():
deepspeed.init_distributed()
rank = torch.distributed.get_rank()
world_size = torch.distributed.get_world_size()
local_rank = int(os.environ.get("LOCAL_RANK", 0))
# 加载模型
config = AutoConfig.from_pretrained(TRAINING_CONFIG["model_name"])
with deepspeed.zero.Init():
model = AutoModelForCausalLM.from_config(config)
# 初始化 DeepSpeed 引擎
model_engine, optimizer, _, _ = deepspeed.initialize(
model=model,
config={
"train_micro_batch_size_per_gpu": TRAINING_CONFIG["micro_batch_per_gpu"],
"gradient_accumulation_steps": (
TRAINING_CONFIG["global_batch_size"]
// TRAINING_CONFIG["micro_batch_per_gpu"]
// world_size
),
"bf16": {"enabled": True},
"zero_optimization": {
"stage": 2,
"overlap_comm": True,
"contiguous_gradients": True,
"allgather_bucket_size": 2e8,
"reduce_bucket_size": 2e8,
},
"gradient_clipping": TRAINING_CONFIG["grad_clip"],
"optimizer": {
"type": "AdamW",
"params": {
"lr": TRAINING_CONFIG["learning_rate"],
"betas": [0.9, 0.95],
"eps": 1e-8,
"weight_decay": TRAINING_CONFIG["weight_decay"],
}
},
"steps_per_print": TRAINING_CONFIG["log_every_steps"],
}
)
# 数据集
train_dataset = StreamingTokenDataset(
"/data/train_tokens.bin",
TRAINING_CONFIG["max_seq_len"],
rank, world_size
)
train_loader = DataLoader(
train_dataset,
batch_size=TRAINING_CONFIG["micro_batch_per_gpu"],
num_workers=4,
pin_memory=True,
)
# 训练循环
total_tokens = 0
target_tokens = TRAINING_CONFIG["total_tokens"]
for batch in train_loader:
if total_tokens >= target_tokens:
break
input_ids = batch["input_ids"].to(model_engine.device)
labels = batch["labels"].to(model_engine.device)
# 学习率调度
warmup_tokens = TRAINING_CONFIG["warmup_tokens"]
lr = cosine_schedule_with_warmup(
total_tokens, warmup_tokens, target_tokens,
TRAINING_CONFIG["learning_rate"], TRAINING_CONFIG["min_lr"]
)
for param_group in optimizer.param_groups:
param_group["lr"] = lr
# 前向/反向传播
outputs = model_engine(input_ids=input_ids, labels=labels)
loss = outputs.loss
model_engine.backward(loss)
model_engine.step()
# 更新 token 计数
batch_tokens = input_ids.numel() * world_size
total_tokens += batch_tokens
# 日志记录 (仅 rank 0)
if rank == 0 and model_engine.global_steps % TRAINING_CONFIG["log_every_steps"] == 0:
print(f"Tokens: {total_tokens/1e9:.2f}B, "
f"Loss: {loss.item():.4f}, "
f"LR: {lr:.2e}, "
f"PPL: {math.exp(loss.item()):.1f}")
# 保存检查点
if model_engine.global_steps % TRAINING_CONFIG["save_every_steps"] == 0:
model_engine.save_checkpoint(
"./checkpoints",
tag=f"step_{model_engine.global_steps}"
)
if rank == 0:
print("训练完成!")
model_engine.save_checkpoint("./checkpoints", tag="final")
def cosine_schedule_with_warmup(step, warmup, total, max_lr, min_lr):
if step < warmup:
return max_lr * step / max(warmup, 1)
progress = (step - warmup) / max(total - warmup, 1)
return min_lr + (max_lr - min_lr) * 0.5 * (1 + math.cos(math.pi * progress))
if __name__ == "__main__":
train()
执行:
deepspeed --num_nodes=4 --num_gpus=8 \
--master_addr=node0 --master_port=29500 \
pretrain.py
结语
100B+ 参数 LLM 的预训练,绝不只是执行代码那么简单,而是需要在无数工程决策与权衡之间取得平衡的工作。
核心要点总结:
- 缩放定律:遵循 Chinchilla 最优化原则,在模型规模与数据量之间取得平衡,但若考虑推理成本,"过度训练(overtrain)"同样合理
- 数据是关键:高质量的数据清洗与均衡的混合比例决定了模型性能
- 3D 并行化:通过 DP + TP + PP 的组合,高效利用数千张 GPU
- 稳定性优先:必须对损失突增、梯度爆炸进行监控并快速应对
- 成本意识:持续监控 MFU 与 GPU 利用率,以优化效率
开源生态(OLMo、GPT-NeoX、torchtitan)正在降低大规模训练的准入门槛。请把本指南中的技巧应用到实际项目中,训练出属于你自己的 LLM。
参考资料
- Chinchilla 论文: Training Compute-Optimal Large Language Models
- Kaplan Scaling Laws: Scaling Laws for Neural Language Models
- Megatron-LM GitHub
- OLMo GitHub
- 3D 并行化论文: Efficient Large-Scale Language Model Training
- torchtitan
- GPT-NeoX
현재 단락 (1/842)
LLaMA-3 405B、GPT-4、Falcon 180B——这些模型究竟是如何训练出来的?简单地说"多用点 GPU 就行"这种说法,大大简化了现实的复杂性。要真正训练出数百亿参数的 LLM,需要基...