说起 ai-engineering很多人第一反应是调个 API、拿现成模型做微调。但如果你想明白一个模型是怎么从无到有被训练出来甚至想亲手搭出一条完整的生产链路那才是真正的 from scratch。我去年花了整整四个月从零开始构建了一个领域专用的推理模型——不是做 LoRA 微调而是从预训练数据、模型架构选型、分布式训练到最终部署推理全流程自己控制。这篇文章就是那个过程的完整复盘有选型逻辑、有崩溃排查、有被数据坑惨的经验适合已经会写 Python、懂一点深度学习、想系统掌握 AI 工程全流程的读者。1. 为什么我坚持 from scratch而不是直接拿来就用1.1 零的真正含义不只是从模型权重为零开始很多人理解的 from scratch 是不用预训练权重自己训练一个模型。但实际上真正难的不只是训练代码而是整个工程链路。你需要的是一套数据管线、一套训练基础设施、一套评估工具、一套部署方案。你从 HuggingFace 上拉一个权重只需要十分钟但从零构建一个能稳定复现训练结果的工程需要的是对每个环节的掌控力。我一开始的目标很明确做一个面向内部技术文档问答的模型。市面上的通用模型在很多场景下表现不错但对公司内部那种缩写满天飞、术语混杂的文本效果总是不够好。我之前也试过 RAG但它解决不了模型本身没有内部知识的问题。于是我想干脆从头训练一个小参数规模的模型专门学习这批数据。这里有个反直觉的结论模型架构只占效果的 10% 左右数据和训练管线决定剩下 90%。我在项目前两周把大部分时间花在了模型结构上后来发现效果提升最快的阶段全是在数据预处理和清洗环节。1.2 这个从零到底值不值先说我踩的第一个大坑高估了自己的时间预算。我原计划一个 1.5B 参数的模型用单机多卡训练两周。结果光是数据清洗和 tokenizer 训练就花了一周训练过程中又因为梯度爆炸和显存管理问题折腾了四天最后实际花费是预期的两倍。但值不值不能只看时间要看目标是什么。如果你是想要一个开箱即用的模型直接下载 Llama 或 Mistral 微调就够了。但如果你想培养模型出问题能自己解决的能力——比如 loss 突然飘了、推理速度不达标、显存烧爆了——from scratch 是一个无法绕开的训练场。我在这四个月里学到的东西比我过去一年做微调和 API 调用加起来都要多。# 一个最简的 from scratch 工程目录参考 project/ ├── data/ # 原始数据、清洗脚本、tokenized 缓存 ├── configs/ # 模型参数、训练超参数、分布式配置 ├── models/ # PyTorch 模型定义基于 transformers 库 ├── trainers/ # 训练循环、分布式封装、混合精度 ├── evals/ # 验证集、指标计算、生成质量评估 └── scripts/ # 一键训练、一键评估、一键导出2. 模型选型框架、架构、硬件这三件事必须一起决策2.1 框架选型别纠结PyTorch 在研究和工程兼容性上依然是首选有人问我为什么不用 TensorFlow 或 JAX。我的答案很简单PyTorch 的调试体验对新手最友好而且 HuggingFace 生态几乎全量基于它你要做任何改动都能找到参考。TensorFlow 的 tf.data 管线在超大吞吐量的生产环境确实有优势但那是工程化的优势对 research 或者中小规模项目来说PyTorch 的动态图特性让你的注意力集中在模型逻辑上不用和静态图编译器搏斗。JAX 我也试过几天它的jit和gradient设计确实优雅但在分布式训练中遇到一个环境配置问题社区的现成方案明显比 PyTorch 少。如果你不是专门做 Pytorch/XLA 和编解码层面的研究我建议别在框架上浪费时间。性能瓶颈从来不在框架本身而在数据管线和 GPU 利用率这一点我后面会展开说。2.2 模型架构从零自己写 attention别自虐但要读懂源码接下来的问题是模型结构怎么搭。如果你只是从网上抄一段 transformer 实现那不算真正的from scratch但如果你要从第一个线性层手写那又过度了。我的建议是使用 HuggingFace 的OPTForCausalLM、LlamaForCausalLM这类官方实现但强迫自己完整读一遍源码尤其是位置编码、attention mask 和 causal mask 的处理逻辑。我最开始决定自己写一个简化版的 transformer结果发现几十行代码就能跑通 forward但一旦加入并行注意力、KV cache、梯度累积各种 index 错误和 mask 错位的 bug 就冒出来了。而官方库的代码经过千锤百炼你只需要改改配置项就能适配不同的数据规模。一个实用的做法是先跑通最小规模的 toy model比如两层 transformerhidden size 128用一段英文文本做 overfit 测试。如果一个模型连小批量样本的 loss 都不能过降到理论值那你加多少数据和工程优化都没用。这一步能帮你快速暴露代码层面的基础错误。模型架构的具体参数怎么定可以参考我下面的经验表这是一套针对单机多卡A6000 或 4090最常用的配置目标参数量layershidden sizeattention heads预估训练显存BF16 混合精度250M1276812约 12GB1B24102416约 28GB1.5B28153612约 40GB3B32204816约 64GB这只是模型权重的估计实际要加上优化器状态、gradient 和 activation通常训练显存大约是权重的 8-10 倍。所以 4090 单卡训练 1B 模型其实很吃力我用的是全精度 4 卡并行才舒服一些。3. 数据和预处理最脏最累的活却决定一切3.1 数据源不是所有网上数据都能无脑爬这里先给个安全提示训练用数据的版权和使用合规问题一定要重视。我用的数据来源主要有三个方向公司内部脱敏文档、公开的学术论文和论文元数据、开源的大规模语料比如 RedPajama、C4 的一个子集。千万不要去爬那些明确声明不可用于训练或者需要授权的网站这不仅是法律风险也会让项目评审阶段直接吹掉。接下来是最关键的清洗工作。我最初用的是一套从网上找来的通用清洗脚本直接跑了一遍然后训练出来的模型效果奇差。具体表现是生成同一句话里面中英文混杂、连续出现无意义的重复片段。后来我逐层排查才发现数据里面密密麻麻的 markdown 标记、HTML 标签、无意义的文件名和路径、重复的段落都没被过滤干净。一份实用的清洗步骤清单去除编码和非法文件扔掉所有非 UTF-8 编码文件所有检测为乱码的段落直接丢弃。统一全角和半角中文还是用全角标点英文用半角避免 tokenizer 把。和.拆出两种 token。语言判定用 fasttext 的 language identification 过滤掉非目标语言或混合语言过多的段落。去重 去近重复不只是精确去重我在 simhash 的基础上加上了一个 64 位哈希的近邻搜索把相似度超过 0.8 的文档全部保留一份。语义过滤用一个小型分类器识别并移除色情、暴力、政治敏感内容这不能依赖关键词黑名单模型层面的过滤要全面很多。3.2 Tokenizer 训练直接用现成的还是自己训没有比 tokenizer 更像垃圾进垃圾出的环节了。我一开始直接用了主流开源模型附带的 tokenizer训练数据是中文技术文档结果模型在生成时总是把Transformer切成一个 token而transformer切成两个导致同一个概念在不同大小写场景下表示为不同 token这严重影响了模型的学习效率。正确的做法是针对你的目标数据统计词频再基于该词频训练一个 tokenizer。HuggingFace 的tokenizers库提供了train_from_iterator的接口你可以用 BPE 算法把词库大小设置在 32K 到 64K 之间同时强制加入应对领域里所有重要词汇的小写变体。训练 tokenizer 的代码只需要几十行但你一定要跑一个统计报告看看 top 100 高频 token 是否合理。比如人工智能、Transformer、损失函数这些领域词在不在里面。如果这些词被拆开放了你就要调低teacher forcing的起步教育或者说加大数据重复。预处理还牵扯到另一个问题样本长度策略。模型的 context length 我设为 2048所以每条训练样本按段落切分并拼接但不强制截断句子。如果你在句子中间直接截断模型就会在边界上的语义完整性上吃亏生成质量肉眼可见地掉。我采用的做法是def make_train_samples(tokenized_lines, seq_len2048): buffer [] for line in tokenized_lines: # 句子级别的 token 序列 buffer.extend(line) if len(buffer) seq_len: sample buffer[:seq_len] buffer buffer[seq_len:] yield {input_ids: sample, attention_mask: [1] * len(sample)} # 最后不足一个完整 seq_len 也保留 if buffer: padded buffer [pad_token_id] * (seq_len - len(buffer)) yield {input_ids: padded, attention_mask: [1] * len(buffer)}注意不要用整段 document 直接做样本而要用按语义段落切分后的 buffer 拼装。这样既不会把长文档直接截断到 2048 损失大量信息又能保证上下文在文档之间的自然过渡。4. 训练崩溃现场实录从 loss3.2 到模型开始说话4.1 开场就崩Loss 不降谁都会慌我训练开始几个 steploss 从基线值一点不往下走我一度怀疑模型结构写错了。排查了半天最后发现原因是我忘了给 embedding 层设置正确的初始化范围导致部分词向量太大软最大化之后输出分布过于均匀。解决方法很简单把 embedding 的权标准方差保持在一个合理的sqrt(1 / hidden_size)量级或者干脆用官方库默认的初始化。如果你也遇到 loss 不降可以先做个快速诊断跑一个极小的样本比如 100 条看是否过拟合到接近 0。如果过拟合正常说明 model 代码没问题你要去查数据管线和批次构造检查 learning rate 是不是太大。1B 左右的模型常用峰值学习率在1e-4到3e-4之间配合 warmup 和 cosine decay。如果我直接上1e-3通常会在早期导致 NaN 或震荡检查 attention mask 和 token embedding 是否对齐。我试过手写样本拼装结果shifted之后标签总是错位一格模型一直在学乱序映射。4.2 显存溢出和梯度爆炸绕不开的恶魔单卡显存肯定不够我最后用 4 张 48GB 的 A6000总算在 1.5B 模型训练上站稳了。但 4 卡的显存规划也有讲究。经验表里说的权重 8-10 倍是人类训练的共识我放一个真实数据我的模型权重约 3.7GB但每张卡占用大约 52GB包含了 frozen weight、optimizer states我用 AdamW 时占了最大的空间、gradient 和 activation。显存不够的第一反应是换成混合精度bf16。大多数情况下能直接减半但如果模型已经是你显存极限的 1.5 倍那就只能用两种方案gradient_checkpointing或activation_checkpointing用时间换空间每层的激活值不保存在显存里反向传播时重新计算。这个操作可能让训练时间增加 30% 到 50%但它能让你在同样的卡上塞下一倍的模型值得。注意需要同时启用use_cacheFalse。减少 batch size 并用梯度累积。我把 global batch size 保持在 256通过 4 个 step 累积完成。一个重要的纪律是梯度的共方差所以如果你使用梯度累积norm 的计算要对累积的梯度统一进行否则训练波动会很大。梯度爆炸出现在大概第 3000 步。我当时的学习率已经降下来了但 loss 突然飙到nan。排查到最后发现是torch.nn.utils.clip_grad_norm_的 max norm 设置得太大我设了 10.0加上 lr schedule 里 warmup 太短参数在某一步更新量过大。密码是max norm 不要超过 1.0对 1B 以上的模型我建议设 0.5同时 warmup 步数不少于全部步数的 5%。4.3 分布式训练的闹剧DDP vs DeepSpeed多卡训练我一开始用的是torch.nn.DataParallel方便是真的方便但跑起来四个 GPU 利用率参差不齐其中一个 GPU 变成数据加载瓶颈。后来我换到DistributedDataParallel配合accelerate库这才是真正意义上的多进程并行。更深一层的需求是模型并行和优化器切分。如果你在 1B 以上模型里想榨干每组卡那正好到了DeepSpeed显身手的时候。我的配置经验是deepspeed_config: train_batch_size: 256 gradient_accumulation_steps: 4 fp16: enabled: false bf16: enabled: true zero_optimization: stage: 2zero stage 2会把优化器状态分片到多卡显存利用率从 42% 提升到 70% 左右。但是要注意模型收敛行为可能和单卡 training 有细微差别评价指标要多盯一段时间避免 DP 带来的 seed 与梯度更新顺序变化让你误以为模型变好了。5. 评估与假收敛分辨感受一下模型真正学会了什么5.1 别只盯着 Perplexity要建一套能反映真实使用的评估集训练过程中很多人的误区是loss 掉到 2.5就觉得模型已经能用了。但 loss 在训练分布内下降和模型在真实使用场景里生成有用的答案是两码事。我在做法上给验证集做了三件小事留出一部分完全没有经过清洗的原始文档作为鲁棒性压力测试。如果训练数据里的格式和符号已经清洗得齐全那模型对噪声格式的表现就是它泛化能力的写照。建立一个小的「任务集」比如一组领域常见问题人工标注标准答案。每个 checkpoint 都用这些 prompt 生成文本再做一次人工打分。不要只算 BLEU 或 ROUGE因为这些指标对长文本生成太不敏感。我更倾向于用 semantic similarity 或者直接让人看 20 条生成结果做五分制打分。你会发现loss 下降一档不如人评分从 2.5 升到 3.5 有意义。我最终的评估分数表格长这样checkpoint训练 loss验证 loss人工评分5分典型错误5000 步2.872.912.1重复句子答案截断15000 步2.642.773.0丢失上下文细节30000 步2.522.693.8偶有编造可接受最后一版2.482.724.1偶尔术语错误有人可能会说最后一版的验证 loss 比 30000 步的还高是不是过拟合了我发现训练 loss 继续下降验证 loss 在 30000 步之后小幅回升但人工评分其实是上升的。这提醒你不要迷信 loss 绝对值和它正常升高的趋势一切以下游任务表现为准则。5.2 推理部署模型跑得起来才算工程闭环训练完的模型如果不部署等于一个 البح 研究成果留在 Jupyter notebook 里。我碰到最大的问题就是推理速度初始的 PyTorch 自回归生成速度在 A6000 上只有大约 7 tokens/s完全无法支撑线下业务。我做的优化路径是这样的优化生成参数temperature0.8top_p0.9关闭采样中的重复惩罚如果明显导致变慢——赋权的位置已经变了开启 KV cache 和推理 batchKV cache 把显存里重复计算的历史 cachedbatch 推理同时喂多个 prompt吞吐量提升到 20 tokens/s导出 INT8 量化模型用torch.compile和torch.int8混合部署到 FasterTransformer 引擎推理吞吐量翻了 3.6 倍质量下降微乎其微。后来我又试了蒸馏到 300M 的 student model质量能保留 80% 以上但是推理速度直接拉到 45 tokens/s。最终我在服务上托管的是量化后的 1.5B 模型显存占用 16GB 左右对于 256 token 的 prompt首 token 延迟 0.8 秒后续每 token 16ms内部 demo 完全够用。6. 工程化落地的几个清醒建议6.1 预算和 MVP 先行的思路如果你参与到预算不宽裕的 side project 里from scratch 很容易变成无底洞。我最后悔的事情是前 10 天全部耗在搭建一个超完善的数据面板可是核心模型还没跑通。后来我把整个 MVP 缩到最小可运行尺寸一个小数据集、一个 250M 的 toy model、一个最简单的前端调用接口用这个跑完一次完整流程后才回头优化性能和数据质量。所以在预算紧张的时候建议你先跑通端到端最小回路再考虑做大数据和模型膨胀。这一步我想做的东西太多最后反而拖慢了节奏。6.2 实验管理做不到可复现等于白干我用的是wandb记录和mlflow做模型表和配置管理。但真正让我意识到这一点的是有一次我把同样的训练代码、同样的参数跑了两遍得到的模型输出却大不相同调了半天才发现是torch的固定随机种子出了问题我调用seed_everything()时会重置全局 seed但因为 DistributedDataParallel 的某些算子产生不定性不同进程之间的for循环执行顺序有差异。最后我只好把每个进程的 seed 设置为base_seed rank并把顺序固定死才算做到可复现。这点是工程经验里最容易被忽略的死角日志记录能帮你发现问题时还有没有回头路。我建议每一项实验至少要记录三样东西完整 config、seed 值、数据版本的哈希。我用huggingface_datasets把数据集版本用 git hash 关联保证以后能查得到。6.3 别执着于完全从零混合路径才是最优解虽然这篇标题是from scratch但我最后还是建议你在某些环节偷懒用开源的 tokenizer 代码、用 HuggingFace 提供的分布式 trainer、用 DeepSpeed 的脚本。真正值得你手搓的只有那两件事数据管线和训练/评估循环。其他的都可以基于成熟实现。我也见到过某位朋友为了完全从零手写 linear 层和 softmax训练速度慢不说数值稳定性都有问题。业内实际的从零构建也不是指从纯数学起始而是指从数据源而不是现成模型权重出发构建一套你自己能完全控制、可复现、可解释的 AI 工程流水线。这就是现阶段行业对ai-engineering from scratch比较务实的理解。四个月下来我觉得最值钱的不是那个 1.5B 模型本身而是我终于建立了模型出了问题我能从数据、代码、环境、超参四个维度去定位的能力。如果你也想尝试别纠结太多理论把第一个小模型跑起来然后让报错带你去下一站。