什么是 Recipes?¶
本深入探讨将引导您了解 torchtune 中训练 Recipes 的设计。
什么是 Recipes?
构成 Recipes 的核心组件是什么?
我应该如何构建一个新的 Recipes?
Recipes 是 torchtune 用户的主要入口点。可以将它们视为训练(以及可选的评估)LLM 的“有针对性”的端到端管道。每个 Recipes 都实现了一种训练方法(例如:完全微调),并具有一组有意义的功能(例如:FSDP + 激活检查点 + 梯度累积 + 混合精度训练),应用于给定的模型系列(例如:Llama2)。
随着模型训练越来越复杂,在预测新的模型架构和训练方法的同时,也难以对所有可能的权衡(例如:内存与模型质量)进行推理。我们相信 a) 用户最适合根据其用例进行权衡,b) 并没有一种万能的解决方案。因此,Recipes 旨在易于理解、扩展和调试,而不是所有可能设置的通用入口点。
根据您的用例和专业知识水平,您会经常发现自己修改现有 Recipes(例如:添加新功能)或编写新的 Recipes。torchtune 通过提供经过良好测试的模块化组件/构建块和通用实用程序(例如:WandB 记录 和 检查点)来简化 Recipes 的编写。
Recipes 设计
torchtune 中的 Recipes 旨在
简单。完全使用原生 PyTorch 编写。
正确。每个组件的数值一致性验证以及与参考实现和基准的广泛比较。
易于理解。每个 Recipes 都提供了一组有限的有意义的功能,而不是隐藏在数百个标志背后的所有可能的功能。代码重复优于不必要的抽象。
易于扩展。不依赖于训练框架,也不存在实现继承。用户无需遍历层层抽象来弄清楚如何扩展核心功能。
- 适用于各种用户。用户可以决定如何与 torchtune Recipes 交互
通过修改现有配置来开始训练模型
为自定义情况修改现有 Recipes
直接使用可用的构建块来编写全新的 Recipes/训练范式
每个 Recipes 都包含三个组件
可配置参数,通过 yaml 配置和命令行覆盖指定
Recipes 脚本,入口点,它将所有内容整合在一起,包括解析和验证配置、设置环境以及正确使用 Recipes 类
Recipes 类,训练所需的核心逻辑,通过一组 API 向用户公开
在接下来的部分中,我们将仔细研究每个组件。有关完整的示例,请参考 torchtune 中的 完整微调 Recipes 以及相关的 配置。
Recipes 不是什么?¶
单体训练器。 Recipes 不是一种单体训练器,旨在通过数百个标志支持所有可能的功能。
通用入口点。 Recipes 不是旨在支持所有可能的模型架构或微调方法。
围绕外部框架的包装器。 Recipes 不是旨在围绕外部框架进行包装。这些完全使用原生 PyTorch 编写,并使用 torchtune 构建块。依赖项主要以附加实用程序的形式出现,或与周围生态系统(例如:EleutherAI 的评估工具)进行互操作。
Recipes 脚本¶
这是每个 Recipes 的主要入口点,它让用户可以控制 Recipes 的设置方式、模型的训练方式以及后续检查点的使用方式。这包括
设置环境
解析和验证配置
训练模型
设置多阶段训练(例如:蒸馏)使用多个 Recipes 类
脚本通常应按以下顺序构建操作
初始化 Recipes 类,该类反过来初始化 Recipes 状态
加载并验证检查点以更新 Recipes 状态(如果恢复训练)
从检查点(如果适用)初始化 Recipes 组件(模型、分词器、优化器、损失和数据加载器)
训练模型
训练完成后清理 Recipes 状态
一个示例脚本如下所示
# Initialize the process group
init_process_group(backend="gloo" if cfg.device == "cpu" else "nccl")
# Setup the recipe and train the model
recipe = FullFinetuneRecipeDistributed(cfg=cfg)
recipe.setup(cfg=cfg)
recipe.train()
recipe.cleanup()
# Other stuff to do after training is complete
...
Recipes 类¶
Recipes 类包含训练模型的核心逻辑。每个类都实现了一个相关的接口并公开了一组 API。对于微调,此类的结构如下
初始化 Recipes 状态,包括种子、设备、数据类型、度量记录器、相关标志等
def __init__(...):
self._device = utils.get_device(device=params.device)
self._dtype = training.get_dtype(dtype=params.dtype, device=self._device)
...
加载检查点,从检查点更新 Recipes 状态,初始化组件并从检查点加载状态字典
def setup(self, cfg: DictConfig):
ckpt_dict = self.load_checkpoint(cfg.checkpointer)
# Setup the model, including FSDP wrapping, setting up activation checkpointing and
# loading the state dict
self._model = self._setup_model(...)
self._tokenizer = self._setup_tokenizer(...)
# Setup Optimizer, including transforming for FSDP when resuming training
self._optimizer = self._setup_optimizer(...)
self._loss_fn = self._setup_loss(...)
self._sampler, self._dataloader = self._setup_data(...)
在所有时期运行前向和反向传播,并在每个时期结束时保存检查点
def train(...):
self._optimizer.zero_grad()
for curr_epoch in range(self.epochs_run, self.total_epochs):
for idx, batch in enumerate(self._dataloader):
...
with self._autocast:
logits = self._model(...)
...
loss = self._loss_fn(logits, labels)
if self.global_step % self._log_every_n_steps == 0:
self._metric_logger.log_dict(...)
loss.backward()
self._optimizer.step()
self._optimizer.zero_grad()
# Update the number of steps when the weights are updated
self.global_step += 1
self.save_checkpoint(epoch=curr_epoch)
清理 Recipes 状态
def cleanup(...)
self.metric_loggers.close()
...
使用配置运行 Recipes¶
若要使用一组用户定义的参数运行 Recipes,您需要编写一个配置文件。您可以在我们的 配置深入探讨 中了解有关配置的所有信息。
使用 parse
解析配置和 CLI¶
我们提供了一个方便的装饰器 parse()
,它包装您的 Recipes 以启用使用 tune 从命令行运行,并使用配置和 CLI 覆盖解析。
@config.parse
def recipe_main(cfg: DictConfig) -> None:
recipe = FullFinetuneRecipe(cfg=cfg)
recipe.setup(cfg=cfg)
recipe.train()
recipe.cleanup()
运行您的 Recipes¶
您应该能够通过提供自定义 Recipes 和自定义配置的直接路径来运行您的 Recipes,方法是使用 tune 命令以及任何 CLI 覆盖
tune run <path/to/recipe> --config <path/to/config> k1=v1 k2=v2 ...