这两年 AI 圈的阵仗确实大,新模型一个接一个,但我始终觉得,真正让技术圈热闹起来的,不是那些只发技术报告不开源的团队,而是愿意把模型、代码、数据管线老老实实放出来的项目。今天想聊的,就是我最近刚完成并开源的一个项目。不止是模型权重,包括整个数据处理流程、训练脚本、评测脚本,全都整理好放出来了。
说实话,做这个项目的初衷很简单:我自己在微调和部署小模型时,发现很多开源仓库要么只给一个训练完的权重文件,要么训练代码和数据处理逻辑写得比较随意,很难复现。我希望能有一个相对完整、可复现的“大语言模型从数据准备到开源发布”全流程项目,于是花了两个多月时间,前后累计消耗了约 100 亿 Token 的数据量用于清洗、配比和训练验证,最终把一套可运行的开源方案整理了出来。
这篇文章我不会只贴一个 GitHub 地址就完事。而是把整个项目从立项、数据工程、模型训练、评测、到开源打包部署的完整链路拆开来讲。如果你正准备开始自己的第一个开源模型项目,或者正在为团队搭建私有化 LLM 推理服务,这篇文章应该能帮你少走不少弯路。
1. 背景与核心概念:为什么值得投入两个月做一个开源项目
1.1 这个项目到底是什么
先解释一下标题中的几个关键词,避免刚接触大模型的朋友产生误解。
“100 亿 Token”指的是整个项目在数据处理、模型训练、评测验证过程中累计消耗的 Token 数量。Token 是 LLM 处理文本时的最小单位,可以粗略理解为一个词或一个子词片段。100 亿 Token 大约相当于 7.5 亿英文单词,或者 4~5 亿汉字,这个量级已经足够训练一个亿级参数的小模型,也足够做一遍高质量的数据筛选和配比实验。
项目最终交付物是:
- 一套完整的数据清洗与配比脚本;
- 一个在清洗后数据上训练的基座模型;
- 一套可复现的指令微调流程;
- 一个轻量的推理服务包装;
- 详细的评测结果和失败案例分析。
整体架构可以概括为:数据工程 -> 预训练 -> 指令微调 -> 评测 -> 开源部署。
1.2 为什么要做“开源”而不是自己闷头用
很多开发者会问:模型训出来了,自己用不就行了,为什么非要开源?
我的理由有三个。
第一,可复现性是技术沉淀的基础。如果训练脚本、数据配比、超参数都不公开,那这次训练就是一次不可复现的“玄学”。开源相当于强迫自己把每一个环节写成文档和脚本,对工程化能力是很好的锻炼。
第二,社区反馈能加速迭代。开源之后,会有使用者反馈新场景下的表现,也可能有贡献者提交更好的数据清洗策略。这种外部输入比闭门造车高效得多。
第三,降低团队试错成本。真正在企业里落地的开发者,往往需要参考成熟的模型仓库来设计自己的训练管线。一个结构清晰的开源项目,能直接作为模板复用。
1.3 哪些场景适合参考这个项目
- 想训练一个特定领域的小模型,但不知道数据怎么清洗、怎么配比;
- 想微调开源模型做私有化部署,但希望补全数据处理到评测的完整链路;
- 想学习大模型训练工程的细节,尤其是 Token 消耗和成本控制的关系;
- 实验室或小团队需要一套可复现的训练代码。
如果你只是想直接用模型做应用,不需要训练,那这个项目的推理部署部分仍然有参考价值,尤其是量化部署部分。
2. 环境准备与版本说明
在跑任何训练或推理之前,先把环境信息说清楚。以下版本是我实际使用的环境,你的环境不一定完全一致,但思路是通用的。
2.1 硬件与运行环境
| 资源 | 配置 |
|---|---|
| GPU | 单机 8 x A800-80G / 或多机多卡 |
| CPU | 32 核以上 |
| 内存 | 256GB 以上 |
| 系统 | Ubuntu 20.04 / 22.04 |
| 存储 | 至少 2TB 可用空间 |
如果你是个人开发者,硬件条件达不到,也完全可以用单卡 RTX 4090 跑通数据处理和微调流程,只是预训练阶段需要调小模型参数规模。
2.2 软件依赖版本
Python 3.10+ CUDA 11.8 / 12.1 PyTorch 2.1+ transformers 4.36+ datasets 2.16+ accelerate 0.26+ peft 0.8+ deepspeed 0.12+ vllm 0.3+这里单独提醒一句:大模型训练项目的依赖版本非常敏感,尤其是 transformers、peft、deepspeed 三者之间经常存在兼容性问题。建议先创建独立的 conda 环境,再按顺序安装。
conda create -n llm-project python=3.10 conda activate llm-project pip install torch==2.1.0 --index-url https://download.pytorch.org/whl/cu118 pip install transformers==4.36.0 datasets==2.16.0 accelerate==0.26.0 pip install peft==0.8.0 deepspeed==0.12.02.3 项目目录结构
构建开源项目从第一天起就要保持良好的目录结构,下面是我实际使用的目录组织方式。
llm-open-project/ ├── configs/ # 配置文件 ├── data/ # 数据处理脚本 │ ├── raw/ # 原始数据存放 │ ├── cleaned/ # 清洗后数据 │ ├── tokenized/ # Token 化后数据 │ └── process_pipeline.py # 数据管道 ├── train/ # 训练脚本 │ ├── pretrain.py # 预训练入口 │ ├── sft.py # 指令微调入口 │ └── deepspeed_config/ # DeepSpeed 配置 ├── eval/ # 评测脚本 │ ├── run_benchmark.py │ └── eval_prompts.json ├── deploy/ # 推理部署 │ ├── vllm_server.py │ └── quantize.py ├── scripts/ # 一键执行脚本 ├── README.md └── requirements.txt3. 核心原理拆解:Token 消耗在哪,数据工程怎么做
3.1 100 亿 Token 到底消耗在哪里
很多朋友看到 100 亿 Token 的第一反应是“好多”,但实际拆开看,这个数字是有清晰去向的。
| 环节 | Token 消耗占比 | 说明 |
|---|---|---|
| 数据清洗与去重 | 约 20% | 多次跑规则过滤和语义去重模型,需要反复读取全量数据 |
| 预训练基座 | 约 60% | 在通用语料上训练模型,是 Token 消耗的大头 |
| 指令微调 | 约 15% | 构建指令样本并完成模型对齐 |
| 评测与验证 | 约 5% | 多次跑基准测试,尤其是对比不同版本效果时 |
这里要特别说明一个容易踩坑的点:数据清洗环节消耗的 Token 远比你以为的多。因为去重阶段需要把全量文本发送给语义去重模型,每跑一遍都要消耗与语料规模相当的 Token 量。如果清洗策略设计得不好,返工次数一多,成本很快就会失控。
3.2 数据清洗的三个关键步骤
大模型训练圈有句名言:数据决定模型的上限,训练只是逼近这个上限。整个项目花费时间最长的不是训练,而是数据处理。
第一步:语言过滤和质量过滤
使用fasttext或cld3做语言识别,过滤掉目标语言之外的语料。质量过滤则依赖规则和分类模型相结合:
# 文件路径:data/process_pipeline.py import re import fasttext # 加载语言识别模型 lang_model = fasttext.load_model("lid.176.bin") MIN_TEXT_LENGTH = 100 MAX_TEXT_LENGTH = 5000 REPETITION_RATIO_THRESHOLD = 0.3 def is_high_quality(text: str) -> bool: """基础规则过滤:长度、重复率、语言""" if len(text) < MIN_TEXT_LENGTH or len(text) > MAX_TEXT_LENGTH: return False # 重复字符比例检测,过滤“哈哈哈哈...”这类无意义文本 char_set = set(text) if len(char_set) / max(len(text), 1) < 0.05: return False # 语言过滤,检测是否为中文/英文 lang = lang_model.predict(text.replace("\n", " "))[0][0] if lang not in ["__label__zh", "__label__en"]: return False return True第二步:MinHash 去重
互联网语料中重复内容非常多,一篇热门文章可能被转载上百次。使用 MinHash + LSH 做近似去重,是数据工程的标配做法。
from datasketch import MinHash, MinHashLSH def build_minhash(text: str, num_perm=128): m = MinHash(num_perm=num_perm) # 使用 5-gram 进行分片 for i in range(len(text) - 5): m.update(text[i:i+5].encode("utf-8")) return m这里使用 5-gram 分片的原因是,太短的 n-gram 容易把完全不相关的文本误判为重复,太长的 n-gram 又可能漏掉改写后的重复文本。5-gram 在中文语料上效果比较均衡。
第三步:数据配比
数据配比是一个经常被人忽略但影响极大的环节。如果你直接把所有清洗后的语料混在一起训练,模型可能会被灌入大量低质量或特定领域的文本,导致下游任务表现失衡。
我的配比策略是:按“通用语料:领域语料 = 7:3”进行混合。领域语料又细分为代码、数学、法律、医学等子领域,每个子领域按 token 数量加权采样。
# 数据配比采样逻辑 import random def sample_by_domain(domain_and_texts, target_domain_ratio=0.3): """按领域比例对流式数据做采样""" # 实现细节根据数据源结构调整 pass3.3 为什么预训练要分阶段
预训练阶段我采用了两阶段策略,而不是一次性把所有数据灌进去。
第一阶段:在海量通用语料上训练,让模型学习基本的语言能力和世界知识。第二阶段:用中道清洗后的领域语料继续训练,增强模型在目标领域的效果。
这种“通用预训练 + 领域继续训练”的思路,比直接混合训练能更容易控制领域能力的增强幅度,也能减少灾难性遗忘。
3.4 指令微调:把“会说话”变成“会办事”
基座模型只能做文本续写,比如你输入“Python 的 GIL 是”,它会尝试补全。但如果你希望它回答“什么是 Python 的 GIL?”,就需要指令微调。
指令微调的关键是样本质量。这里的原则是:样本宁缺毋滥。我最后用来微调的高质量指令样本大约是 10 万条,但中间清洗掉的不合格样本超过 30 万。
一个典型指令样本格式如下:
{ "instruction": "解释什么是 Python 的 GIL", "input": "", "output": "GIL 是 CPython 解释器中的全局解释器锁,它保证同一时刻只有一个线程执行 Python 字节码..." }4. 完整实战案例:从数据处理到开源发布
下面进入整篇文章最核心的部分,我会把整个项目从训练到发布的流程走一遍。如果你想完整复现,可以直接对照本节操作。
4.1 数据准备与 Token 化
首先是准备原始数据。这里假设你已经有一批文本语料放在data/raw/目录下。第一步是执行清洗管道:
python data/process_pipeline.py \ --input_dir data/raw \ --output_dir data/cleaned \ --language zh,en \ --min_length 100 \ --max_length 5000清洗完成后,需要将文本 Token 化为模型输入的 ID 序列。使用 Hugging Face 的datasets库进行批量处理:
# 文件路径:data/tokenize_dataset.py from datasets import load_dataset from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("your-base-model") def tokenize_function(examples): # 拼接文本并按 chunk 长度截断 texts = [t + tokenizer.eos_token for t in examples["text"]] tokenized = tokenizer( texts, truncation=True, max_length=2048, padding=False, return_tensors=None ) return tokenized dataset = load_dataset("json", data_files="data/cleaned/train.jsonl") tokenized_dataset = dataset.map( tokenize_function, batched=True, remove_columns=["text"], num_proc=32 ) tokenized_dataset.save_to_disk("data/tokenized/train")Token 化这一步是计算密集型的,强烈建议使用num_proc开启多进程,单进程处理几十 GB 数据会慢到怀疑人生。
4.2 配置 DeepSpeed 训练环境
由于项目在预训练阶段需要处理大量 Token,单卡显存远远不够,因此多卡分布式训练是必须的。这里选择 DeepSpeed ZeRO-2 来切分优化器状态和梯度。
// 文件路径:train/deepspeed_config/zero2.json { "train_batch_size": 64, "gradient_accumulation_steps": 4, "train_micro_batch_size_per_gpu": 2, "optimizer": { "type": "AdamW", "params": { "lr": 3e-5, "weight_decay": 0.01 } }, "zero_optimization": { "stage": 2, "offload_optimizer": { "device": "cpu", "pin_memory": true } }, "gradient_clipping": 1.0, "fp16": { "enabled": true } }关键参数解释:
train_micro_batch_size_per_gpu:每张卡一次前向传播的 batch 大小,这里设为 2 是因为显存有限;gradient_accumulation_steps:梯度累积步数,用于模拟更大的 batch;offload_optimizer:将优化器状态放到 CPU,节省 GPU 显存。
4.3 编写预训练脚本
预训练脚本的核心逻辑是加载配置、初始化模型、设置数据加载器、启动训练循环。
# 文件路径:train/pretrain.py import os import torch from transformers import ( AutoConfig, AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments ) from datasets import load_from_disk def main(): model_name = "your-base-model" config = AutoConfig.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name, config=config) tokenizer = AutoTokenizer.from_pretrained(model_name) # 设置填充 token,避免训练时报错 if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token train_dataset = load_from_disk("data/tokenized/train") training_args = TrainingArguments( output_dir="./checkpoints/pretrain", evaluation_strategy="steps", eval_steps=500, save_steps=1000, logging_steps=50, per_device_train_batch_size=2, per_device_eval_batch_size=2, gradient_accumulation_steps=4, learning_rate=3e-5, warmup_steps=200, weight_decay=0.01, fp16=True, deepspeed="train/deepspeed_config/zero2.json", save_total_limit=3, report_to="none", ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=train_dataset.select(range(1000)), # 示例用法,实际应单独划分 tokenizer=tokenizer, ) trainer.train() trainer.save_model("./checkpoints/pretrain/final") tokenizer.save_pretrained("./checkpoints/pretrain/final") if __name__ == "__main__": main()运行命令:
cd train && deepspeed --num_gpus=8 pretrain.py4.4 指令微调:让模型学会对话
基座模型训练完成后,需要进行监督微调(SFT)。这一步的核心是使用高质量的指令数据,让模型对齐用户意图。
# 文件路径:train/sft.py from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig, get_peft_model from datasets import load_dataset model = AutoModelForCausalLM.from_pretrained("./checkpoints/pretrain/final") tokenizer = AutoTokenizer.from_pretrained("./checkpoints/pretrain/final") # LoRA 配置 lora_config = LoraConfig( r=8, lora_alpha=16, target_modules=["q_proj", "v_proj", "k_proj", "o_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() # 输出 LoRA 层可训练参数量使用 LoRA 的核心原因是效率。虽然你也可以全量微调,但 LoRA 只需要训练约 0.5% 的参数,显存占用和训练时间都大幅缩减,效果在指令跟随场景下已经足够好。
指令数据集的格式参考前面提到的 JSON 结构。训练完成后保存 LoRA 权重:
model.save_pretrained("./checkpoints/sft/lora") tokenizer.save_pretrained("./checkpoints/sft/lora")如果需要合并 LoRA 权重到基座模型:
from peft import PeftModel base_model = AutoModelForCausalLM.from_pretrained("./checkpoints/pretrain/final") merged_model = PeftModel.from_pretrained(base_model, "./checkpoints/sft/lora") merged_model = merged_model.merge_and_unload() merged_model.save_pretrained("./checkpoints/sft/merged")4.5 评测:不要只盯着 Loss 曲线
模型训练完成后,评测环节直接决定你是否能发布。我的评测分为三类:通用能力评测(如 C-Eval、MMLU)、专项任务评测(领域问答、代码生成)、人工抽检。
# 文件路径:eval/run_benchmark.py from datasets import load_dataset from transformers import pipeline generator = pipeline( "text-generation", model="./checkpoints/sft/merged", device=0 ) eval_prompts = [ "请解释什么是反向传播算法", "用 Python 实现一个快速排序", "列出数据库索引失效的三种场景", ] for prompt in eval_prompts: output = generator( prompt, max_new_tokens=256, temperature=0.3, top_p=0.9, do_sample=True ) print("Prompt:", prompt) print("Output:", output[0]["generated_text"]) print("=" * 50)评测过程中要有“对比基线”,我的建议是至少拿一个同参数规模的开源模型作为对照,否则你无法判断自己的模型到底是好还是坏。
4.6 开源打包与部署
4.6.1 模型权重发布
开源大模型项目,权重发布建议使用 Hugging Face 仓库或 ModelScope。权重文件会包含config.json、tokenizer.json、model-*.safetensors等文件。在 README 中必须写清楚:
- 基座模型来源与许可证;
- 训练数据来源与清洗说明;
- 评测结果;
- 模型限制与已知问题。
4.6.2 推理服务部署
模型部署最终需要做成可调用的服务。这里用 vLLM 搭建一个高吞吐的推理服务:
# 文件路径:deploy/vllm_server.py from vllm import LLM, SamplingParams llm = LLM(model="./checkpoints/sft/merged", tensor_parallel_size=2) sampling_params = SamplingParams( temperature=0.3, top_p=0.9, max_tokens=512 ) prompts = [ "用一句话解释大语言模型中的 Token 是什么", "写一段 Python 代码,读取 CSV 文件并计算每列平均值", ] outputs = llm.generate(prompts, sampling_params) for output in outputs: print(output.outputs[0].text) print("=" * 50)使用 vLLM 可以实现连续批处理,大幅提升吞吐,是生产环境推荐的方案。如果你的显存有限,可以先对模型做量化,再部署:
python deploy/quantize.py \ --model_path ./checkpoints/sft/merged \ --quant_method gptq \ --bits 4 \ --output_path ./checkpoints/sft/merged-int44.6.3 开源仓库编写
最后一步,写 README。这步千万不能敷衍,因为开源项目的 README 就是项目的门面。我建议的 README 结构是:
- 项目简介(三句话内说清楚);
- 整体架构图(使用文字描述);
- 快速开始(环境安装、推理 Demo);
- 训练复现(数据处理、预训练、微调);
- 评测结果(用表格);
- 已知问题与后续规划;
- 许可证与引用。
5. 常见问题与排查思路
整个项目踩过的坑实在不少,这里把高频问题整理成表,方便遇到类似报错的读者直接对照排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 训练 Loss 为 NaN | 学习率过大 / 数据中包含异常值 | 降低学习率,检查数据清洗流程,增加梯度裁剪 |
| 显存不足 OOM | batch size 过大 / 序列过长 | 减小 micro batch size,开启 DeepSpeed offload,降低 max_length |
| Token 化速度极慢 | 未开启多进程 | 使用num_proc参数开启多进程,提高数据读取效率 |
| 微调后模型胡言乱语 | 指令样本格式不一致 / 基座模型未充分训练 | 统一模板格式,减少低质量样本,必要时对基座模型继续预训练 |
| DeepSpeed 启动失败 | 版本不匹配 / 未正确设置环境变量 | 对照官方文档检查deepspeed、transformers、torch版本 |
| 评测分数低于基线模型 | 数据配比不合理 / 评测 prompt 不一致 | 检查数据配比,统一评测 prompt 模板 |
| 推理速度太慢 | 未使用 vLLM / 未开启 tensor parallel | 使用 vLLM 推理框架,显存充足时开启多卡并行 |
6. 最佳实践与工程建议
6.1 数据永远是第一优先级
这个项目做完,我最深的体会就是:80% 的问题可以通过更好的数据解决,而不是更大的模型。如果你准备做自己的模型项目,先不要急着买 GPU,先用 3~5 天时间把数据管道做到位。数据清洗、去重、配比这三步,值得反复迭代。
6.2 训练工程要留好监控点
训练跑起来不代表万事大吉。我在训练每个阶段都会做以下监控:
- 每 50 步看一次 Loss 曲线;
- 每 500 步跑一次小样本评测,而不是等全部训练结束;
- 每个 checkpoint 都保存优化器状态,方便中途恢复;
- 训练日志必须带有时间戳和 Token 消耗计数。
6.3 开源许可证要提前确定
开源项目最容易忽略的是许可证问题。如果你的基座模型使用了 Llama 或 ChatGLM 等模型权重,请先确认基座的许可证允许派生作品商用。另外,训练数据如果来自网上抓取,需要评估数据集的合规性。
6.4 版本控制与可复现性
训练工程和普通软件开发一样需要可复现性。我强烈建议:
- 使用
requirements.txt锁定依赖版本; - 每个训练实验记录完整超参数和配置文件;
- 使用实验管理工具记录每次实验的代码版本、数据版本和训练结果。
6.5 安全与合规红线
内容安全审核是部署大模型前必须做的环节。建议增加输入输出过滤,至少对以下类型内容做拦截:
- 违法和不良信息;
- 个人隐私数据;
- 未经授权的商业化内容。
可以引入开源的安全审核模型或关键词过滤方案,避免模型生成风险内容后再被动处理。
7. 总结与学习路线
这篇文章从头到尾梳理了一个开源大模型项目的完整链路:从项目立项、数据清洗与配比、Token 消耗分析、预训练与指令微调,到评测、量化、部署和开源打包。核心内容可以归纳为以下几条:
- Token 消耗不只是训练,数据清洗和评测同样是大头;
- 数据质量决定模型上限,数据配比是容易被忽视的关键点;
- 预训练最好分阶段,指令微调优先尝试 LoRA;
- 评测不能只看 Loss,必须有基线和人工抽检;
- 开源不等于丢权重,README、许可证、文档同样重要。
如果你想继续深入,建议按以下顺序学习:
- 先熟悉 Hugging Face Transformers 库的基本用法;
- 理解 Tokenizer 的原理与不同分词策略的区别;
- 自己实现一套小规模数据清洗流程,体会数据质量的差异;
- 用单卡完成一次小模型的 LoRA 微调;
- 再扩展到多卡训练和 DeepSpeed;
- 最后才是完整的预训练和开源发布。
做开源模型项目,最重要的一点是:不要追求一步到位,先把最小可用的闭环跑通,再逐步迭代数据和训练策略。希望这篇文章能帮你省下一些摸索时间,也欢迎在使用过程中交流问题。如果对你有帮助,可以收藏备用,后续我会继续补充推理优化和评测相关的实战内容。