作者在MacBook Air(16GB + MLX + Metal)上完整跑通从随机权重到本地推理的全流程,包括数据生成、tokenization、预训练、SFT与评估,揭示API隐藏的工程细节。
训练 Transformer 最简便的方式是把大部分过程隐藏在框架背后。最容易忽略的要点是:文本如何变成 token、梯度如何改变随机权重、checkpoint 如何变成可加载的模型,以及为什么出色的验证损失可以和糟糕的实际回答共存。
我构建了 Local Transformer Training Lab,在个人电脑上运行完整链路:
Dataset generation
→ Tokenizer
→ Decoder-only Transformer
→ Pretraining
→ Supervised fine-tuning
→ Independent evaluation
→ Checkpoints and export
→ Local inference
实验在一台 16GB 内存的 MacBook Air 上运行,使用 Apple MLX 和 Metal GPU。每个模型均从随机权重开始,没有一个是基于 Qwen、Llama 或其他预训练模型微调而来。

模型 API 非常适合构建产品,但当系统出现故障时,有几个重要的问题被隐藏了:
从头训练的价值不在于参数数量,而在于没有任何异常可以被归结为"黑箱"。数据、架构、优化和推理之间的关系变得具体可感。
第一个程序员世界的超小数据集包含约 10 MiB 的本地生成材料,涵盖 Go、Linux、Redis、MySQL、Docker、代码补全和工程面试。
模型刻意做得很小:4 个解码器块、4 个注意力头、隐藏宽度 128、FFN 宽度 384。128 token 版本有 742,272 个参数;将上下文扩展到 512 token 后增加到 791,424。
src/model.py 中的实现包含完整路径:
Token embedding + position embedding
→ RMSNorm
→ causal multi-head self-attention
→ residual connection
→ RMSNorm
→ GELU feed-forward network
→ residual connection
→ language-model head
因果掩码阻止位置 t 看到未来的 token。下一个 token 的训练将一个序列移动一个位置:模型接收除最后一个之外的所有 token,预测除第一个之外的所有 token。
训练使用 AdamW、梯度裁剪、定期验证,以及分离的 best、checkpoint 和 final 导出。可重新加载的包包含三个必要文件:
config.json
model.safetensors
tokenizer.json
没有配置的权重无法重建网络。没有精确 tokenizer 的模型无法解释其 token ID。
第一个 tokenizer 直接表示 UTF-8 字节。其词表包含 263 个条目:256 个字节值加上特殊标记。它不需要训练 tokenizer,是可逆的,并且覆盖中文、英文和代码。
代价是序列长度。一个中文字符通常消耗三个字节 token。因此名义上的 512 token 上下文实际上容纳的中文字符远少于 512 个。模型还可能生成不完整的 UTF-8 字节序列,在终端产生替换字符 �。
经过 2,500 步后,512 token 模型达到了最佳验证损失 0.135433,最终困惑度为 1.1514,耗时约 110.63 秒。这些数字看起来很亮眼。但它的实际回答并非如此。
训练和验证记录来自同一模板分布。低损失主要证明模型学会了这些规律,并不意味着它拥有通用的编程知识。
v1 将模型扩展到 8,534,272 个参数:8 层、8 个头、隐藏宽度 256、4,096 token 的 ByteLevel BPE 词表。
效率差异立竿见影:
"Why is Redis so fast?"(中文提示)
字节分词器:28 个 token
BPE 分词器:7 个 token
更重要的变化是将训练分为两个阶段。
预训练材料被转换为技术笔记和完整代码,而不是问答包装。其目标是学习领域语言、代码结构和下一个 token 的规律。
监督微调
SFT 数据移除了"附加约束"和"模拟业务上下文"等合成短语,然后应用语义去重和类别平衡。损失仅在第一个 <|assistant|> 标记之后计算,因此模型优化的是答案,而不是将大部分能力花在复现用户文本和控制 token 上。
v1 完成了 800 步预训练和 1,500 步 SFT。其最佳 SFT 验证损失为 0.235517。独立改写的提示揭示了不同的现实:Redis、MySQL 和 Docker 的回答可能相互渗透;身份类回答带上了面试模板;改写后的泛化能力很差;有些提示直接产生了一个终止标记。
有用的结论不仅仅是损失有所改善:
规模、BPE 和训练技术改善了表示能力和拟合程度。自然的变异性、监督质量和独立评估决定了模型是否真正能够回答问题。
ByteLevel BPE 仍然允许 v1 产生不构成有效 UTF-8 的字节片段。v2 没有盲目扩大模型规模,而是切换到 Unicode 字符 tokenizer。
语料库包含 750 个不同字符加上 9 个特殊 token,词表共 759 个。每个发出的 token 都代表一个有效的 Unicode 码点。未知的字符显式映射到 <|unk|>,生成过程不再产生 �。
v2 有 6,825,728 个参数、8 层、8 个头、512 token 上下文。它在 115.45 秒内完成了 800 步预训练和 1,500 步 SFT。
字符分词并不简单地"优于"BPE。序列更长,没有 KV 缓存时生成更慢。该实验分离了通常被合并为一个指标的三个目标:模型能力、tokenizer 效率和输出有效性。
能够加载权重的脚本还不是可靠的推理接口。v2 添加了:
--no-history 防止未经训练的多轮上下文累积错误。默认禁用历史可能是最不直观的设计决定。512 token 窗口并不意味着模型学会了多轮对话。没有可靠的多轮 SFT,将一个错误的答案输入下一个提示会让错误累积。当某个能力尚未就绪时,做得更少比假装支持它更值得信赖。
一个模板分布的随机分割会产生过于乐观的指标。该项目最终使用了三层检查:
实现正确性:tokenizer 往返、掩码行为、形状、保存/重载和最小训练测试。
分布内指标:训练损失、验证损失、困惑度和梯度范数。
模板外行为:人工改写、领域泄露、空回答、重复、无效文本和多轮污染。
最有价值的产出往往是失败,而不是最好的数字。每个失败都指向需要改进的层面:数据、tokenizer、损失、架构或推理策略。
1. 数据结构比原始大小更重要
10 MiB 的高度模板化数据可以产生低损失但泛化能力很弱。去重必须超越字符串相等性,防止语义模板在训练集和验证集之间泄露。
2. Tokenizer 是架构的一部分
它决定了词表参数、序列长度、上下文利用率、未知字符行为和输出有效性。它不是一个可替换的预处理细节。
3. 预训练和 SFT 解决不同的问题
预训练学习语言和领域分布。SFT 教模型如何在任务协议内做出响应。将两者混合成一个无差异的下一个 token 数据集会模糊这些角色。
4. 损失只回答它被设计来回答的问题
分布内验证损失问的是模型是否预测了相似的 token。它不问模型是否理解自然用户语言。指标必须与所声称的能力相对应。
5. 推理控制不会掩盖模型弱点
停止重复、安全回退和禁用历史并不会让模型更聪明。它们让模型的边界变得诚实、稳定和可观察。可靠的系统必须限制错误如何传播,而不仅仅是最大化正确输出。
该项目现在完成了从随机权重到本地推理的旅程,但它仍然是一个教学模型。下一个有价值的步骤不是更多的合成模板或盲目增加训练步数。它们是:
从头训练并没有让现代语言模型看起来不那么令人印象深刻。它让每一个好答案背后的隐藏协调变得可见:数据、分词、架构、优化、评估和推理策略都必须协同工作。
Repository and full experiment reports: hh696-wq/local-transformer-training-lab