- 引言 —— 选框架,选的是要藏起什么
- 核实基准 —— 版本与核实日期
- 第 1 层 —— 分布式执行引擎
- 第 2 层 —— 训练循环框架
- 第 3 层 —— 靠配置文件驱动的后训练工具
- 一条消失的谱系告诉我们什么 —— torchtune 与 torchforge
- 微调路径与对齐阶段 —— 当下的叫法
- 上层框架反而碍事的时刻
- 结语 —— 选一层,但别被困在那一层里
引言 —— 选框架,选的是要藏起什么
"该用哪个框架训练"这个问题没有单一答案,因为大家所在的层不一样。用 torchrun 自己写的循环,和一份 YAML 就能跑起来的微调,不是竞争关系,而是上下级关系。
所以本文不打算罗列框架,而是分三层画出谱系。对每一层只看三件事:这一层替你做了什么、这一层藏起了什么,以及需要重新打开被藏起的东西时,有没有一个逃生舱口。
这个领域按月在变。下面所有版本号都是 2026 年 8 月 2 日直接在 PyPI 发布历史和 GitHub release tag 上核实的数值,无法核实的项目已在正文中注明。等你读到这篇文章时,其中几个数字大概率已经过时了——比起数字本身,更值得带走的是这套分层结构,它更耐用。
核实基准 —— 版本与核实日期
| 工具 | 核实的版本 | 发布日期 | 出处 |
|---|---|---|---|
| PyTorch | 2.13.0 | 2026-07-08 | PyPI |
| DeepSpeed | 0.19.3 | 2026-07-23 | PyPI |
| Megatron-Core | 0.18.2 | 2026-07-21 | GitHub |
| Transformer Engine | 2.17.0 | 2026-07-09 | PyPI |
| torchtitan | v0.2.2 | 2026-02-20 | GitHub |
| NeMo Toolkit | 2.7.3 | 2026-04-23 | PyPI |
| NeMo-RL | v0.7.0 | 2026-07-29 | GitHub |
| Transformers | 5.14.1 | 2026-07-16 | PyPI |
| TRL | 1.9.2 | 2026-07-28 | PyPI |
| PEFT | 0.20.0 | 2026-07-28 | PyPI |
| Accelerate | 1.14.0 | 2026-06-11 | PyPI |
| Axolotl | 0.18.0 | 2026-07-17 | PyPI |
| LLaMA-Factory | 0.9.5 | 2026-05-30 | PyPI |
| Unsloth | 2026.7.6 | 2026-07-29 | PyPI |
| verl | 0.8.0 | 2026-06-01 | PyPI |
| OpenRLHF | 0.10.4 | 2026-06-08 | PyPI |
| Liger Kernel | 0.8.1 | 2026-07-23 | PyPI |
| torchao | 0.17.0 | 2026-03-30 | PyPI |
| torchtune | 0.6.1(最后版本) | 2025-04-07 | GitHub |
第 1 层 —— 分布式执行引擎
最底下这一层负责"这个张量放在哪块 GPU 上、什么时候发起哪种集合通信"。这里有三条谱系。
PyTorch 分布式系列。 torch.distributed 把 DDP、FSDP2、DTensor、DeviceMesh、torch.distributed.pipelining 全都提供出来。以 2026 年为节点,一个标志性的变化是并行化不再是一个独立的库,而是变成了 PyTorch 本体的 API。FSDP2 的 fully_shard 不会把模块包起来,而是原地把参数变成 DTensor,所以参数名得以保留,checkpoint 兼容性问题也随之减少。官方教程已经把 FSDP1 标记为弃用状态。
DeepSpeed 系列。 ZeRO 1 到 3 阶段、offloading、序列并行(Ulysses)、推理 kernel 全部装在一个包里。常有人问"它还活着吗"——0.19.3 是 2026 年 7 月 23 日发布的,所以答案是活跃。所有权结构已经改变:GitHub 组织从 microsoft 迁到了 deepspeedai,现在既是 LF AI & Data 的孵化项目,也是 PyTorch Foundation 托管的项目。如果哪份文档里还留着旧链接和旧组织名,那份文档就是过时的。
Megatron 系列。 从 NVIDIA 的 Megatron-LM 仓库里抽出可复用部分,就是 Megatron-Core,它是张量并行、流水线并行、序列并行、上下文并行、专家并行事实上的参考实现。挂在它上面的是负责低精度训练的 Transformer Engine。FP8 训练很早就已支持,NVFP4 4-bit 预训练也在 Transformer Engine 中得到支持——NVIDIA 的论文报告称,把一个 12B 的混合 Mamba-Transformer 模型训练到 10T token,效果与 FP8 基线相当。不过这个结果能不能原样搬到你自己的模型上是另一回事——如果打算引入 4-bit 预训练,应该先跑一个短程对比,直接比较损失曲线。
直接接触这一层时的接口通常是配置文件。DeepSpeed 的话是这个样子。
{
"bf16": { "enabled": true },
"zero_optimization": {
"stage": 3,
"overlap_comm": true,
"contiguous_gradients": true,
"reduce_bucket_size": 5e8,
"stage3_prefetch_bucket_size": 5e8,
"stage3_param_persistence_threshold": 1e6,
"stage3_gather_16bit_weights_on_model_save": true
},
"gradient_accumulation_steps": "auto",
"train_micro_batch_size_per_gpu": "auto",
"gradient_clipping": 1.0,
"steps_per_print": 50,
"wall_clock_breakdown": false
}
这里实务上最常出问题的一行是 stage3_gather_16bit_weights_on_model_save。在第 3 阶段,参数是按 GPU 切分的,如果不打开这个选项就直接保存,得到的会是一堆服务化时读不了的分片碎片。而打开 overlap_comm 能让通信与计算重叠从而加速,但代价是压缩了激活内存的余量——如果配置正卡在 OOM 边缘,应该先怀疑这个值。
这一层几乎什么都不藏。相应地,它也几乎不替你做任何事。数据加载器、checkpoint 格式、恢复逻辑、日志记录,全都要自己写。
第 2 层 —— 训练循环框架
在第 1 层之上加了一层"从头到尾跑完训练的循环"。
torchtitan 是 PyTorch 团队自己做的参考实现,可以组合 FSDP2、张量并行(含异步变体)、流水线并行、上下文并行、HSDP,并支持 Float8 和 MXFP8 训练。仓库把自己描述为"面向快速实验与大规模训练的 PyTorch 原生平台",并建议用 PyTorch nightly 版本才能用上最新功能。也就是说,它的设计初衷不是跑在稳定版之上的生产级技术栈。作为阅读和抄写代码的参考,它是最好的。
Megatron-Bridge 是 NVIDIA 做的一个库,负责 Hugging Face checkpoint 与 Megatron-Core 之间的双向转换,并在其上叠加了一套使用 Megatron-Core 的训练循环。这座桥之所以重要,是因为实务流程本身如此:模型以 Hugging Face 格式接收,训练用 Megatron-Core 的并行方式跑,服务化又得转回 Hugging Face 格式导出。这段转换如果自己写,张量名映射和合并/拆分规则几乎必错无疑。文档中提到 NeMo-RL 和 SkyRL 都把这个当作各自的 Megatron-Core 连接器采用了。
NVIDIA NeMo 是再往上一层的综合性框架。从预训练到对齐再到部署都被打包成了一份份 recipe,强化学习部分则拆分成了 NeMo-RL。如果能直接用 NVIDIA 的容器环境,这是最快的路径;如果不能,依赖的体量本身就会成为负担。
这一层替你做的是并行化配置、分布式 checkpoint、恢复、吞吐量日志记录。它藏起的是优化器 step 的细节和通信调度,调试损失尖峰时你得重新把它打开。
第 3 层 —— 靠配置文件驱动的后训练工具
最上面这一层把数据集格式、prompt 模板、LoRA 配置、评估钩子全都打包在一起,一条命令就能跑起来。
TRL 是 Hugging Face 生态里后训练的标准工具。以 1.9.2 的 release notes 为准,它内置了 SFT、DPO、GRPO、RLOO、KTO、奖励模型的 trainer,KTO trainer 在 1.8.0 里摘掉了实验性标签。就像 1.7.0 把默认损失换成了分块(chunked)NLL、并加入了 MoE 辅助损失那样,默认值本身会在小版本发布里发生变化。我在 release 页面上看到过一句提到实验性命名空间下还有几个额外 trainer 的说明,但没有逐一核实具体算法,请在你实际使用的版本文档里直接确认清单。TRL 是一个必须把版本号钉死的库。
直接使用 TRL 的最小写法是这样的。我把版本锁定和损失掩码检查一并放了进去。
# pip install "trl==1.9.2" "transformers==5.14.1" "peft==0.20.0" "accelerate==1.14.0"
from datasets import load_dataset
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer
ds = load_dataset("json", data_files="data/sft-train.jsonl", split="train")
cfg = SFTConfig(
output_dir="out/sft-7b",
per_device_train_batch_size=4,
gradient_accumulation_steps=8,
learning_rate=1e-5,
num_train_epochs=2,
bf16=True,
gradient_checkpointing=True,
max_length=4096,
packing=False, # 在亲自验证掩码之前先关掉
logging_steps=10,
save_steps=200,
report_to="mlflow",
)
trainer = SFTTrainer(
model="meta-llama/Llama-3.1-8B",
args=cfg,
train_dataset=ds,
peft_config=LoraConfig(r=32, lora_alpha=64, lora_dropout=0.05,
target_modules="all-linear", task_type="CAUSAL_LM"),
)
# 开始训练前,先用肉眼确认一个 batch
batch = next(iter(trainer.get_train_dataloader()))
ids, labels = batch["input_ids"][0], batch["labels"][0]
tok = trainer.processing_class
print("完整内容:", tok.decode(ids[:200]))
print("损失作用对象:", tok.decode([i for i, l in zip(ids, labels) if l != -100][:200]))
trainer.train()
最后五行是这段脚本里最重要的部分。被标记为 -100 的位置会从损失中排除,而模板哪怕只错位一点点,就会导致损失落在整个 prompt 上,或者相反——把整个回复都排除在外。两种情况下损失曲线看起来都会像模像样地下降,所以往往要等训练结束、开始评估时才会被发现。
Axolotl 把模型、数据集、并行化、LoRA 配置全部塞进一份 YAML 里。它最大的价值是能自动帮你把多种数据格式做好 tokenize,底层则在 Accelerate 和 DeepSpeed/FSDP 之间选择使用。
# axolotl==0.18.0 基准。accelerate launch -m axolotl.cli.train config.yaml
base_model: meta-llama/Llama-3.1-8B
load_in_4bit: false
strict: false
datasets:
- path: data/sft-train.jsonl
type: chat_template
field_messages: messages
sequence_len: 4096
sample_packing: true
gradient_checkpointing: true
bf16: auto
adapter: lora
lora_r: 32
lora_alpha: 64
lora_dropout: 0.05
lora_target_linear: true
micro_batch_size: 4
gradient_accumulation_steps: 8
num_epochs: 2
learning_rate: 1e-5
warmup_ratio: 0.03
lr_scheduler: cosine
deepspeed: deepspeed_configs/zero3_bf16.json
output_dir: ./out/axolotl-8b
LLaMA-Factory 在类似定位上加了一个 Web UI 和更广泛的模型支持,Unsloth 则是靠 kernel 优化来提升单 GPU 微调的速度和内存表现。Unsloth 的多 GPU 支持范围随着发行形态一直在变化,写这篇文章时我没能确认它当下的确切政策——如果需要多 GPU,请在引入之前直接查阅仓库文档确认。
大规模强化学习那一侧是完全独立的谱系。verl 和 OpenRLHF 采用的结构是把 rollout 引擎(通常是 vLLM 或 SGLang)和训练引擎(FSDP 或 Megatron-Core)拆开、再挂接在一起。如果打算在数千张 GPU 的规模上跑 GRPO 系的训练,应该看的是这条谱系,而不是 TRL。
一条消失的谱系告诉我们什么 —— torchtune 与 torchforge
这个领域里为什么选框架是件有风险的事,有一个案例把这一点说得很清楚。
torchtune 最初以 PyTorch 原生微调库的身份起步,获得了不错的评价。可到了 2025 年 7 月,仓库的 issue #2883 宣布活跃开发已经停止,PyPI 上最后一次发布停在 0.6.1,时间定格在 2025 年 4 月 7 日。作为继任者,torchforge 被发布出来。可截至 2026 年 8 月 2 日,torchforge 仓库最上方挂着这样一条横幅。
Development paused: Development in Forge has paused. LLM training at PyTorch is being consolidated in torchtitan.
也就是说,继任者的继任者,现在也正在被吸收。从这段历史里该拿走的教训,不是指责某个具体的库,而是这样一个事实:上层框架的预期寿命,比它下面的那一层更短。 PyTorch 分布式 API 和 Megatron-Core 已经存活了好几年,可它们之上那层便利工具,却按大约两年一个周期被推倒重来。
所以实务规则就变成了这样:项目真正的核心资产,不应该放在框架的配置文件里,而应该放在数据管线、评估套件、checkpoint 格式这三样东西上。把这三样放在框架之外,哪怕上层消失了,换一个就是。
微调路径与对齐阶段 —— 当下的叫法
决定训练什么,有两个维度。一个是动哪些权重,另一个是用什么信号来训练。
| 路径 | 训练对象 | 大致内存开销 | 什么时候用 |
|---|---|---|---|
| Full fine-tuning | 全部权重 | 每参数 16 字节 + 激活 | 领域本身差异很大时、数据充足时 |
| LoRA | 仅低秩适配器 | 基座模型 2 字节 + 适配器状态 | 大多数实务微调场景 |
| QLoRA | 4-bit 基座 + 适配器 | 基座约 0.5 字节 + 适配器 | 需要在单卡上处理大模型时 |
LoRA 系列的各种变体(DoRA、rsLoRA、LoRA+、PiSSA 等)一直在被 PEFT 吸收进去。哪个变体出现在哪个 PEFT 版本里,每个 release 都不一样,请查文档;实务中首先该调的旋钮不是选哪个变体,而是 rank、目标模块,以及学习率。把 QLoRA 用到真实生产服务上的过程,单独整理在《QLoRA 生产环境微调》里;而微调本身是不是正确的选择,最好先看一眼《RAG 与微调、提示工程的比较》。
对齐阶段目前的叫法是这样的。
- 有监督微调(SFT)。 用指令-回复对来定格式和风格。体感质量的大部分在这一步就已经定型。
- 偏好优化。 分为直接使用偏好对的离线一派(DPO 及其变体),和从策略中采样、在组内计算优势的在线一派(GRPO、RLOO)。TRL 里两种 trainer 都有。
- 基于可验证奖励的强化学习。 使用数学答案是否一致、测试是否通过这类可按规则打分的奖励。因为不使用学习出来的奖励模型,奖励黑客(reward hacking)的攻击面很窄,已经事实上成为训练推理能力的标准做法。
新名称还在不断增加。每次冒出一个新缩写,值得问的问题只有三个:奖励从哪来,需不需要 rollout,要不要把一个参考模型常驻在显存里。这三个问题定下来,需要多少张 GPU 也就随之定了。
上层框架反而碍事的时刻
便利层大多数情况下都是净收益。但在以下场景中,成本会超过收益。
- 需要改损失函数或掩码的时候。 哪些 token 承担损失,是后训练质量的核心旋钮,而上层框架把这一点藏在了数据格式背后。要确认损失掩码实际落在了哪里,只能把一个 batch 解码出来亲眼看。不做这个检查就白烧好几天的事故最常见。
- 并行化组合超出标准范围的时候。 比如同时开启上下文并行和专家并行这类组合,上层框架的配置 schema 往往表达不出来。
- 需要调试的时候。 要找到损失尖峰的原因,得逐 step 查看梯度范数、特定层的激活统计、优化器状态。如果能靠回调打开这些信息,算你走运;打不开的话,就得去 fork 这个框架了。
- 版本无法锁定的时候。 好几个上层工具各自要求同一个底层库的不同版本,这种情况真实存在。除了自己构建容器镜像、提交 lockfile,没有别的办法。
# 开始训练前,至少应该把这些记进日志
python - <<'PY'
import importlib.metadata as md
for p in ("torch", "transformers", "trl", "peft", "accelerate",
"deepspeed", "flash-attn", "datasets"):
try:
print(f"{p:14} {md.version(p)}")
except md.PackageNotFoundError:
print(f"{p:14} (未安装)")
PY
nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv
python -c "import torch; print('nccl', torch.cuda.nccl.version())"
结语 —— 选一层,但别被困在那一层里
选技术栈的标准不是"哪个最好",而是"我们愿意亲自为哪一层负责"。要做预训练,就得下沉到 Megatron-Core 或 PyTorch 分布式 API 这一层;如果只是用内部数据做微调,停在 TRL 或 Axolotl 之上就是合理的选择。
但无论选哪一层,数据管线、评估套件、checkpoint 格式都应该放在框架之外。从 torchtune 到 torchforge、再到 torchtitan 这两年的历程,说的正是这件事。框架是可以更换的零件,而把它设计成"可以被更换"才是真正的设计工作。并行化本身的原理整理在《多 GPU 训练的四种并行方式》里。
현재 단락 (1/148)
"该用哪个框架训练"这个问题没有单一答案,因为大家所在的层不一样。用 `torchrun` 自己写的循环,和一份 YAML 就能跑起来的微调,不是竞争关系,而是上下级关系。