如果你对“训练大语言模型”的印象还停留在“千卡集群、数亿预算”的新闻通稿阶段,那 MiniMind 这个开源项目会直接把这层滤镜摘掉。64M 参数、一份 2 小时左右的训练计划、折合人民币 3 块钱左右的成本——注意,它做的不是用现成底座做微调,而是从随机权重开始,完完整整地预训练一个小型大语言模型。
我第一次看到这个标题的时候,心里第一反应是“参数这么少也叫大语言模型?”。但等我顺着项目的思路把训练链路跑通一遍之后,反而理解了这种“小”的价值:它把动辄以“亿”为单位计算的训练工程,压缩成了一个人在家里、用一张消费级显卡、在一个周末下午就能亲手做完的实验。这篇东西不是替官方做宣传,而是想把我复现过程中算的那笔账、踩过的坑、以及调参的心得分享出来,给那些一直想“自己从头训一个 LLM”但觉得门槛太高的朋友做个参考。
1. MiniMind 给我的第一印象:64M 参数到底凭什么算“大语言模型”
1.1 从零训练不是微调,这才是项目最值钱的地方
很多人第一次接触大模型是从微调开始的:加载一个别人训练好的底座,用自家业务数据把参数“拨一拨”,模型就能学会新任务。这套流程确实方便,但有个天然盲区——你并不清楚底座里那些参数是怎么来的,也理解不了数据配比、学习率、词表选择这些因素对最终效果的影响到底有多大。
MiniMind 做的事情是“从零训练”。这四个字意味着:
- 模型权重初始化为随机值,没有任何“家底”可继承;
- 需要自己准备原始语料,自己设计清洗流程;
- 要亲手搭建网络结构,决定层数、隐层维度、注意力头数;
- 盯着 loss 曲线从几跳到平滑收敛,全程自己判断该停在哪。
换句话说,它把大模型公司里那条完整的预训练流水线,原封不动地搬到了个人电脑上。规模缩小了,但该有的环节一个不少。
1.2 64M 不是“弱”,而是恰到好处的实验样本
现在主流大模型动辄几十 B 甚至上百 B 参数,64M 连零头都不到。但正因为小,它才有不可替代的调试价值。我在实践中体会到,64M 这个量级对个人开发者来说几乎是最优的“手感训练”区间:
- 显存友好:8GB 显存的显卡就能跑,不用考虑多卡并行、张量并行这些复杂话题;
- 迭代极快:改一个参数重新训练,半小时到两小时就能看到结果,适合反复试错;
- 问题可复现:模型小,出问题时更容易定位是数据问题、结构问题还是学习率问题,不会被复杂的工程环境干扰判断。
如果你是想入行大模型领域的初学者,或者想低成本验证某个架构想法(比如换个位置编码、换个词表策略),又或者需要在团队里做一个“大模型原理演示 Demo”,MiniMind 都是一个很合适的起点。
2. 花钱花时间的第一步:3 块钱、2 小时这笔账是怎么算出来的
2.1 先搞明白训练一个 64M 模型需要多少算力
所有关于成本和时间的话题,本质上都要回归到一个公式。业界常用的大语言模型训练计算量估算方法是:
FLOPs ≈ 6 × N × D
其中 N 是模型参数量,D 是训练语料的 token 总数。这个 6 倍系数来自前向传播和反向传播的加总,实际操作中加上中间激活和损失计算,会在这个基础上再上浮一点,但用来估算量级足够。
拿 MiniMind 的设定来算:N = 64 × 10⁶,假设语料 D = 0.5 × 10⁹ tokens(5 亿 token),那么理论计算量就是:
6 × 64 × 10⁶ × 0.5 × 10⁹ ≈ 1.92 × 10¹⁷ FLOPs
一张常见的消费级显卡(比如 RTX 4090),FP16 峰值算力大概在 80+ TFLOPS,也就是每秒 8 × 10¹³ 次浮点运算的量级。但实际训练中因为显存带宽、算子调度、同步开销等原因,算力利用率很难跑到 100%,35% 到 50% 是比较现实的区间。
我按 35% 利用率估算的话,有效算力大约是:
8 × 10¹³ × 0.35 = 2.8 × 10¹³ FLOP/s
于是理论训练时间是:
1.92 × 10¹⁷ ÷ 2.8 × 10¹³ ≈ 6857 秒 ≈ 1.9 小时
看到没有,约 2 小时这个数字就是这么来的。它不是拍脑袋,而是在“64M 参数 + 5 亿 token 语料 + 一张消费级显卡”这个组合下自然收敛到的结果。
2.2 把不同语料规模的账都算一遍
为了让你心里更有数,我整理了一张不同语料规模下的训练时间估算表,依然按单张 RTX 4090、35% 算力利用率来算:
| 语料规模 | 理论计算量 | 估算训练时间 | 电费口径成本 |
|---|---|---|---|
| 0.3B tokens | 约 1.15 × 10¹⁷ FLOPs | 约 1.1 小时 | 约 2 元 |
| 0.5B tokens | 约 1.92 × 10¹⁷ FLOPs | 约 1.9 小时 | 约 3 元 |
| 1B tokens | 约 3.84 × 10¹⁷ FLOPs | 约 3.8 小时 | 约 5 元 |
注意,后面的“电费口径成本”不会跑偏成云厂商账单。一张显卡满载功耗在 300W 左右,整机加上散热、硬盘等,按 500W 算,2 小时约消耗 1 度电,居民电费按 0.6 元/度算,不到 1 块钱。但考虑到显卡长期高负载下的折旧、数据下载传输的耗时耗电,标 3 块钱其实是个比较宽松的上限。
2.3 必须说清楚的口径:3 块钱怎么理解
这里有句实话要讲在前面:标题里的“3 块钱”通常是自有机器的电费口径,不是租云 GPU 的账单。如果你用按量计费的云主机,2 小时的费用大概率远不止 3 块钱,具体看行情和实例规格。
但我觉得这并不影响这个项目的价值。真正贵的从来不是电费,而是你愿意花多少时间去理解一整套训练流程。MiniMind 把价格压到近乎免费,让你可以把注意力全部放在“我怎么把模型训得更好”这件事上。从这个角度讲,它的门槛已经不是钱,而是你是否愿意打开终端、动起手来。
3. 实操复现:从语料清洗到跑出第一行 Loss
3.1 环境与依赖准备
先说硬件底线。64M 模型占用显存很小,理论上 6GB 显存都能跑,但我建议至少准备一张 8GB 显存的显卡,比如 RTX 3060 或更高型号。显存更大当然更好,因为可以开更大的 batch,训练速度更快。
软件环境方面,我的推荐组合是:
- Python 3.10 以上;
- PyTorch 2.x,支持 bf16 混合精度;
- transformers、tokenizers、datasets 这几个生态库;
- 一个趁手的终端工具,用来盯训练日志。
如果你用的显卡支持 bf16,强烈建议开混合精度。64M 模型虽然小,但混合精度能让训练速度提升不少,而且显存占用更少,属于“免费的午餐”。
3.2 语料准备:决定模型下限的环节
很多人在这一步容易敷衍,但实际上语料质量对最终模型效果的影响,比模型结构更大。MiniMind 这类小模型对语料的要求是“干净、多样、有一定数量级”。
我的处理思路大致是这样:
# 清洗语料的几个关键步骤(示例) import re def clean_text(text: str) -> str: # 去掉 HTML 标签 text = re.sub(r"<[^>]+>", "", text) # 去掉多余空白和换行,压缩成一个空格 text = re.sub(r"\s+", " ", text) # 过滤过短的碎片 if len(text) < 20: return "" return text.strip()清洗之后还需要做几件事:
- 去重:按文本内容做哈希,重复样本直接丢弃。小模型见过的样本本来就少,重复语料会让它把训练集“背下来”,而不是学到通用的语言规律;
- 过滤噪声:去掉纯标点行、乱码、无意义字符;
- 切分与拼接:固定序列长度 512 token,长文本按窗口切,短文本拼接在一起,避免大量 padding 浪费算力。
语料来源方面,我用了开源中文语料的一个子集,混入了少量英文和技术文档数据。总规模控制在 5 亿 token 左右,这样训练时间正好落在 2 小时这个目标区间。
3.3 模型结构设计与参数量手算
MiniMind 这类项目一般不搞花哨结构,就是一个标准 decoder-only Transformer。我复现时用的是一组很朴素的配置:
| 配置项 | 取值 |
|---|---|
| vocab_size | 32000 |
| hidden_size | 512 |
| num_hidden_layers | 12 |
| num_attention_heads | 8 |
| intermediate_size | 2048 |
| seq_length | 512 |
这里有个值得聊的细节:64M 参数到底是怎么凑出来的?你可以自己手算一遍,心里会非常踏实。大语言模型的参数主要由两部分构成。
第一部分是 token embedding 和输出层。输入 embedding 的参数量是 vocab_size × hidden_size,即:
32000 × 512 ≈ 16.4M
如果按常见的权重共享做法,输出层直接复用输入 embedding,那这部分就只算一份;如果不共享,就要再加一份 16.4M。
第二部分是每一层 Transformer 的参数。注意力部分包括 QKV 三个矩阵和输出投影矩阵,约 4 × hidden_size²,也就是:
4 × 512² ≈ 1.05M
前馈网络 FFN 部分,按“升维再降维”的结构,参数量约 2 × hidden_size × intermediate_size:
2 × 512 × 2048 ≈ 2.10M
加上 LayerNorm 等零头,每一层大约 3.2M。12 层加起来就是约 38.4M。再加上位置编码 512 × 512 ≈ 0.26M。
汇总一下:
16.4M(共享 embedding)+ 38.4M(12 层)+ 0.26M(位置编码)≈ 55M
如果把层数加到 13 到 14 层,或者把词表稍微扩大一点,就非常接近 64M 了。这个手算过程在项目里其实很值得做一遍,它能帮你建立“模型大小由什么决定”的直觉。
3.4 训练脚本的核心配置
训练脚本不复杂,关键是学习率和步数这两个超参数别乱来。下面是我跑通的一份参考配置:
python train.py \ --model_name minmind-64m \ --vocab_size 32000 \ --hidden_size 512 \ --num_hidden_layers 13 \ --num_attention_heads 8 \ --intermediate_size 2048 \ --seq_length 512 \ --batch_size 32 \ --gradient_accumulation_steps 4 \ --learning_rate 4e-4 \ --warmup_steps 500 \ --max_steps 30000 \ --weight_decay 0.01 \ --grad_clip 1.0 \ --bf16几个参数我想稍微解释一下:
- batch_size × 梯度累积步数 × 序列长度,就是单步实际看到的 token 数。这里单步约 32 × 4 × 512 ≈ 65K tokens,比较符合小模型训练的常规尺度;
- 学习率 4e-4是 64M 模型比较舒服的起点。小于 2e-4 会学得很慢,大于 8e-4 容易出现 loss 飙升甚至梯度爆炸;
- warmup 500 步,让优化器在初期别太激进,这是训练稳定的关键;
- grad_clip 1.0,梯度裁剪。小模型参数少,偶尔出现极端梯度很正常,裁剪一下能避免一次大步导致 loss 冲到 NaN。
训练过程中,最需要盯住的就是 loss 曲线。正常情况是前几百步快速下降,然后进入平稳下降期,到后半段下降越来越慢。如果 loss 突然变成 NaN,或者在某一步暴涨又回落,说明学习率太高或数据里混入了异常样本,需要及时停下来检查。
3.5 怎么判断训练是否健康
我自己的判断标准有两层:
- 定量看 loss 和 PPL:64M 模型在 0.5B token 语料上训练完,验证集 perplexity 大致落在 50 到 120 这个区间都算正常。如果明显高于这个数,优先怀疑数据问题,而不是模型结构;
- 定性看输出:训练结束后,直接输入一个中文句子让模型续写,看看语句是否通顺、是否出现大面积复读。这一步比任何指标都直观。
补充一点:训练时最好预留一个验证集,每 1000 步跑一次,记录验证 loss。这一步很多人偷懒跳过,但后续判断“是不是过拟合”的时候,验证 loss 几乎是唯一依据。
4. 训练完成之后:这样验收一个 64M 模型
4.1 用 PPL 和生成样例给模型“体检”
训练结束后先别急着部署,按下面的清单过一遍:
- 验证集 PPL:如果验证集 loss 还在稳步下降,说明模型还没吃饱,可以加大语料或步数继续训;
- 自由续写:给模型一个开放式的开头,比如“今天天气很好”,观察它能不能自然接下去;
- 简单问答:输入“什么是机器学习”,看看模型是给出相关字眼,还是开始胡言乱语。
我在复现时看到的一个比较有代表性的输出风格是这样的:
输入:写一句关于秋天的短句
输出:秋天的风轻轻吹过,树叶落下来,好像在说,这个季节……
说实话,它离“可用”还有距离,偶尔会有逻辑断裂或重复用词的毛病。但考虑到这是一个从零训练、成本极低、只有几亿 token 语料的小模型,这个表现已经证明训练链路是完全走通的。
4.2 小模型的能力边界在哪里
这里想说句实话:64M 模型不可能像百亿参数大模型那样对答如流。它的知识覆盖面窄,推理能力基本为零,复杂任务完全做不了。但它有一个大模型给不了的优点——你能完整看到每一个能力缺陷背后的原因。
比如说,模型经常答非所问,你去看训练语料,会发现原始数据里本身就没什么“一问一答”的格式,模型自然没学过这种交互模式。这种“现象对应原因”的推理过程,是大模型黑盒里完全体会不到的。从这个角度讲,MiniMind 的教育意义远大于实用意义。
4.3 让模型“说人话”:做一点指令微调
如果你觉得生成结果太“野”,还有一个标准解法:拿几千条“指令-回答”风格的数据,在预训练模型基础上做一小段全量参数微调。
我试过用大约 1 万条干净的中文问答对,训上两三个小时,模型在“收到指令 → 给出回应”这件事上会有肉眼可见的改善。微调的学习率要显著降低,一般在 1e-5 到 5e-5 之间,训练步数也不用太多,两三千步就足够。
这一步做完之后,模型虽然还是经常说错话,但至少形式上会“像模像样”了。如果你打算拿 MiniMind 做课程作业、内部演示,或者作为后续继续训练的基础,微调这一步值得做。
5. 复现避坑记录与调参笔记
5.1 最常见的两个崩溃现场
我在跑这类小模型训炼时,遇到最多的两个问题都与超参数有关。
第一个,loss 变成 NaN。几乎都是在学习率设置过高,或者数据里混入了超长无意义字符时出现。解决办法也很朴素:把学习率降到 2e-4,加上梯度裁剪,同时检查语料里有没有“ ”这类标签字符串没被清干净。
第二个,loss 下降很快但生成效果极差。这种情况通常是训练语料里重复片段太多,模型把训练集“背”下来了。处理方式是在清洗阶段强制按内容哈希去重,宁可少一点数据,也不要一堆重复样本。
5.2 语料少的时候怎么防止过拟合
5 亿 token 听起来不少,但对于从零训练一个语言模型,其实只是小样本量。这时候特别容易出现过拟合:训练 loss 一路走低,验证 loss 反而慢慢回升。
我的操作表是这样的:
- 训练过程中每 1000 步记录验证 loss,发现验证 loss 连续多步不降反升,就提前停止;
- 语料里保留一部分代码、数学、英文内容,让数据分布更分散,模型不容易“背题”;
- epoch 数控制在 1 到 2 个。小模型不要反复在同一个语料上转圈,还不如把每次遍历的数据量加大。
5.3 显存和速度的取舍
64M 模型虽然小,但如果你把序列长度拉到 1024 甚至 2048,显存占用照样会涨得很快。我的建议是保持序列长度 512,靠加大 batch 来提高单步吞吐量。序列长度带来的长距离依赖收益,在 64M 这个参数规模上本来就体现不出来。
另外一个容易被忽略的点是 padding。把一批数据里的文本全 pad 到 512 token 长度会浪费大量算力。更聪明的做法是按长度分桶,让同一批内的样本长度差异尽量小,短的短的在一组,长的长的一组,这样显存和算力利用率都能提升。
5.4 我的最终参数推荐
把前面所有经验汇总成一张可以直接照抄的参数表:
| 配置项 | 推荐值 | 备注 |
|---|---|---|
| 语料规模 | 0.3B - 1B tokens | 优先保证干净,再追求数量 |
| 序列长度 | 512 | 小模型不需要太长上下文 |
| 单步 token 数 | 32K - 64K | 用 batch × 梯度累积凑出来 |
| 学习率 | 2e-4 - 5e-4 | 65M 模型推荐 4e-4 起步 |
| warmup 步数 | 500 - 1000 | 占总量 2% 左右 |
| 学习率调度 | 余弦衰减到 5% | 比固定学习率稳定得多 |
| 混合精度 | bf16 | 老显卡注意先确认支持 |
| 梯度裁剪 | 1.0 | 防止极端梯度导致 NaN |
| 验证频率 | 每 1000 步 | 必须留验证集,别偷懒 |
这套参数是我反复试过之后性价比最高的组合。如果你按这套配置跑完,2 小时左右看到模型开始输出相对流畅的中文,那就说明整条链路没问题。之后再想调整,也完全可以在这个基准上一步步来。
最后再分享一点个人体会:跑 MiniMind 这类小项目,最宝贵的收获不是“我训练了一个模型”这个结果,而是你能亲手把大模型训练里的每一个变量都摸摸清楚。词表怎么选、语料怎么洗、学习率怎么调、loss 波动代表什么,这些在几百亿参数的大模型项目里很难有条件反复试错的问题,在 64M 的尺度上都能以极低的成本做实验。我花了一个下午把它完整跑通之后,对那些“训练大模型需要什么”的抽象概念,终于有了实打实的体感。如果你也想建立这种体感,从这样一个小参数、少花钱的项目开始,是很值得的一步。