预训练策略:从 GPT 到 Llama,大模型的"九年义务教育"
7934 字
40 分钟
预训练策略:从 GPT 到 Llama,大模型的"九年义务教育"
1. 引言:什么是预训练?
1.1 一个生动的比喻
如果说大模型是一个学生,那么:
监督学习: 老师(人工标注)→ 学生 少量数据、精准指导 像家教
预训练: 海量文本(互联网)→ 学生 大量数据、自主学习 像"阅读大量书籍自学"预训练(Pre-training)是利用海量无标注文本让模型自我学习的过程。
1.2 预训练的核心思想
海量无标注文本 │ ▼┌─────────────────────────────┐│ 语言建模任务 ││ "预测下一个 token" │└─────────────────────────────┘ │ ▼ 基础模型 (Foundation Model) 具备通用语言能力 │ ▼ 下游任务微调 (Fine-tuning)关键洞察:通过预测下一个 token,模型被迫学会:
- 语法
- 语义
- 世界知识
- 推理能力
- 上下文学习
1.3 预训练简史
2017: Transformer (Vaswani et al.) │2018: GPT-1 (Radford et al.) │ 首次展示预训练 + 微调的范式 │2018: BERT (Devlin et al.) │ 双向编码预训练 │2019: GPT-2 (1.5B) │ 零样本学习的曙光 │2020: GPT-3 (175B) │ In-context learning 涌现 │2021: Switch Transformer (1.6T) │ 稀疏 MoE │2022: Chinchilla (70B) │ 提出 Chinchilla Scaling Law │2023: Llama 2 / GPT-4 │ 开源 + 闭源双轨发展 │2024: Llama 3 (405B) │ 更大规模,更优数据 │2025+: 多模态预训练 + RLHF 集成 + 工具集成1.4 预训练 vs 其他训练阶段
| 训练阶段 | 数据量 | 计算量 | 目标 |
|---|---|---|---|
| 预训练 | TB 级 | 万卡·月 | 学习通用能力 |
| 继续预训练 | GB 级 | 百卡·天 | 学习新领域 |
| 监督微调 (SFT) | MB 级 | 十卡·天 | 学习任务格式 |
| RLHF | KB 级 | 单卡·小时 | 对齐人类偏好 |
类比:
| 阶段 | 教育阶段 | 数据规模 |
|---|---|---|
| 预训练 | 9 年义务教育 | 海量课本 |
| 继续预训练 | 专业预科 | 一些专著 |
| SFT | 专业培训 | 案例练习 |
| RLHF | 行为矫正 | 评价反馈 |
2. 预训练核心任务
2.1 语言建模:基础任务
最经典的任务:给定上文,预测下一个 token:
# 输入文本text = "今天天气很好,我们去"
# 模型任务:预测下一个 token# 可能: "公园", "吃饭", "散步", ...
# 训练目标target = "公园"
# 损失函数loss = CrossEntropy(prediction, target)数学表示:
即最大化所有 token 的对数似然。
2.2 因果语言建模 (Causal LM)
GPT 系列使用的单向语言建模:
# 输入tokens = [今天, 天气, 很好, ,, 我们, 去]
# 因果注意力掩码# 位置 i 只能看到位置 0..i-1
# 第 0 个 token: 看不到任何上下文# 第 1 个 token (天气): 只能看到 [今天]# 第 2 个 token (很好): 只能看到 [今天, 天气]# ...数学形式:
2.3 掩码语言建模 (Masked LM)
BERT 使用的双向语言建模:
# 输入tokens = [今天, 天气, [MASK], ,, 我们, 去]
# 模型任务:预测 [MASK] 处是什么# 期望输出: "很好"
# 双向注意力:可以看到两侧特点:
- ✓ 充分利用上下文
- ✗ 不能直接用于生成
2.4 多种预训练目标
class PretrainingObjectives: """预训练目标集合"""
CAUSAL_LM = "clm" # 因果 LM MASKED_LM = "mlm" # 掩码 LM PREFIX_LM = "prefix_lm" # 前缀 LM SPAN_MASKING = "span_mask" # 跨度掩码 NEXT_SENTENCE = "nsp" # 下一句预测 SENTENCE_ORDER = "sop" # 句子顺序 REPLACEMENT = "RTD" # 替换检测 (ELECTRA) CONTRASTIVE = "contrastive" # 对比学习
# 不同模型采用不同组合MODELS_OBJECTIVES = { 'GPT': ['clm'], 'BERT': ['mlm', 'nsp'], 'T5': ['span_mask'], 'ELECTRA': ['RTD'], 'XLNet': ['permutation_lm'], 'Llama': ['clm'],}2.5 前缀语言建模 (Prefix LM)
混合 CLM 和 MLM 的优点的方案:
输入: [用户: 今天天气] [助手: 很好,我们去]
训练目标:- 用户部分: 使用双向注意力- 助手部分: 使用因果注意力- 整体可以同时看到用户上下文# 前缀 LM 掩码# [用户文本 ] [助手文本 ]# 双向 + 因果 因果# ◀───▶ ◀──在 PaLM、UL2 等模型中广泛使用。
2.6 去噪自编码 (Denoising AE)
T5 系列的预训练方式:
# 原始句子"今天天气很好,我们去公园散步"
# 加入噪声(随机打乱、删除等)"今天 [MASK] [MASK],我们去 [MASK] 散步"
# 模型: 重建完整句子"今天天气很好,我们去公园散步"优势:双向注意力 + 自然的下游任务格式。
3. Tokenization:预训练的第一步
3.1 为什么需要 Tokenization?
神经网络无法直接处理文本,需要切分成 token:
"今天天气很好"
字符级: [今, 天, 天, 气, 很, 好] (6 tokens)词语级: [今天, 天气, 很, 好] (4 tokens, 需要中文分词)BPE: [今, 天, 天, 气, 很, 好] (6 subwords)3.2 BPE 算法
字节对编码 (Byte Pair Encoding):
def bpe_train(corpus, vocab_size): """训练 BPE""" # 1. 初始化: 字符集合 vocab = set(char for text in corpus for char in text)
while len(vocab) < vocab_size: # 2. 统计相邻 token 对的频率 pairs = count_pairs(corpus)
# 3. 找最频繁的对 most_frequent = max(pairs, key=pairs.get)
# 4. 合并 merge_pair(most_frequent, corpus) vocab.add(most_frequent)
return vocab
# 示例:# 初始: 今天 天气 很 好# 统计: (今天, 天气) 出现 100 次最多# 合并: 今天天气 → 一个 token# 继续: 看 (今天天气, 很好) ...3.3 现代 Tokenizer 工具
3.3.1 SentencePiece
Google 开发,主流选择- 支持 BPE / Unigram- 多语言友好- Llama、PaLM、Falcon 使用3.3.2 tiktoken
OpenAI 开发- 高性能- GPT-3.5/4 使用- 50x faster than alternatives3.3.3 HuggingFace Tokenizers
- 支持 BPE, WordPiece, Unigram- 多语言- 易于集成3.4 Tokenizer 选型
# 不同模型的 tokenizerTOKENIZERS = { 'GPT-4': 'cl100k_base (tiktoken) - 100K vocab', 'GPT-3': 'BPE - 50K vocab', 'Llama': 'SentencePiece BPE - 32K vocab', 'Llama 3': 'SentencePiece BPE - 128K vocab', 'BERT': 'WordPiece - 30K vocab', 'T5': 'SentencePiece - 32K vocab', 'Falcon': 'BPE - 65K vocab', 'Mistral': 'BPE - 32K vocab', 'Claude': 'BPE - ~100K vocab',}3.5 中文 Tokenization 挑战
# 挑战 1: 中文字符多# 基础汉字 ~3500, 常用 ~7000, 大字符集 > 10 万
# 挑战 2: 分词歧义"乒乓球拍卖完了" → 乒乓球拍卖 / 完了 ?"喜欢吃 / 苹果" vs "喜欢 / 吃苹果"
# 解决方案:# 1. 增加中文训练数据# 2. 使用更大 vocab (32K → 64K)# 3. 字符级别的 fallback# 4. 专门的 Chinese-aware tokenizer3.6 多语言 Tokenization
class MultilingualTokenizer: """多语言 tokenizer"""
def __init__(self, vocab_size=100000): # 让高频语言共享 token # 低频语言用较长表示 self.vocab_size = vocab_size
def token_efficiency(self, text, lang): """不同语言的 token 效率""" tokens = self.encode(text) compression_ratio = len(tokens) / len(text)
# 高资源语言(英语): 1 token ≈ 4 字符 # 低资源语言(冰岛语): 1 token ≈ 1 字符 return compression_ratio4. 数据工程
4.1 数据的重要性
“在机器学习中,垃圾进,垃圾出。最关键的资产是数据,而非模型。”
Llama 2 论文的关键发现:
- 数据质量 > 数据量
- 数据多样性 > 单一来源
- 数据清洗 > 原始数据
4.2 数据规模
模型规模 训练 token 数─────────────────────────────────GPT-2 (1.5B) ~10BGPT-3 (175B) ~300BGLaM (1.2T) ~1.5TPaLM (540B) ~780BChinchilla (70B) ~1.4T (按 Chinchilla Law)Llama 1 (65B) ~1.4TLlama 2 (70B) ~2TLlama 3 (405B) ~15.6T4.3 数据来源
DATA_SOURCES = { 'CommonCrawl': { 'size': '~250B 网页 (清洗后)', 'quality': '中', '使用率': '~70%', }, 'GitHub': { 'size': '~50B 行代码', 'quality': '高(需要过滤)', '使用率': '~10% (Llama)', }, 'Books': { 'size': '~10B books', 'quality': '高', '使用率': '~5% (Books3, Project Gutenberg)', }, 'Wikipedia': { 'size': '~20M articles', 'quality': '极高', '使用率': '~5%', }, 'Papers': { 'size': '~10M papers', 'quality': '高', '使用率': '~5% (arXiv, PubMed)', }, '问答/论坛': { 'size': '~10B', 'quality': '中', '使用率': '~10% (StackExchange)', }}4.4 数据配比
# Llama 1 的数据配比DATA_MIX = { 'CommonCrawl': 67.0, 'C4': 15.0, 'GitHub': 4.5, 'Wikipedia': 4.5, 'Books': 4.5, 'StackExchange': 2.0, '其他': 2.5,}
# Llama 2 调整为:LLAMA2_MIX = { 'CommonCrawl': 60+, '高质量来源': ~40%, # 高质量来源占比增加}
# 关键: 在训练过程中可能调整配比4.5 数据清洗流程
原始数据 │ ▼[1] HTML/格式清洗(去标签、提内容) │ ▼[2] 语言识别(filter_english, fasttext) │ ▼[3] 质量过滤(启发式规则 + 模型评分) │ │ ├──▶ 去重(文档级、段落级) │ ▼[4] NSFW 过滤 │ ▼[5] PII(个人身份信息)移除 │ ▼[6] 数据增强/标准化 │ ▼最终训练数据4.6 数据去重
class Deduplicator: """数据去重"""
def exact_dedup(self, documents): """精确去重""" seen = set() unique = [] for doc in documents: if doc.hash not in seen: seen.add(doc.hash) unique.append(doc) return unique
def fuzzy_dedup(self, documents, threshold=0.85): """模糊去重(基于 MinHash)""" # MinHash 用于估计 Jaccard 相似度 unique = [] for doc in documents: is_dup = False for existing in unique: similarity = self._minhash_similarity(doc, existing) if similarity > threshold: is_dup = True break
if not is_dup: unique.append(doc)
return unique4.7 数据质量评分
class QualityScorer: """基于模型的文档质量评分"""
def __init__(self, quality_model): self.model = quality_model
def score(self, documents): """使用 kenlm / fastText 等打分""" scores = [] for doc in documents: # kenlm 困惑度(越低越好) perplexity = self.model.perplexity(doc.text)
# 文档长度 length = len(doc.text.split())
# 符号比例 symbol_ratio = self._symbol_ratio(doc.text)
# 综合评分 score = self._compute_score(perplexity, length, symbol_ratio) scores.append(score)
return scores4.8 数据污染检测
class ContaminationDetector: """检测训练数据与测试集的重叠"""
def detect(self, train_doc, test_questions): """检测污染""" contaminated = []
for q in test_questions: # 1. n-gram 重叠检测 overlap = self._ngram_overlap(train_doc, q) if overlap > 0.5: contaminated.append(q) continue
# 2. 语义相似度 sim = self._semantic_similarity(train_doc, q) if sim > 0.8: contaminated.append(q)
return contaminated5. 模型架构选择
5.1 Transformer 变体
class TransformerArchitectures: """Transformer 架构变体"""
DECODER_ONLY = { 'name': 'Decoder-Only', '代表': 'GPT, Llama, Mistral', '优势': '适合生成任务', '劣势': '不能同时看到完整上下文', '应用': '大多数现代 LLM', }
ENCODER_ONLY = { 'name': 'Encoder-Only', '代表': 'BERT, RoBERTa', '优势': '双向注意力', '劣势': '不适合生成长文本', '应用': '分类、检索', }
ENCODER_DECODER = { 'name': 'Encoder-Decoder', '代表': 'T5, BART, FLAN-T5', '优势': '输入输出分离', '劣势': '复杂、参数利用率低', '应用': '翻译、摘要', }5.2 现代 Decoder 改进
5.2.1 RMSNorm
# 替代 LayerNormclass RMSNorm(nn.Module): def __init__(self, dim, eps=1e-6): super().__init__() self.eps = eps self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x): rms = torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) return x * rms * self.weight5.2.2 SwiGLU Activation
# Llama 使用的激活函数class SwiGLU(nn.Module): def forward(self, x): x, gate = x.chunk(2, dim=-1) return F.silu(gate) * x
# 在 FFN 中class SwiGLU_FFN(nn.Module): def __init__(self, dim, hidden_dim): super().__init__() # 注意:hidden_dim 是 2/3 维度以补偿门控 self.w1 = nn.Linear(dim, hidden_dim, bias=False) self.w3 = nn.Linear(dim, hidden_dim, bias=False) self.w2 = nn.Linear(hidden_dim, dim, bias=False)
def forward(self, x): return self.w2(F.silu(self.w1(x)) * self.w3(x))5.2.3 RoPE 旋转位置编码
# Llama 等模型使用def apply_rope(x, cos, sin): # x: [..., seq_len, dim] # cos, sin: [seq_len, dim]
x1, x2 = x[..., 0::2], x[..., 1::2] rotated = torch.cat([ x1 * cos - x2 * sin, x2 * cos + x1 * sin ], dim=-1)
return rotated
# 优势: 相对位置、外推到长序列5.2.4 GQA 分组查询注意力
# Multi-Head Attention (MHA)class MHA: """标准多头注意力""" def __init__(self, n_heads): self.n_heads = n_heads # 每个 head 都有自己的 K, V
# Grouped Query Attention (GQA)class GQA: """分组查询注意力 - Llama 2 70B 等使用""" def __init__(self, n_heads, n_kv_heads): # n_heads 个 Q head # 但只有 n_kv_heads 个 KV head # 多个 Q head 共享 K, V passGQA 优势:
- 减少 KV cache 内存
- 加快推理
- 性能接近 MHA
5.2.5 Flash Attention
# IO 感知的注意力计算# 传统 attention 需要 O(N²) 内存来存储注意力矩阵# Flash Attention: 不存储矩阵,分块计算
class FlashAttention: """硬件优化的注意力"""
def forward(self, Q, K, V): # 分块计算,避免存储完整注意力矩阵 # 复杂度 O(N²) 计算,O(N) 内存 # 利用 GPU 的 SRAM 加速 ...5.3 Llama 架构详解
class LlamaBlock(nn.Module): """Llama 块"""
def __init__(self, dim, n_heads, n_kv_heads=None, hidden_dim=None): super().__init__() n_kv_heads = n_kv_heads or n_heads hidden_dim = hidden_dim or 4 * dim
# 1. RMSNorm self.attention_norm = RMSNorm(dim)
# 2. Self-Attention (GQA + RoPE) self.attention = Attention(dim, n_heads, n_kv_heads)
# 3. RMSNorm self.ffn_norm = RMSNorm(dim)
# 4. SwiGLU FFN self.ffn = SwiGLU_FFN(dim, int(2/3 * 4 * dim))
def forward(self, x): # 残差连接 + Pre-Norm x = x + self.attention(self.attention_norm(x)) x = x + self.ffn(self.ffn_norm(x)) return x
class LlamaModel(nn.Module): """完整 Llama 模型"""
def __init__(self, vocab_size, dim, n_layers, n_heads, n_kv_heads=None): super().__init__() # 1. Token 嵌入 self.embed_tokens = nn.Embedding(vocab_size, dim)
# 2. Rotary Embeddings self.rotary = RotaryEmbedding(dim)
# 3. N 个 Transformer Block self.layers = nn.ModuleList([ LlamaBlock(dim, n_heads, n_kv_heads) for _ in range(n_layers) ])
# 4. 最终 Norm + 输出 self.norm = RMSNorm(dim) self.lm_head = nn.Linear(dim, vocab_size, bias=False)
# 5. 权重共享 self.lm_head.weight = self.embed_tokens.weight
def forward(self, input_ids): x = self.embed_tokens(input_ids)
for layer in self.layers: x = layer(x, self.rotary)
x = self.norm(x) return self.lm_head(x)5.4 关键超参数
LLAMA_CONFIGS = { 'Llama-7B': { 'dim': 4096, 'n_layers': 32, 'n_heads': 32, 'vocab_size': 32000, }, 'Llama-13B': { 'dim': 5120, 'n_layers': 40, 'n_heads': 40, }, 'Llama-65B': { 'dim': 8192, 'n_layers': 80, 'n_heads': 64, }, 'Llama-3-8B': { 'dim': 4096, 'n_layers': 32, 'n_heads': 32, 'vocab_size': 128000, }, 'Llama-3-405B': { 'dim': 16384, 'n_layers': 126, 'vocab_size': 128000, },}6. Scaling Laws:规模定律
6.1 Chinchilla 定律
DeepMind 的核心发现:
其中:
- :损失
- :参数数量
- :训练 token 数
- :拟合常数
核心结论:
- 模型和 token 应该同比增长
- 最优比例约为 每个参数 20 个 token
模型大小 最优 token 数──────────────────────────────────7B 140B13B 260B70B 1.4T175B 3.5T6.2 Scaling Law 实证
# Kaplan 2020 早期发现KAPLAN_LAW = """固定计算预算下:- 大模型 + 少数据 比 小模型 + 多数据 好- 但只看模型大小是不够的"""
# Chinchilla 2022 修正CHINCHILLA_LAW = """固定计算预算下:- 模型 和 数据 应该同比增长- 最优比例 ≈ 20 token/参数"""
# Llama 3 (2024) 实践LLAMA3_APPROACH = """训练了 405B 模型,超 15T tokens虽然 Chinchilla 说应该 8T tokens但高数据量在下游表现更好"""6.3 Compute-Optimal Frontier
# 给出固定的计算预算,最优的 (模型大小, 数据量)def optimal_compute_budget(compute_budget): """计算最优配置""" # Chinchilla 公式 # C ≈ 6 N D # L 是 N 和 D 的函数
# 近似计算 a, b = 0.34, 0.28 A = 406.4 B = 410.7 E = 1.69
# 由 C = 6ND 给出 N 和 D # 假设 C = compute_budget C = compute_budget
# 数值求解 N = (C / (6 * 20)) ** 0.5 # 20 tokens/param D = 20 * N
return N, D6.4 涌现能力 (Emergent Abilities)
某些能力只在规模达到一定程度时出现:
规模门槛─────────────────────────────────~10B: 基本推理~70B: 复杂推理~100B+: In-context learning 显著涌现~500B+: 多步推理、CoTEMERGENT_ABILITIES = { 'in_context_learning': '规模 > 数十亿时涌现', 'chain_of_thought': '规模 > 100B 时可用', 'instruction_following': '主要靠 SFT 和 RLHF', 'reasoning': '大规模时显著提升',}6.5 Scaling 实践
class ScalingExperiment: """Scaling 实验管理"""
def __init__(self): self.results = []
def run(self, model_sizes, compute_budgets): """运行 Scaling 曲线""" for compute in compute_budgets: for size in model_sizes: # 训练模型 model = train_model(size, compute)
# 评估 loss = evaluate(model)
self.results.append({ 'compute': compute, 'model_size': size, 'loss': loss, })
# 拟合 Scaling Law params = fit_scaling_law(self.results) return params7. 训练基础设施
7.1 分布式训练架构
┌──────────────────────────────────────────────────────────────┐│ LLM 训练集群 │├──────────────────────────────────────────────────────────────┤│ ││ ┌─────────────────────────────────────────────────────┐ ││ │ ZeRO / FSDP (内存优化) │ ││ │ Model + Optimizer states 分片到所有 GPU │ ││ └─────────────────────────────────────────────────────┘ ││ ││ ┌─────────────────────────────────────────────────────┐ ││ │ Tensor Parallel (张量并行) │ ││ │ 单层切分到 8/16 个 GPU │ ││ └─────────────────────────────────────────────────────┘ ││ ││ ┌─────────────────────────────────────────────────────┐ ││ │ Pipeline Parallel (流水线并行) │ ││ │ 不同层在不同的 GPU 组上 │ ││ └─────────────────────────────────────────────────────┘ ││ ││ ┌─────────────────────────────────────────────────────┐ ││ │ Data Parallel (数据并行) │ ││ │ 不同的数据批次到不同 GPU │ ││ └─────────────────────────────────────────────────────┘ ││ ││ ┌─────────────────────────────────────────────────────┐ ││ │ 节点间: InfiniBand / NVLink │ ││ │ 节点内: NVLink / PCIe │ ││ └─────────────────────────────────────────────────────┘ ││ │└──────────────────────────────────────────────────────────────┘7.2 数据并行 (DP)
class DataParallel: """数据并行"""
def setup(self, model, n_gpus=8): # 复制模型到每个 GPU replicas = [copy(model) for _ in range(n_gpus)]
# 每个 GPU 处理不同的数据批次 # 梯度同步(all-reduce)
def step(self, batch_id, gpu_id): # 在 GPU gpu_id 上处理批次 batch_id loss = replicas[gpu_id].forward(batch[batch_id]) loss.backward()
# 同步梯度 all_reduce(gradients)
# 同步更新 replicas[gpu_id].step()问题:每个 GPU 都存完整模型,不切分。
7.3 模型并行 (MP)
7.3.1 张量并行 (TP)
class TensorParallel: """张量并行"""
def parallelize_linear(self, layer, n_gpus): """切分线性层""" # Column Parallel: 切分输出维度 # 假设 output_dim = 1024, n_gpus = 4 # 每个 GPU 处理 256 维
# Row Parallel: 切分输入维度 pass
def forward(self, x): # 每个 GPU 独立计算自己的部分 # 通过 all-reduce 整合结果 ...
# Megatron-LM 风格的实现class MegatronLinear(nn.Linear): def __init__(self, in_features, out_features, world_size, rank): # Column-parallel: 切分输出 self.out_features_per_partition = out_features // world_size super().__init__(in_features, self.out_features_per_partition) self.weight.data = self.weight.data.chunk(world_size, dim=0)[rank]7.3.2 流水线并行 (PP)
class PipelineParallel: """流水线并行"""
def __init__(self, model, n_stages=4, n_microbatches=8): self.n_stages = n_stages self.n_microbatches = n_microbatches
# 将模型分为 n_stages 个阶段 # 每个阶段是一个连续的子层 self.stages = self._split_model(model, n_stages)
def forward(self, x): # 将 micro-batch 流水线化 # 阶段 1 处理 micro-batch 1 # 阶段 2 处理 micro-batch 1, 阶段 1 处理 micro-batch 2 # ... ...7.4 内存优化
7.4.1 ZeRO
class ZeROOptimizer: """ZeRO 优化器"""
def __init__(self, stage=3): # Stage 1: Optimizer states 分片 # Stage 2: + Gradients 分片 # Stage 3: + Parameters 分片
if stage >= 1: self._shard_optimizer_states() if stage >= 2: self._shard_gradients() if stage >= 3: self._shard_parameters()7.4.2 FSDP
class FSDPModel: """Fully Sharded Data Parallel"""
def setup(self, model): # 类似 ZeRO-3 # 模型参数完全分片到所有 GPU # Forward/Backward 时才 all-gather ...7.5 混合精度训练
class MixedPrecisionTraining: """混合精度训练"""
def __init__(self, model, optimizer): self.model = model self.optimizer = optimizer self.scaler = GradScaler()
def step(self, batch): # 1. 前向 (autocast FP16/BF16) with autocast(): output = self.model(batch) loss = compute_loss(output)
# 2. 反向 self.scaler.scale(loss).backward()
# 3. 优化器步骤 self.scaler.step(self.optimizer) self.scaler.update()精度选择:
- FP32:高精度但慢、内存大
- FP16:加速但易溢出
- BF16:与 FP32 相同范围,适合训练 ⭐
7.6 梯度累积与检查点
class GradientAccumulation: """梯度累积"""
def __init__(self, accumulation_steps=8): self.accumulation_steps = accumulation_steps
def step(self, batch_id, total_batches): # 等效 batch_size = accumulation_steps * micro_batch_size
loss = model_forward(batch) loss = loss / self.accumulation_steps loss.backward()
if (batch_id + 1) % self.accumulation_steps == 0: optimizer.step() optimizer.zero_grad()
class GradientCheckpointing: """梯度检查点"""
def forward(self, x): # 不保存中间激活值 # 反向时重新计算 with checkpoint: return self._forward(x)7.7 实际集群配置
Llama 3 训练:─────────────────────────────────GPU: 16,000 + NVIDIA H100内存: 80GB/GPUInterconnect: NVLink + InfiniBand存储: FUSE + 数百 PB训练时间: ~30 天功耗: 数兆瓦GPT-4 据传:─────────────────────────────────GPU: 25,000 + NVIDIA A100/H100训练时间: ~3-6 个月成本: $100M+8. 优化器和学习率
8.1 AdamW 优化器
class AdamW: """AdamW - 主流 LLM 优化器"""
def __init__(self, params, lr=1e-4, betas=(0.9, 0.95), weight_decay=0.1): self.params = params self.lr = lr self.beta1, self.beta2 = betas self.weight_decay = weight_decay
# 一阶、二阶动量 self.m = [torch.zeros_like(p) for p in params] self.v = [torch.zeros_like(p) for p in params]
def step(self): for i, p in enumerate(self.params): # 权重衰减(解耦) p.data.mul_(1 - self.lr * self.weight_decay)
# 梯度 g = p.grad
# 动量更新 self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * g self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * g ** 2
# 偏差修正 m_hat = self.m[i] / (1 - self.beta1 ** self.t) v_hat = self.v[i] / (1 - self.beta2 ** self.t)
# 参数更新 p.data -= self.lr * m_hat / (torch.sqrt(v_hat) + self.eps)8.2 学习率调度
8.2.1 Cosine Schedule
def cosine_schedule(step, warmup_steps, total_steps, max_lr, min_lr=0): """Cosine 学习率调度""" if step < warmup_steps: # Warmup 阶段 return max_lr * step / warmup_steps
# Cosine 退火 progress = (step - warmup_steps) / (total_steps - warmup_steps) return min_lr + 0.5 * (max_lr - min_lr) * (1 + math.cos(math.pi * progress))
# Llama 使用def llama_schedule(step, warmup=2000, max_lr=3e-4, total=100000, min_lr=3e-5): return cosine_schedule(step, warmup, total, max_lr, min_lr)8.2.2 WSD (Warmup-Stable-Decay)
def wsd_schedule(step, warmup, total, max_lr): """WSD 调度""" # 1. Warmup 阶段 if step < warmup: return max_lr * step / warmup
# 2. 稳定阶段(保持 max_lr) if step < total * 0.8: return max_lr
# 3. 衰减阶段 decay_start = int(total * 0.8) progress = (step - decay_start) / (total - decay_start) return max_lr * 0.5 * (1 + math.cos(math.pi * progress))8.3 关键超参数
TYPICAL_HYPERPARAMETERS = """模型规模 峰值 LR Batch Size Warmup───────────────────────────────────────────────0.1B - 1B 3e-4 512 - 2M 2000-5000 steps1B - 10B 1e-4 1M - 4M 2000-5000 steps10B - 100B 6e-5 4M - 16M 5000 steps100B+ 3e-5 16M+ 5000+ steps"""9. 训练目标与损失函数
9.1 交叉熵损失
def cross_entropy_loss(logits, targets): """预训练的标准损失"""
# logits: [batch, seq_len, vocab_size] # targets: [batch, seq_len]
# Shift: 预测下一个 token shift_logits = logits[..., :-1, :] # 去掉最后一个 shift_targets = targets[..., 1:] # 去掉第一个
# 交叉熵 loss = F.cross_entropy( shift_logits.reshape(-1, shift_logits.size(-1)), shift_targets.reshape(-1) )
return loss9.2 Loss 的多种解释
等价于:
- 困惑度 (Perplexity) 最小化
- 压缩 (Compression) 优化
- 预测准确率最大化
9.3 损失掩码
def compute_loss_with_masking(logits, targets, mask): """部分 token 的损失计算""" # 例如: 不对填充 token 计算损失
loss = F.cross_entropy(logits, targets, reduction='none') loss = (loss * mask).sum() / mask.sum()
return loss9.4 多任务预训练
class MultiTaskPretraining: """多任务预训练目标"""
def compute_loss(self, batch): task_type = batch['task']
if task_type == 'causal_lm': return self.causal_lm_loss(batch) elif task_type == 'masked_lm': return self.masked_lm_loss(batch) elif task_type == 'span_mask': return self.span_mask_loss(batch) elif task_type == 'classification': return self.classification_loss(batch) # ...9.5 辅助损失
# 一些模型添加辅助损失:AUX_LOSSES = { 'load_balancing': 'MoE 模型的负载均衡', 'routing': '路由稳定性', 'contrastive': '对比学习',}
# Llama 等稠密模型: 没有辅助损失10. 训练稳定性
10.1 Loss Spike
训练中可能出现 loss 突然飙升:
class SpikeHandler: """Loss spike 处理"""
def __init__(self, spike_threshold=20, cooldown_steps=10): self.spike_threshold = spike_threshold self.cooldown_steps = cooldown_steps
def check(self, current_loss, previous_loss): if current_loss > previous_loss * self.spike_threshold: return True return False
def handle(self): # 跳过最近的梯度更新 # 减小学习率 # 在 cooldown 期间 ...10.2 梯度裁剪
def clip_grad_norm(parameters, max_norm=1.0): """梯度裁剪""" total_norm = 0
for p in parameters: if p.grad is not None: param_norm = p.grad.data.norm(2) total_norm += param_norm.item() ** 2
total_norm = total_norm ** 0.5
# 缩放梯度 clip_coef = max_norm / (total_norm + 1e-6) if clip_coef < 1: for p in parameters: if p.grad is not None: p.grad.data.mul_(clip_coef)
return total_norm10.3 初始化
# 标准初始化def init_weights(model): """标准初始化策略"""
for name, param in model.named_parameters(): if 'weight' in name: if 'embed' in name: # 嵌入层: 标准初始化 nn.init.normal_(param, mean=0, std=0.02) elif 'norm' in name: # Norm 层: 1 nn.init.ones_(param) elif 'lm_head' in name: # 输出层: 标准初始化 nn.init.normal_(param, mean=0, std=0.02) else: # 其他: 缩放初始化 std = 0.02 / math.sqrt(2 * n_layers) nn.init.normal_(param, mean=0, std=std)
elif 'bias' in name: nn.init.zeros_(param)10.4 监控指标
class TrainingMonitor: """训练监控"""
METRICS = [ 'loss', 'grad_norm', 'learning_rate', 'tokens_per_second', 'mfu', # Model FLOPs Utilization 'memory_used', 'throughput', ]
def log(self, step, metrics): """记录每步""" # 使用 wandb / tensorboard wandb.log({k: v for k, v in metrics.items()}, step=step)
def check_health(self, metrics): """健康检查""" if metrics['grad_norm'] > 100: return 'gradient_explosion' if metrics['grad_norm'] < 0.001: return 'vanishing_gradients' if metrics['loss'] > 20: return 'loss_spike' if metrics['mfu'] < 0.3: return 'low_hardware_utilization' return 'healthy'10.5 MFU: Model FLOPs Utilization
def calculate_mfu(model, batch_size, seq_len, step_time): """MFU: 模型利用硬件算力的效率"""
# 理论 FLOPs n_params = sum(p.numel() for p in model.parameters()) forward_flops = 6 * n_params * batch_size * seq_len # 6 = 3(forward/backward)+ 3(multiplications)
# 实际吞吐 achieved_flops = forward_flops / step_time
# GPU 理论算力 gpu_peak = 312e12 # H100: 312 TFLOPS
# MFU mfu = achieved_flops / gpu_peak return mfu
# 良好的 MFU: 40% - 60%# 当前最优: 50-60%11. 评估预训练效果
11.1 训练中的评估
class PretrainingEvaluation: """训练中评估"""
EVAL_TASKS = [ 'validation_loss', # 验证集损失 'hellaswag', # 常识推理 'arc_challenge', # 科学问答 'mmlu', # 多任务理解 'humaneval', # 代码生成 'gsm8k', # 数学 'truthfulqa', # 真实性 'winogrande', # 常识 ]
def evaluate(self, model, eval_tasks): results = {} for task in eval_tasks: score = self._run_eval(model, task) results[task] = score return results11.2 评估基准对照
BENCHMARK_RESULTS = { 'Llama-3-8B': { 'MMLU': 68.4, 'HumanEval': 33.5, 'GSM8K': 50.0, 'HellaSwag': 80.0, }, 'Llama-3-70B': { 'MMLU': 79.3, 'HumanEval': 70.2, 'GSM8K': 93.0, 'HellaSwag': 88.0, }, 'Llama-3-405B': { 'MMLU': 88.6, 'HumanEval': 89.0, 'GSM8K': 96.8, },}11.3 训练 Loss 曲线分析
LOSS_CURVE_INTERPRETATION = """观察训练曲线:
1. 健康曲线: - 平滑下降 - Train loss 与 val loss 都下降 - 没有 spike
2. 过拟合: - Train loss 下降, val loss 上升 - 减少训练时间 - 或增加数据
3. 欠拟合: - Loss 平稳但较高 - 增加模型大小 - 或改进架构
4. Loss spike: - 突然飙升 - 通常降低 LR - 或回滚参数"""12. 高级训练策略
12.1 课程学习 (Curriculum Learning)
class CurriculumScheduler: """课程学习"""
def __init__(self): self.stages = [ # 阶段 1: 简单文本 {'difficulty': 0, 'data': 'simple_wikipedia'}, # 阶段 2: 中等 {'difficulty': 0.3, 'data': 'mixed'}, # 阶段 3: 复杂 {'difficulty': 0.7, 'data': 'full'}, ]
def get_data(self, progress): """根据训练进度返回数据""" stage_idx = int(progress * len(self.stages)) stage_idx = min(stage_idx, len(self.stages) - 1) return self.stages[stage_idx]12.2 数据调度
class DataSchedule: """训练过程中的数据配比变化"""
def get_mix(self, step): """根据训练步数返回数据配比""" if step < 100000: # 早期: 多用高质量 return {'高质量': 0.7, 'CommonCrawl': 0.3} else: # 中后期: 增加数量 return {'高质量': 0.4, 'CommonCrawl': 0.6}12.3 MoE 训练 (Sparse Upcycling)
class MoEUpcycling: """从稠密模型升级到 MoE"""
def upcycle(self, dense_model, n_experts=8): # 1. 复制 FFN 为多个专家 for layer in dense_model.layers: layer.ffn_experts = nn.ModuleList([ copy(layer.ffn) for _ in range(n_experts) ])
# 2. 添加路由器 for layer in dense_model.layers: layer.router = nn.Linear(dim, n_experts)
# 3. 继续训练 # MoE 通常比稠密模型训练效果更好12.4 退火训练 (Annealing)
class AnnealingPhase: """训练末期的退火阶段"""
def __init__(self, anneal_steps=2000, anneal_lr=1e-5): self.anneal_steps = anneal_steps self.anneal_lr = anneal_lr
def anneal(self, model, data_loader): # 1. 使用小学习率持续训练 # 2. 使用最高质量数据 # 3. 通常 0.1% - 1% 的总训练量
for step in range(self.anneal_steps): # 取最高质量的数据 batch = self.high_quality_loader.next()
# 低学习率 lr = self._anneal_lr(step)
# 训练一步 loss = model.step(batch, lr=lr)13. 持续预训练
13.1 什么时候需要继续预训练?
CONTINUE_TRAINING_REASONS = { 'new_domain': '需要专业领域知识(医学、法律)', 'new_language': '增加新语言支持', 'data_update': '知识需要更新到最新', 'capability_boost': '增强特定能力(代码、数学)', 'recovery': '修复某个能力退化',}13.2 继续预训练的实现
class ContinualPretraining: """继续预训练"""
def __init__(self, base_model, new_data): self.model = base_model self.new_data = new_data
def train(self): # 1. 加载原模型 # 2. 准备新数据(domain-specific) # 3. 使用较小学习率继续训练 # 4. 监控 catastrophic forgetting
# 学习率通常很小(1/10 of original) lr = 6e-5 # vs original 6e-4
# 混合新旧数据避免遗忘 old_data_ratio = 0.5 # 50% 原始数据13.3 Catastrophic Forgetting
class ForgettingPreventer: """防止灾难性遗忘"""
def mix_data(self, new_data, old_data, ratio=0.7): """混合新旧数据""" mixed = []
for i in range(self.num_batches): if random.random() < ratio: # 70% 旧数据(防遗忘) mixed.append(next(old_data)) else: # 30% 新数据(学新知识) mixed.append(next(new_data))
return mixed
def elastic_weight_consolidation(self): """EWC: 保护重要参数""" # 计算参数重要性 (Fisher 信息) # 限制重要参数的变化 ...14. 高效训练技术
14.1 Flash Attention
class FlashAttention: """Flash Attention - 内存高效注意力"""
def forward(self, Q, K, V): # 1. 将 Q, K, V 分块到 GPU SRAM # 2. 在 SRAM 中计算注意力 # 3. 不需要存储中间注意力矩阵 # 复杂度: O(N²) 计算, O(N) 内存
# 优势: # - 长序列支持 (32K, 128K) # - 计算更快 # - 内存更少 ...版本演进:
- v1: 基础实现
- v2: 进一步优化,并行性
- v3: 支持 FP16 低精度
14.2 Ring Attention
class RingAttention: """环形注意力 - 跨设备长序列"""
def forward(self, Q, K, V, devices): # Q 在 device 0, K,V 分片到所有设备 # 环形通信计算注意力 # 支持超长上下文(百万级 token) ...14.3 Sequence Parallelism
class SequenceParallel: """序列并行 - 长序列训练"""
def forward(self, sequence): # 沿序列维度分片到不同设备 # 每台设备处理序列的一部分 # 通过 all-gather 整合 ...14.4 Activation Recomputation
class ActivationRecomputation: """激活重计算"""
def forward(self, x): # 不保存中间激活值 # 反向时重新计算 # 节省内存,代价是 ~30% 计算量 with checkpoint(self.layer): return self.layer(x)14.5 8-bit 优化器
class BitsAndBytesOptimizer: """8-bit 优化器 (bitsandbytes)"""
def __init__(self): # AdamW 状态用 8-bit 存储 # 节省 75% 优化器内存 # 几乎不影响性能 pass14.6 CPU Offloading
class CPUOffloader: """CPU 卸载"""
def __init__(self): # 优化器状态卸载到 CPU # GPU 内存不够时使用 # 通信开销较大 pass15. 成本估算
15.1 训练成本
模型规模 训练 token GPU 数 训练时间 估算成本─────────────────────────────────────────────────────7B 1T 256 14 天 $0.5M13B 2T 512 20 天 $1.5M70B 10T 1024 60 天 $10M175B 3T 1024 30 天 $8M405B 15T 16000 30 天 $100M+15.2 训练 FLOPs 估算
def estimate_training_flops(n_params, n_tokens): """估算训练 FLOPs""" # 简化公式: 6 * N * D # 6 = 前向(2N) + 反向(4N) flops = 6 * n_params * n_tokens return flops
# 示例# Llama 3 405B, 15T tokensflops = 6 * 405e9 * 15e12# = 3.65e22 FLOPs# = 36.5 ZettaFLOPs15.3 成本优化策略
COST_STRATEGIES = """1. 模型架构优化 - 使用 GQA 减小 KV cache - 使用 SwiGLU 提升效率 - 使用 RMSNorm 减小计算
2. 训练技术优化 - Flash Attention 加速 - ZeRO-3 减少内存 - Gradient checkpointing
3. 硬件选择 - H100 比 A100 快 2-3 倍 - 但价格更贵 - 平衡性价比
4. 训练时长 - 不要训练过长时间 - 在 Chinchilla 最优点停止"""16. 最新进展与未来
16.1 创新点
LATEST_INNOVATIONS = """2023-2024:- Chinchilla Scaling Law 验证- Llama 2/3 开源- Flash Attention 2/3- 混合专家模型 (Mixtral)
2025+:- 多模态预训练 (文本 + 图像 + 音频)- 长上下文 (1M+ tokens)- 更高效架构 (Mamba, RWKV 等)- Agent 集成训练- 自我演化训练"""16.2 替代架构
ALTERNATIVE_ARCHITECTURES = """1. State Space Models - Mamba - RWKV - Jamba (Mamba + Transformer) 优势: O(n) 复杂度,长序列
2. Mixture of Experts - Mixtral 8x7B - DBRX 优势: 大参数,低计算
3. Hybrid Models - Striped Hyena - Jamba 优势: Transformer + SSM
4. Multi-modal Native - GPT-4V, Gemini - 文图一体训练"""16.3 持续训练趋势
FUTURE_DIRECTIONS = """1. Synthetic Data - 用更强模型生成训练数据 - Phi-3 / Llama 3 都采用
2. Test-Time Training - 推理时也学习
3. Self-Play - 模型自我对抗训练
4. Continual Learning - 不遗忘旧能力
5. RL 与预训练的融合 - 不只是 SFT + RLHF - 预训练阶段就加入 RL 信号"""17. 核心概念总结
17.1 预训练的关键决策
KEY_DECISIONS = """1. 训练目标 - 因果 LM (GPT 风格) ⭐ - 掩码 LM (BERT 风格) - 前缀 LM - 去噪自编码 (T5)
2. 模型架构 - Decoder-Only ⭐ (Llama, GPT) - 位置编码: RoPE, ALiBi - 注意力: MHA / GQA / MQA - 激活: SwiGLU - 归一化: RMSNorm
3. 数据工程 - 数据质量 > 数量 - 去重、清洗、配比 - 多领域、多语言
4. 训练策略 - Optimizer: AdamW - Schedule: Cosine with warmup - 混合精度: BF16 - 分布式: TP + PP + DP + ZeRO
5. 规模 - Chinchilla: 20 tokens/param - 实际略高(Llama 3: 38 tokens/param)"""17.2 数学表达
- 预训练损失:
- Chinchilla 标度律:
- MFU 公式:
- RoPE 旋转编码:
18. 实战建议
18.1 决策清单
DECISION_CHECKLIST = """□ 选择模型规模(7B, 13B, 70B...)□ 选择训练目标(CLM, MLM...)□ 准备 tokenizer□ 数据收集与清洗□ 数据配比□ 选择架构(Llama 风格 / 其他)□ 设计超参数□ 准备分布式训练脚本□ 准备监控和 checkpoint□ 评估策略□ Loss spike 应对方案"""18.2 常见问题
FAQ = """Q: 训练 Loss 不下降?A: 检查学习率、数据质量、初始化
Q: 训练 Loss spike?A: 降低 LR、回滚到 spike 前的 checkpoint
Q: GPU 内存不够?A: 使用 ZeRO-3 + activation checkpointing + CPU offload
Q: 训练很慢?A: 优化 MFU, 检查数据加载瓶颈
Q: 训练成本过高?A: 减小模型规模、使用高效训练技术、按 Chinchilla 停止
Q: 模型没有涌现能力?A: 规模可能不够;继续按 Scaling Law 扩展"""18.3 推荐资源
RESOURCES = """学习资源:📚 论文:- Language Models are Few-Shot Learners (GPT-3)- Training Compute-Optimal LLMs (Chinchilla)- Llama 2 / Llama 3- Scaling Laws for Neural LMs (Kaplan)
🛠️ 工具:- DeepSpeed (Microsoft)- Megatron-LM (NVIDIA)- FSDP (PyTorch)- Hugging Face Transformers
📖 框架:- PyTorch Lightning- Hugging Face Transformers- Megatron-DeepSpeed"""19. 总结与展望
19.1 预训练的核心思想
预训练是 LLM 的根基,决定了模型的能力上限:
- 自监督学习:利用海量无标注数据
- 规模法则:更多数据、更大模型 = 更好能力
- 通用能力:一次预训练,多种任务
- Transformer 主宰:当前主流架构
19.2 关键挑战
| 挑战 | 描述 | 方向 |
|---|---|---|
| 数据 | 高质量数据耗尽 | 合成数据、自我训练 |
| 算力 | 训练成本指数增长 | 高效架构、稀疏化 |
| 架构 | Transformer 局限 | SSM、混合架构 |
| 记忆 | 知识更新困难 | RAG、持续学习 |
| 安全 | 越狱、有害生成 | RLHF、对齐研究 |
19.3 未来方向
2025+ 的预训练趋势:┌──────────────────────────────────────────┐│ - 多模态原生预训练 ││ - Agent-原生预训练 (工具集成) ││ - O(n) 复杂度架构成熟 ││ - 合成数据规模化 ││ - 持续在线学习 ││ - 推理与训练深度融合 │└──────────────────────────────────────────┘19.4 结语
预训练是 AI 时代最激动人心的技术之一。
每一次规模扩展都带来惊喜:
- 10 亿参数能学语言
- 100 亿能推理
- 1000 亿能涌现
- 万亿能做什么?未来由你书写
“我们不在构建模型,我们是在扩展智能的边界。” — Sam Altman
从 GPT 到 Llama,从 Transformer 到 Mamba,预训练策略的演进见证了 AI 从专用模型到通用智能的飞跃。掌握预训练,就是掌握 LLM 时代的入场券。
参考资料
- Vaswani, A., et al. (2017). “Attention Is All You Need.” NeurIPS.
- Radford, A., et al. (2018). “Improving Language Understanding by Generative Pre-Training.” OpenAI.
- Radford, A., et al. (2019). “Language Models are Unsupervised Multitask Learners.” OpenAI.
- Brown, T., et al. (2020). “Language Models are Few-Shot Learners.” NeurIPS.
- Kaplan, J., et al. (2020). “Scaling Laws for Neural Language Models.” arXiv.
- Hoffmann, J., et al. (2022). “Training Compute-Optimal Large Language Models.” arXiv.
- Touvron, H., et al. (2023). “Llama 2: Open Foundation and Fine-Tuned Chat Models.” arXiv.
- Meta. (2024). “Llama 3: The Most Capable Open Large Language Model.” Meta AI.
- Fedus, W., et al. (2022). “Switch Transformer: Scaling to Trillion Parameter Models.” JMLR.
- Su, J., et al. (2021). “RoFormer: Enhanced Transformer with Rotary Position Embedding.” Neurocomputing.
- Chowdhery, A., et al. (2022). “PaLM: Scaling Language Modeling with Pathways.” arXiv.
- Dao, T., et al. (2022). “FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.” NeurIPS.
- Rajbhandari, S., et al. (2020). “ZeRO: Memory Optimizations Toward Training Trillion Parameter Models.” SC.
- DeepSpeed Team. (2024). “DeepSpeed: Extreme-scale model training.” Microsoft.
- Korthikanti, V., et al. (2023). “Reducing Activation Recomputation in Large Transformer Models.” arXiv.
- Longpre, S., et al. (2024). “A Review of Deep Learning Approaches for Pretraining.” ACL.
- Touvron, H., et al. (2023). “Llama: Open and Efficient Foundation Language Models.” arXiv.
- Biderman, S., et al. (2023). “Pythia: A Suite for Analyzing Large Language Models.” arXiv.
- Computer, T. (2023). “RedPajama: An Open Source Recipe to Reproduce LLaMA Training Dataset.”
- Penedo, G., et al. (2023). “The RefinedWeb Dataset for Falcon LLM.” arXiv.
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
预训练策略:从 GPT 到 Llama,大模型的"九年义务教育"
https://aiattnstudio.link/posts/pretraining-strategies/
