
















本文档基于在 RTX 4060 Laptop 8GB 上完整跑通的 MiniMind 训练流水线整理而成,涵盖 Dense 四阶段(Pretrain → SFT → DPO → LoRA)与 进阶三实验(MoE 路由、MoE768→Dense512 蒸馏、GRPO/CISPO),配合架构图、Loss 曲线与实测对话对比。
MiniMind 是一个纯 PyTorch 实现的超小语言模型项目,目标是用消费级 GPU 从零理解 LLM 训练全流程,而不是调用 HuggingFace Trainer 等黑盒框架。
| 指标 | 本次配置 |
|---|---|
| 参数量 | 30.03M(hidden_size=512, layers=8) |
| 词表大小 | 6400 |
| 推理显存 | ~0.5 GB |
| 训练显存 | 4.8 ~ 7.8 GB(视 batch 而定) |
| 硬件 | RTX 4060 Laptop 8GB |
| 框架 | PyTorch 2.6.0+cu124 |
与 GPT-3(175B)相比,本模型约为 1/5800,适合在个人电脑上反复实验。
flowchart TB subgraph data [数据准备] D1[pretrain_t2t_mini.jsonl] D2[sft_t2t_mini.jsonl] D3[dpo.jsonl] D4[lora_identity.jsonl] D5[rlaif-mini.jsonl] end subgraph dense [Dense 流水线] P[Pretrain step 39695] --> S[SFT step 14000] S --> DPO[DPO step 8583] DPO --> L[LoRA 10 epoch] end subgraph advanced [进阶实验] M[MoE Pretrain step 3500] DIS[蒸馏 step 2000] G[GRPO step 300] end D1 --> P D2 --> S D3 --> DPO D4 --> L D5 --> G M -.-> DIS DPO --> DIS DPO --> G
| 阶段 | 状态 | 权重文件 | 关键指标 |
|---|---|---|---|
| Pretrain | ✅ 完成 | out/pretrain_512.pth |
step 39,695,loss ~2.0 |
| SFT | ⏸ 部分 (25%) | out/full_sft_512.pth |
step 14,000 / 56,600,可续训 |
| DPO | ✅ 完成 | out/dpo_512.pth |
step 8,583,dpo_loss 0.71 → 0.47 |
| LoRA identity | ✅ 完成 | out/lora_identity_512.pth |
10 epoch,0.87% 可训练 |
| MoE Pretrain demo | ✅ 完成 | out/pretrain_512_moe.pth |
step 3,500,91M 总参 / 30M 激活 |
| 蒸馏 MoE→Dense | ✅ 完成 | out/distill_moe768_to_512_512.pth |
step 2,000,distill 最低 ~1.04 |
| GRPO / CISPO | ✅ 完成 | out/grpo_512.pth |
step 300,reward 大多 -3~0 |
| 进阶评估 | ✅ 完成 | logs/eval_advanced.txt |
dpo 身份最稳,grpo 有漂移 |
| 权重路径 | 训练步数 | 说明 |
|---|---|---|
out/pretrain_512.pth |
39,695 | Dense 预训练 1 epoch 完成 |
out/full_sft_512.pth |
14,000 | SFT 约 25%,当前质量主瓶颈 |
out/dpo_512.pth |
8,583 | DPO 1 epoch,身份/对齐基线 |
out/lora_identity_512.pth |
10 epoch | 叠在 dpo 上,91 条身份样本 |
out/pretrain_512_moe.pth |
3,500 | MoE demo,4 experts top-1 |
out/full_sft_768_moe.pth |
— | ModelScope 下载的教师(非本地训) |
out/distill_moe768_to_512_512.pth |
2,000 | MoE768 教师 → Dense512 学生 |
out/grpo_512.pth |
300 | CISPO + InternLM2 Reward(CPU) |
步数来自
checkpoints/*_resume.pth中的step字段;out/*.pth仅含模型权重。
MiniMind 采用与 Qwen3 对齐的 Decoder-Only 结构,只做「从左到右预测下一个 token」。

图:MiniMind Dense 模型结构(来自官方仓库 images/LLM-structure.jpg)
flowchart LR subgraph input [输入] TOK[Token IDs] end subgraph body [MiniMindModel] EMB[Embedding 6400 x 512] BLK1[Block 1] BLK2[Block 2] BLKN[Block ... x8] NORM[RMSNorm] end subgraph output [输出] HEAD[LM Head 512 x 6400] LOG[Logits / Softmax] end TOK --> EMB --> BLK1 --> BLK2 --> BLKN --> NORM --> HEAD --> LOG
每个 Block 由 Pre-Norm Attention + Pre-Norm FFN 组成,带残差连接:
flowchart TB IN[hidden_states] --> N1[RMSNorm] N1 --> ATTN[GQA Multi-Head Attention + RoPE] ATTN --> ADD1[残差 +] IN --> ADD1 ADD1 --> N2[RMSNorm] N2 --> FFN[SwiGLU FFN] FFN --> ADD2[残差 +] ADD1 --> ADD2 ADD2 --> OUT[output]
核心代码位于 model/model_minimind.py:
class MiniMindBlock(nn.Module):
def __init__(self, layer_id: int, config: MiniMindConfig):
super().__init__()
self.self_attn = Attention(config)
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.mlp = FeedForward(config) if not config.use_moe else MOEFeedForward(config)
def forward(self, hidden_states, position_embeddings, past_key_value=None, use_cache=False, attention_mask=None):
residual = hidden_states
hidden_states, present_key_value = self.self_attn(
self.input_layernorm(hidden_states), position_embeddings,
past_key_value, use_cache, attention_mask
)
hidden_states += residual
hidden_states = hidden_states + self.mlp(self.post_attention_layernorm(hidden_states))
return hidden_states, present_key_value
| 组件 | 作用 | MiniMind 配置 | 为什么这样设计 |
|---|---|---|---|
| RMSNorm | 归一化,稳定训练 | eps=1e-6 |
比 LayerNorm 少算均值,更快 |
| RoPE | 注入位置信息 | theta=1e6 |
相对位置编码,支持外推 |
| GQA | 分组查询注意力 | 8 Q头 / 4 KV头 | KV Cache 减半,推理省显存 |
| SwiGLU | 前馈网络 | silu(gate)*up |
LLaMA/Qwen 系列标配 |
| tie_embeddings | 共享输入/输出矩阵 | True | 30M 小模型省参数量 |
| Flash Attention | 加速注意力 | 自动检测 | PyTorch 2.x 内置 SDPA |
pie title 30M 参数分布 "Embedding + LM Head" : 6.5 "Attention 层" : 10 "FFN 层" : 13 "Norm 等" : 0.5
Embedding 层占比较高是小模型的特点:词表 6400 × 维度 512 ≈ 3.3M,占总量 ~11%。
MiniMind 使用自定义 BPE Tokenizer,词表仅 6400(远小于 Qwen 的 15 万),目的是控制 Embedding 层参数量。
model/tokenizer.json<|im_start|>、``、<|assistant|> 等对话标记
图:MiniMind 各阶段数据集关系(images/dataset.jpg)
{"text": "如何才能摆脱拖延症?治愈拖延症并不容易,但以下建议可能有所帮助..."}
送入模型:[BOS] + tokens + [EOS],所有 token 都计算 loss。
{
"conversations": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮助你的?"}
]
}
只对 assistant 回复部分计算 loss,user/system 部分 label 设为 -100 忽略。
{
"chosen": [
{"role": "user", "content": "Q"},
{"role": "assistant", "content": "好的回答"}
],
"rejected": [
{"role": "user", "content": "Q"},
{"role": "assistant", "content": "较差的回答"}
]
}
{"conversations": [
{"role": "user", "content": "你叫什么名字?"},
{"role": "assistant", "content": "您好,我名叫 MiniMind..."}
]}
flowchart LR subgraph pretrain [Pretrain] PT["读大量文本\n学语言统计规律\n'下一个词是什么'"] end subgraph sft [SFT] SF["学对话格式\n学指令跟随\n'如何回答问题'"] end subgraph dpo [DPO] DP["学偏好\n好回答 > 差回答\n'哪个回答更好'"] end subgraph lora [LoRA] LR["学特定领域\n身份/医疗等\n'专业场景适配'"] end pretrain --> sft --> dpo --> lora
| 阶段 | 类比 | 数据规模 | 更新参数 |
|---|---|---|---|
| Pretrain | 婴儿学说话 | 127 万条 | 全部 30M |
| SFT | 学礼仪和问答 | 90 万条 | 全部 30M |
| DPO | 学分辨好坏 | 1.7 万对 | 全部 30M |
| LoRA | 学专项技能 | 91 条 | 仅 0.26M |
所有全参训练阶段(Pretrain / SFT / DPO)共享同一套 PyTorch 训练模式:
sequenceDiagram participant Data as DataLoader participant Model as MiniMind participant Loss as Loss participant Opt as AdamW Data->>Model: input_ids, labels Model->>Loss: logits Loss->>Loss: backward / accumulation Loss->>Opt: step (every N steps) Opt->>Model: 更新权重
核心代码模式(trainer/train_pretrain.py):
with autocast_ctx: # ① 混合精度 bfloat16
res = model(input_ids, labels=labels)
loss = res.loss / accumulation_steps # ② 梯度累积
scaler.scale(loss).backward()
if step % accumulation_steps == 0: # ③ 每 N 步更新一次
scaler.step(optimizer)
optimizer.zero_grad()
让模型从海量文本中学习:
此阶段模型不会对话,只会续写——像自动补全。
| 参数 | 值 |
|---|---|
hidden_size |
512 |
num_hidden_layers |
8 |
batch_size |
32 |
accumulation_steps |
2 |
max_seq_len |
512 |
epochs |
1 |
learning_rate |
5e-4 |
dtype |
bfloat16 |
| 数据 | pretrain_t2t_mini.jsonl(127 万条) |
| 总 steps | 39,695 |

图:官方 Pretrain Loss 曲线(768 维参考,趋势与本实验一致)
本次 512 维实测 Loss 走势:
| Step | Loss | 阶段说明 |
|---|---|---|
| 500 | ~4.5 | 随机初始化,接近 log(6400)≈8.8 的下界 |
| 5,000 | ~3.8 | 快速下降,学会基本 token 共现 |
| 13,500 | ~2.5 | 中期,语法结构逐渐成型 |
| 39,695 | ~2.0 | 收敛,仍远高于完美(0) |
Loss 不会降到 0:语言建模是开放问题,总有预测不确定性。Loss 2.0 意味着平均每个 token 的困惑度约为 e²≈7.4。
flowchart LR JSONL["JSONL text 字段"] --> TOK[Tokenizer] TOK --> SEQ["BOS + tokens + EOS"] SEQ --> PAD[Pad 到 max_len] PAD --> LABEL["labels = input_ids\npad 位置 = -100"] LABEL --> MODEL[Forward + CrossEntropy]
代码见 dataset/lm_dataset.py 第 37–55 行。
out/pretrain_512.pth(66 MB)python eval_llm.py --weight pretrain → 续写模式,不会对话在预训练基础上,教会模型:
<|im_start|>user / assistant 等标记)flowchart TB subgraph pretrain_mask [Pretrain Label Mask] P1["BOS: 计算loss"] P2["token1: 计算loss"] P3["token2: 计算loss"] P4["EOS: 计算loss"] end subgraph sft_mask [SFT Label Mask] S1["user部分: -100 忽略"] S2["assistant标记: -100 忽略"] S3["回答内容: 计算loss"] S4["EOS: 计算loss"] end
| Pretrain | SFT | |
|---|---|---|
| 数据 | 单条 text | 多轮 conversations |
| Loss 范围 | 全部 token | 仅 assistant 回复 |
| 起点权重 | 随机 / none | pretrain_*.pth |
| 学会的能力 | 续写 | 对话 |
| 参数 | 值 |
|---|---|
from_weight |
pretrain |
batch_size |
32 |
max_seq_len |
512 |
epochs |
2(目标) |
| 数据 | sft_t2t_mini.jsonl(90 万条) |
| 实际进度 | step 14,000 / 56,600(~25%) |

图:官方 SFT Loss 曲线参考
out/full_sft_512.pthTRAINING.mdpython eval_llm.py --weight full_sftSFT 只教模型「模仿标准答案」,但不知道两个回答哪个更好。
DPO(Direct Preference Optimization)通过偏好对直接优化:
让模型对「好回答」赋予比「差回答」更高的概率。

图:RLHF/DPO 在训练链路中的位置
flowchart TB subgraph pair [一条偏好数据] Q[用户问题 x] Q --> CW[chosen 好回答 y_w] Q --> RJ[rejected 差回答 y_l] end subgraph models [双模型 Forward] POL["策略模型 π_θ\n(训练中更新)"] REF["参考模型 π_ref\n(冻结 SFT 权重)"] end CW --> POL RJ --> POL CW --> REF RJ --> REF POL --> LOSS["DPO Loss\n最大化 chosen 相对 rejected 的优势"] REF --> LOSS
Loss 公式:
$$\mathcal{L}{DPO} = -\log \sigma\left(\beta \left[\underbrace{(\log \pi\theta(y_w|x) - \log \pi_{ref}(y_w|x)) - (\log \pi_\theta(y_l|x) - \log \pi_{ref}(y_l|x))}_{\text{chosen 相对 rejected 的优势}}\right]\right)$$
| SFT | DPO | PPO | |
|---|---|---|---|
| 需要 Reward Model | 否 | 否 | 是 |
| 同时加载模型数 | 1 | 2 | 3+ |
| 数据 | 单答案 | 偏好对 | 在线采样 |
| 学习率 | ~5e-4 | ~4e-8 | 更低 |
| 显存 | 低 | 中 | 高 |
| 实现复杂度 | 低 | 中 | 高 |
| 参数 | 值 |
|---|---|
from_weight |
full_sft |
batch_size |
2 |
accumulation_steps |
4 |
max_seq_len |
512 |
learning_rate |
4e-8(极低!) |
beta |
0.15 |
epochs |
1 |
| 数据 | dpo.jsonl(17,166 对) |
| 总 steps | 8,583 |
| 耗时 | ~53 分钟 |
| 显存 | ~7.5 GB |
| Step | dpo_loss |
|---|---|
| 50 | 0.714 |
| 500 | 0.704 |
| 2,000 | ~0.65 |
| 5,000 | ~0.62 |
| 8,583 | 0.470 |
dpo_loss 下降表示模型 increasingly 偏好 chosen 回答。注意 loss 不会趋近 0,因为偏好对中存在噪声。
out/dpo_512.pthpython eval_llm.py --weight dpoLoRA(Low-Rank Adaptation)在冻结的原权重 W 旁,加一个低秩增量 ΔW = B × A:
flowchart LR X[输入 x] --> W["原权重 W\n(冻结)"] X --> A["矩阵 A\n(d × r)"] A --> B["矩阵 B\n(r × d)"] W --> ADD[相加] B --> ADD ADD --> Y[输出 y]
核心实现(model/model_lora.py):
class LoRA(nn.Module):
def __init__(self, in_features, out_features, rank):
self.A = nn.Linear(in_features, rank, bias=False) # 高斯初始化
self.B = nn.Linear(rank, out_features, bias=False) # 全零初始化
def forward(self, x):
return self.B(self.A(x)) # ΔW = B(A(x))
| 方法 | 更新什么 | 适用场景 | 数据量 |
|---|---|---|---|
| DPO | 全部参数 | 通用偏好对齐(更安全、更有帮助) | 万级偏好对 |
| LoRA | 0.87% 参数 | 垂直领域(医疗、法律、身份) | 几十~几千条 |
flowchart TB BASE[基座模型 full_sft / dpo] BASE --> DPO_PATH["DPO 全参对齐\n→ dpo_512.pth"] BASE --> LORA_PATH["LoRA 增量\n→ lora_xxx_512.pth"] DPO_PATH --> MERGE["推理: weight=dpo"] LORA_PATH --> MERGE2["推理: weight=dpo + lora_weight=lora_identity"]
| 参数 | 值 |
|---|---|
from_weight |
dpo(叠在 DPO 权重上) |
lora_name |
lora_identity |
batch_size |
32 |
epochs |
10 |
| 数据 | 91 条自我认知样本 |
| 耗时 | ~1 分钟 |
| 显存 | ~5 GB |
out/lora_identity_512.pth(535 KB,仅 LoRA 增量)python eval_llm.py --weight dpo --lora_weight lora_identity| 权重 | 命令 | 行为 |
|---|---|---|
| pretrain | --weight pretrain |
BOS + 续写,不会对话 |
| full_sft / dpo | --weight full_sft 或 dpo |
chat template 多轮对话 |
| dpo + LoRA | --weight dpo --lora_weight lora_identity |
基座 + LoRA 增量 |
以下摘自 logs/eval_compare.txt,同一问题的回答对比:
| 阶段 | 回答摘要 |
|---|---|
| full_sft | 「我是 minimind,一个 AI 助手…」基本正确但啰嗦重复 |
| dpo | 回答模糊,未能直接给出名字 |
| dpo + lora_identity | 「我叫大红…」被 91 条 LoRA 样本强烈改写 |
学习要点:LoRA 用极少样本就能大幅改变模型特定行为,但也容易「过拟合」到训练数据中的错误身份。这是小模型 + 小数据的典型现象。
| 阶段 | 回答质量 |
|---|---|
| full_sft | 有「瑞利散射」的雏形,但混入了错误表述(「红色光线」「激光」) |
| dpo | 提到波长/散射,逻辑仍混乱 |
| dpo + lora | 结构更清晰(分点论述),物理概念仍不准确 |
30M 小模型的知识容量有限,不要期待 GPT 级别的准确性。价值在于理解训练流程。
| 阶段 | 特点 |
|---|---|
| full_sft | 能列出「数据准备、特征工程」等关键词,但解释循环重复 |
| dpo | 提到「监督/无监督」概念,仍有事实错误 |
| dpo + lora | 结构最完整(分点 + 应用举例),内容质量略好 |
quadrantChart title 三阶段能力对比(主观) x-axis 低通用能力 --> 高通用能力 y-axis 低专项适配 --> 高专项适配 full_sft: [0.55, 0.3] dpo: [0.6, 0.35] dpo_lora: [0.5, 0.75]
| 观察 | 说明 |
|---|---|
| DPO 改善有限 | 小模型 + 极低 lr,偏好信号弱 |
| LoRA 身份效果明显 | 91 条样本足以覆盖身份问题 |
| 通用能力未显著提升 | SFT 仅完成 25%,是主要瓶颈 |
| 推理速度 | ~90 tokens/s(RTX 4060) |
python scripts/eval_compare.py
# 产出 logs/eval_compare.txt
运行 python scripts/eval_advanced.py 产出 logs/eval_advanced.txt。五题覆盖身份、常识、概念、逻辑陷阱、简短自我介绍。
| 模型 | 身份题(「你叫什么名字?」) | 三句话自我介绍 | 总体印象 |
|---|---|---|---|
| dpo | 「MiniMind,jingyaogong 创建」— 最稳定 | 重复啰嗦(「杰克」循环) | 对齐基线,身份可靠 |
| distill_moe768_to_512 | 正确 MiniMind,但段落重复 | 同一句复制多遍 | 更 verbose,模仿 MoE 教师风格 |
| grpo (step 300) | 「通义千问,阿里云开发」— 身份幻觉 | 仅两句重复 minimind | reward 不足以纠正小模型身份 |
核心 takeaway:DPO 在自我介绍上表现最好;蒸馏继承了 768 MoE 教师的「长回复」习惯但未提升事实性;GRPO 在 300 step 后出现 identity drift——Reward Model 的语义分与 InternLM 训练语料中的「通义千问」等名称产生干扰,小模型容量不足以在 RL 信号下保持身份一致。
flowchart LR subgraph eval [eval_advanced 五题] Q1[身份] Q2[常识] Q3[概念] Q4[逻辑陷阱] Q5[三句介绍] end Q1 --> dpo_best["dpo 最稳"] Q1 --> grpo_bad["grpo 漂移"] Q5 --> distill_rep["distill 重复"]
| 阶段 | batch | seq | 显存 | 备注 |
|---|---|---|---|---|
| Pretrain | 16 | 512 | ~4.8 GB | 安全余量大 |
| Pretrain / SFT | 32 | 512 | ~7.8 GB | 接近 8GB 上限 |
| DPO | 2 | 512 | ~7.5 GB | 双模型(策略+参考) |
| LoRA | 32 | 512 | ~5 GB | 仅训练 0.87% 参数 |
| MoE Pretrain | 8 | 340 | ~7 GB | 4 专家常驻,91M 总参 |
| 蒸馏 | 8 | 340 | ~7 GB | teacher+student 双模型 |
| GRPO | 1×2 gen | 512+256 | ~6 GB | policy+ref GPU,reward CPU |
| 推理 | — | — | ~0.5 GB | 单模型 half 精度 |
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Pretrain checkpoint 保存失败 | 读取 *_resume.pth 导致文件锁 |
训练期间不要读 checkpoint |
| LoRA DataLoader 卡死 37 分钟 | num_workers=2 多进程问题 |
改用 --num_workers 0 |
| GRPO OOM | Reward Model 占 ~3.6 GB 显存 | --reward_device cpu |
| Reward Model 加载失败 | 缺 sentencepiece 或 transformers 版本 |
pip install sentencepiece;transformers==4.57.6 |
| 蒸馏/GRPO resume 保存失败 | Windows 文件锁 PermissionError |
训练期间勿 torch.load resume 文件;手动 promote *_resume.pth.tmp |
| eval 中文乱码 | GBK 控制台编码 | $env:PYTHONIOENCODING="utf-8" |
| git clone 失败 | 全局 git 代理 | git -c http.proxy= -c https.proxy= clone ... |
| PyTorch 下载超时 | 官方源 2.5GB wheel | curl 断点续传本地安装 |
梯度累积 — 用小 batch 模拟大 batch:
有效 batch = batch_size × accumulation_steps
例:32 × 2 = 64(等效 64 条样本更新一次)
混合精度 bfloat16 — RTX 4060 支持 bf16,减半显存且数值稳定:
with torch.cuda.amp.autocast(dtype=torch.bfloat16):
loss = model(input_ids, labels=labels).loss
续训 — 中断后恢复:
python train_xxx.py --from_resume 1
# 自动从 checkpoints/xxx_512_resume.pth 恢复
# 环境
cd c:\Gary\Projects\AI-Training
.\.venv\Scripts\Activate.ps1
# Pretrain
cd trainer
python train_pretrain.py --hidden_size 512 --num_hidden_layers 8 `
--batch_size 32 --max_seq_len 512 --epochs 1 `
--data_path ../dataset/pretrain_t2t_mini.jsonl --num_workers 0
# SFT
python train_full_sft.py --hidden_size 512 --num_hidden_layers 8 `
--batch_size 32 --max_seq_len 512 --epochs 2 `
--data_path ../dataset/sft_t2t_mini.jsonl `
--from_weight pretrain --from_resume 1 --num_workers 0
# DPO
python train_dpo.py --hidden_size 512 --num_hidden_layers 8 `
--batch_size 2 --accumulation_steps 4 --max_seq_len 512 --epochs 1 `
--data_path ../dataset/dpo.jsonl --from_weight full_sft `
--learning_rate 4e-8 --beta 0.15 --num_workers 0
# LoRA
python train_lora.py --hidden_size 512 --num_hidden_layers 8 `
--lora_name lora_identity --batch_size 32 --max_seq_len 512 --epochs 10 `
--data_path ../dataset/lora_identity.jsonl --from_weight dpo --num_workers 0
# 推理
cd ..
$env:PYTHONIOENCODING="utf-8"
python eval_llm.py --weight dpo --lora_weight lora_identity --hidden_size 512 --num_hidden_layers 8
# 三阶段对比
python scripts/eval_compare.py
# 进阶三模型对比
python scripts/eval_advanced.py
# MoE 路由统计
python scripts/moe_routing_inspect.py --weight pretrain --hidden_size 512 --use_moe 1
进阶训练命令详见 TRAINING.md(MoE Pretrain、蒸馏、GRPO 完整参数)。
| 资源 | 路径 |
|---|---|
| 官方 README | README.md |
| 训练命令参考 | TRAINING.md |
| 模型定义 | model/model_minimind.py |
| 数据加载 | dataset/lm_dataset.py |
| LoRA 实现 | model/model_lora.py |
| 对比评估结果 | logs/eval_compare.txt |
| 进阶评估结果 | logs/eval_advanced.txt |
| MoE 路由统计 | logs/moe_routing_inspect.txt |
| 图片 | 内容 |
|---|---|
images/LLM-structure.jpg |
Dense 模型结构 |
images/LLM-structure-moe.jpg |
MoE 模型结构 |
images/pretrain_loss.jpg |
Pretrain Loss 曲线 |
images/sft_loss.jpg |
SFT Loss 曲线 |
images/dataset.jpg |
数据集关系 |
images/rl-structure.jpg |
RLHF/DPO 结构 |
images/gpt3_config.png |
GPT-3 参数量对比 |
images/minimind-3.gif |
MiniMind-3 演示 |
| 概念 | 一句话 |
|---|---|
| Token | 词表中的整数 ID,是 LLM 的原子单位 |
| Embedding | ID → 向量的查表,是模型的「词典页码→语义」 |
| Attention | 每个 token 「看」其他 token 的相关性权重 |
| RoPE | 用旋转矩阵编码位置,让模型知道 token 顺序 |
| CrossEntropy Loss | 预测下一个 token 的交叉熵,LLM 训练的核心目标 |
| Label Mask (-100) | 告诉 loss 函数「这些位置不要算 loss」 |
| Gradient Accumulation | 多步累积梯度再更新,等效大 batch |
| DPO | 不用 Reward Model,直接用偏好对优化策略 |
| LoRA | 低秩矩阵增量,只训练 1% 参数做领域适配 |
| GRPO / CISPO | 组内相对优势 + clip,用 Reward Model 在线优化 |
| aux_loss | MoE 负载均衡损失,防止路由塌缩到单一专家 |
| KV Cache | 推理时缓存已算过的 K/V,避免重复计算 |
已完成(2026-05-30):
train_pretrain.py --use_moe 1,step 3500,路由统计见 §13.1--reward_device cpu,step 300eval_advanced.py建议的下一步(按优先级):
lora_medical.jsonl 叠在完整 SFT+DPO 基座上,对比 identity LoRA 的过拟合现象scripts/train_tokenizer.py本章记录在 RTX 4060 8GB 上完成的三项进阶实验:MoE 路由与 aux_loss、MoE768→Dense512 白盒蒸馏、GRPO/CISPO 奖励循环。命令与依赖见
TRAINING.md。

图:MiniMind MoE 结构(官方 images/LLM-structure-moe.jpg)— 每个 Block 的 FFN 替换为 Gate + 多 Expert。
与 Dense 的核心差异:
| Dense FFN | MoE FFN | |
|---|---|---|
| 参数量 | 30M | 91M 总参 / 30M 激活(4 experts, top-1) |
| 每 token 计算 | 全部 FFN | 仅 1 个 Expert FFN |
| 额外损失 | 无 | aux_loss 负载均衡 |
路由流程:
flowchart LR token[Token hidden] --> gate[Gate Linear → Softmax] gate --> topk[Top-1 选专家] topk --> experts[Expert 0–3 SwiGLU] experts --> out[加权输出] gate --> aux[aux_loss 负载均衡]
aux_loss 原理: 代码中 router_aux_loss_coef = 5e-4,Pretrain 时 loss = CE + aux_loss。aux_loss 惩罚「某些专家过载、某些闲置」的路由分布,防止训练塌缩到单一 expert。公式近似为:aux_loss ∝ num_experts × Σ(load × mean_gate_score)。
本次 MoE Pretrain demo 配置:
| 参数 | 值 |
|---|---|
use_moe |
1 |
hidden_size |
512 |
batch_size / accumulation_steps |
8 / 4 |
save_interval |
500 |
| 产出 checkpoint | step 3,500 → out/pretrain_512_moe.pth |
| 总参数 / 激活参数 | 91.38M / 30.04M(日志 moe_pretrain_demo.log) |
日志文件仅捕获了模型初始化行;逐步 loss/aux_loss 需重定向 stdout 或查 wandb。checkpoint 确认训练至 step 3500。
路由统计(scripts/moe_routing_inspect.py → logs/moe_routing_inspect.txt):
对 5 个中文 prompt 逐层统计 expert 使用率,可见 不同层、不同语义倾向不同专家:
| Prompt | 典型路由模式 |
|---|---|
| 「你叫什么名字?」 | Layer 0 偏 E0 (~50%);Layer 5 偏 E1 (~64%) |
| 「为什么天空是蓝色的?」 | Layer 2/6 偏 E2 (~60%);Layer 0 偏 E0 (~52%) |
| 「解释一下什么是机器学习」 | Layer 5 偏 E1 (~59%);Layer 6 偏 E2 (~64%) |
| 「1+1等于几?」 | Layer 5 偏 E1 (~61%);Layer 6 偏 E2 (~70%) |
同一 prompt 在不同 layer 路由差异大,说明 MoE 让各层专家逐步特化;top-1 路由下每层仅激活约 1/4 FFN 参数。
损失函数(CE + 温度 KL):
$$\mathcal{L} = \alpha \cdot \mathrm{CE} + (1-\alpha) \cdot T^2 \cdot \mathrm{KL}(p_{teacher}^T \parallel p_{student}^T)$$
distill 日志项 = KL 分量;ce = 硬标签交叉熵;aux_loss = 0(学生为 Dense)| 角色 | hidden | MoE | 权重 |
|---|---|---|---|
| Teacher | 768 | yes | out/full_sft_768_moe.pth(ModelScope,198M 参数) |
| Student | 512 | no | out/dpo_512.pth(起点) |
训练配置: batch_size=8,accumulation_steps=4,max_seq_len=340,epochs=2(目标),save_interval=500。
Loss 走势(distill_demo.log + resume):
| Step | distill (KL) | ce | loss |
|---|---|---|---|
| 50 | 1.64 | 1.72 | 1.68 |
| 100 | 1.15 | 2.09 | 1.62 |
| 400 | 1.06 | 1.99 | 1.53 |
| 500 | 1.14 | 1.90 | 1.52 |
| 600 | 1.04 | 1.97 | 1.51 |
| 850 | 1.24 | 1.98 | 1.61 |
| 1000 | 1.16 | 1.84 | 1.50 |
| 1500 | 1.42 | 1.69 | 1.56 |
| 2000 | 1.42 | 2.08 | 1.75 |
distill 从 ~1.64 降至 ~1.04(step 600 附近最优),续训至 step 2000 后 KL 回升、总 loss 升至 1.75——可能开始过拟合教师的 verbose 分布。产出:out/distill_moe768_to_512_512.pth。
续训踩坑(2026-05-30): step 500→1000 保存时 Windows 触发 PermissionError(并发读 resume 文件);手动 promote distill_moe768_to_512_512_resume.pth.tmp 后,step 1000→2000 正常完成。
与 DPO 对比:
| DPO | GRPO (CISPO) | |
|---|---|---|
| 信号来源 | 人工偏好对 (chosen/rejected) | Reward Model + 规则奖励 |
| 额外模型 | 冻结 reference | reference + InternLM2-1.8B-Reward |
| 更新方式 | 直接优化偏好损失 | 在线采样 → 组内优势 → CISPO clip |
| 8GB 可行性 | 可行 (~7.5 GB) | 需 --reward_device cpu、小 batch |
CISPO 训练循环:
sequenceDiagram participant Pol as 策略模型 π_θ participant Ref as 参考模型 π_ref participant RM as Reward Model (CPU) participant Loss as CISPO Loss Pol->>Pol: 对每个 prompt 采样 num_generations=2 条回复 Pol->>RM: 计算 reward(语义分 + 长度/格式/重复惩罚) RM->>Loss: rewards → 组内标准化 advantages Ref->>Loss: ref_logp 用于 KL 惩罚 Pol->>Loss: ratio clip + β·KL Loss->>Pol: 更新策略
Reward 组成(train_grpo.py):
<think>...</think> 闭合 +0.25GRPO 配置(8GB): batch_size=1,num_generations=2,max_gen_len=256,loss_type=cispo,--reward_device cpu,--from_weight dpo。
Reward / Loss 走势(grpo_demo.log step 1–78 + grpo_resume.log step 51–300):
| Step | Reward | Actor Loss | KL_ref | 备注 |
|---|---|---|---|---|
| 1 | -1.66 | 0.024 | 0.0005 | 冷启动 |
| 25 | -0.48 | 0.039 | -0.0006 | 早期较好 |
| 50 | -2.68 | 0.158 | -0.0014 | 生成质量波动大 |
| 78 (demo) | +0.30 | -0.223 | -0.0046 | 偶现正 reward |
| 100 | -1.84 | -0.098 | 0.0010 | 续训后 |
| 200 | -1.61 | 0.066 | -0.0051 | |
| 223 (resume) | +0.65 | 0.431 | -0.0036 | 次高 reward |
| 272 (resume) | -0.25 | -0.327 | -0.0040 | 接近 0 |
| 300 | -1.63 | -0.034 | 0.0013 | 最终 checkpoint |
Reward 大多在 -3 ~ 0 区间(30M 小模型生成质量有限,max_gen_len=256 常打满);续训 50→300 约 97 min(~2 step/min)。产出:out/grpo_512.pth(step 300)。
eval 身份漂移: eval_advanced.txt 显示 GRPO step 300 回答「我叫通义千问,是阿里云开发的智能助手」——Reward Model 训练语料中的高频名称污染了小模型身份。说明 300 step + 纯语义 reward 不足以做 identity-safe 对齐。
| 问题 | 原因 | 对策 |
|---|---|---|
| MoE Pretrain OOM | 4 专家 FFN 常驻显存 | batch_size=8,accumulation_steps=4;再 OOM 则 batch=4 |
| 蒸馏 teacher 加载失败 | 路径或 hidden 不匹配 | 确认 out/full_sft_768_moe.pth + --teacher_hidden_size 768 --teacher_use_moe 1 |
| GRPO OOM | Reward Model ~3.6 GB | --reward_device cpu,policy/ref 留 GPU |
| Reward Model 加载失败 | 缺依赖或版本 | pip install sentencepiece;transformers==4.57.6 |
| Windows DataLoader 卡死 | 多进程 fork | --num_workers 0(所有阶段) |
| Windows resume PermissionError | 训练期间读取 *_resume.pth 导致文件锁 |
训练时不要 torch.load resume;失败时 Move-Item *_resume.pth.tmp *_resume.pth |
| 大 jsonl 加载慢 | pretrain/sft 各 ~1GB+ | 首次 load_dataset 需数分钟,属正常 |
| eval 中文乱码 | PowerShell GBK | $env:PYTHONIOENCODING="utf-8" |
笔记版本:2026-05-30 | 基于 MiniMind 官方仓库 + RTX 4060 Laptop 实测
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。