拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

大模型From Scratch全链路实战:从数据到部署的工程指南

大模型From Scratch全链路实战:从数据到部署的工程指南

1. 为什么“from scratch”这条路值得走:一个工程判断力的自我修炼

很多朋友看到“ai-engineering-from-scratch”这个标题,第一反应是“又要造轮子”。说实话,两年前我也是这么想的。当时工作里已经天天在用开源模型和推理框架,觉得从零训一个模型既费钱又费力,还不如把时间花在调 prompt、调参数、优化 RAG 上。

直到有一次,线上模型的输出出现了大面积重复,同事排查了三天,最后定位到是采样参数和重复惩罚配置冲突。那三天里我一直在想:如果我们连模型生成的基本机制都只是停留在“好像是这样”的层面,出了问题只能靠猜。后来我下决心完整走了一遍 from scratch 的流程,包括数据清洗、分词器训练、模型实现、预训练、指令微调、偏好对齐,再到部署评估。那个过程让我把过去工作中所有“知其然不知其所以然”的点全部补上了。

这篇文章就围绕这条路线展开。它适合两类人:一类是刚入门、想真正理解大语言模型原理的工程师或学生;另一类是已经用开源模型做应用的开发者,想补上底层认知、提升问题排查能力。我会把路线图、选型理由、关键步骤和踩坑经验都写出来,尽量做到能直接照着走。

1.1 三条做 AI 的路,我为什么选了最“笨”的一条

现在做 AI 应用,基本有三条路。

第一条是调 API。优点是快,几天就能出原型。缺点是成本高、数据出境合规麻烦、有上下文长度限制,而且核心机制完全黑盒。调 API 做应用,本质上更像系统集成,离“理解模型”其实很远。

第二条是微调开源模型。比如拿一个 7B 的底座模型做 LoRA 或全参数微调,让它适配自己的业务数据。这条路比调 API 深一截,至少你能看到训练循环、损失曲线和 checkpoint。但它同样有局限——开源底座已经把数据、分词、预训练、对齐这些环节做完了,你接触的只是最后一段,底层仍然不透明。

第三条就是 from scratch:自己收集数据、自己训练分词器、自己写模型结构、自己跑预训练和后训练。这条路最费时间,但对理解最彻底。我当时的判断是,如果目标是成为一个合格的 AI 工程师,而不是只会套模板的人,这条笨路必须走一遍。

1.2 一个反直觉的事实:小模型走全链路,比大模型做局部优化更有价值

很多人觉得,from scratch 就一定要做一个几十B参数的大模型,动辄几千张显卡。实际上完全不是这样。

我在完整走流程时,先是跑了两个 19M 参数的 toy model 用来调试代码,再上了一个 157M 的模型,最后才跑到 1.4B。整个过程中学到的东西,跟模型尺寸完全不成正比——即便只是 157M 的模型,当你亲手把数据、分词、预训练、SFT、DPO 全流程跑通,你会对所有大模型的新闻产生完全不同的感知。比如看到别人说“我们用了多少 T token 做预训练”,你脑子里会自动换算:这个规模的数据大概是多大体量的网页、清洗后剩多少、按我的 GPU 要跑多久。

这个判断力,是看论文和调 API 永远给不了的。论文给你的是一张地图,from scratch 是让你亲手把这块地走一遍。

1.3 这条路真正的产出,不是模型,而是正确的心智模型

我做完整个项目复盘时最深的感受是:兜里多了一个能跑的小模型,但它远不是最重要的收获。重要的是三套心智模型。

第一套是数据心智。现在我拿到任何数据集,第一反应是看它的去重情况、语言分布、噪声水平。第二套是训练心智。看到 loss 曲线,我能大概判断是学习率问题、数据问题还是梯度稳定性问题。第三套是评估心智。我不再轻信 benchmark 分数,因为我知道那些测试集可能就在我自己的训练语料里——评估集的污染问题在 from scratch 场景下暴露得太明显了。

这也是我把这篇文章的重点放在全链路而非单个点上的原因。每个环节单独拆开都有人写过教程,但把它们串起来、用成本可接受的规模走通,才是“from scratch”这个标题真正的价值所在。

2. 一张完整的路线图:从零到能跑的模型,一共分几步

在动手之前,最重要的事情不是写代码,而是把整个工程拆成清晰的阶段。AI engineering 本质上是复杂的系统工程,如果你没有路线图,很容易卡在某个环节出不来,或者做到一半发现前面某个决策错了,被迫重构。

2.1 五阶段全景:数据、预训练、后训练、评估、部署

我最终把整个 from scratch 项目拆成五个阶段。这里用一张表格先把全貌列出来,后面每一段我都会展开讲。

阶段核心交付物关键决策点建议投入时间占比
数据工程清洗后的训练语料、分词器数据源选择、去重策略、词表大小、混合配比30%
模型实现可训练的 Transformer 源码架构选型、初始化、超参15%
预训练checkpoint、loss 曲线学习率调度、稳定性、监控指标30%
后训练指令跟随模型、偏好对齐模型SFT 数据、DPO/RL 策略15%
评估与部署可调用的推理服务评测集、量化方案、推理引擎10%

我特意把数据工程放在 30% 的投入,这不是拍脑袋,而是血泪教训。第一次做的时候,我把 40% 时间花在了模型代码上,结果预训练时发现语料里全是重复和低质量内容,loss 死活降不下去,最后回头重做数据,等于白跑了两个礼拜。

2.2 规模与成本估算:一张消费级显卡能跑到什么程度

先算账,再动手。做 AI engineering 如果不算账,很容易一开始就买车买炮然后发现打不起仗。

模型规模的选择,核心是理解两个数字:参数量 N 和训练 token 数 T。业界有一个经验规律——一个大模型的总计算量大致是 6×N×T FLOPs(6 倍的参数乘以 token 数,前向反向各占一部分)。所以你选定了模型大小,再选定训练数据量,基本就锁定了总计算量,进而锁定 GPU 时长和成本。

我建议的入门规模是这样三档:

  • 19M 参数:仅用于调试代码,比如验证 forward、backward、数据 loader 有没有 bug。我用它在单张 4090 上跑了几百万 token,十几分钟就完事。
  • 157M 参数:这是我最推荐的学习主力规模。搭配 3B 左右的训练 token,单张 A100 大概 2 到 3 天能跑完。没有 A100 的话,用 4090 也能跑,但时间翻倍。
  • 1.4B 参数:这是“有点真实感”的规模,能明显看到语言能力的涌现。搭配 10B 到 20B token,大概需要 4 到 8 张 A100 跑一周。如果没有多卡资源,这一档暂时可以先跳过。

关于硬件,我想纠正一个常见误区:from scratch 并不一定要从 H100 起步。我自己在 4090 上跑过 157M 的模型,24GB 显存完全够用。关键是用混合精度(bf16),别用 fp16,后者在 LLM 训练里特别容易炸 loss。如果你只有一张 4090,那就从 100M 到 300M 这个区间起步,先把全链路走通,以后再加规模。

2.3 项目的最终目标:是要一个模型,还是要一项能力

这一点很多教程不会讲,但你的路线图应该由目标来倒推。我见过两种常见诉求。

一种诉求是想做产品。比如要做垂直领域的客服助手。这种情况下,我的建议是:from scratch 只做学习闭环,产品线仍然以开源底座模型为基础去做后训练,这样可控性和效率都高得多。你不需要为了一个垂直场景投入巨额算力去训底座。

另一种诉求是想吃透技术。这时候我建议你坚持把 from scratch 走到 1B 左右,甚至更远,因为只有到了这个规模,很多在 157M 上观察不到的现象才会出现,比如思维链长度的扩展、评估基准上的非线性提升。

想清楚你要的是“一个能用的模型”还是“一套可迁移的工程能力”,这决定了从零开始的真正路径。否则很容易做到一半发现方向错了。

3. 数据与分词:模型的一切,在这里就已经决定大半

Jim Fan 有句话我特别认同:数据才是真正的护城河。做 from scratch 之后,我对此体会更加深刻。模型架构是公开的、训练代码是公开的,唯一让一个模型与众不同的,是你喂给它的语料和喂料方式。

3.1 数据收集与清洗:从公开数据集到你真正能用的语料

第一次做 from scratch 时,最容易犯的错是直接下载一个巨型公开数据集就开始训练。实际上一份原始语料下载下来,至少要经历三道处理。

第一道是去重。公开语料里相同文本的变体非常多,尤其是新闻和百科类内容。我用的方法是两层:先做 exact match 去重,再做 MinHash 近似去重。MinHash 的思路是把文本拆成 shingle(连续片段集合),然后算 Jaccard 相似度,超过阈值就丢。这个做法成本低、效果好,清洗后语料体积往往能减少 20% 到 30%。

第二道是质量过滤。这里我建议至少跑以下规则:语言检测(只保留目标语言)、长度过滤(丢弃过短的和过长的)、URL 过滤(很多公开数据集直接提供了过滤模板)、重复字符比例过滤。更进阶一点,可以训练一个小型的 perplexity 分类器,把生成文本和正常文本区分开。但这一步新手可以先跳过。

第三道是内容安全过滤。这里不做展开,但要记住:你的模型会学到训练语料里的任何东西,垃圾进垃圾出。公开数据里大量的成人内容、广告和机器生成文本,都需要过滤掉。我用的方案是关键词黑名单加分类模型双层过滤,这一步不要省。

数据量的选择也很有意思。我最终给 157M 模型配的是 3B token。为什么是 3B?一个简单的经验值是——数据量应该是参数量的 20 到 30 倍以上,但这个比例在超大模型上并不成立。实际做的时候,你可以先用一个小子集跑通流程,再逐步增加数据,观察 loss 下降的趋势有没有停。

3.2 亲手训练一个分词器:为什么它比模型结构更影响体感

如果你的任务是中文和代码混合的语料,分词器好坏对你的模型效果影响非常大。我一开始直接用了一个和别的开源模型完全相同的分词器,结果生成中文时 token 效率奇低,同样一段中文,token 数是英文的两倍多。

训练分词器我用的是 SentencePiece 里的 BPE 模式,词表大小设成了 16k。这里有一个关键选择:用 byte-level BPE,还是 character-level 的?我建议直接用 byte-level。这样做的最大好处是永远不会遇到 OOV(词表外词),任意一个 Unicode 字符都能被拆成字节并编码;代价只是 token 序列会稍长一点,但换来的是极强的泛化能力。

训练分词器的一个经验是:语料配比一定要和你预训练语料保持一致。如果你预训练语料 50% 是代码,那分词器训练语料也应该给代码 50% 的权重,这样分词器学到的是代码里常见的 token 分布,而不是一堆自然语言里根本不出现的碎片字符。

另外,我强烈建议在词表里加入几个特殊 token,除了标准的 bos、eos、pad、unk 之外,至少再加一个<|endoftext|>。如果你要做对话模型,还需要考虑 chat 模板的特殊 token,比如<|user|>和<|assistant|>。这个决策要放在分词器阶段想清楚,否则后面改 tokenizer 等于重训整个模型。

3.3 数据混合配比:一份可以直接抄的起点配置

训练语料不是简单地拼在一起。我见过初学者把代码语料和自然语言语料直接 concat,结果一个 batch 里全是代码,下一个 batch 全是英文新闻,模型的梯度更新像喝醉酒一样乱跳。

正确的做法是设置采样权重,也叫数据混合配比。我的起点配置是这样的:

  • 通用自然语言(网页、书籍、百科):60%
  • 代码(GitHub 类语料):20%
  • 数学与科学文本:10%
  • 多语言文本:10%

这个配比不是金科玉律,但它有一个合理的内核:通用语料是基础,代码和数学能让模型获得更好的结构化能力,多语言保证泛化性。实际执行时,我会为每个数据源设置一个采样概率,而不是简单拼接到一起。

还有个细节:数据集混入的顺序也可以做课程学习。初期多放通用语料,后期逐步增加代码和数学比例。我实验下来,这种渐进式混合有助于稳定起始阶段的训练,但收益不算巨大,属于有精力再做的优化项。

4. 模型架构与实现:从零手写 Transformer 的关键决策

到了模型实现阶段,最容易犯的错是:上来就 import 一个大模型库,调一个Trainer开始训练。用现成的库当然高效,但那不是 from scratch。我建议在项目的第一版,至少要把模型的前向传播和训练循环亲手写一遍。

4.1 架构选型:Decoder-only + RoPE + RMSNorm + SwiGLU

当前大语言模型的默认架构已经相当收敛,我的选择和主流开源模型基本一致:decoder-only 的因果语言模型。原因很简单,自回归式生成是目前经过验证最稳定、最简单的范式。Encoder-decoder 在翻译任务上有优势,但通用能力和训练稳定性不如 decoder-only。

组件选型方面,我也站在了主流一边:

  • 归一化用 RMSNorm 而不是 LayerNorm。RMSNorm 去掉了均值中心化,只保留均方根归一化,计算量更小,训练效果和 LayerNorm 几乎持平。
  • 位置编码用 RoPE(旋转位置编码)。它通过旋转矩阵把位置信息注入注意力计算,天然支持相对位置表达。相比绝对位置编码,RoPE 在推理时能更好地适应比训练更长的序列。
  • 激活函数用 SwiGLU。纯粹的 ReLU 在 Transformer 中表达力偏弱,而 SwiGLU 是 gated 结构,能带来稳定且可感的收益,代价是多了一点参数量。我当时的参数表里,FFN 的中间层维度和 hidden size 的关系是按 SwiGLU 的公式算的,不是随便设的。

4.2 一个最简可运行的 Transformer Block 骨架

这里给一个极简的核心骨架,它不是一个完整可训练的代码,但能让你一眼看清模块结构。我用 PyTorch 写,方便和nn.Transformer里的概念对照。

import torch import torch.nn as nn import torch.nn.functional as F class RMSNorm(nn.Module): def __init__(self, dim, eps=1e-6): super().__init__() self.weight = nn.Parameter(torch.ones(dim)) self.eps = eps def forward(self, x): rms = torch.sqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) return x / rms * self.weight class SwiGLU(nn.Module): def __init__(self, dim, hidden_dim): super().__init__() self.w1 = nn.Linear(dim, hidden_dim, bias=False) self.w2 = nn.Linear(hidden_dim, dim, bias=False) self.w3 = nn.Linear(dim, hidden_dim, bias=False) def forward(self, x): return self.w2(F.silu(self.w1(x)) * self.w3(x)) class TransformerBlock(nn.Module): def __init__(self, dim, n_heads, hidden_dim): super().__init__() self.attn_norm = RMSNorm(dim) self.attn = nn.MultiheadAttention(dim, n_heads, batch_first=True) self.ffn_norm = RMSNorm(dim) self.ffn = SwiGLU(dim, hidden_dim) def forward(self, x, mask=None): # 注意这里为了演示省略了 RoPE 的实现, # 实际应在 attention 的 Q、K 上注入旋转位置编码。 x = x + self.attn(self.attn_norm(x), self.attn_norm(x), self.attn_norm(x), attn_mask=mask, need_weights=False)[0] x = x + self.ffn(self.ffn_norm(x)) return x

看到这个骨架,你应该能直观理解两件事。第一,每个子层都是“残差连接 + 预归一化”的结构。归一化在残差分支之前,这个顺序在现代 LLM 中几乎已经是标准。第二,FFN 和 Attention 都被包裹在残差里,所以整个网络的主干是一条恒等路径。这保证了深层网络的梯度可以顺畅回流,是训练稳定性的结构基础。

我自己第一版写的代码远比这个复杂,还加了 attention mask、KV cache 的 switch,以及 RoPE 的矩阵计算。但如果你把它拆到这个骨架的粒度去理解,后续看任何开源模型的源码都会轻松很多。

4.3 初始化和超参数:论文里往往不写的那张表

模型能跑起来之后,决定训练顺不顺的往往是几个不太起眼的参数。我最开始就是随意初始化,结果模型一度训不懂。

先看初始化。我用了 GPT-2 风格的初始化方案:所有 Linear 层权重用均值为 0、标准差为 0.02 的正态分布初始化;残差分支的输出层用一个更小的标准差,公式是0.02 / sqrt(2 * num_layers)。这样做的理由是:层数越深,残差累加的量越大,如果每层都用同样的幅度,到深层时梯度会过大。缩小残差分支的初始化幅度,能保证整个网络前向传播的方差维持在某个稳定范围。

再看优化器。LLM 训练的默认选项基本就是 AdamW。但一个细节是 AdamW 的 epsilon——很多框架默认是1e-8,但在大模型训练里,我实际更倾向于1e-5到1e-6。一个更小的 epsilon 会导致数值稳定性变差,容易遇到 loss spike。

学习率方面,我的起点是峰值 3e-4,配合 1% 的 warmup 步数,然后 cosine 衰减到峰值的 1/10。这个设置对 157M 的模型非常稳,但如果你发现 loss 波动剧烈,可以降到 1e-4。另外,权重衰减我设的是 0.1,是针对非 bias 和 non-norm 参数的。

最后是一个不写进论文的实战指标:如果你的 loss 在训练初期完全不降,先别折腾模型结构,按“数据问题 → 学习率问题 → 代码 bug 问题 → 架构问题”的顺序排查。我第一次遇到 loss 不动,花了两天检查 attention 实现,最后发现只是数据 loader 里文本顺序写错了。

5. 预训练实战:让 loss 曲线的每一步都能被你理解

预训练是整个 from scratch 项目里最磨人也最需要直觉的阶段。你面对的是一块黑屏,唯一的反馈是每几百步打印一次的 loss 数字。一个好的工程师,得能从这些数字里读出整个训练系统的健康状况。

5.1 训练稳定的三件套:调度、裁剪、精度

我第一版训练脚本的三个关键配置,直接决定了我后面能不能顺利睡觉。

学习率调度上,我采用 “warmup + cosine decay” 的组合。warmup 让学习率从 0 缓慢升到峰值,避免一开始就对随机初始化的参数施加过大更新。cosine decay 让训练后期步长逐渐变小,有助于收敛到更平滑的极小值。warmup 比例我设为总步数的 1%,峰值学习率 3e-4。

梯度裁剪是第二道保险。我设max_grad_norm = 1.0。不要听别人说“梯度裁剪会让训练变慢”,实际不会。它只是把异常大的梯度拉回正常范围,防止一步更新把参数甩到未知区域。遇到偶发 loss spike,它就是你最好的防御。

精度策略是第三道,也是最容易踩坑的一环。在 NVIDIA 显卡上,bf16 几乎总是优于 fp16。fp16 的数值范围窄,loss 稍微大一点就会溢出成 NaN。bf16 牺牲了精度但保住了范围,对大模型训练来说,指数范围比尾数精度重要得多。我一开始用 fp16,稳定踩了几天 loss spike,换成 bf16 后世界清净了。

5.2 怎么读 loss 曲线:几种典型病态模式的判断

普通的教程只告诉你“loss 下降就是好”。但实际上,loss 曲线有几种典型形态,每一种都对应不同的问题。

正常形态:loss 平滑下降,斜率逐渐趋缓。训练初期(前 10% 步数)下降最快,后面越来越慢。这表示数据、模型、调度器都在正常运作。

平台形态:loss 在某个值附近横盘不动。先别急着改模型,检查数据循环是否在循环同一个文件。我遇到过数据 shuffle 写错导致每个 epoch 看到完全相同的顺序,模型陷入局部循环。这种问题的特征是 loss 在学习率 warmup 结束后就不再变化。

反弹形态:loss 下降到某个点后突然上升。大概率是优化器状态被破坏或数据里混入了异常样本。checkpoint 后重新加载训练通常能解决。

发散形态:loss 变 NaN 或直接冲到几百。检查三件事——梯度裁剪有没有生效、bf16 有没有被意外关掉、输入数据里有没有大量非 UTF-8 字符。

5.3 中间评估:除了 loss,人眼抽查才是最靠谱的评估

到训练中期,我开始每隔一定步数做一次“人眼抽测”。具体做法是:从验证集里抽几条开头,用当前 checkpoint 做 continuation,肉眼观察输出是否连贯、有没有复读、有没有中文英文夹杂。

这一步的灵感来自一次惨痛经历。当时 loss 已经降到 3.0 以下,看起来一切正常,但我抽测了几条中文样本,发现模型在中文输入后回应的是英文。检查分词器才发现,我的语料混合比例里英文占比过高,中文 token 几乎被边缘化了。

如果你的模型出现严重的复读现象,几个常用的缓解手段是:降低采样温度、增加重复惩罚、限制上下文长度。但如果是预训练阶段就严重复读,更可能是训练数据里重复片段太多,需要回到数据清洗阶段补一刀。

6. 从“会续写”到“会推理”:后训练与推理能力的打磨

预训练结束,你得到的模型本质上是“一个超强自动补全工具”。它会续写,但不会回答问题。从“续写”到“对话”,中间隔着后训练。而如果要走向“推理模型”,还需要专门的力量。

6.1 SFT 指令微调:让模型学会“回答问题”

SFT(有监督微调)的第一步是准备指令数据。一个好的初始配置是:公开的 SFT 数据集(比如 Alpaca 风格或 OpenOrca 风格),加几十到几百条你自己写的领域数据。重点永远在质量,不在数量——2000 条手工筛选的高质量数据,效果远好于 20 万条爬虫数据。

数据格式上,我统一用 chat template。每条样本是 role 为 user 和 assistant 的对话,SFT 时只对 assistant 部分的 token 计算 loss,user 部分只作为上下文输入,不参与梯度更新。这一点非常关键,否则模型会学会“复述用户问题”,而不是学会回答。

训练技巧上,全参数微调在 157M 规模是可行的,显存占用不算大。但如果你的 base 模型到了 1B 以上,我更推荐 LoRA。LoRA 并不会让效果一定变差,它的核心价值是省显存、省时间,以及方便做多个领域的试验。

6.2 让模型学会“思考再答”:推理能力 from scratch

如果你关注最近开源社区“build a reasoning model from scratch”的热点,一定会好奇:推理能力到底是怎么来的?

一个直觉而有效的做法是:让模型在输出最终答案之前,先输出一段推理过程。这就是 CoT(Chain of Thought)。为什么它有效?我的理解是,它把原本必须在“隐空间”里完成的中间计算,显式写到了 token 序列里。模型的每一次自回归生成,本质上都是“计算一步”,把中间步骤写出来,相当于给模型扩容了计算深度。

数据从哪里来?常见做法有三种:

  • 直接用公开的 CoT 数据集。这类数据集里有大量“先推理再作答”的样本。
  • 自己构造合成数据。你可以用既有的强模型生成带步骤的答案,但要注意合规问题,不能未经授权使用受保护的数据。
  • 半合成方法:拿纯答案数据,用小模型或规则先生成推理过程,再人工筛选。

我在做 from scratch 项目时,用的是一种很轻量的“思考前缀”方案。做法是在推理数据中,把模型生成强制分成两个阶段:先用若干 token 写reasoning区域,再写final answer区域。训练时,模型被引导在回答前先“思考”。这不需要复杂的强化学习,只是数据格式设计的改变,效果却立竿见影。

6.3 偏好对齐:DPO 是门槛最低的一步

再往下走,就是偏好对齐。传统 RLHF 需要训练奖励模型、跑 PPO,工程复杂度太高。DPO(直接偏好优化)的诞生,把这一步简化到只需要正负样本对。

DPO 的原理不复杂:它不需要单独训练奖励模型,而是直接用偏好数据,在原有 SFT 模型上做一次对比优化。你只需要准备 chosen(好回答)和 rejected(差回答)的数据对。训练时,模型被推动去提高 chosen 的概率、降低 rejected 的概率,同时用一个参考模型约束住更新的范围,防止模型被带偏。

我的建议是:SFT 做完后不要直接部署,补一轮 DPO。我自己做过对照,同样的 base 模型,SFT 加 DPO 之后的回答“礼貌度”和“稳定性”都明显好于纯 SFT。

如果之后你想做更接近 R1 风格的强化学习路线,比如用 GRPO(Group Relative Policy Optimization)做推理奖励优化,那 DPO 这一步就是最好的热身。它让你先理解“偏好数据如何影响模型行为”,然后再进入更复杂的强化学习循环。

7. 部署与评估:怎么证明你的模型不是“自我感觉良好”

最后一个阶段,也是所有 AI engineering 项目的终极拷问:它真的可以吗?这里需要把评估和部署分开看,因为评估回答的是“模型能力”,部署回答的是“能不能稳定提供服务”。

7.1 评估集的选择:别只用困惑度,也别只信跑分

预训练阶段看 loss,后训练阶段如果还只看 loss,那就容易被表面数字欺骗。损失函数下降并不等于任务能力提升,这是 LLM 训练里最经典的一个陷阱。

我的评估矩阵分了三个层次:

  • 基础层:语言建模困惑度。这是最弱的指标,只能告诉你模型“有没有学会语料分布”,不能告诉你“懂不懂”。
  • 任务层:用开源评测工具跑 few-shot 基准,比如 MMLU、HellaSwag、ARC 这类。对这个领域比较陌生的话,先去了解lm-evaluation-harness这个工具,它是目前生态里最主流的评估框架。
  • 应用层:针对你的使用场景设计几十条真实测试题,人肉逐个看回答质量。比如我的目标是对话助手,就准备 50 条语气、知识、安全边界相关的测试题,每条都人工打分。

这里必须踩一个坑提醒:评估基准的数据很可能在你预训练语料里出现过,这叫“评估污染”。如果 benchmark 分数很高,但真实对话常识错误百出,先怀疑污染,再怀疑评估方式。

7.2 部署的最小闭环:从 Python 推理到生产级服务的跳跃

157M 的模型,直接用 PyTorch 写推理也不是不行,但一旦要考虑并发和服务化,就要走标准路线。

第一步是量化。我通常先用bitsandbytes做 4bit 量化,或者用 GPTQ/AWQ 做离线量化。对 157M 模型来说收益不明显,但到 1B 级别,量化能把显存占用减半以上。

第二步是推理引擎。如果你的服务是单用户、低并发,直接写一个简单的 FastAPI 调用模型生成就够了。但如果是多用户、高并发,强烈建议上 vLLM 或 TGI。它们实现了 PagedAttention 和 continuous batching,吞吐量比朴素 PyTorch 推理高一个数量级。我实测同样一个模型,裸推理每秒处理 5 个请求,vLLM 可以到 50 个以上,代价只是一个额外的依赖和几行启动代码。

第三步是上线前的安全边界。包括:输入长度限制、输出 token 数限制、敏感词过滤、以及拒绝服务时给用户的“安全回答”。这些属于工程细节,但往往决定你的模型是“演示品”还是“产品”。

7.3 我现在回头看,这些坑是最值得提前知道的

整个 from scratch 项目做完,我复盘了那些最消耗时间的坑,集中写在这里,希望能帮你跳过。

第一个坑是分词器语料和预训练语料不一致。我最初用纯英文语料训练分词器,换到中英混合语料时发现中文 token 效率极低,重头训练了分词器才好转。现在我的流程是:先确定最终语料配比,再用同样的配比训练分词器。

第二个坑是 fp16 训练导致 loss spike。前面在精度策略里已经说过,这里再强调一次,直接使用 bf16 作为默认精度。

第三个坑是评估集污染。我在 benchmark 上看到一个虚高的分数,后来抽样测试才发现很多测试题在训练语料里有原文。从那以后,我养成了“任何数据集进训练之前,先和评估集做一次 overlap 检查”的习惯。

第四个坑是贪多求大。第一次做 from scratch 的朋友往往一上来就想训 1B 模型,结果连 loss 都不下降,排查成本极高。我更推荐先用 19M 模型跑通整个 pipeline,再逐步放大。

如果你完整走一遍这条路——数据清洗、分词器、模型实现、预训练、SFT、DPO、评估、部署——你得到的不仅是一个模型文件和一份训练日志,而是一套能迁移到任何模型项目上的工程直觉。以后再看到任何新模型发布,你的第一反应会从“卧槽好强”变成“它的数据是什么?训练配置是什么?评测是怎么做的?”。这种思维方式,才是 from scratch 这个项目给一个工程师留下的真正资产。

返回列表