如果你最近在尝试跑通一个大模型训练项目,可能会发现一个尴尬的问题:网上教程很多,但几乎每一篇都只讲某一个环节。要么告诉你“数据要清洗”,却不给清洗后的格式;要么给你一段训练脚本,但环境一换就跑不起来。真正动手去搭一个“公开训练全流程”,从数据到模型发布,往往要踩十几轮坑才能跑通。
这篇博客就以一个名为 Marin 的训练项目为例,把一条可以复制的公开训练全流程拆开来讲。我的判断是:公开训练的真正难点不在模型结构,也不在单条命令,而在于数据、训练、评测、发布这几个环节之间的衔接。文章会从环境准备开始,一步步走到模型导出,尽量把每一步为什么这么做、做错会出现什么问题讲清楚。
如果你正准备训练自己的模型,或者团队需要把训练过程标准化、开放给更多人复现,这篇文章值得你收藏。
1. 为什么要关注公开训练全流程:先解决一个“工程化”问题
先定义一个概念:公开训练,并不是把训练脚本丢到 GitHub 上就叫公开。它至少包含这几层含义:
- 数据是开放的,别人能理解数据来源、清洗规则和格式。
- 训练过程是可复现的,依赖、版本、随机种子、超参数都要可追踪。
- 结果是可验证的,评测方式、指标口径、对比基线都要明确。
- 发布物是完整的,包括模型权重、tokenizer、配置文件、推理示例。
很多人第一次接触公开训练时,会误以为只要单机跑通一个模型就完成了。但实际项目中,真正的成本往往集中在这些地方:
- 数据脚本和训练脚本脱节,开发机用的是精简数据,全量数据在另一套机器上又出现编码问题。
- 训练中断后,checkpoint 加载逻辑不完整,导致恢复训练后 loss 爆炸。
- 没有建立评测基线,模型训完了却说不清楚比 baseline 好在哪里。
- 模型导出后 tokenizer 配置和推理脚本不一致,部署端调用时出现乱码或维度错误。
所以,这篇文章要解决的核心问题,不只是“怎么调用某个训练 API”,而是“从零到一建立一条稳定的训练流水线”。
Marin 在这里不是某个开源项目的代称,而是一个示例训练项目代号。后续所有命令、目录结构、脚本都围绕这个项目展开,你可以直接把它替换为自己的项目名。
2. 公开训练的基础概念与整体阶段划分
在动手写代码之前,先建立一个全局视图。一个大模型公开训练全流程,可以划分为六个阶段:
| 阶段 | 核心目标 | 主要产出物 | 常见失败点 |
|---|---|---|---|
| 数据准备 | 形成高质量、可追踪的训练数据集 | 清洗脚本、数据集文件、数据卡片 | 数据格式不一致、脏数据混入 |
| 环境准备 | 复现训练所需的运行环境 | 依赖清单、镜像或环境导出文件 | CUDA、PyTorch、框架版本冲突 |
| 模型训练 | 让模型在目标分布上收敛 | 训练日志、checkpoint、损失曲线 | 显存溢出、loss 发散、训练中断 |
| 训练监控 | 及时发现问题并调整训练策略 | 指标面板、日志记录 | 指标口径错误、日志缺失 |
| 评测与验证 | 量化模型效果,确认是否达到基线 | 评测脚本、评测报告、对比结果 | 评测数据污染、指标计算不一致 |
| 发布与部署 | 把模型打包并交付给下游使用 | 模型权重、tokenizer 配置、推理示例 | 配置丢失、版本不匹配 |
这六个阶段不是各管各的,而是一条流水线。一个典型的公开训练项目,数据团队产出干净数据后,训练团队才能开始跑实验;训练稳定后,评测团队再介入。如果前期数据脚本不完善,后面的训练、评测、发布都会跟着返工。
你还会听到一些相关术语,比如“预训练”“微调”“分布式训练”。这些是更细分的概念,本文把它们融入到流程中讲解,不单独展开成百科式介绍。
2.1 公开训练与内部训练的区别
很多人会问:公开训练和平时自己调参训练有什么区别?
核心区别在于“可复现性”和“开放边界”。
- 内部训练可以默认“跑通就行”,数据放在固定路径,脚本只在自己机器上有效;公开训练要求任何拿到文档的人都能复现。
- 内部训练可以用随机种子加日志口头沟通;公开训练需要把种子、超参数、数据版本、代码 commit 全部记录在案。
- 内部训练的模型只要任务跑完就行;公开训练还要考虑别人如何用你的 checkpoint 继续训练或做推理。
理解了这一点,就能理解为什么后面每一步都要强调“记录”和“验证”。
3. 环境准备与前置条件
3.1 硬件环境
训练大模型对硬件有基本要求。这里不写死具体配置,因为不同规模的项目差异很大,但可以给出一个通用参考:
| 场景 | 建议硬件 | 说明 |
|---|---|---|
| 小规模微调 | 单张 24GB 以上显存显卡 | 适合验证流程、跑小 batch |
| 全参数训练 | 多卡服务器或云上 GPU 实例 | 需要支持 NCCL 多机通信 |
| 数据预处理 | CPU 内存 32GB 以上 | tokenization 可能非常吃内存 |
如果你是第一次接触训练,建议先在小数据集上跑通流程,不要一上来就全量数据。
3.2 操作系统与基础软件
强烈建议使用 Linux 环境,主流的训练框架、分布式通信库、CUDA 支持都对 Linux 更友好。Windows 也可以跑单机小任务,但遇到多卡分布式训练时,坑会明显增多。
基础软件清单如下:
- 操作系统:Ubuntu 20.04 或 22.04,或其他等价 Linux 发行版
- Python:3.10 或 3.11,版本以训练框架官方要求为准
- GPU 驱动:NVIDIA 驱动,配合对应 CUDA 版本
- 版本管理:git、conda 或 venv
- 大文件管理:git-lfs,用于托管模型权重和大型数据集
版本细节不要照搬网上教程,因为你的显卡、驱动、框架版本都可能不同。最稳妥的顺序是:先确认 GPU 驱动和 CUDA 版本,再安装 PyTorch,最后安装上层训练框架。
3.3 安装核心依赖
Marin 项目建议使用 conda 创建独立环境,避免污染系统 Python。
conda create -n marin python=3.10 -y conda activate marin # 安装 PyTorch,具体命令以 PyTorch 官网根据本机 CUDA 版本生成的命令为准 pip install torch torchvision torchaudio # 安装 Hugging Face 生态组件 pip install transformers datasets tokenizers accelerate # 分布式训练辅助 pip install deepspeed安装完成后,用下面命令验证 GPU 是否可见:
python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.device_count())"如果输出True和正确的显卡数量,说明环境基本没问题。如果输出False,优先检查 PyTorch 版本是否匹配 CUDA 版本,而不是直接重装驱动。
3.4 项目目录结构规划
环境准备好后,先规划目录。Marin 项目建议采用如下结构:
marin/ ├── configs/ # 训练、评测、导出配置 ├── data/ # 数据集(可能通过软链接指向实际存储) ├── scripts/ # 数据清洗、训练、评测、导出脚本 ├── src/ # 自定义代码,比如数据集类、模型封装 ├── outputs/ # 训练输出,包括 checkpoint 和日志 ├── eval_results/ # 评测结果 ├── README.md # 项目说明和复现文档 └── requirements.txt # 依赖清单目录结构看起来很简单,但实际项目中,大部分混乱都源于目录不清晰。比如数据脚本和输出混在一起、长时间训练后找不到某个 checkpoint 对应的配置。提前把目录定好,后面能省很多事。
4. 数据准备与预处理
数据质量直接决定训练效果。公开训练对数据的要求更高,因为你要让别人理解你为什么选择这些数据、如何清洗。
4.1 数据收集与格式统一
一开始拿到手的数据往往是“脏”的,可能来自多个来源,格式各不相同。第一步是先统一成一种目标格式,Marin 项目采用 JSONL 格式,每行一个 JSON 对象,是当前大模型训练的主流格式。
示例数据格式:
{"instruction": "解释什么是梯度消失", "input": "", "output": "当深层网络的反向传播过程中,梯度逐层相乘后变得非常小,导致浅层参数几乎无法更新,这种现象称为梯度消失。"} {"instruction": "写一个 Python 函数判断回文串", "input": "", "output": "def is_palindrome(s):\n s = s.lower()\n return s == s[::-1]"}每行包含instruction、input、output字段。具体字段名可以根据任务调整,但需要保证整份数据集格式一致。
4.2 数据清洗脚本
清洗脚本负责过滤低质量内容。常见规则包括:
- 去重:基于文本 hash 或 MinHash,去掉重复样本。
- 过滤过短样本,比如少于 10 个字符。
- 过滤噪音样本,比如全是符号、乱码或超长重复文本。
- 根据领域关键词过滤广告、垃圾信息。
下面给一个最小清洗脚本示例,文件路径为scripts/clean_data.py:
# scripts/clean_data.py import json import hashlib from pathlib import Path def get_text_hash(text: str) -> str: return hashlib.md5(text.encode("utf-8")).hexdigest() def is_valid_sample(sample: dict, min_length: int = 10) -> bool: text = sample.get("instruction", "") + sample.get("output", "") if len(text) < min_length: return False # 过滤掉正常比例过低的“符号噪音” symbol_count = sum(not ch.isalnum() and not ch.isspace() for ch in text) if symbol_count / max(len(text), 1) > 0.3: return False return True def main(input_path: str, output_path: str) -> None: seen_hashes = set() with open(input_path, "r", encoding="utf-8") as fin, \ open(output_path, "w", encoding="utf-8") as fout: for line in fin: line = line.strip() if not line: continue try: sample = json.loads(line) except json.JSONDecodeError: continue if not is_valid_sample(sample): continue text_hash = get_text_hash(sample["instruction"] + sample["output"]) if text_hash in seen_hashes: continue seen_hashes.add(text_hash) fout.write(json.dumps(sample, ensure_ascii=False) + "\n") if __name__ == "__main__": main("data/raw_data.jsonl", "data/clean_data.jsonl")运行方式:
python scripts/clean_data.py这里的关键逻辑是:先过滤明显无效的行,再做去重,最后输出标准化格式。实际项目中清洗规则可能复杂得多,但最小脚本可以帮助你验证流水线是通的。
4.3 Tokenization 与数据集对象
数据清洗完成后,还需要把文本转换为模型可以读取的 token id。这里推荐直接使用 Hugging Facedatasets和tokenizer。
# scripts/load_dataset.py import json from datasets import Dataset from transformers import AutoTokenizer # 假设你有一个本地 tokenizer 目录,或者从模型仓库加载 tokenizer = AutoTokenizer.from_pretrained("your-tokenizer-path") def preprocess(example): text = example["instruction"] + "\n" + example["input"] + "\n" + example["output"] return tokenizer(text, max_length=512, truncation=True) def load_and_tokenize(jsonl_path: str, output_dir: str) -> None: samples = [] with open(jsonl_path, "r", encoding="utf-8") as f: for line in f: samples.append(json.loads(line.strip())) dataset = Dataset.from_list(samples) dataset = dataset.map(preprocess, remove_columns=dataset.column_names) dataset.save_to_disk(output_dir) if __name__ == "__main__": load_and_tokenize("data/clean_data.jsonl", "data/tokenized_data")这段代码把清洗后的 JSONL 转换成 Hugging Face Dataset,tokenize 后保存到磁盘。这样训练脚本就可以直接加载预处理好后的数据,不用每次训练前重复清洗和 tokenize。
5. 训练配置与模型训练
5.1 训练配置文件
Marin 项目采用 YAML 作为训练配置。配置文件的好处是:超参数变化不再改代码,而是改配置,方便实验记录和复现。
# configs/train_config.yaml model_name_or_path: your-base-model tokenizer_name_or_path: your-tokenizer-path train_file: data/tokenized_data output_dir: outputs/marin-checkpoints logging_dir: outputs/logs num_train_epochs: 3 per_device_train_batch_size: 4 per_device_eval_batch_size: 8 gradient_accumulation_steps: 8 learning_rate: 2e-5 weight_decay: 0.01 warmup_ratio: 0.03 logging_steps: 50 save_steps: 500 eval_strategy: steps eval_steps: 500 save_total_limit: 3 seed: 42 fp16: true deepspeed: configs/deepspeed_config.json字段的解释如下:
per_device_train_batch_size:每张卡上的 batch size。gradient_accumulation_steps:梯度累积步数,等效 batch size = 单卡 batch size × 卡数 × 梯度累积步数。save_steps:每隔多少步保存一次 checkpoint。save_total_limit:最多保留几个 checkpoint,防止磁盘被写满。fp16:混合精度训练,能显著节省显存。
注意:不要照抄这里的超参数,需要根据模型大小、数据量、显存情况调整。
5.2 分布式训练启动
当数据量和模型规模上来后,单卡很难满足要求。DeepSpeed + torchrun 是当前常用的分布式训练方案。
DeepSpeed 配置示例:
{ "train_batch_size": 64, "gradient_accumulation_steps": 8, "fp16": { "enabled": true }, "zero_optimization": { "stage": 2 } }训练脚本本身可以直接使用 Hugging FaceTrainer,它已经封装了 DeepSpeed 的接入逻辑。训练入口写法如下:
# scripts/train.py from transformers import ( AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments, HfArgumentParser, ) from datasets import load_from_disk import yaml def main(): parser = HfArgumentParser(TrainingArguments) training_args = parser.parse_args_into_dataclasses()[0] tokenizer = AutoTokenizer.from_pretrained(training_args.tokenizer_name_or_path) model = AutoModelForCausalLM.from_pretrained(training_args.model_name_or_path) dataset = load_from_disk(training_args.train_file) trainer = Trainer( model=model, args=training_args, train_dataset=dataset, tokenizer=tokenizer, ) trainer.train() if __name__ == "__main__": main()实际启动命令用torchrun,假设有 4 张显卡:
torchrun --nproc_per_node=4 \ --master_port=29500 \ scripts/train.py \ --model_name_or_path your-base-model \ --tokenizer_name_or_path your-tokenizer-path \ --train_file data/tokenized_data \ --output_dir outputs/marin-checkpoints \ --logging_dir outputs/logs \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 8 \ --learning_rate 2e-5 \ --num_train_epochs 3 \ --fp16 \ --deepspeed configs/deepspeed_config.json这里要强调一个容易踩的坑:--deepspeed参数必须和--gradient_accumulation_steps等参数配合,deepspeed 配置里的train_batch_size是全局等效 batch size,不是单卡值。配置不一致时,训练能启动,但日志里的 batch 信息会变得难以理解。
5.3 训练中断与断点续训
长训练任务几乎必然会遇到中断。机器重启、显存溢出、网络波动都可能导致训练停止。因此断点续训不是可选项,而是必须项。
断点续训的关键在于加载 checkpoint 时要同时恢复模型权重、优化器状态、学习率调度器状态、随机种子状态和全局 step。Hugging FaceTrainer的resume_from_checkpoint参数已经封装了大部分逻辑。
在训练脚本中增加:
trainer.train(resume_from_checkpoint=True)命令行方式:
torchrun --nproc_per_node=4 scripts/train.py ... --resume_from_checkpoint outputs/marin-checkpoints/checkpoint-1000注意:如果训练脚本或模型代码在断点后发生了结构性变化,加载旧 checkpoint 可能会报维度不匹配或 key 缺失。这种情况下,优先排查 checkpoint 是否完整,而不是盲目改代码绕过报错。
6. 运行监控与效果验证
训练启动不是终点,更重要的是观测训练状态。很多问题在训练早期不会暴露,需要持续监控。
6.1 日志和 loss 观察
训练过程中,日志里最重要的指标是 loss。正常情况下,loss 应呈下降趋势。可以通过 TensorBoard 查看:
tensorboard --logdir outputs/logs --port 6006如果看到 loss 突然变成nan或inf,第一反应应该是:
- 检查学习率是否过大。
- 检查数据中是否混入了异常样本。
- 检查是否启用了 fp16 但梯度缩放器没有正常工作。
- 检查 checkpoint 加载是否完整。
很多人在 loss 发散时会立刻调学习率,但更稳妥的做法是先保留现场日志,再用小数据集复现,确认是否数据问题。
6.2 显存和吞吐量监控
训练过程中还需要关注显存占用和吞吐量。可以用nvidia-smi快速查看显存状态:
nvidia-smi -l 5-l 5表示每 5 秒刷新一次。如果显存接近上限,可以调小per_device_train_batch_size或开启梯度累积。
一个合理训练状态的判断标准是:
- GPU 利用率保持较高,而不是长期 0%。
- 显存没有频繁 OOM。
- 训练吞吐量稳定,没有周期性骤降。
如果训练速度忽快忽慢,可能需要检查数据加载是否有瓶颈,比如磁盘 IO 太慢或者 DataLoader 的num_workers设置过低。
7. 模型评测与发布
训练完成后,模型是否达到预期效果,不能只靠训练集 loss 判断,必须有独立的评测流程。
7.1 评测集与指标
评测集需要和训练集严格分离,不能有重叠。建议在数据准备阶段就预留评测数据,而不是训练完成后再临时找评测样本。
Marin 项目使用一个简单的指令评测脚本:
# scripts/evaluate.py import json from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_path = "outputs/marin-checkpoints/checkpoint-3000" tokenizer = AutoTokenizer.from_pretrained(model_path) model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto") eval_samples = [ {"instruction": "解释什么是过拟合", "input": ""}, {"instruction": "用 Python 写一个读取 CSV 文件的函数", "input": ""}, ] for sample in eval_samples: prompt = sample["instruction"] + "\n" + sample["input"] inputs = tokenizer(prompt, return_tensors="pt").to(model.device) outputs = model.generate(**inputs, max_new_tokens=128) response = tokenizer.decode(outputs[0], skip_special_tokens=True) print("==" * 20) print("输入:", prompt) print("输出:", response)运行方式:
python scripts/evaluate.py评测时要注意两个问题:
- 不要用训练过的样本做评测,否则结果虚高。
- 生成式模型的评测指标往往不只是准确率,还需要人工抽查回答质量。
7.2 模型导出
训练完成后,需要把 checkpoint 导出为适合部署或上传的格式。Hugging Face 的AutoModelForCausalLM保存目录已经包含了模型权重和配置,但部署前仍需检查完整性。
标准导出命令:
python -c " from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained('outputs/marin-checkpoints/checkpoint-3000') model.save_pretrained('outputs/marin-final') tokenizer = AutoTokenizer.from_pretrained('outputs/marin-checkpoints/checkpoint-3000') tokenizer.save_pretrained('outputs/marin-final') "导出后,outputs/marin-final目录下应有以下关键文件:
outputs/marin-final/ ├── config.json ├── generation_config.json ├── model.safetensors ├── tokenizer_config.json ├── tokenizer.json └── tokenizer.model缺少tokenizer.json或tokenizer.model,部署端通常无法正常 encode/decode,这是最常见的发布事故之一。
7.3 发布前检查清单
发布前建议做一遍检查:
- 训练配置、数据清洗脚本、模型代码是否提交到版本库。
- checkpoint 是否做过完整性校验,比如加载后能正常执行一次前向传播。
- tokenizer 配置是否与训练时一致。
- 是否提供最小推理示例,方便使用者快速验证。
8. 常见问题与排查
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动训练时报 CUDA out of memory | batch size 过大,或显存不足以支撑模型驻留 | 查看nvidia-smi实际显存占用 | 调小per_device_train_batch_size,或开启梯度累积、降低序列长度 |
| loss 直接变成 nan/inf | 学习率过大、数据有异常、fp16 溢出 | 查看训练日志确认出现 nan 的 step,检查数据样本 | 降低学习率,检查数据清洗规则,尝试关闭 fp16 或使用 bf16 |
| 恢复训练后 loss 明显高于中断前 | checkpoint 加载不完整,或优化器状态未恢复 | 检查trainer_state.json中的 global step | 确保resume_from_checkpoint指向正确 checkpoint,不要重新初始化 trainer |
| 训练速度很慢,GPU 利用率低 | 数据加载成为瓶颈,或单卡 batch 太小 | 观察训练日志中迭代耗时,检查 DataLoader worker 数 | 增加num_workers,把数据预处理好存为内存映射格式,避免在线 tokenize |
| 多个进程互相抢占端口 | torchrun 默认端口冲突 | 查看master_port是否被占用 | 更换--master_port端口,或在训练脚本中动态分配端口 |
| 模型推理输出乱码 | 保存和加载时 tokenizer 不一致 | 比较训练时和推理时的 tokenizer 文件 | 统一使用同一个 tokenizer 目录,导出时确保 tokenizer 文件完整 |
| 数据集加载时直接内存溢出 | 全量数据一次性读入内存 | 查看系统内存占用情况 | 使用datasets的流式加载或分片加载,避免load_dataset一次性读入大文件 |
9. 工程最佳实践与建议
9.1 记录一切可复现信息
公开训练最看重可复现性。建议从第一天起就记录以下信息:
- 数据版本:用 DVC 或简单哈希记录数据集的 commit。
- 代码版本:每个实验对应一个 git commit。
- 超参数:YAML 配置文件入库,训练脚本自动读取。
- 环境信息:
pip freeze > requirements.txt,必要时记录 CUDA 版本和驱动版本。
9.2 先小规模验证,再全量训练
任何人直接跑全量训练都可能浪费大量算力。建议先用 1% 的小数据子集跑通完整流水线,验证数据格式、训练脚本、评测脚本都没有问题后,再启动全量训练。这样可以在 30 分钟内暴露 80% 的流程问题。
9.3 保持幂等性
数据清洗脚本、训练脚本最好具备幂等性,即重复执行结果一致。数据清洗脚本应该做到“重新执行不会改变输出”,这样别人才能放心复现。有条件时,还可以在脚本入口加上参数校验。
9.4 及时保存 checkpoint 并设计回滚策略
训练过程中的 checkpoint 是最大的资产。建议:
- 设置合理的
save_steps,不要太稀疏。 - 保留最后若干个 checkpoint,而不是只留一个。
- 对关键 checkpoint 做一次独立备份,防止磁盘损坏或误删。
9.5 安全与权限
如果训练数据涉及敏感信息,不要把数据直接提交到公开仓库。建议:
- 用
.gitignore忽略data/raw_data等目录。 - 通过环境变量或配置文件管理 API Key。
- 公开发布前,确认数据符合版权和隐私要求。
9.6 文档也是一种交付物
公开训练项目的文档,至少要包含:
- 数据来源和清洗规则。
- 环境安装步骤。
- 训练启动命令。
- 评测指标和结果。
- 模型使用示例。
文档不是写完代码后的装饰,而是让别人理解你训练流程的重要管道。
10. 总结与下一步实践建议
这篇文章以 Marin 项目为例,把公开训练全流程拆成了六个阶段:数据准备、环境准备、模型训练、训练监控、评测与发布。你可能会发现,任何一个阶段单独拿出来都不算难,难的是把阶段之间衔接好。
我建议你按照以下步骤开始实践:
- 先复制文章里的目录结构,新建一个最小项目。
- 准备一个几百条数据的小数据集,跑通数据清洗脚本。
- 在单卡上用小模型跑一次训练,确认 checkpoint 可以正常保存和恢复。
- 加上评测脚本,验证模型输出。
- 最后再评估是否需要切换到多卡分布式训练和 DeepSpeed。
这整个过程中,最值得投入精力的不是调模型结构,而是把数据版本、训练配置、评测流程串起来。你可以先在自己的机器上搭建一条最小闭环流程,之后再把大数据量、多机训练逐步加进去。如果你后续对 RAG 知识库搭建、模型微调部署、推理加速感兴趣,这篇文章里提到的 tokenizer 配置、checkpoint 保存、评测验证等基础能力都会继续复用。