很多人想学大模型的训练,但一查教程就被各种“千卡集群”“万卡并行”“多机多卡”劝退,觉得没有几百万预算根本碰不了。我最早也是这个想法,直到有一天看到这个训练 64M 参数大语言模型的开源项目 MiniMind,标题直接写着“花 3 块钱、2 小时,从零训练”。说实话,我当时第一反应是不信,以为又是标题党。但仔细读完用法介绍,发现它真的把门槛压到了个人开发者能承受的范围内——不需要高端显卡,不需要海量数据,一台云服务器就能跑通完整流程。
这篇文章就是想把 MiniMind 这条路完整拆给你看:3 块钱的账单是怎么算出来的,训练一个 64M 参数的模型到底要准备什么,跑通整个流程需要多久,以及我在实操过程中踩到的坑和总结出来的经验。适合想入门大模型训练、又暂时买不起高端显卡的朋友,也适合准备用小型模型做一些垂直场景 demo 的人。
1. 为什么说 3 块钱能训一个模型:MiniMind 的定位与成本账
1.1 64M 参数到底是什么级别
先把这个参数规模说清楚。64M 参数等于 6400 万参数,放在如今动不动几十亿、上百亿参数的 AI 大模型里,是个十足的“小个子”。但小有小的好处——它正好卡在普通消费级硬件能跑的区间,这也是 MiniMind 能把成本压到极低的核心原因。
作为对比,常见的小型开源模型一般在 1B 到 7B 之间,也就是说至少有 10 亿参数往上。而 64M 比它们再小两个数量级,训练所需的显存、算力和数据量都会低很多。有人可能会问,参数这么少,模型会不会很笨?确实,它学不到太复杂的知识,做不了长文本的复杂推理,但用来演示“从零开始训练一个能说话的大语言模型”这个流程,已经完全够用了。
我个人的看法是,MiniMind 的价值不在于训练出一个“生产力级”的模型,而在于把训练过程透明化。它让你在短短两小时内完整经历数据准备、分词、预训练、损失下降、对话生成这一整条链路。跑完一次之后,你对大模型“怎么学会说话”这件事的理解,会比看一百篇原理文章都深刻。
1.2 硬件账单:显存怎么算出来的
训练显存的占用主要由四部分构成:模型参数、梯度、优化器状态、中间激活值。
先算模型参数。64M 参数如果是用半精度(fp16)训练,每个参数占用 2 字节,换算下来就是 64M × 2 = 128MB。梯度同样是半精度,再占 128MB。优化器用 AdamW,需要保存一阶动量和二阶动量两个状态,通常也是半精度,这两块加起来又是 256MB。也就是说,单单是参数、梯度和优化器状态,就已经到了 512MB。
再加上训练过程中的中间激活值,取决于序列长度和批次大小。假设序列长度取 256,批次大小取 8,激活值大概会额外占用几百 MB 到 1GB。整体算下来,峰值显存需求大约在 1.5GB 左右。这是个什么概念呢?现在一些主流显卡的显存至少 6GB 起步,部分老显卡也有 4GB,都已经超出了训练 MiniMind 的最低硬件要求。
所以,即便没有高端 GPU,只要有一张显存 4GB 以上的普通显卡就能本地跑起来。连显卡都没有的话,租一台按小时计费的云 GPU 实例也完全可行。按目前市场上比较常见的按量计费价格,约 1 到 2 元每小时,跑完整个训练大概需要 2 小时,总花费确实就在 3 块钱上下。
1.3 两小时能完成什么
这里需要打破一个预期误区:2 小时不是让你训练出一个能写诗的模型,而是训练出一个“语言结构”初步形成的模型。
预训练的本质,是用大量文本让模型去预测下一个词。64M 参数模型,以中等大小的公开中文语料为例,大约几万条文本,每条文本长度设定为 256 个 token,那么总 token 数大概在一两千万的量级。这个数据规模配合小批量训练,在低配单卡上跑两个 epoch 左右,基本能在 2 小时内完成。
训练结束后,模型能表现出最基础的“语感”:知道标点符号通常出现在哪里,知道“因为”后面常接“所以”,能生成语法基本通顺、但内容空洞的句子。这就已经证明了整套训练链路是通的。如果你想让模型真正具备对话能力、回答指令,那还需要做后面的 SFT(监督微调),这部分同样可以在这套框架里继续做。
2. 动工之前,先搞懂这三件事:数据、分词器、训练策略
我不是那种“上来就跑代码”的风格。在把命令粘贴到终端之前,我认为有必要花点时间把训练流程背后的三个核心组件弄清楚,否则遇到问题你会完全不知道该从哪排查。
2.1 数据从哪里来、要处理成什么格式
预训练阶段的数据不需要人工标注,只需要纯文本,越“像人话”越好。MiniMind 的设计里,它会从公开的文本语料中读取原始文章、百科条目、日常对话等内容,然后按固定格式组合起来喂给模型。
我用的是一份公开的中文文本语料,大约 5 万条。在写入训练脚本之前,需要人工做几步简单清洗:去除重复过高的行、删除乱码、过滤掉明显是 HTML 标签的残留内容。清洗这一步别省,脏数据对模型的影响很大,后面我会在踩坑部分专门说。
清洗完的文本会被切分成一条条的样本,每条样本在送入模型之前会被 tokenizer 编码成数字序列,这就是模型真正看到的“文字”。整个过程可以用一个普通 Python 脚本来完成,耗时几分钟。
2.2 分词器:模型认字不认词
大模型不认识英文单词,也不认识中文字符,它只认识数字。分词器的任务就是把文本变成一串数字 id。
中文场景下的分词是一个很有意思的问题。最简单的做法是按单个汉字切分,这样虽然不会出现“未登录词”,但会让模型的序列变长,而且丢掉了词与词之间的语义信息。另一种做法是使用 BPE(字节对编码),它会把常见词组、常用字符组合学成一个个 token。比如“人工智能”这个词,在北京大学开源的中文 BPE 词表中可能会被整体编码为同一个 id,也可能被切成“人工”和“智能”两个 id。
MiniMind 推荐的方式是使用已有的中文 tokenizer 文件,不需要你自己去训练一个。这样做的原因很简单:64M 参数的模型已经很“小”了,如果 tokenizer 质量还不好,那模型的表达能力会更受限。直接复用成熟的中文词表,相当于让模型从一套已经学好的“字母表”开始写字,而不是从零发明语言。
在代码层面,你只需要加载一个 tokenizer 文件,然后用 encode 方法把文本变成 id 序列。看到的一行行训练数据会形如 [101, 2354, 892, 3104...],这就是模型眼中的文本。
2.3 预训练、SFT、DPO:三步走的梯度
MiniMind 的训练路线不是一上来就做对话,而是分了阶段。
第一步是预训练(Pre-training)。这一步的目标非常纯粹:让模型学会“接话”。喂进去一篇文章,遮住后半部分,让模型根据前文预测下一个词。比如输入“今天天气真”,模型要学着预测出“好”的概率最大。在几万条语料上跑完这一步后,模型已经具备基本的文本生成能力,但它的输出往往是自言自语,不能准确地理解用户的问题。
第二步是指令微调(SFT)。这一步需要准备一问一答的数据,比如用户问“1+1等于几”,答案是“2”。训练时把问题和答案拼在一起,让模型学习这种“问答格式”。经过几千条这样的数据微调后,模型才初步学会“回答问题”。SFT 的好处是数据要求低,几千条就能见效。
第三步是偏好优化(DPO)。如果想让模型更进一步、学会“拒绝无礼问题”和“符合人类偏好”,需要正负回答对数据,让模型学会往更好的答案靠拢。对于 MiniMind 这种 64M 的小模型,DPO 不是必须的,但如果想完整走一遍对齐流程,也可以尝试。
2.4 训练策略:从零预训练、LoRA 微调和量化的取舍
从零预训练一个 64M 模型,是所有方案里最耗资源、但也是最彻底的路线。训练出来的模型是真正的“用自己的语言体系”学会了文本,但它的上限受限于数据和模型容量。
如果有现成的大模型底座,通常大家会优先考虑 LoRA 微调。这种技术只训练一小部分新增的低秩矩阵参数,原有的大模型权重冻结不动,因而显存占用大幅降低。不过 MiniMind 的价值恰恰是让你走一遍“从零到一”的完整流程,所以我建议第一次跑的时候不要跳过预训练阶段。
量化是另一个常被提到的选项,比如把模型从 fp16 压到 int8。它能显著减少显存占用、提升推理速度,但训练时一般不考虑,因为量化训练会引入额外的精度损失和调试复杂度。我的建议是:训练用 fp16 就好,推理阶段如果想进一步压显存,再尝试把训练得到的权重转成 int8。
3. 实操全流程:从克隆仓库到跑完 2 小时训练
这一部分是从零到跑通训练全过程的完整操作链。我会尽量把每一步的命令、原因和预期结果都写出来,方便你跟着操作。
3.1 环境准备:Python、依赖和显卡驱动
首先确认本地机器的 Python 环境。我建议使用 3.10 以上的版本,因为后续的深度学习框架对老版本 Python 兼容性较差。如果机器上同时有多个版本,用 conda 为这个项目单独建一个环境,避免污染。
接下来是安装依赖。MiniMind 需要的依赖很少,核心就是 PyTorch、transformers 和 tokenizers。在终端里执行:
pip install torch transformers tokenizers如果你用的是带 CUDA 的显卡,安装完 torch 后最好检查一下 CUDA 是否可用:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))这一步的目的是确认训练时能把张量放到 GPU 上。输出为 True 说明正常。如果你用的是纯 CPU 机器,理论也能训练,但 2 小时可能要变成十几个小时,体验会差很多。
最后,从代码仓库克隆 MiniMind 到本地,然后用 cd 进入项目目录:
git clone <仓库地址> cd MiniMind不要急着运行任何脚本,先看下项目结构,找到 train.py 或类似入口文件。这一步能让你对整个项目是怎么组织的有个直观认知。
3.2 数据准备:5 万条文本的清洗和包装
MiniMind 的数据目录里一般会有一个默认的存放位置,比如 data/。你把自己拿到的原始语料放进去,然后运行数据预处理脚本。
我实际用的是一份公开的通用中文语料,约 5 万条短文本。第一步是去重和过滤。这一步写一个小脚本就能完成,逻辑很简单:逐条读取文本,统计长度,筛掉长度小于 20 字的,去掉包含乱码字符或重复连续片段过长的。需要注意,这里不要引入外部依赖,用 Python 自带的正则库就足够。
处理完的文本存入一个干净的 txt 文件,每一行表示一条独立样本。然后运行项目的预处理脚本,把它转成训练用的二进制格式:
python preprocess.py --input_file data/raw_text.txt --output_file data/processed.bin看到控制台输出“共处理 5XXXX 条样本”之类的提示,就说明数据转换成功。如果你拿到的语料格式比较乱,这一步可能会报编码错误,最常见的元凶是“utf-8 编解码失败”,解决办法是在读取时指定 errors="ignore"。
3.3 一键训练:脚本参数背后的意义
MiniMind 的入口脚本通常叫 train.py,它需要你指定几个关键参数:批次大小、序列长度、学习率、训练步数。
我当时用的参数组合是这样的:
python train.py --batch_size 8 --seq_len 256 --lr 2e-4 --epochs 2 --device cuda逐个解释参数的含义:batch_size 是每次迭代送入多少条样本,它直接决定显存占用和训练稳定性,8 是一个安全值;seq_len 是每条样本被截断或补齐后的长度,256 表示每句话最多 256 个 token,超过的部分会被截掉,不足会填充;lr 是学习率,控制每次更新的步长,2e-4 适合小模型;epochs 表示整个数据集过几遍,2 轮就够。
点击运行之后,你会在终端看到不断刷新的训练日志,每行都会有一个 loss 值。
这里我特别建议你观察一下训练一开始的 loss。正常情况下,loss 会先从 10 左右向下走,即使数据不同、随机种子不同,初始量级也差不了太多。这是因为模型一开始完全随机,对任何词都只能给出均匀概率,预测一个词的平均交叉熵就在 ln(vocab_size) 附近。中文词表一般好几万,所以初始 loss 在 8-11 之间是正常的。
如果初始 loss 就直接在 2 以下,反而要警惕,很可能是数据切分或者标签对齐出了问题,模型在“作弊”地预测下一句,而不是在学语言规律。
3.4 中途观测:不要只看 loss 降没降
训练日志里的 loss 不是唯一需要关心的指标。更重要的,是观察 loss 下降的曲线是不是平滑。
正常情况下,前几百步 loss 会快速下降,从 10 降到 7 左右,然后进入一个缓慢下降的通道。这个过程就像人学说话,先掌握高频符号,再慢慢掌握句式结构。如果你的 loss 在下降到 6 左右时突然开始震荡,甚至反弹,那大概率是学习率太高了,后面踩坑部分我会详细展开。
两个小时的时间,按照 5 万条数据、batch size 8 来估算,大约能跑完一万多步。我实测下来,loss 最终会收敛到 4-5 左右的水平。这个数字不要觉得高,64M 模型的能力就在这里,它已经学会了最高频的语言规律。
训练结束后,脚本会自动保存模型权重文件。用 ls 命令看一下输出目录,确认权重文件已经生成,命名里一般会包含步数或者时间戳,方便后续版本管理。
4. 踩坑实录:显存溢出、学习率震荡、数据重复
整个流程我只跑了一遍就成功了,但这种“顺利”其实是建立在大量前期调研基础上的。事实上,我把训练拆分成了很多次小实验,踩过不少坑。挑几个典型的写出来,希望能帮你节省排查时间。
4.1 显存溢出(OOM):卡死在加载阶段?先查这三处
最让人血压升高的错误就是 CUDA out of memory。我遇到的情况不是发生在训练迭代中,而是在脚本启动后的数据加载阶段。信息量很小,就一行“RuntimeError: CUDA out of memory”,没有告诉你到底谁的显存超了。
排查思路要分三层。
第一,确认是不是 batch size 太大。这是最常见的原因。如果你的显存只有 4GB,而 batch size 设成了 32,那大概率会 OOM。直接调小到 8,甚至 4,能解决大部分问题。小 batch size 并不代表训练效果变差,只是每一步迭代的步数需要更多。
第二,检查数据并行或者验证阶段有没有额外分配显存。MiniMind 这类项目通常会在每 N 步做一次验证,验证同样要占显存,如果训练时已经把显存占满,验证就会爆。有些脚本设计了验证后清缓存的操作,但如果不生效,可以手动加上 torch.cuda.empty_cache(),或者直接减少验证频率。
第三,查看是否残留下了上一次运行的显存碎片。同一个终端里反复跑不同实验时,偶尔会遇到显存没有完全释放的情况。不用重启机器,运行 nvidia-smi 先查看 GPU 显存占用,把占着显存的僵尸进程找出来 kill 掉即可。
4.2 学习率震荡:loss 不降反升的真相
第一次训练时,我把学习率设到了 8e-3,想着这样“学得快”。结果前 200 步 loss 确实掉得飞快,但 200 步之后直接开始震荡,甚至一路走高,完全不可控。
这里涉及一个基础概念:学习率是每次参数更新的步长。学习率太高,模型参数会在最优解附近来回弹跳,永远落不了地;太低,训练会变得极其缓慢,2 小时根本跑不完。对小模型来说,2e-4 到 5e-4 是一个比较稳妥的区间。
如果你已经跑了一段发现 loss 在震荡,不需要从头重开。可以在训练脚本里添加学习率预热(warmup)和衰减(decay):前几百步让学习率从零缓慢爬升到目标值,之后逐步下降。这样既避免了初始阶段参数骤变,也能在后面微调阶段稳定收敛。具体实现可以直接用 transformers 里的调度器,也可以用简单的线性衰减。
4.3 数据重复导致的“假成绩”
这个坑比较隐蔽。我在数据清洗阶段做过去重,但只做了完全重复(整行字符串相同)的去重,忽略了近似重复。结果训练到后半段时,我发现模型能“背诵”出一些固定的段落,但泛化能力很差。
原因是语料里有一批文章格式高度相似,比如几百条都是“XX百科:词条名,介绍,历史沿革”,清洗脚本没有把它们识别为重复,模型就反复看到同一种句式,产生了“过拟合”反应。
处理办法是在清洗脚本里增加一个基于 Jaccard 相似度的去重逻辑,对两两相邻的文本计算重合字符比例,超过 0.8 的只保留一条。如果你清洗时只做了完全字符串去重,现在就可以回头补上这一层。这是我在实际操作中获益最大的一个调整。
4.4 数据泄漏:标签错误让模型“未卜先知”
再补一个经常被忽视的坑。预训练任务是根据前文预测下一个 token,所以训练样本的构造方式是:把整段文本的 id 序列喂进去,然后用同一个序列向右错开一位作为标签。也就是说,模型在位置 i 的输入是前 i 个字符,要预测的是第 i+1 个字符。
如果你在造样本时不小心让模型看到了“正确答案”,loss 就会瞬间降到几乎为 0,看起来效果非常好,但实际上是数据泄漏。判断方法很简单:训练完成后,用一段没见过的文本去测试,如果生成结果一塌糊涂,而训练 loss 又低得离谱,那大概率就是标签切分出了问题。正确实现是,输入序列和标签序列保持长度一致,标签在某位置的值对应输入序列下一位置的值。
5. 训练完只是开始:推理、对话、继续微调
模型训练出来不是终点,能把它用起来才算真正跑通了整条链路。
5.1 用几句话让模型“开口”
项目里一般会带一个简单的推理脚本,例如 chat.py 或 generate.py。我自己的调用方式如下:
from model import MiniMindLM from tokenizer import Tokenizer model = MiniMindLM.from_pretrained("checkpoints/mymodel.bin") tokenizer = Tokenizer.from_file("tokenizer.json") text = "有一天,小明走在路上" input_ids = tokenizer.encode(text, max_len=128) output_ids = model.generate(input_ids, max_new_tokens=50, temperature=0.7, top_p=0.85) print(tokenizer.decode(output_ids))生成时有两个重要参数:temperature 控制随机性,数值越大越“天马行空”,0.7 是一个平衡值;top_p 控制采样范围,模型只会从概率最高的前 85% 的 token 里选词,避免长期在低概率词上打转。对于 64M 的小模型,top_p 不建议设置太低,因为可选词本来就少,设太低容易产生大量重复。
实际运行后你会发现,模型输出的句子语法基本完整,但内容有点“飘”,经常说不完整就转折了。这是小模型的正常表现,不代表训练失败。
5.2 继续做 SFT:让模型从“会说话”变成“会回答问题”
预训练模型不知道什么叫“用户”,也不知道什么叫“指令”。如果直接拿预训练权重去对话,你问“你好”,它可能会回答“你好,你好,我叫你,你是一个…”这种毫无理解力的输出。
SFT 的作用就是教会模型对话格式。准备几千条一问一答的数据,格式形如:
{"instruction": "中国的首都是哪里", "output": "北京"}训练时,把 instruction 和 output 拼成一个完整的对话文本,仍然用预测下一个词的方式去微调。和预训练不一样的是,SFT 只需要几个 epoch 就能看到明显效果,因为模型已经掌握了语言能力,现在只是换一种输出格式。
用 MiniMind 跑完预训练后,我对同一个 checkpoint 继续跑了 3000 条 SFT 数据,只花不到半小时,模型就能在一个限定知识范围内给出比较像样的回答。这说明整个训练框架的“可连续性”非常好,不是一次性用品。
5.3 这套流程还能迁移到哪些场景
64M 模型的训练逻辑,其实和训练几百亿参数的大模型是同构的。你完全可以把这套流程迁移到以下场景:
一是垂直领域内容生成器。用特定行业的文本语料从头训练一个小模型,让它生成符合该行业风格的内容片段,比如商品描述、新闻导语、剧本对白。只需要更换语料库,重新跑一遍预训练。
二是分类任务的特征提取器。把 MiniMind 训练得到的权重拿去初始化一个小分类模型,在文本分类、情感分析等任务上做微调,往往比直接用词向量要好。
三是教学演示和原理验证。就拿数据泄漏那件事来说,亲手感受过训练 loss 异常下降和最终模型崩溃之间的因果关系后,你对训练机制的理解会非常扎实。
写在最后
如果你问我,花 3 块钱跑一次 64M 参数的训练,到底值不值,我的答案是:值,非常值。它带来的最大收获不是那个看起来说话都磕磕绊绊的模型,而是让你把大模型从“神秘黑盒”变成了“可操作对象”。只要你完整跑过一遍 MiniMind,再回去看大模型相关的论文和技术博客,那些关于预训练、损失函数、学习率、SFT 的概念就不会再是空中楼阁。
给还在犹豫的朋友一个建议:第一次跑的时候先别追求完美,不要试图一口气把参数调到最优,也不要在数据清洗上耗费太多时间。直接按默认配置跑通全流程,拿到一个能生成文本的模型,先建立信心。然后再回头去调数据、调学习率、加 SFT。整个流程你多跑几遍之后就会发现,训练一个大语言模型,真的没有想象中那么高不可攀。