ARTICLE DETAIL

资讯详情

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

从零构建AI推理模型:数据、架构、训练与部署全链路实践

从零构建AI推理模型:数据、架构、训练与部署全链路实践 1. 从零不等于返祖先拆清楚一条AI流水线的每一根链条先讲个真实的场景。上个月有个朋友问我说想入门AI工程是不是买两门课、会调几个接口就能上手。我说能但那充其量是AI用户不是AI工程师。真正的AI工程尤其是从零起步的那种核心在于你要亲手把整条流水线建起来——数据从哪来、文本怎么变成数字、模型结构长什么样、训练怎么不炸、训完怎么跑起来、跑起来怎么不被用户吐槽。这套链路非常长比写一个普通软件长得多。ai-engineering-from-scratch里最关键的词其实是from scratch。我见过太多人误会这个词以为是让你不用框架、手写矩阵乘法那叫返祖不叫工程。我的理解是不加载任何现成的预训练权重不依赖别人打包好的模型服务而是从公开数据、从一行行代码开始把一个能用的模型真正跑起来。PyTorch、Hugging Face这些工具该用就要用它们是锤子和螺丝刀核心是你自己有没有能力把零件组装成一台能运转的机器。从零构建一条AI工程流水线到底包括哪几根链条我一般这么跟人拆数据链路数据采集、清洗、去重、分词tokenization这一步决定模型能学到什么。模型链路网络结构设计、初始化、参数量估算这一步决定模型的天花板。训练链路损失函数、优化器、学习率策略、分布式训练、断点续训这一步决定你得跑多少次实验。对齐链路指令微调SFT、偏好优化DPO/RLHF这一步决定模型听不听话。推理链路采样策略、KV Cache、推理服务化、评测这一步决定用户实际体验。和普通软件工程最大的区别在哪普通软件的bug是确定的你改一行代码行为立刻变但训练模型的时候改一个参数你看到的可能是三个小时后loss曲线斜率变化了。整个过程充满不确定性你得学会跟概率、跟分布、跟玄学打交道。也正因为这样能把整条流水线都亲手走过一遍的人对AI项目的判断力是完全不同的看到项目需求你能立刻估算出数据够不够、模型多大合适、训练要多久、推理成本多高而不是糊里糊涂地把一个7B模型丢到单卡上去跑然后发现显存炸了。这篇文章我想按我实际做过的项目路线把从零构建一个推理模型reasoning model的完整过程拆给你看。注意不是讲怎么下载Llama权重然后微调而是讲怎么从一堆文本语料出发训练自己的tokenizer搭建自己的模型训练它具备类似慢思考的能力最后把它部署成服务。这条路线不长但每一个环节都会精确地打在你的认知盲区上。2. 数据工程真正拉开差距的是清洗策略与配比设计很多人一想到从零训练模型第一反应是模型结构第二反应是训练卡贵不贵。但实际上我在自己动手之后最大的感受是——数据才是那个真正的主战场。模型结构可以有无数开源参考但数据质量只能靠你自己把关。同一个架构喂什么样的数据训出来就是完全不同的东西。2.1 数据从哪来公开语料与自建推理链数据第一步预训练语料。这个方向上的公开选择不少我列一下常见的好用选项数据集规模特点RedPajama-V2约30TB原始文本覆盖面广但需要大量清洗FineWeb / FineWeb-Edu1.3T token以上清洗质量较高Edu版偏知识含量SlimPajama约627B tokenRedPajama清理版规模适中OpenWebText约40GB小规模实验利器跑通流程足够C4 / mC4约750GB / 数TB经典通用语料多语言可选如果是第一次从零跑通全流程建议不要贪大求全。我记得自己第一次做的时候脑子一热下了个超大语料包结果清洗脚本全速跑了两天都没跑完显卡在一边闲着。后面学乖了小规模验证流程先拿OpenWebText这种42GB量级的东西跑通五步采集→清洗→过滤→分词→构造样本然后再考虑上规模。从零为了学不是为了一口气练出GPT-5完全没必要一上来就烧一个月算力。比预训练语料更关键的是推理链数据CoT data。既然是reasoning model你的模型必须学会分步骤推理。这个能力没法靠让模型读维基百科的文章自然长出来你得在预训练或微调阶段显式地喂给它大量先想后答的样本。自建CoT数据最常规的姿势是从数学题和逻辑题上做GSM8K、MATH、AQuA这些公开数学数据集题目自带标准答案你只要把答案改写成带思考过程的格式。我在Karpathy的一个教程里学到过一个非常实用的构造思路后来自己也用了很多次把数学题的答案重写成第一遍先形成困惑感→拆解条件→逐步推导→验证这样的显式推理过程。具体到数据格式上我是这样设计的|prompt|A farmer has 3 cows and buys 5 more. How many cows in total?|reasoning|Let me think step by step. The farmer starts with 3 cows. She buys 5 more, which means we need to add 5 to the starting count: 3 5 8. I should double check: 3 cows plus 5 new cows gives exactly 8 cows.|answer|8 cows|end|2.2 清洗与过滤垃圾进垃圾出公开语料的真实状态用脏乱差来形容一点都不夸张。我清洗OpenWebText的过程中发现里面有大量HTML残留、连续的乱码字符、重复文章、甚至半个被截断的网页文本。清洗管线我一般按下面几道来格式清理去掉HTML标签、Markdown标记如果你不需要、URL把断行归一化。语言过滤用小规模的语言识别模型按语种分类只保留目标语言避免杂语污染。质量过滤计算文本的困惑度用一个现成的小模型判分、句子长度分布、重复度。重复度这个很关键——网上到处都是SEO批量生成的垃圾文章它们的n-gram重复率高得吓人。去重推荐用MinHash/LSH做实义去重不然相同内容反复出现在数据集里模型会被复读机化。这一套跑完通常能剩下原料的四成到六成。数据从42GB洗到25GB是很正常的事别心疼。好数据永远比大数据重要。2.3 配比设计预训练、指令、推理链怎么掺一个模型要既博学又听话还会思考靠单一数据集是不够的。我在实践里用的配比大约是这样70% 预训练语料负责语言能力和世界知识。15% 指令数据负责让模型理解用户在问什么、该怎么响应也就是SFT阶段用的问答对。10% 推理链数据负责让模型养成先思考再回答的习惯。5% 纯正确性数据比如数学题的简答版、代码题的可运行解法帮助模型建立标准答案的感觉。配比最大的作用是防止灾难性遗忘。以前我做过一个傻事先预训练得好好的然后拿一堆指令数据微调结果模型变成只会答题、不会接续文本的残疾人。所以从预训练到微调数据配比要一点点过渡预训练语料要始终占大头让模型在学会新能力的同时别把旧知识全忘掉。3. 模型架构与tokenizer小模型大讲究每一层都有它的数学直觉说句实在话现在这个时代自己发明模型结构是最不划算的。Transformer这个架构经过几十万篇论文验证直接站在巨人肩膀上就好。从零做模型真正的价值在于你能亲手把每个组件写出来理解它为什么存在而不是调一个AutoModel.from_pretrained就完事。3.1 先做tokenizer再做模型词汇表大小是第一个决定我见过不少从零项目一上来就调模型结构调了一半才发现词表没定embedding矩阵尺寸写不出来。tokenizer其实是整个工程的地基它直接决定两个指标文本被切成多少块影响序列长度、嵌入层的规模影响参数量。常用方案是字节级BPEByte-level BPE。为什么要字节级因为它能处理任何语言的任何字符包括表情符号和特殊符号不会遇到词表外字符OOV的问题而且英文单词和常见子词能被高效压缩。训练tokenizer直接用Hugging Face的tokenizers库就行关键参数就三个词表大小我建议小模型用8k-16k。词表太大embedding矩阵占的参数量在整个模型里占比过高不划算太小则文本碎片太多序列被拉长训练成本上升。特殊token至少要有|endoftext|文档分隔、|prompt|用户输入、|reasoning|思考开始、|answer|答案开始。这些token在训练和推理时都承担着格式边界的作用相当于代码里的缩进非常值得提前设计好。分词正则化用预设的规则把连续空格、标点边界处理干净防止训练出来的词表里混进半个句子这种垃圾token。3.2 手写一个可训练的迷你Transformer下面是我自己用来做小模型实验的骨架经过了删改结构上保留了当前主流开源模型都在用的几件套RMSNorm、旋转位置编码RoPE、SwiGLU激活、分组查询注意力GQA。这几样是有数学逻辑的不是堆砌RMSNorm替代LayerNormLayerNorm要算均值方差RMSNorm只做缩放少了一半计算量实验里效果几乎没差训练还能更快收敛。RoPE旋转位置编码在attention的QK向量里注入旋转相位模型能天然理解相邻位置和相对距离比绝对位置编码在处理长文本时更稳。SwiGLU激活把普通FFN里单一的激活函数换成门控结构用两倍的中间维度做乘积在很多实验里效果比ReLU/GeLU有明显提升。GQA分组查询注意力把KV头分组共享显著减少KV Cache的显存占用和一个部分的计算量推理的时候特别划算。import torch import torch.nn as nn import torch.nn.functional as F class RMSNorm(nn.Module): def __init__(self, dim, eps1e-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, keepdimTrue) self.eps) return x / rms * self.weight class RotaryEmbedding(nn.Module): def __init__(self, dim, max_seq_len2048): super().__init__() inv_freq 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim)) self.register_buffer(inv_freq, inv_freq) self.max_seq_len max_seq_len def forward(self, seq_len): t torch.arange(seq_len, deviceself.inv_freq.device) freqs torch.outer(t, self.inv_freq) return torch.cat([freqs, freqs], dim-1) def rotate_half(x): x1, x2 x[..., : x.shape[-1] // 2], x[..., x.shape[-1] // 2 :] return torch.cat([-x2, x1], dim-1) def apply_rotary(q, k, cos, sin): return q * cos rotate_half(q) * sin, k * cos rotate_half(k) * sin class Attention(nn.Module): def __init__(self, dim, n_heads, n_kv_heads): super().__init__() self.n_heads n_heads self.head_dim dim // n_heads self.n_kv_heads n_kv_heads self.q nn.Linear(dim, dim, biasFalse) self.k nn.Linear(dim, self.n_kv_heads * self.head_dim, biasFalse) self.v nn.Linear(dim, self.n_kv_heads * self.head_dim, biasFalse) self.o nn.Linear(dim, dim, biasFalse) def forward(self, x, cos, sin): B, T, C x.shape q self.q(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2) k self.k(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2) v self.v(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2) q, k apply_rotary(q, k, cos[:T], sin[:T]) k k.repeat_interleave(self.n_heads // self.n_kv_heads, dim1) v v.repeat_interleave(self.n_heads // self.n_kv_heads, dim1) attn F.scaled_dot_product_attention(q, k, v, is_causalTrue) return self.o(attn.transpose(1, 2).contiguous().view(B, T, C)) class FeedForward(nn.Module): def __init__(self, dim, hidden_dim): super().__init__() self.w1 nn.Linear(dim, hidden_dim, biasFalse) self.w2 nn.Linear(hidden_dim, dim, biasFalse) self.w3 nn.Linear(dim, hidden_dim, biasFalse) def forward(self, x): return self.w2(F.silu(self.w1(x)) * self.w3(x)) class Block(nn.Module): def __init__(self, dim, n_heads, n_kv_heads, ff_dim): super().__init__() self.attn Attention(dim, n_heads, n_kv_heads) self.ff FeedForward(dim, ff_dim) self.norm1 RMSNorm(dim) self.norm2 RMSNorm(dim) def forward(self, x, cos, sin): x x self.attn(self.norm1(x), cos, sin) x x self.ff(self.norm2(x)) return x class TinyLLM(nn.Module): def __init__(self, vocab_size, dim768, n_layers12, n_heads12, n_kv_heads4, max_seq_len2048): super().__init__() self.token_embedding nn.Embedding(vocab_size, dim) self.rope RotaryEmbedding(dim // n_heads, max_seq_len) self.blocks nn.ModuleList([ Block(dim, n_heads, n_kv_heads, 4 * dim) for _ in range(n_layers) ]) self.norm RMSNorm(dim) # tied embeddings输出层复用输入嵌入显著减少参数量 self.lm_head nn.Linear(dim, vocab_size, biasFalse) self.lm_head.weight self.token_embedding.weight self.apply(self._init_weights) def _init_weights(self, module): if isinstance(module, nn.Linear): nn.init.normal_(module.weight, std0.02) elif isinstance(module, nn.Embedding): nn.init.normal_(module.weight, std0.02) def forward(self, idx, targetsNone): B, T idx.shape x self.token_embedding(idx) cos, sin self.rope(T) cos, sin cos.to(x.device), sin.to(x.device) for block in self.blocks: x block(x, cos, sin) logits self.lm_head(self.norm(x)) loss None if targets is not None: loss F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1)) return logits, loss这段代码大概涵盖了1300行左右等效功能放在文章里既可作为入门骨架也能直接跑起来。我特别想强调tied embeddings这个小设计输出层的词嵌入矩阵直接复用输入层的权重一个15k词表、768维的模型能省下约1100万个参数对从零起步的小项目来说省出来的显存可以多喂不少数据。3.3 参数量级和算力预算怎么估算有了架构就要估算要练多大的模型、得租多少卡。这有个经验公式我一直在用参数量 ≈ L层数 × [12 × H² 13 × H]粗略用上面的例子12层768维词表15k大约150M参数也就是0.15B。这个量级在单张24G显存的卡上用bf16混合精度批量大小16序列长度1024显存占用大概10G左右完全可以跑。下面的对照表可以帮你快速选型模型规模参数量训练数据量建议单卡(24G)可行性用途微型实验约150M1-10B token可以跑几天级验证数据管线、调参小型约400M-500M20-50B token勉强需要优化测试推理能力雏形1B级约1B50-200B token需要多卡或云GPU接近可用的reasoning model3B级约3B200B token基本需多卡集群商用级小模型我的建议是第一个从零项目就把目标定在微型/小型区间150M到400M。别嫌模型小你的目标不是超越Llama而是亲手走完全链路。等你真的把一个150M的小模型训到会分步推理、能解简单应用题再去冲刺大模型你会发现流程上的坑你已经全趟过了。4. 训练全流程实操从预训练到推理强化损失曲线就是方向盘训练阶段是整个项目中体验感最玄学的部分。这一阶段你会有大量的时间盯着loss曲线发呆一会儿觉得它在下稳降一会儿又觉得它在摆烂。这里我把从预训练到推理强化整条训练链路拆开讲每一步都写清楚怎么判断成功、怎么做决策。4.1 第一步预训练Pretraining训练语言能力的地基预训练目标就是标准的next token prediction输入一段文本让模型预测下一个token是什么。这一阶段不需要任何人工标注你准备的预训练语料本身就是标签——把每段文本平移一个token就是target了。通用做法是搞个DataLoader把语料切成固定长度比如1024的块每个块做随机偏移保证模型看不到恒定的切分位置否则它可能会学会猜第一节开头这种偷懒策略。训练超参我觉得可以直接抄成熟经验的推荐值不建议自己瞎发明优化器AdamWweight decay设0.1学习率峰值大约3e-4150M模型这个量级先warmup大概2000步再按cosine衰减到峰值的十分之一混合精度用bf16省显存且稳定批量大小累积到等效0.5M token左右比较稳妥小批量会让梯度噪声太大梯度裁剪max_norm设1.0防止个别batch产生爆炸梯度关键是怎么看loss。一个好的loss曲线应该是最初1-2个step内从10快速掉到6-7因为起点相当于随机猜之后随着数据见得多稳定下探。150M模型在10B token上跑到最后train loss大概能到2.8-3.2perplexity在16-22之间说实话这个数值不算低但对于小模型来说已经达标了。我最想提醒你的一点是时刻区分training loss和validation loss。我最初做实验的时候只看train loss觉得好得不得了结果上推理一测模型只会生成训练集里见过的话。后来规规矩矩做了validation set从语料里抽出5%绝不参与训练两个loss的差距就是过拟合程度的晴雨表。一旦val loss开始反弹或长期不降立刻要停不是降学习率的问题是数据不够或者模型太小的结构性问题。4.2 第二步指令微调SFT让模型听得懂人话预训练完的模型本质是个文本续写机。你给它一句今天天气真它最擅长接不错而不是查询一下气温。SFT就是为了解决这个问题。你用大量标注好的「用户问题标准回答」样本继续训练模型。这一步的学习率要比预训练低很多通常降到1e-5到3e-5因为模型已经有很强的权重先验你只需要轻轻推一下让它学会响应格式推太猛会直接毁掉预训练学到的知识。数据格式按我在2.1节里写的模板来|prompt|开头是用户输入|reasoning|和|answer|是模型输出。训练时loss只计算output部分prompt部分的token要mask掉不然模型会学会copy用户问题这种作弊方式。4.3 第三步推理强化RL/DPO慢思考能力的临门一脚这就是所谓build a reasoning model from scratch最特别的一步。如果你只做预训练和SFT模型的思考过程是流水账式的、无目标的——它会写一堆废话然后碰运气给出答案。要让模型真正学会推理-修正-给结论需要在后面加一个强化阶段的训练。这里我推荐对新手比较友好的DPODirect Preference Optimization而不是传统的RLHFPPO。原因很实际PPO要同时维护actor、critic、reward model好几套模型训练过程极不稳定调试起来全是泪。DPO只需要正例和负例用简单的对比损失把好的回答概率push高、坏的push低在中小模型上效果足够成本也小一个数量级。构造正负例数据的方法也没有想象中神秘正例模型自己带CoT完整推理出正确答案的样本。负例同一个问题模型推理过程冗长、跑偏最后答错的样本。训练时给正例更高的可能性、负例更低的几轮下来模型会自发学到一个行为倾向多花token去做推理比直接给结论更能提升正确率。这就是reasoning model和普通chat模型的分水岭。4.4 训练中途的止损与恢复断电、OOM、NaN训练跑起来之后最大的敌人不是模型是各种工程事故。我把自己踩过的几种情况列一下故障表现原因处理办法loss变成NaN学习率过大、梯度爆炸、bf16溢出检查梯度裁剪是否生效、改用bfloat16或fp32降低lrCUDA OOM批量太大、序列太长减小batch用梯度累积补足或打开activation checkpointingVal loss回升过拟合或数据污染看train/val差距如果差太大加数据集或增dropout中途断电显卡过热或云厂商重启训练脚本配好checkpoint和resume_from_checkpoint每500-1000步存一次loss抖动剧烈数据混入异常样本检查是否有空文档或异常长的文档做数据过滤断点续训这个一定要在开工前就做好。Trainer里开个resume_from_checkpoint或者自己每N步存model_state_dict和optimizer_state_dict顺手把随机种子恢复也存下来。我吃过一次大亏1200美元的算力烧到一半云平台因为账单问题把实例杀了结果重启才发现只存了model没存优化器状态learning rate scheduler也要从零重来。那一版白白重跑了50%的训练量。5. 推理、评测与部署模型权重只是半成品训练完成后你手里有了一堆权重文件。这时候离能用的产品还差一大半路程。很多从零项目死在最后这步——模型训出来了但推理效率低、效果无人验证、部署方案没设计最后成了我跑通了我的模型的纪念品。说实话passion没有意义把模型变得可用才是工程师的活。5.1 推理生成从argmax到真正的采样策略最朴素的推理就是每次取概率最高的token这叫greedy decoding。但实际用过都知道greedy出来的文本非常单调且容易陷入重复。reasoning model更麻烦因为它的思考过程本身就有随机性需求。我用下来比较实用的一套参数组合temperature0.7控制随机性0.7是有创意又不乱来的甜区。top_p0.9核采样只从概率累计90%的token里选防止采样到尾巴上的乱码token。top_k40加一道保险限制候选集上限。max_new_tokens要留够思考空间。reasoning model生成的CoT往往比答案本身长好几倍我一般设置生成上限1024-2048还允许它生成完|reasoning|段后再续写|answer|段。建议加上一个截断控制如果模型输出了|end|停下来如果生成了重复循环的三元组比如一段话反复出现三遍强制截断并只保留前面部分。除了节省时间还能防止越到后面滚雪球式地崩溃。生成循环用PyTorch写一个根版本并不难核心代码如下torch.no_grad() def generate(model, tokenizer, prompt, max_new256, temperature0.7, top_p0.9): model.eval() tokens tokenizer.encode(prompt).ids input_ids torch.tensor([tokens]).cuda() for _ in range(max_new): logits, _ model(input_ids) next_logits logits[0, -1, :] / temperature probs F.softmax(next_logits, dim-1) sorted_probs, sorted_idx torch.sort(probs, descendingTrue) cumsum torch.cumsum(sorted_probs, dim0) mask cumsum - sorted_probs top_p sorted_probs[mask] 0 sorted_probs / sorted_probs.sum() next_id torch.multinomial(sorted_probs, 1) input_ids torch.cat([input_ids, next_id.unsqueeze(0)], dim-1) if next_id.item() tokenizer.eos_id: break return tokenizer.decode(input_ids[0].tolist())注意第一行的torch.no_grad()推理阶段不计算梯度能省一大截显存和计算。批量推理时如果你不考虑批次内不同样本的文本长度还得做padding这会浪费不少算力所以很多推理框架用continuous batching动态切块来处理。5.2 推理优化KV Cache是必须迈过的坎不做KV Cache的生成循环有个致命问题每生成一个新token都要把所有历史token重新算一遍注意力。序列越长冗余计算越多。KV Cache的核心思想很简单——把已经算过的Key和Value矩阵缓存下来下次生成时只算新token的Q直接和缓存的K、V做注意力。我上面那个generate循环没写KV Cache是为了保持教学结构清晰。生产级推理必须上缓存效果极其显著1024序列长度下速度提升3-5倍不是问题显存多占用反而可接受。如果你不想手写可以直接用transformers库的past_key_values参数它内部已经帮你缓存好了。再懒一点直接上vLLM这种推理框架它在并发和显存管理上做得很深从零项目到了需要面向用户的阶段就该切过去了。5.3 评测别只信loss要看真实行为训练阶段的loss只能衡量模型学到了多少语言规律衡量不了模型是否好用。我从零项目里总结了一套自己的评测方案量级很小但足够问出我的模型能不能干活。困惑度perplexity在held-out验证集上算长期盯着看趋势。数学应用题准确率从GSM8K抽100题测试重点关注用CoT比对不使用CoT的提升幅度。reasoning model的价值就该体现在这个差值上。指令遵循手写50条有明确约束的问题比如用三句话回答、不要给出例子检查模型的不越界率。重复率指标直接统计生成的连续n-gram重复trigram重复率超过30%就是复读机需要调采样参数或检查训练数据。我见过最多人忽略的是评测集的数据泄露——微调数据里本身包含了评测集的题目那评测分数再高也没意义。所以自我评测前一定检查题目是否在训练语料里出现过。实在嫌麻烦就从GSM8K的测试集test split抽题不要用train split。5.4 部署从脚本到服务的一公里部署这块从零项目的需求通常是让模型可以被网络调用。最简单的方案是FastAPI包一层from fastapi import FastAPI, Request from pydantic import BaseModel app FastAPI() model, tokenizer load_model() # 你的加载函数 class Prompt(BaseModel): text: str temperature: float 0.7 app.post(/generate) async def generate_api(p: Prompt): out generate(model, tokenizer, p.text, temperaturep.temperature) return {output: out}一个接口加几行代码模型就跑起来了。要注意的点是加载模型占显存所以服务应该常驻不要每次请求都重新加载并发上来之后要锁或排队不然GPU显存瞬间爆掉。如果想要吞吐更高就把模型导出成ONNX/GPTQ做量化400M的模型量化成int8后在CPU上都能跑得动这个对小模型项目很友好。6. 我的踩坑清单六个让新手崩溃的细节最后这部分我把整个过程中让我印象最深、也最想让后来人避开的六个坑写下来。它们都不是什么高深问题但每一个都能让你白烧好几个小时的算力和成堆的头发。6.1 坑一tokenizer词表看起来好好的实际上碎了第一次训练完tokenizer我检查我爱你中国这种高频句子完美各种英文单词完美。于是直接开训结果模型loss一直在高位下不来。后面一查发现tokenizer在标点符号和数字处理上完全崩了3.14被切成3、.、14三段还好说最糟的是大量半截词裸露得到处都是整个训练语料被撑长了30%。原因是BPE训练的预处理正则没配对。所以训练完tokenizer务必做一轮批次级抽样检查随机抽100个文本看token切分的碎片化程度如果平均每词token数超过1.5就要重新设计正则或提高合并次数。6.2 坑二用最大显存去定义batch size忽略了梯度稳定性刚开始训练时我图省事直接开了24G显存能塞下的最大batch结果loss飘忽不定学了三天还在原地打转。后来才反应过来batch太大学习率没同步放大等于每个batch的梯度都被大batch洗白了。正确思路是先定需要等效多少token的batch比如目标0.5M token/step用150M模型的话就是512个1024-token的序列显存放不下就拆小梯度累积补足并把learning rate按实际batch size缩放。这个坑的教训显存能装下不代表训练效果最好。6.3 坑三盲目堆长上下文一开始做reasoning model我想着让模型想得更久就把训练序列长度从1024拉到4096。结果显存飙升、训练速度掉了一半实际效果提升却微乎其微。事后分析GSM8K这类题的完整CoT最多800个token1024窗口足够拉到4096就是浪费。正确做法是先统计目标任务的生成长度分布按常见长度30%余量定窗口。长上下文能力以后有了需要再专门练不是一上来就加的属性。6.4 坑四推理时max_new_tokens设得太短这坑特别讽刺——我一边在训练时费尽心思让模型多思考一边在部署时把max_new_tokens设成了128。结果模型只来得及写半截推理或者写了个开头就被截断推理能力直接折损80%。后来我把默认上限调到了1024模型在GSM8K上的准确率肉眼可见地涨了一截。reasoning model的生成长度的确比普通chat长得多给它空间它才肯发挥。6.5 坑五看loss只看了train没看val我已经在4.1节提过一次这个坑但因为它威力实在太大值得再说一遍细节。当时我无比自信地看到一个完美收敛的train loss差不多2.7觉得离成品只差最后一步。结果手动跑推理一测全是在背语料里的片段。后来把验证集加回来才发现val loss比train loss高出来1个多点模型早就进入过拟合通道而不自知。所以从项目第一天起训练集和验证集分开每条数据都要打来源标记这比任何花哨的工具都重要。6.6 坑六忽视数据泄露评测分数是假的最后一个坑和评测相关。我用GSM8K评估模型准确率从35%一路涨到60%高兴得以为自己马上要造出开源推理模型了。后来队友提醒我查一下训练集——数据集里确实有GSM8K的一部分题。当时用的预训练语料本身可能混入了评测内容。修复方法也简单把评测集所有题目的字符串哈希做一遍在清洗数据时直接按哈希过滤。这个坑说明从零做模型不只是训练阶段的事数据在采集那一刻就决定了评测的真实性。如果让我总结这一路上的最大体会就一句话从零构建AI工程真正困难的地方全在那些看起来不用解释的环节——数据的坑、tokenizer的坑、评测的坑。你亲手趟过一遍模型的能力边界、成本结构、失败模式就全在脑子里了。这种手感是任何调用现成模型的方式都换不来的。
返回列表