
花两个多小时我在一张 RTX 4090 上从零训练了一个 64M 参数的中文小模型。听到这件事的人第一反应基本都一样你图什么图它参数少还是图它训练快其实都有但最根本的原因是——当大模型的门槛已经被推到千亿参数、上万张显卡的时候反而应该有人踏踏实实地把一个小模型从数据到权重的全过程完整跑一遍。minimind 这个开源项目就是干这个的而我这次实测想搞清楚的问题只有一个2 小时的时间投入换来的到底是一个会蹦字的玩具还是一个真能看出门道的东西。先说结论2 小时后我拿到的不只是一个玩具。它能接续一段话能完成基础的中文问答偶尔还会给出让我意外的连贯表达——但它也会一本正经地胡说八道。如果你有 24G 显存或者手里只有一张 8G 显存的卡也想完整见识一下大模型训练全流程这篇文章值得你看完。我会把环境准备、数据选择、Tokenizer 训练、stage1 预训练、stage2 指令微调的完整过程以及我踩过的三个比较隐蔽的坑全部摊开来讲该给参数给参数该给命令给命令尽量不让你走弯路。1. 为什么我把学习重心放在 64M 参数而不是一步到位追大模型1.1 一个完整但不贵的训练链路很多人聊到大模型训练第一反应是我没有 A100这事跟我没关系。但 minimind 这类项目恰好证明了另一条路大模型训练的完整链路——数据清洗、分词器训练、预训练、指令微调、偏好对齐——在小模型上同样存在而且成本低到个人开发者完全承受得起。我这次选 64M 参数版本核心原因是它处在学习价值和时间成本的平衡点上。26M 参数连基础句式都容易学崩很多模型层面的问题会被参数规模掩盖训练完你只会觉得它笨是正常的200M 参数训练时间会拉长到一天级别不适合2 小时见结果的定位。64M 则刚好模型结构足够复杂过拟合、梯度不稳定、数据敏感这些真实问题都会一一暴露同时显存占用只要 5GB 左右4090 跑起来非常轻松甚至一些旧一点的显卡也能跟上。这个选择还有一层更现实的考虑模型越大排查问题越困难。64M 参数下你对数据做任何一点改动都能在很短的时间内看到 loss 曲线的变化。这种即时反馈是学习阶段最珍贵的东西。等你在小模型上练出了手感再切换到 200M、1B 甚至更大的模型很多操作其实是同一套逻辑。1.2 2 小时的时间预算是怎么算出来的关于标题里的2 小时我需要说明这不是随便定的而是有一个粗略的算账过程。训练一个大模型的计算量近似等于 6 倍参数量乘以训练 token 数也就是 6ND。64M 参数训练 2 亿 token总计算量大约就是6 × 64M × 2亿 ≈ 7.68 × 10^16 FLOPsRTX 4090 的 BF16 理论算力是 165 TFLOPs。实际训练过程中由于小模型的计算密度低显存带宽和数据加载会拖后腿整体利用率通常只有三成到四成。按 40% 利用率算每秒实际算力大约 66 TFLOPs那么理论耗时是7.68 × 10^16 / (66 × 10^12) ≈ 1164 秒也就是大约 20 分钟等一下这个结果比我实际花的时间少很多。原因在于我计算的理论算力没有计入数据加载、日志打印、评测、tokenizer 训练以及频繁的中间保存开销。把这些问题算进去2 小时是一个比较务实的预估。如果你是 3090 或者 4080时间会拉长一些但整体仍然可控。不过这里也要泼一盆冷水64M 参数不是缩小版的 ChatGPT它更像一块教学芯片用来理解训练机制。你要指望它像 7B 模型那样聊得面面俱到肯定不现实。它的价值在于训练过程中每一步都会反馈到模型行为上参数变化是可观察的适合拿来做实验和积累直觉。2. 实测环境与依赖安装一张 4090 能解决的事别想太多2.1 硬件与软件栈先交代一下我这次实测的环境项目配置GPUNVIDIA RTX 4090 24GBCPUIntel i7-13700K内存64GB DDR5系统Ubuntu 22.04 LTSPython3.10PyTorch2.1.0 CUDA 12.1transformers4.36.0datasets2.16.0tokenizers0.15.0accelerate0.26.0实际跑下来最大的感受是64M 模型对显存几乎没有压力。stage1 预训练过程中显存峰值大概在 5.2GB 左右加上数据缓存也没有超过 6GB。所以如果你手里只有一块 8G 显存的显卡也可以跑通整个流程只是训练时间会翻倍尤其要留意数据加载和序列长度的设置。2.2 两个容易卡住的依赖问题第一个坑是 PyTorch 版本。很多人新装环境时图方便直接pip install torch结果装成了 CPU 版训练时发现一晚上跑不完一个 epoch。检查方法很简单在 Python 里执行torch.cuda.is_available()返回 False 就说明装错版本了。正确做法是到 PyTorch 官网找对应 CUDA 版本的安装命令安装。第二个坑是 flash-attention。网上不少教程一上来就推荐装这个依赖结果不少人卡在编译环节折腾半小时。对于 64M 这种小模型注意力计算量非常小默认的 attention 实现已经足够快完全不需要 flash-attention。省掉这个依赖环境配置能少走很多弯路。装完依赖之后建议先跑一次模型 forward确认能正常输出再做任何训练from transformers import AutoConfig, LlamaForCausalLM config AutoConfig.from_pretrained(minimind_64_config.json) model LlamaForCausalLM(config) print(model.num_parameters())如果你看到的参数数量在 6400 万左右说明环境基本就绪。这一步提前验证可以避免训练到一半才发现配置不对白白浪费时间。3. 数据与 Tokenizer 先行小模型的偏科从这里埋下3.1 数据量怎么定才是符合 2 小时约束的做法很多人第一次训练小模型看到网上说训练数据越多越好就直接把几个 GB 的语料丢进去。结果就是 2 小时连一个 epoch 都跑不完最后只能草草收场。我这次一开始就按参数量 × 训练总 token 数这条线来配数据64M 参数对应 2 亿 token 的训练量是比较务实的文本文件大约在 500MB 左右。实际准备数据时我用了两部分一部分是从开源中文语料中抽出来的通用文本另一部分是自己攒的问答对。在喂给模型之前做了三件事去掉重复段落避免模型把高频样本背下来而不是学到规律过滤明显乱码、纯英文广告和过短的无意义句子按 512 token 的长度切块超出部分直接截断。这里比较推荐直接用 Hugging Face datasets 的Dataset.map来做切块速度和内存都友好。处理完的数据统一转成 jsonl 格式每一行是一条训练样本。数据量不用贪多关键在于干净和覆盖度。还有一点值得注意小模型的数据混合比例会直接影响它的偏科方向。如果通用语料占 90%问答对只占 10%最后模型会更像一个续写机器而不是一个问答助手。这个比例在 stage2 指令微调前要重新考量很多人在小模型上做得不理想不是参数太小而是数据配比一开始就没想清楚。3.2 为什么不用现成 tokenizer而要自己训一个minimind 项目支持使用现成的中文词表但如果你真的想从零训练我建议自己训一个 BPE tokenizer。原因很简单通用 tokenizer 对中文支持往往不理想一个常用汉字会被拆成 2 到 3 个字节级 token等于白白放大序列长度训练效率会下降 30% 以上。我用的策略是训练一个 vocab_size20000 的 BPE tokenizermin_frequency 设为 2。代码非常简单几分钟就能训完from tokenizers import Tokenizer, models, pre_tokenizers, trainers tokenizer Tokenizer(models.BPE(unk_tokenunk)) tokenizer.pre_tokenizer pre_tokenizers.Metaspace() trainer trainers.BpeTrainer( vocab_size20000, min_frequency2, special_tokens[s, /s, unk] ) tokenizer.train(files[data/all_corpus.txt], trainertrainer) tokenizer.save(model/tokenizer.json)训完之后我自己统计过一个平均 20 个汉字的短句在这个 tokenizer 下会被切成 18 个 token 左右效率比直接套用通用词表高不少。如果你发现某些常用字频繁变成unk说明语料覆盖不够或者 vocab_size 太小可以适当把 min_frequency 降到 1 重新训练。这一步的重要性很容易被低估。我见过很多人花大力气准备训练数据却在 tokenizer 上草草了事最后模型生成质量上不去还找不到原因。Tokenizerr 是模型看到的第一层世界这一步偷懒后面所有调参都要加倍还回去。4. 2 小时训练过程全记录stage1 预训练 stage2 指令微调4.1 stage1 预训练让模型先学会中文的语感minimind 的训练链路分三个阶段stage1 预训练、stage2 指令微调、stage3 偏好对齐。我这次在 2 小时内完成了前两个阶段。stage1 的目标是让模型学会中文句子的基本统计规律——不是理解世界而是先形成语感。我用的模型配置没有大调直接参考项目仓库里 64M 的默认配置词表大小是 20000。关键超参数如下参数数值effective batch size64micro batch size32gradient_accumulation_steps2max_seq_len512learning_rate5e-4warmup_steps2000schedulercosine decay末值 1e-4optimizerAdamWbeta(0.9, 0.95)weight_decay0.1训练命令大概长这样python train.py \ --model_type minimind_64 \ --data_file data/pretrain_data.jsonl \ --tokenizer model/tokenizer.json \ --max_seq_len 512 \ --batch_size 32 \ --gradient_accumulation_steps 2 \ --learning_rate 5e-4 \ --max_steps 6100这里我解释一下 6100 步怎么来的。effective batch 是 64每条样本最长 512 token那么每步吃掉 64 × 512 32768 token。目标训练 2 亿 token用除法一算就是约 6100 步。实际跑下来stage1 大约用了一个半小时。训练日志里能很清晰地看到模型在进入状态前 2000 步 loss 从 8.4 快速下降到 3.0 左右这是 warmup 阶段学习率从 0 爬升模型开始抓住中文的高频词汇搭配2000 到 5000 步loss 缓慢降到 1.8 附近下降速度明显变慢说明模型开始学习更隐晦的语义结构5000 步之后 loss 曲线出现小幅波动属于正常现象。4.2 stage2 指令微调让模型学会回答而不是接话stage1 结束后模型更像一个续写器。你给它一句开头它本能地想把句子写完但如果你问它问题它大概率不会回答而是继续预测最可能的下一个 token。所以必须做 stage2 指令微调用大量问题-回答的数据教会它角色转换。SFT 数据我用了约 12 万条中文指令数据考虑到 2 小时的时间预算stage2 只训练 600 步大约 15 分钟就结束。SFT 阶段的关键是学习率要用小很多否则预训练学到的语感会被冲掉出现训练几分钟就变回傻瓜的灾难性遗忘现象。SFT 参数如下python train_sft.py \ --model_weights model_ckpt.pt \ --data_file data/sft_data.jsonl \ --batch_size 16 \ --max_seq_len 512 \ --learning_rate 2e-5 \ --max_steps 600数据格式很简单每行一个 JSON 对象{prompt: 请用一句话介绍自己, response: 我是一个用 minimind 训练的中文小模型。}我的切身体会SFT 阶段不是训练步数越多越好。600 步跑完我拿同样的 prompt 测试模型已经能稳定给出像模像样的回答。如果继续加到 1200 步回答反而会变得死板因为模型开始过拟合训练数据里的固定句式。所以小模型的 SFT 阶段宁可少训一点也不要贪多。4.3 训练过程中的实测数字整个训练过程的几个关键数字我记录如下方便你对照自己的机器估算指标stage1stage2显存峰值5.2GB6.8GB平均吞吐约 28k token/s约 22k token/s总步数6100600耗时约 1.5 小时约 15 分钟stage2 的吞吐比 stage1 低原因是 prompt 和 response 拼接后输入序列的平均长度更长训练数据也更集中。整体看下来2 小时的时间预算非常紧凑但老话说得好——时间越紧越能逼你少做无用功。5. 2 小时后它到底能干嘛生成、问答与能力边界实测5.1 续写与主题生成有语感没逻辑训练结束后我没有马上上测试集做量化评估而是先做了一轮人的直觉评估。因为对于 64M 参数这种规模BLEU 和困惑度只能告诉你有没有学会统计规律但用户最关心的其实是它说的话像不像人话。第一轮测试是文本续写。输入是人工智能正在改变世界它不仅改变了人工智能正在改变世界它不仅改变了我们的生活方式还改变了我们的思维方式。这句话语法通顺逻辑也基本成立尽管内容比较安全没什么信息量。对 64M 参数来说能稳定输出这种句子说明 stage1 的语感学习是有效果的。第二轮测试是主题生成。我给了一个开放主题冬天的早晨模型生成了一段话大意是冬天的早晨寒冷的风吹过街道雪地上没有行人只有几棵老树在风中摇曳。这段文字里寒冷、雪、风、树这些词都和冬天强相关句子也有画面感但整体没有故事线更像是在堆叠高频词汇。小模型的生成能力就是这样的它能抓住词的搭配抓不住事件的发展。如果你想让它写一个完整的小故事第三句话开始就会出现重复或者跑偏。这不是 bug而是参数容量决定的必然结果。5.2 指令问答简单任务能哄住人复杂推理原形毕露经过 stage2 指令微调之后模型倒是学会了一个非常重要的能力知道自己是在被提问而不是在续写。我测试了几个典型问题。问请用一句话介绍自己它能答出我是一个人工智能助手。问中国的首都是哪个城市它答北京。这类高频事实性问题只要在训练语料里出现得足够多模型就能记住并正确复现。但到了逻辑推理环节问题就出来了。我问它小王有 3 个苹果又买了 2 个现在一共有几个它回答5 个——这个侥幸答对了。但同样的题目换成小王有 7 个苹果用掉了 3 个又收到 4 个现在一共有几个它的回答就开始混乱有时甚至给出一个大于 10 的答案。这说明它并没有真正学会加法只是记住了一些高频问答组合。有一个更典型的例子我问如果一个数除以 3 余 2除以 4 余 3问这个数最小是多少小模型直接开始编完全意识不到这是个数学问题。这个现象恰恰说明语言模型的知识本质上还是统计关联不是形式逻辑。5.3 和 7B 模型放在一起看差距到底在哪为了让64M 到底能干嘛这个问题有参照系我把手头一个 7B 量级的开源模型拉出来做了同样的测试结果对比如下能力维度64M 模型7B 参考模型单次回答稳定长度50 字以内可长可短逻辑推理极弱中等知识覆盖面仅训练语料高频内容更广重复问题频繁出现偶发FP16 部署显存约 0.2GB约 14GB单次推理耗时CPU毫秒级秒级这张表不是为了打击 64M 模型的信心而是为了说明小模型的作用半径是短小、高频、简单的任务超出这个半径的任务就会频繁答非所问。对我来说这个结果已经值回 2 小时的投入了因为我看清了它的能力边界在哪里——比背一百遍论文更直观。6. 这段训练里踩过的三个坑显存、过拟合与评估失真6.1 显存 OOM 的元凶不是模型而是序列长度小模型显存是真的够用但我第一版 stage2 配置把 max_seq_len 设成了 1024部分长指令样本被填充得很满显存瞬间冲到 11GB。虽然 4090 扛得住但如果你手头的卡只有 8G 显存这一步已经 OOM 了。排查过程其实很有意思。我一开始以为是 batch size 设太大了从 16 一路降到 4结果显存只降了一点点。后来仔细看数据加载器里每个 batch 的 token 数才发现问题出在序列长度训练数据里有些 prompt 特别长被 padding 到 1024 之后显存开销成倍增加。改成 512 长度并把超过长度的样本直接截断显存立刻降回 6GB 以下。所以给大家一个经验小模型出现 OOM优先检查序列长度和数据加载器而不是立刻怀疑模型本身。很多时候模型占用的显存只占小头padding 带来的无效计算才是大头。6.2 SFT 阶段一多训就过拟合生成开始复读机第一次跑 SFT我把 12 万条数据训了 3 个 epoch结果生成质量反而变差任何 prompt 都会引向训练集中几个高频回答的变体你问它什么是人工智能它回一段套话你问它今天天气怎么样它还是回一段套话。整个模型就像一个坏掉的复读机。这个问题的本质是 64M 参数的容量太小装不下 12 万条问答模式的全部多样性。模型学习了高频回答的统计规律却无法为每条 prompt 建立独立的特征映射于是所有输入都被折叠到少数几个输出模式上。把 epoch 改成 1学习率保持 2e-5 之后复读机症状明显缓解。后来我翻训练日志发现第二个 epoch 的评估 loss 已经开始上升——典型的过拟合信号。所以对于 64M 参数这种规模SFT 阶段 1 个 epoch 通常是够用的如果不够你更应该怀疑数据质量而不是盲目加训练轮数。6.3 数据污染让测试正确率虚高另一个很隐蔽的坑出现在评估环节。我想测模型知不知道中国首都是北京这个问题就把这句问话写进测试集结果模型答对了。但后来检查数据 pipeline 才发现这句问话在某个开源语料里出现过已经被训练数据吃进去了——也就是说我做了一次标准的开卷考试但以为是闭卷。在大模型评测里数据污染同样存在而且更防不胜防因为网上开源数据太多你很难保证测试集里的句子没在预训练语料里出现过。但小模型对这种污染的敏感度更高因为它的泛化能力弱很容易通过背题拿到高分本质上并没有真正学会对应能力。我后来改用自己现写的 20 条问题做盲测并明确排除所有在语料中出现过的问句得到的结果才算可信。所以做小模型评估时一个基本的纪律是测试数据必须在训练完成后再准备或者至少保证测试样本没有出现在来源语料中。7. 从 2 小时到下一步用 DPO 和量化把项目推向可用7.1 用 DPO 补上偏好对齐这一步stage1 学会生成stage2 学会回答但这还不够。实际测试中我注意到模型对同一个问题可能给出两个互相矛盾的答案因为它只是学到了答案的统计分布没有学会挑选更符合人类偏好的那个。minimind 的 stage3 做的就是这件事DPO 偏好对齐。我计划再花 1 到 2 小时用大约 2 万条偏好对数据把这一步补上。偏好对数据的组织方式很简单同一个 prompt配一个模型自己的回答和一个更优的人工回答DPO 会让模型逐渐加大优质回答的概率。对小模型来说DPO 的效果通常比 RLHF 更容易稳定也不需要复杂的 PPO 工程实现是个人开发者做对齐的首选方案。7.2 导出 GGUF部署到本地离线环境训练完模型之后我准备走一遍训练到部署的最后一公里把权重导出成 GGUF 格式再用 llama.cpp 跑 CPU 推理。64M 模型量化后大约只有 30 到 40MB完全可以在没有 GPU 的机器上实时运行这正好发挥它低延迟、低资源消耗的优势。这个步骤对个人开发者来说很有参考价值。很多人在本地部署的所谓小模型动辄几个 GB 甚至十几个 GB而 64M 模型在 CPU 上几乎可以做到即时响应。虽然能力有限但作为个人知识库问答、离线文本辅助工具便宜好用反而变成了它的核心竞争力。7.3 再做一轮 64M 与 200M 的对照实验最后我还想做一个对照实验同样的数据同样的脚本把模型从 64M 换成 200M 再训一次。两个模型在统一测试集上的差异会比任何论文里的 Scaling Law 曲线都直观。毕竟在真实的数据上亲手做一次规模对比比读一百遍理论都管用。这个对照实验我还打算加上一组数据量减半的变体用来观察到底是参数规模对结果影响更大还是训练数据量对结果影响更大。到手之后我可以把四个模型的生成结果、loss 曲线、推理速度和显存占用放在同一张表里那会是一份很有参考价值的实测笔记。跑完这 2 小时我心里对大模型训练这件事的认知发生了一个关键变化它不再是黑箱而是一个由数据、tokenizer、超参数、训练步数共同决定的系统工程。64M 参数的小模型不会改变世界但它把大模型训练从玄学拉回了工程。如果你也想搞懂从零训练的每一步到底在干嘛我给你一个最直接的实操建议别纠结参数太小、显存不够先从 minimind 的 64M 配置开跑先跑通再跑大。我自己就是从这次实测开始才真正理解了数据、参数和训练步数三者之间那个微妙的三角形关系。