Qwen-VL传统方法微调
LLaMA Factory 与传统微调方法在大模型微调中的应用
LLaMA Factory 为大模型微调提供了一个强大且方便的平台,集成了多种模型、训练方法和先进算法,能够提升训练效率和模型性能。如果它能够满足您的需求,并且您愿意投入时间去学习和适应新的工具,那么采用 LLaMA Factory 可能会为您的工作带来效率提升。
然而,传统的微调方法(如 LoRA、QLoRA、全参数微调等)仍然在广泛使用,并且在许多场景下依然适用。它们有着成熟的工具链、丰富的社区支持和大量的实践经验,对于需要高度灵活性和可控性的项目来说,可能仍然是最佳选择。
建议您根据项目的具体情况,做出最适合的选择。如果有可能,不妨同时熟悉多种方法,以便在不同的场景下灵活运用。
1. 什么是 LLaMA Factory 方法?
LLaMA Factory 是一个针对大模型优化的微调框架,最初为 LLaMA 模型设计,但现已支持了大量的模型,包括 LLaMA、Qwen、ChatGLM、Mistral、DeepSeek 等。它旨在简化和加速大模型的微调过程,提供了一套完整的工具链,包括数据处理、训练脚本、模型评估和部署等,方便用户快速上手微调各种大模型。
2. 与传统微调方法的对比
传统微调方法主要包括:
- 全参数微调:更新模型的所有参数,充分发挥模型潜力,但需要大量的计算资源和时间。
- LoRA(Low-Rank Adaptation):通过在模型部分层引入低秩适配器,只需训练少量新增参数,大幅降低显存占用和计算成本。
- QLoRA:在 LoRA 的基础上,将模型参数进行量化(如 4 位或 8 位整数表示),进一步减少显存占用。
LLaMA Factory 方法的特点:
- 广泛的模型支持:现已支持 LLaMA、Qwen、ChatGLM 等多种模型,用户可以在同一框架下微调不同的模型。
- 集成多种训练方法:包括连续预训练、多模态监督微调、奖励建模、PPO、DPO 等,满足不同任务需求。
- 先进的算法与实用技巧:集成了 BAdam、APOLLO 等优化算法,以及 FlashAttention-2、RoPE scaling 等实用训练技巧,提升训练效率和模型性能。
- 丰富的任务支持:支持多轮对话、工具使用、图像理解、视频识别、音频理解等任务,适用范围广泛。
- 易于监控和部署:支持 LlamaBoard、TensorBoard、Wandb 等实验监控工具,并提供 OpenAI 风格的 API、Gradio UI 和 CLI,方便模型部署。
3. 是否应该使用 LLaMA Factory 取代传统方法?
目前来看,LLaMA Factory 并没有完全取代传统的微调方法,原因如下:
- 成熟度与稳定性:LoRA、QLoRA、全参数微调等方法经过大量实践验证,社区支持完善,工具链成熟,适用于多种模型和任务。
- 灵活性与可控性:传统方法允许用户根据具体需求自定义微调流程和参数,例如选择微调哪些层、如何配置优化器等,具备更高的灵活性。
- 适用范围:虽然 LLaMA Factory 支持多种模型,但仍可能无法涵盖所有模型,特别是一些私有或定制的模型。
- 学习成本与迁移成本:切换到新的框架需要一定的学习和适应时间,如果现有方法已经满足需求,并且团队已经熟练掌握,可能暂时没有必要切换。
4. 如何选择微调方法?
选择微调方法应考虑以下因素:
- 模型兼容性:如果您的模型已经得到 LLaMA Factory 的良好支持,并且框架提供了您所需的功能,那么使用它可能会更加便捷。
- 功能需求:如果您需要 LLaMA Factory 提供的特定功能或优化算法(如高级优化器、特殊的训练技巧等),那么它可能是更好的选择。
- 资源限制:如果计算资源有限,LoRA 和 QLoRA 是不错的选择,能够在保证性能的同时降低资源需求。
- 性能需求:如果追求最佳性能,且有充足的资源,可以考虑全参数微调或结合 LLaMA Factory 的优化策略。
- 自定义需求:如果需要对微调过程进行高度定制,传统方法可能更适合,允许更细粒度地控制训练过程。
- 团队技能与学习成本:评估团队对新工具的接受程度,考虑学习新框架所需的时间和成本。
Qwen-VL传统方法微调
Qwen-VL github 提供了官方的训练脚本 finetune.py
,供用户以简单的方式对预训练模型进行下游任务的微调。提供了学习传统微调方法的绝佳示例。此外,还提供了Shell脚本来启动微调。该脚本支持使用 DeepSpeed 和 FSDP 进行训练。提供的 Shell 脚本默认使用 DeepSpeed,因此在开始之前,建议您安装 DeepSpeed:
pip install deepspeed
数据准备
要准备您的训练数据,您需要将所有样本放入一个列表中,并将其保存为一个 JSON 文件。每个样本是一个包含id
和对话列表的字典。下面是一个包含 3 个样本的简单示例:
[
{
"id": "identity_0",
"conversations": [
{
"from": "user",
"value": "你好"
},
{
"from": "assistant",
"value": "我是Qwen-VL,一个支持视觉输入的大模型。"
}
]
},
{
"id": "identity_1",
"conversations": [
{
"from": "user",
"value": "Picture 1: <img>https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg</img>\n图中的狗是什么品种?"
},
{
"from": "assistant",
"value": "图中是一只拉布拉多犬。"
},
{
"from": "user",
"value": "框出图中的格子衬衫"
},
{
"from": "assistant",
"value": "<ref>格子衬衫</ref><box>(588,499),(725,789)</box>"
}
]
},
{
"id": "identity_2",
"conversations": [
{
"from": "user",
"value": "Picture 1: <img>assets/mm_tutorial/Chongqing.jpeg</img>\nPicture 2: <img>assets/mm_tutorial/Beijing.jpeg</img>\n图中都是哪"
},
{
"from": "assistant",
"value": "第一张图片是重庆的城市天际线,第二张图片是北京的天际线。"
}
]
}
]
对于视觉语言(VL)任务,有一些特殊的标记需要使用,包括 <img>
、</img>
、<ref>
、</ref>
、<box>
、</box>
。
图片在对话中的表示为:Picture id: <img>img_path</img>\n{你的提示}
,其中 id
表示图片在对话中的位置,从 1 开始。img_path
可以是本地文件路径或网络链接。
坐标框表示为 <box>(x1,y1),(x2,y2)</box>
,其中 (x1, y1)
和 (x2, y2)
是归一化到范围 [0, 1000) 的值。其对应的文字描述可以使用 <ref>文字描述</ref>
进行标识。
数据准备完成后,您可以使用提供的 Shell 脚本来运行微调。记得在脚本中指定数据文件路径 $DATA
。
微调方式
微调脚本允许您执行:
- 全参数微调
- LoRA
- Q-LoRA
全参数微调
全参数微调需要在整个训练过程中更新 LLM 的所有参数。在我们的实验中,在微调阶段冻结 ViT(视觉Transformer)的参数可以获得更好的性能。要启动训练,请运行以下脚本:
sh finetune/finetune_ds.sh
记得在脚本中指定正确的模型名称或路径、数据路径以及输出目录。如果您想进行更改,只需删除参数 --deepspeed
,或者根据您的需求在 DeepSpeed 配置的 JSON 文件中进行更改。此外,该脚本支持混合精度训练,因此您可以使用 --bf16 True
或 --fp16 True
。根据经验,如果您的机器支持bf16,我们建议您使用bf16,以使您的训练与我们的预训练和对齐过程保持一致,因此我们默认使用bf16。
LoRA
同样地,要运行 LoRA,请使用另一个脚本来运行,如下所示。在开始之前,确保您已安装 peft
。此外,您需要指定模型、数据和输出的路径。我们建议您使用预训练模型的绝对路径。这是因为 LoRA 只保存适配器,并且适配器配置的 JSON 文件中的绝对路径用于找到要加载的预训练模型。
单 GPU 训练:
sh finetune/finetune_lora_single_gpu.sh
分布式训练:
sh finetune/finetune_lora_ds.sh
与全参数微调相比,LoRA(论文)只更新适配器层的参数,而保持原始的大语言模型层冻结。这允许大大减少内存消耗,从而降低计算成本。
注意,如果您使用 LoRA 对基础语言模型(例如 Qwen-VL
)进行微调,而不是聊天模型(例如 Qwen-VL-Chat
),脚本会自动将嵌入层和输出层设置为可训练参数。这是因为基础语言模型不了解由 ChatML 格式引入的特殊标记。因此,这些层应被更新,以使模型理解和预测这些标记。换句话说,如果您的训练中在 LoRA 中引入了特殊标记,您应该通过在代码中设置 modules_to_save
来将这些层设置为可训练参数。此外,我们发现 LoRA 在是否包含这些可训练参数的情况下,其内存占用存在显著差异。因此,如果您遇到内存问题,我们建议您对聊天模型进行 LoRA 微调。请查看下面的性能分析以获取更多信息。
Q-LoRA
如果您仍然面临内存不足的问题,可以考虑 Q-LoRA(论文),它使用量化的大语言模型和其他技术(如分页注意力)来实现更低的内存消耗。要运行 Q-LoRA,直接运行以下脚本:
单 GPU 训练:
sh finetune/finetune_qlora_single_gpu.sh
分布式训练:
sh finetune/finetune_qlora_ds.sh
对于 Q-LoRA,我们建议您加载我们提供的量化模型,例如 Qwen-VL-Chat-Int4
。您不应该使用 bf16 模型。与全参数微调和 LoRA 不同,Q-LoRA 仅支持 fp16。此外,对于 Q-LoRA,LoRA 中的特殊标记问题仍然存在。然而,由于我们只为聊天模型提供了 Int4 模型,这意味着语言模型已经学习了 ChatML 格式的特殊标记,您无需担心这些层。请注意,Int4 模型的这些层不应是可训练的,因此如果您在训练中引入特殊标记,Q-LoRA 可能无法正常工作。
与全参数微调不同,LoRA 和 Q-LoRA 的训练仅保存适配器参数。您可以按如下所示加载微调后的模型进行推理:
from peft import AutoPeftModelForCausalLM
model = AutoPeftModelForCausalLM.from_pretrained(
path_to_adapter, # 适配器输出目录的路径
device_map="auto",
trust_remote_code=True
).eval()
如果您想合并适配器并将微调后的模型保存为独立的模型(您只能对 LoRA 执行此操作,不能合并 Q-LoRA 的参数),可以运行以下代码:
from peft import AutoPeftModelForCausalLM
model = AutoPeftModelForCausalLM.from_pretrained(
path_to_adapter, # 适配器输出目录的路径
device_map="auto",
trust_remote_code=True
).eval()
merged_model = model.merge_and_unload()
# max_shard_size 和 safe_serialization 不是必要的。
# 它们分别用于分片检查点和将模型保存为 safetensors 格式
merged_model.save_pretrained(new_model_directory, max_shard_size="2048MB", safe_serialization=True)
注意:对于多 GPU 训练,您需要根据您的机器指定适当的分布式训练超参数。此外,我们建议您根据数据、内存占用和训练速度的考虑,通过参数 --model_max_length
指定您的最大序列长度。
内存和速度的性能分析
我们在单 GPU 训练的设置下,对 LoRA(Base 指训练嵌入层和输出层,而 LoRA(Chat)没有可训练的嵌入层和输出层)和 Q-LoRA 的 GPU 内存和训练速度进行了分析。在此测试中,我们在一台 A100-SXM4-80G GPU 上进行实验,使用 CUDA 11.8 和 PyTorch 2.0。我们统一使用批大小为 1,梯度累积为 8。每个样本包含一张图片。我们对不同输入长度(分别为 384、512、1024 和 2048)的内存(GB)和速度(秒/迭代)进行了分析,统计结果如下:
方法 | 序列长度 | 内存占用 / 速度 |
---|---|---|
384 | ||
LoRA (Base) | 37.1G / 2.3s/it | |
LoRA (Chat) | 23.3G / 2.2s/it | |
Q-LoRA | 17.0G / 4.2s/it | |
512 | ||
LoRA (Base) | 37.3G / 2.4s/it | |
LoRA (Chat) | 23.6G / 2.3s/it | |
Q-LoRA | 17.2G / 4.5s/it | |
1024 | ||
LoRA (Base) | 38.7G / 3.6s/it | |
LoRA (Chat) | 25.1G / 3.5s/it | |
Q-LoRA | 18.2G / 5.5s/it | |
2048 | ||
LoRA (Base) | 38.7G / 6.1s/it | |
LoRA (Chat) | 27.3G / 5.9s/it | |
Q-LoRA | 19.3G / 7.9s/it |
从表中可以看出,LoRA(Chat)在内存占用和速度方面都优于 LoRA(Base),而 Q-LoRA 内存占用最低,但训练速度较慢。
总结
通过以上步骤,您可以根据自己的需求和资源情况,选择合适的微调方式:
- 全参数微调:更新所有参数,适合有足够计算资源且希望模型达到最佳性能的情况。
- LoRA:只更新适配器层的参数,减少了内存和计算成本,适合资源有限的情况。
- Q-LoRA:使用量化模型和适配器,进一步降低内存占用,适合内存非常有限的情况,但训练速度可能会变慢。
请根据您的具体情况,选择合适的微调策略,并在训练过程中注意调整相应的超参数以获得最佳效果。
微调代码
下面,我将对您提供的微调代码进行详细的注释和解释。这段代码基于改进后的 FastChat 代码,来自 tatsu-lab/stanford_alpaca
。主要用于对预训练的语言模型进行微调,支持 LoRA、Q-LoRA、DeepSpeed 等技术。
总体概述
该脚本的主要功能是:
- 定义模型、数据和训练的参数;
- 加载并预处理数据,包括对话格式的处理;
- 定义数据集类,用于训练和评估;
- 根据参数设置,加载预训练模型并应用 LoRA 或 Q-LoRA;
- 进行模型微调训练;
- 保存微调后的模型。
代码详解
导入必要的库和模块
from dataclasses import dataclass, field
import json
import math
import logging
import os
from typing import Dict, Optional, List
import torch
from torch.utils.data import Dataset
from deepspeed import zero
from deepspeed.runtime.zero.partition_parameters import ZeroParamStatus
import transformers
from transformers import Trainer, GPTQConfig, deepspeed
from transformers.trainer_pt_utils import LabelSmoother
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from accelerate.utils import DistributedType
这里导入了 PyTorch、Transformers、DeepSpeed、PEFT(用于 LoRA)、Accelerate 等库,以及一些数据处理和类型注释的模块。
定义常量
IGNORE_TOKEN_ID = LabelSmoother.ignore_index
IGNORE_TOKEN_ID
用于在计算损失时忽略作用,是 LabelSmoother
中定义的忽略索引。
定义参数数据类
@dataclass
class ModelArguments:
model_name_or_path: Optional[str] = field(default="Qwen/Qwen-7B")
@dataclass
class DataArguments:
data_path: str = field(
default=None, metadata={"help": "Path to the training data."}
)
eval_data_path: str = field(
default=None, metadata={"help": "Path to the evaluation data."}
)
lazy_preprocess: bool = False
@dataclass
class TrainingArguments(transformers.TrainingArguments):
cache_dir: Optional[str] = field(default=None)
optim: str = field(default="adamw_torch")
model_max_length: int = field(
default=8192,
metadata={
"help": "Maximum sequence length. Sequences will be right padded (and possibly truncated)."
},
)
use_lora: bool = False
fix_vit: bool = True
@dataclass
class LoraArguments:
lora_r: int = 64
lora_alpha: int = 16
lora_dropout: float = 0.05
lora_target_modules: List[str] = field(
default_factory=lambda: ["c_attn", "attn.c_proj", "w1", "w2"]
)
lora_weight_path: str = ""
lora_bias: str = "none"
q_lora: bool = False
这些数据类用于存储模型、数据、训练和 LoRA 的参数:
ModelArguments
:包括模型名称或路径。DataArguments
:包括训练数据和评估数据的路径,以及是否进行懒惰预处理。TrainingArguments
:继承自transformers.TrainingArguments
,增加了一些自定义参数,例如是否使用 LoRA、是否固定 ViT 参数等。LoraArguments
:包括 LoRA 的超参数设置,例如秩(lora_r
)、缩放因子(lora_alpha
)、Dropout 等。
处理 DeepSpeed Zero 3 相关的函数
def maybe_zero_3(param):
if hasattr(param, "ds_id"):
assert param.ds_status == ZeroParamStatus.NOT_AVAILABLE
with zero.GatheredParameters([param]):
param = param.data.detach().cpu().clone()
else:
param = param.detach().cpu().clone()
return param
def get_peft_state_maybe_zero_3(named_params, bias):
if bias == "none":
to_return = {k: t for k, t in named_params if "lora_" in k}
elif bias == "all":
to_return = {k: t for k, t in named_params if "lora_" in k or "bias" in k}
elif bias == "lora_only":
to_return = {}
maybe_lora_bias = {}
lora_bias_names = set()
for k, t in named_params:
if "lora_" in k:
to_return[k] = t
bias_name = k.split("lora_")[0] + "bias"
lora_bias_names.add(bias_name)
elif "bias" in k:
maybe_lora_bias[k] = t
for k, t in maybe_lora_bias:
if bias_name in lora_bias_names:
to_return[bias_name] = t
else:
raise NotImplementedError
to_return = {k: maybe_zero_3(v) for k, v in to_return.items()}
return to_return
这些函数用于在 DeepSpeed Zero 3 模式下正确地获取和处理模型参数,特别是 LoRA 的参数,以便在保存和加载模型时能够正确处理分布式环境下的参数。
maybe_zero_3
:检查参数是否在 DeepSpeed Zero 3 的管理下,如果是,则使用zero.GatheredParameters
来收集参数。get_peft_state_maybe_zero_3
:获取 LoRA 模型的状态字典,考虑了不同的偏置选项(none
、all
、lora_only
),并使用maybe_zero_3
来处理参数。
打印函数和安全保存模型
local_rank = None
def rank0_print(*args):
if local_rank == 0:
print(*args)
def safe_save_model_for_hf_trainer(trainer: transformers.Trainer, output_dir: str, bias="none"):
"""Collects the state dict and dump to disk."""
# check if zero3 mode enabled
if deepspeed.is_deepspeed_zero3_enabled():
state_dict = trainer.model_wrapped._zero3_consolidated_16bit_state_dict()
else:
if trainer.args.use_lora:
state_dict = get_peft_state_maybe_zero_3(
trainer.model.named_parameters(), bias
)
else:
state_dict = trainer.model.state_dict()
if trainer.args.should_save and trainer.args.local_rank == 0:
trainer._save(output_dir, state_dict=state_dict)
rank0_print
:只在主进程(local_rank == 0
)上打印信息,以避免在分布式训练中重复打印。safe_save_model_for_hf_trainer
:在 Hugging Face 的Trainer
中安全地保存模型,考虑了 DeepSpeed Zero 3 和 LoRA 的情况。
数据预处理函数
def preprocess(
sources,
tokenizer: transformers.PreTrainedTokenizer,
max_len: int,
system_message: str = "You are a helpful assistant."
) -> Dict:
roles = {"user": "user", "assistant": "assistant"}
im_start = tokenizer.im_start_id
im_end = tokenizer.im_end_id
nl_tokens = tokenizer('\n').input_ids
_system = tokenizer('system').input_ids + nl_tokens
_user = tokenizer('user').input_ids + nl_tokens
_assistant = tokenizer('assistant').input_ids + nl_tokens
# 应用提示模板
input_ids, targets = [], []
for i, source in enumerate(sources):
# 如果第一个消息不是用户的,则跳过,以确保对话以用户开始
if roles[source[0]["from"]] != roles["user"]:
source = source[1:]
input_id, target = [], []
# 添加系统消息
system = [im_start] + _system + tokenizer(system_message).input_ids + [im_end] + nl_tokens
input_id += system
# 系统消息在目标中被忽略(不计算损失)
target += [im_start] + [IGNORE_TOKEN_ID] * (len(system)-3) + [im_end] + nl_tokens
assert len(input_id) == len(target)
for j, sentence in enumerate(source):
role = roles[sentence["from"]]
_input_id = tokenizer(role).input_ids + nl_tokens + \
tokenizer(sentence["value"]).input_ids + [im_end] + nl_tokens
input_id += _input_id
if role == 'user':
# 用户消息在目标中被忽略
_target = [im_start] + [IGNORE_TOKEN_ID] * (len(_input_id)-3) + [im_end] + nl_tokens
elif role == 'assistant':
# 助手消息的输入和目标相同,但角色和特殊标记被忽略
_target = [im_start] + [IGNORE_TOKEN_ID] * len(tokenizer(role).input_ids) + \
_input_id[len(tokenizer(role).input_ids)+1:-2] + [im_end] + nl_tokens
else:
raise NotImplementedError
target += _target
assert len(input_id) == len(target)
# 填充到最大长度
input_id += [tokenizer.pad_token_id] * (max_len - len(input_id))
target += [IGNORE_TOKEN_ID] * (max_len - len(target))
input_ids.append(input_id[:max_len])
targets.append(target[:max_len])
input_ids = torch.tensor(input_ids, dtype=torch.int)
targets = torch.tensor(targets, dtype=torch.int)
return dict(
input_ids=input_ids,
labels=targets,
attention_mask=input_ids.ne(tokenizer.pad_token_id),
)
该函数将原始的对话数据转换为模型可以接受的输入和标签。主要流程:
- 定义角色标记和特殊标记(如
和
)。 - 循环遍历每个对话,将其转换为输入 ID 和目标 ID。
- 对系统的提示消息进行处理,并将其添加到输入中。
- 对于用户消息,在目标中使用
IGNORE_TOKEN_ID
,以避免计算损失(因为模型无需预测用户输入)。 - 对于助手消息,模型需要预测实际的回复内容。
- 最后,确保输入和目标的长度一致,并进行填充或截断。
定义数据集类
class SupervisedDataset(Dataset):
"""Dataset for supervised fine-tuning."""
def __init__(self, raw_data, tokenizer: transformers.PreTrainedTokenizer, max_len: int):
super(SupervisedDataset, self).__init__()
rank0_print("Formatting inputs...")
sources = [example["conversations"] for example in raw_data]
data_dict = preprocess(sources, tokenizer, max_len)
self.input_ids = data_dict["input_ids"]
self.labels = data_dict["labels"]
self.attention_mask = data_dict["attention_mask"]
def __len__(self):
return len(self.input_ids)
def __getitem__(self, i) -> Dict[str, torch.Tensor]:
return dict(
input_ids=self.input_ids[i],
labels=self.labels[i],
attention_mask=self.attention_mask[i],
)
SupervisedDataset
类用于创建训练所需的数据集。它在初始化时对数据进行处理,生成 input_ids
、labels
和 attention_mask
。
class LazySupervisedDataset(Dataset):
"""Dataset for supervised fine-tuning."""
def __init__(self, raw_data, tokenizer: transformers.PreTrainedTokenizer, max_len: int):
super(LazySupervisedDataset, self).__init__()
self.tokenizer = tokenizer
self.max_len = max_len
rank0_print("Formatting inputs...Skip in lazy mode")
self.raw_data = raw_data
self.cached_data_dict = {}
def __len__(self):
return len(self.raw_data)
def __getitem__(self, i) -> Dict[str, torch.Tensor]:
if i in self.cached_data_dict:
return self.cached_data_dict[i]
ret = preprocess([self.raw_data[i]["conversations"]], self.tokenizer, self.max_len)
ret = dict(
input_ids=ret["input_ids"][0],
labels=ret["labels"][0],
attention_mask=ret["attention_mask"][0],
)
self.cached_data_dict[i] = ret
return ret
LazySupervisedDataset
类在每次获取数据时才对单个样本进行预处理,适用于数据量较大或处理开销较大的情况。
创建数据模块
def make_supervised_data_module(
tokenizer: transformers.PreTrainedTokenizer, data_args, max_len,
) -> Dict:
"""Make dataset and collator for supervised fine-tuning."""
dataset_cls = (
LazySupervisedDataset if data_args.lazy_preprocess else SupervisedDataset
)
rank0_print("Loading data...")
train_json = json.load(open(data_args.data_path, "r"))
train_dataset = dataset_cls(train_json, tokenizer=tokenizer, max_len=max_len)
if data_args.eval_data_path:
eval_json = json.load(open(data_args.eval_data_path, "r"))
eval_dataset = dataset_cls(eval_json, tokenizer=tokenizer, max_len=max_len)
else:
eval_dataset = None
return dict(train_dataset=train_dataset, eval_dataset=eval_dataset)
该函数根据参数决定使用哪种数据集类,然后加载训练和评估数据集。
训练主函数
def train():
global local_rank
parser = transformers.HfArgumentParser(
(ModelArguments, DataArguments, TrainingArguments, LoraArguments)
)
(
model_args,
data_args,
training_args,
lora_args,
) = parser.parse_args_into_dataclasses()
if getattr(training_args, 'deepspeed', None) and getattr(lora_args, 'q_lora', False):
training_args.distributed_state.distributed_type = DistributedType.DEEPSPEED
compute_dtype = (
torch.float16
if training_args.fp16
else (torch.bfloat16 if training_args.bf16 else torch.float32)
)
local_rank = training_args.local_rank
device_map = None
world_size = int(os.environ.get("WORLD_SIZE", 1))
ddp = world_size != 1
if lora_args.q_lora:
device_map = {"": int(os.environ.get("LOCAL_RANK") or 0)} if ddp else None
if len(training_args.fsdp) > 0 or deepspeed.is_deepspeed_zero3_enabled():
logging.warning(
"FSDP or ZeRO3 are not incompatible with QLoRA."
)
# 设置 RoPE 缩放因子
config = transformers.AutoConfig.from_pretrained(
model_args.model_name_or_path,
cache_dir=training_args.cache_dir,
trust_remote_code=True,
)
config.use_cache = False
# 加载模型和分词器
model = transformers.AutoModelForCausalLM.from_pretrained(
model_args.model_name_or_path,
config=config,
cache_dir=training_args.cache_dir,
device_map=device_map,
trust_remote_code=True,
quantization_config=GPTQConfig(
bits=4, disable_exllama=True
)
if training_args.use_lora and lora_args.q_lora
else None,
)
# 如果不使用 LoRA,且需要固定 ViT(视觉部分)的参数
if not training_args.use_lora:
if training_args.fix_vit and hasattr(model,'transformer') and hasattr(model.transformer,'visual'):
model.transformer.visual.requires_grad_(False)
if hasattr(model.transformer.visual,'attn_pool'):
model.transformer.visual.attn_pool.requires_grad_(True)
tokenizer = transformers.AutoTokenizer.from_pretrained(
model_args.model_name_or_path,
cache_dir=training_args.cache_dir,
model_max_length=training_args.model_max_length,
padding_side="right",
use_fast=False,
trust_remote_code=True,
)
tokenizer.pad_token_id = tokenizer.eod_id
if training_args.use_lora:
# 判断是否需要保存其他模块,例如嵌入层和输出层
if lora_args.q_lora or "chat" in model_args.model_name_or_path.lower():
modules_to_save = None
else:
modules_to_save = ["wte", "lm_head"]
lora_config = LoraConfig(
r=lora_args.lora_r,
lora_alpha=lora_args.lora_alpha,
target_modules=lora_args.lora_target_modules,
lora_dropout=lora_args.lora_dropout,
bias=lora_args.lora_bias,
task_type="CAUSAL_LM",
modules_to_save=modules_to_save # 这个参数用于添加新标记
)
if lora_args.q_lora:
model = prepare_model_for_kbit_training(
model, use_gradient_checkpointing=training_args.gradient_checkpointing
)
model = get_peft_model(model, lora_config)
if training_args.gradient_checkpointing:
model.enable_input_require_grads()
# 加载数据
data_module = make_supervised_data_module(
tokenizer=tokenizer, data_args=data_args, max_len=training_args.model_max_length
)
# 开始训练
trainer = Trainer(
model=model, tokenizer=tokenizer, args=training_args, **data_module
)
trainer.train()
trainer.save_state()
safe_save_model_for_hf_trainer(trainer=trainer, output_dir=training_args.output_dir, bias=lora_args.lora_bias)
train
函数是主要的训练入口,执行以下步骤:
-
解析命令行参数:使用
transformers.HfArgumentParser
解析模型、数据、训练和 LoRA 的参数。 -
设置计算设备和类型:根据参数,确定是否使用 FP16、BF16 还是 FP32,以及是否使用分布式训练。
-
加载配置和模型:
- 设置模型配置,并禁用缓存(在训练时不需要缓存)。
- 加载预训练模型,根据是否使用 LoRA 和 Q-LoRA,设置相应的配置。
- 如果不使用 LoRA,且需要固定 ViT 的参数,则冻结视觉部分的参数,只训练语言模型部分。
-
加载分词器:并设置填充标记。
-
配置 LoRA:
- 如果使用 LoRA,设置 LoRA 的配置,包括哪些模块需要 LoRA 修改。
- 如果使用 Q-LoRA,准备模型以支持低比特训练。
- 应用
get_peft_model
将 LoRA 应用于模型。
-
加载数据:使用预先定义的数据集类加载训练和评估数据。
-
启动训练:使用 Hugging Face 的
Trainer
进行训练。 -
保存模型:在训练完成后,保存训练状态和模型。
程序入口
if __name__ == "__main__":
train()
当直接运行该脚本时,调用 train
函数开始训练。
总结
这段代码实现了一个可配置的、支持多种微调方法的训练脚本,主要特点包括:
- 可配置性:使用了数据类来管理参数,可以方便地通过命令行或配置文件来设置训练参数。
- 支持 LoRA 和 Q-LoRA:可以根据需要选择是否使用 LoRA,以及是否使用量化的 Q-LoRA,以适应不同的资源和需求。
- 数据预处理:提供了高效的对话数据预处理函数,支持懒惰加载和缓存,以提高训练效率。
- 兼容 DeepSpeed:考虑了在 DeepSpeed Zero 3 模式下的参数处理和模型保存,适用于大模型的分布式训练。
全尺寸微调
下面,我将对微调脚本 finetune/finetune_ds.sh
进行详细的解释,逐行说明其作用和用途,帮助您理解如何使用该脚本进行模型微调。
脚本概述
该 Bash 脚本旨在使用 DeepSpeed 和 PyTorch 的 torchrun
工具,在多 GPU 环境下对 Qwen-VL-Chat
模型进行微调。它设置了环境变量、分布式训练参数,并调用 finetune.py
脚本进行训练。
脚本详细解释
Shebang 行
#!/bin/bash
- 指定脚本使用 Bash 解释器执行。
设置 CUDA 设备最大连接数
export CUDA_DEVICE_MAX_CONNECTIONS=1
- 设置环境变量
CUDA_DEVICE_MAX_CONNECTIONS
为1
。 - 作用:限制每个 CUDA 设备的最大连接数,有助于减少多 GPU 训练时的 GPU 内存占用。
获取当前工作目录
DIR=`pwd`
- 使用
pwd
命令获取当前工作目录,并将其赋值给变量DIR
。 - 注意:在这个脚本中,
DIR
并未被使用,这可能是为了将来扩展或在此前版本中使用的遗留变量。
设置分布式训练参数
GPUS_PER_NODE=8
NNODES=1
NODE_RANK=0
MASTER_ADDR=localhost
MASTER_PORT=6001
GPUS_PER_NODE=8
:每个节点的 GPU 数量,这里设置为 8。NNODES=1
:参与训练的节点总数,这里设置为 1,表示单节点训练。NODE_RANK=0
:当前节点的排名(从 0 开始),单节点情况下为 0。MASTER_ADDR=localhost
:主节点的地址,单节点情况下为localhost
。MASTER_PORT=6001
:主节点通信的端口号,可以根据需要更改,避免端口冲突。
指定模型路径
MODEL="Qwen/Qwen-VL-Chat" #"Qwen/Qwen-VL-Chat"/"Qwen/Qwen-VL" # Set the path if you do not want to load from huggingface directly
MODEL
:指定要微调的预训练模型,这里使用 Hugging Face 上的Qwen/Qwen-VL-Chat
模型。- 注释部分指出,如果您不想直接从 Hugging Face 下载模型,可以将模型的本地路径设置为
MODEL
。
指定训练数据路径
# ATTENTION: specify the path to your training data, which should be a json file consisting of a list of conversations.
# See the section for finetuning in README for more information.
DATA="path_to_data"
DATA
:设置您的训练数据文件的路径。- 注释提醒您需要将
DATA
设置为实际的数据文件路径,数据应为一个包含对话列表的 JSON 文件。更多关于数据格式的信息,请参考项目的 README 中的微调部分。
构建分布式训练参数
DISTRIBUTED_ARGS="
--nproc_per_node $GPUS_PER_NODE \
--nnodes $NNODES \
--node_rank $NODE_RANK \
--master_addr $MASTER_ADDR \
--master_port $MASTER_PORT
"
DISTRIBUTED_ARGS
:包含分布式训练所需的参数,将传递给torchrun
命令。--nproc_per_node $GPUS_PER_NODE
:每个节点上的进程(GPU)数量。--nnodes $NNODES
:节点总数。--node_rank $NODE_RANK
:当前节点的排名。--master_addr $MASTER_ADDR
:主节点地址。--master_port $MASTER_PORT
:主节点端口。
运行训练脚本
torchrun $DISTRIBUTED_ARGS finetune.py \
--model_name_or_path $MODEL \
--data_path $DATA \
--bf16 True \
--fix_vit True \
--output_dir output_qwen \
--num_train_epochs 5 \
--per_device_train_batch_size 1 \
--per_device_eval_batch_size 1 \
--gradient_accumulation_steps 16 \
--evaluation_strategy "no" \
--save_strategy "steps" \
--save_steps 1000 \
--save_total_limit 10 \
--learning_rate 1e-5 \
--weight_decay 0.1 \
--adam_beta2 0.95 \
--warmup_ratio 0.01 \
--lr_scheduler_type "cosine" \
--logging_steps 1 \
--report_to "none" \
--model_max_length 2048 \
--gradient_checkpointing True \
--lazy_preprocess True \
--deepspeed finetune/ds_config_zero3.json
- 使用
torchrun
命令启动分布式训练,传入先前定义的分布式参数DISTRIBUTED_ARGS
。 - 传递给
finetune.py
脚本的参数解释如下:
模型与数据路径
--model_name_or_path $MODEL
:指定预训练模型的名称或路径,这里使用变量$MODEL
。--data_path $DATA
:指定训练数据的路径,这里使用变量$DATA
。
混合精度训练
--bf16 True
:启用 bfloat16(bf16)混合精度训练,这有助于减少内存占用和提高计算效率。- 注意:您的硬件需要支持 bf16,例如 NVIDIA A100 GPU。
- 如果硬件不支持 bf16,可以使用
--fp16 True
来启用 fp16 混合精度训练。
模型设置
--fix_vit True
:在训练过程中固定视觉 Transformer(ViT)的参数,只训练语言模型部分。- 如果您想要微调视觉编码器,可以将其设置为
False
。
- 如果您想要微调视觉编码器,可以将其设置为
输出设置
--output_dir output_qwen
:指定模型和检查点的输出目录。
训练超参数
--num_train_epochs 5
:训练周期数,训练 5 个 Epoch。--per_device_train_batch_size 1
:每个设备(GPU)的训练批次大小为 1。--per_device_eval_batch_size 1
:每个设备的评估批次大小为 1。--gradient_accumulation_steps 16
:梯度累积步数,为 16。- 有效批次大小计算方式:
所以有效批次大小为有效批次大小 = per_device_train_batch_size * gradient_accumulation_steps * GPU数量
1 * 16 * 8 = 128
。
- 有效批次大小计算方式:
--learning_rate 1e-5
:学习率,设置为 1e-5。--weight_decay 0.1
:权重衰减系数,用于防止过拟合。--adam_beta2 0.95
:Adam 优化器的 Beta2 参数。--warmup_ratio 0.01
:学习率预热比例,为总训练步数的 1%。--lr_scheduler_type "cosine"
:使用余弦退火学习率调度器。
检查点与保存策略
--evaluation_strategy "no"
:不在训练过程中进行评估。--save_strategy "steps"
:按照训练步数来保存模型。--save_steps 1000
:每 1000 个训练步骤保存一次模型。--save_total_limit 10
:最多保留 10 个检查点,超过的将被删除。
日志记录与报告
--logging_steps 1
:每个训练步骤都进行日志记录。--report_to "none"
:不将日志报告到外部工具,例如 TensorBoard 或 WandB。
模型与数据处理
--model_max_length 2048
:模型的最大序列长度,输入将被截断或填充到这个长度。--gradient_checkpointing True
:启用梯度检查点,减少显存占用,代价是一些计算开销。--lazy_preprocess True
:延迟数据预处理,有助于处理大型数据集。
DeepSpeed 配置
--deepspeed finetune/ds_config_zero3.json
:指定 DeepSpeed 的配置文件,使用 ZeRO 3 优化器阶段。- ZeRO 3 可以在 GPU 间分片模型参数和优化器状态,有效减少大模型的内存占用。
附加说明
调整硬件设置
- 如果您的服务器上 GPU 数量不同,请调整
GPUS_PER_NODE
参数。 - 多节点训练时,请相应地调整
NNODES
、NODE_RANK
、MASTER_ADDR
和MASTER_PORT
。
数据路径
- 确保
DATA
变量指向正确的训练数据文件路径,并且数据格式符合finetune.py
脚本的要求。
模型检查点
- 保存在
--output_dir
指定目录下的模型检查点可能占用大量磁盘空间。 - 根据存储空间和需求,调整
--save_total_limit
和--save_steps
以控制保存的检查点数量。
混合精度训练
- 如果您的 GPU 不支持 bf16,可以切换到 fp16 训练,将
--bf16 True
替换为--fp16 True
。 - 混合精度训练有助于加速计算和减少显存占用。
调整超参数
- 根据任务和数据集的规模,可以调整学习率、批次大小、梯度累积步数等超参数。
- 增加
per_device_train_batch_size
或减小gradient_accumulation_steps
都会增大每个步骤的内存占用,需要根据 GPU 显存情况进行调整。
启用评估和报告
- 如果在训练过程中需要进行评估,可以修改
--evaluation_strategy
参数,例如设置为"steps"
或"epoch"
。 - 可以将
--report_to
设置为"tensorboard"
或"wandb"
,以启用日志报告到相应的工具。
修改模型
- 如果您需要微调视觉编码器部分,可以将
--fix_vit
设置为False
。
DeepSpeed 配置
- DeepSpeed 的配置文件
ds_config_zero3.json
应该根据您的硬件和需求进行设置。 - ZeRO 3 阶段可以在 GPU 间分片模型参数、梯度和优化器状态,以支持更大模型的训练。
总结
该脚本用于在多 GPU 环境下,对 Qwen-VL-Chat
模型进行微调,使用 DeepSpeed 和 PyTorch 的分布式训练功能。它设置了训练所需的各种参数,包括模型和数据路径、训练超参数、检查点保存策略,以及 DeepSpeed 的配置。
在运行该脚本之前,请确保:
- 设置正确的模型和数据路径。
- 调整分布式训练参数以匹配您的硬件设置。
- 根据您的任务和资源,调整超参数以获得最佳性能。
ZeRO-3
ZeRO-3是什么?
**ZeRO(Zero Redundancy Optimizer)**是DeepSpeed提出的一系列优化技术,旨在大幅降低大型深度学习模型的内存消耗,从而使训练超大规模模型成为可能。ZeRO分为三个阶段:
- ZeRO Stage 1:优化器状态分片,将优化器状态在数据并行进程间进行分片,减少内存占用。
- ZeRO Stage 2:在Stage 1的基础上,进一步对梯度进行分片,进一步降低内存需求。
- ZeRO Stage 3:在Stage 2的基础上,进一步对模型参数进行分片,实现对优化器状态、梯度和模型参数的全分片,极大地减少了内存冗余。
**ZeRO Stage 3(ZeRO-3)**通过对优化器状态、梯度和模型参数的全分片,将所有这些组件在多个GPU之间进行分布式存储和计算。这意味着每个GPU只需要存储和处理模型的一部分参数,从而大幅降低了单个GPU的内存占用,使得训练超大规模模型成为可能,即使模型的总大小超过了单个GPU的显存容量。
ZeRO-3的关键特性:
- 内存效率高:通过对模型参数、优化器状态和梯度的全分片,极大地减少了每个GPU的内存消耗。
- 扩展性强:支持数千个GPU协同训练,使得训练超大规模模型成为可能。
- 支持参数和优化器状态的Offloading:可以选择将参数和优化器状态Offload到CPU或NVMe存储,进一步减少GPU内存占用。
- 通信与计算重叠:利用通信与计算的重叠技术,最大化训练吞吐量。
ds_config_zero3.json
配置文件解析
下面,我们逐节解释您提供的ds_config_zero3.json
配置文件,详细说明每个参数的含义和作用。
{
"fp16": {
"enabled": "auto",
"loss_scale": 0,
"loss_scale_window": 1000,
"initial_scale_power": 16,
"hysteresis": 2,
"min_loss_scale": 1
},
"bf16": {
"enabled": "auto"
},
"optimizer": {
"type": "AdamW",
"params": {
"lr": "auto",
"betas": "auto",
"eps": "auto",
"weight_decay": "auto"
}
},
"scheduler": {
"type": "WarmupLR",
"params": {
"warmup_min_lr": "auto",
"warmup_max_lr": "auto",
"warmup_num_steps": "auto"
}
},
"zero_optimization": {
"stage": 3,
"offload_optimizer": {
"device": "none",
"pin_memory": true
},
"offload_param": {
"device": "none",
"pin_memory": true
},
"overlap_comm": true,
"contiguous_gradients": true,
"sub_group_size": 1e9,
"reduce_bucket_size": "auto",
"stage3_prefetch_bucket_size": "auto",
"stage3_param_persistence_threshold": "auto",
"stage3_max_live_parameters": 1e9,
"stage3_max_reuse_distance": 1e9,
"stage3_gather_16bit_weights_on_model_save": true
},
"gradient_accumulation_steps": "auto",
"gradient_clipping": "auto",
"steps_per_print": 100,
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"wall_clock_breakdown": false
}
1. 混合精度设置
fp16设置:
"fp16": {
"enabled": "auto",
"loss_scale": 0,
"loss_scale_window": 1000,
"initial_scale_power": 16,
"hysteresis": 2,
"min_loss_scale": 1
}
- enabled:
"auto"
- 自动启用或禁用FP16训练。根据命令行参数或训练参数决定是否启用。
- loss_scale:
0
- 当设置为
0
时,DeepSpeed将使用自动丢失缩放(Automatic Loss Scaling)。
- 当设置为
- loss_scale_window:
1000
- 调整损失缩放的步数窗口。
- initial_scale_power:
16
- 初始损失缩放的幂指数,即初始损失缩放值为
2^16
。
- 初始损失缩放的幂指数,即初始损失缩放值为
- hysteresis:
2
- 在降低损失缩放之前,需要连续出现溢出的次数。
- min_loss_scale:
1
- 最小的损失缩放值。
bf16设置:
"bf16": {
"enabled": "auto"
}
- enabled:
"auto"
- 自动启用或禁用BF16训练。根据命令行参数或训练参数决定是否启用。
说明:
- 这两个设置意味着DeepSpeed将根据训练脚本的参数,自动决定是使用FP16还是BF16进行混合精度训练。
- 混合精度训练可以减少显存占用,提高训练速度。
2. 优化器设置
"optimizer": {
"type": "AdamW",
"params": {
"lr": "auto",
"betas": "auto",
"eps": "auto",
"weight_decay": "auto"
}
}
- type:
"AdamW"
- 使用的优化器类型是AdamW。
- params:
- lr:
"auto"
- 学习率,将自动从训练参数中获取(通常在训练脚本中指定)。
- betas:
"auto"
- Adam优化器的beta参数,将自动使用默认值或从训练参数中获取。
- eps:
"auto"
- Adam优化器的epsilon参数,将自动使用默认值或从训练参数中获取。
- weight_decay:
"auto"
- 权重衰减系数,将自动从训练参数中获取。
- lr:
说明:
"auto"
表示这些参数将从训练脚本的配置中获取,方便在训练脚本中统一管理超参数。- AdamW 优化器在NLP任务中被广泛使用,适合Transformer模型的训练。
3. 学习率调度器设置
"scheduler": {
"type": "WarmupLR",
"params": {
"warmup_min_lr": "auto",
"warmup_max_lr": "auto",
"warmup_num_steps": "auto"
}
}
- type:
"WarmupLR"
- 使用预热学习率调度器。
- params:
- warmup_min_lr:
"auto"
- 预热阶段的最小学习率,将从训练参数中获取。
- warmup_max_lr:
"auto"
- 预热阶段的最大学习率,将从训练参数中获取,通常与初始学习率相同。
- warmup_num_steps:
"auto"
- 预热的步数,将从训练参数中获取。
- warmup_min_lr:
说明:
- 预热学习率调度器在训练开始时逐步增加学习率,有助于稳定训练并避免梯度爆炸。
- 使用
"auto"
可以从训练脚本中统一管理这些参数。
4. ZeRO优化设置
"zero_optimization": {
"stage": 3,
"offload_optimizer": {
"device": "none",
"pin_memory": true
},
"offload_param": {
"device": "none",
"pin_memory": true
},
"overlap_comm": true,
"contiguous_gradients": true,
"sub_group_size": 1e9,
"reduce_bucket_size": "auto",
"stage3_prefetch_bucket_size": "auto",
"stage3_param_persistence_threshold": "auto",
"stage3_max_live_parameters": 1e9,
"stage3_max_reuse_distance": 1e9,
"stage3_gather_16bit_weights_on_model_save": true
}
- stage:
3
- 启用ZeRO Stage 3优化。
- offload_optimizer:
- device:
"none"
- 不进行优化器状态的Offload,即优化器状态保留在GPU上。
- pin_memory:
true
- 如果启用了Offload,是否使用固定内存,提高数据传输速度。
- device:
- offload_param:
- device:
"none"
- 不进行参数的Offload,模型参数保留在GPU上。
- pin_memory:
true
- 同上。
- device:
- overlap_comm:
true
- 启用通信与计算重叠,提升训练速度。
- contiguous_gradients:
true
- 使用连续的内存空间存储梯度,提高内存访问效率。
- sub_group_size:
1e9
- 参数切片的子组大小,设置为
1e9
意味着基本不进行子分组,参数将尽可能少地被拆分。
- 参数切片的子组大小,设置为
- reduce_bucket_size:
"auto"
- 梯度归约(梯度聚合)的bucket大小,设置为
"auto"
让DeepSpeed自动决定。
- 梯度归约(梯度聚合)的bucket大小,设置为
- stage3_prefetch_bucket_size:
"auto"
- Stage 3中预取(Prefetch)参数的bucket大小,
"auto"
表示自动确定。
- Stage 3中预取(Prefetch)参数的bucket大小,
- stage3_param_persistence_threshold:
"auto"
- 控制参数持久化到GPU内存的阈值,大于该大小的参数将保留在GPU内存中。
- stage3_max_live_parameters:
1e9
- 允许同时存在于GPU内存中的最大参数数量(按大小计)。
- stage3_max_reuse_distance:
1e9
- 控制参数交换优化的高级参数。
- stage3_gather_16bit_weights_on_model_save:
true
- 在保存模型时,收集所有分片的16位权重,确保保存完整的模型权重。
说明:
- ZeRO Stage 3启用了完整的参数、梯度和优化器状态分片,但没有启用Offload,即所有数据都保留在GPU上。
- overlap_comm和contiguous_gradients的设置有助于提高训练效率。
- 由于没有启用参数和优化器状态的Offload,训练速度更快,但需要GPU有足够的显存。
- 如果面对显存不足的问题,可以考虑将offload_optimizer和offload_param的device设置为
"cpu"
或"nvme"
,将部分数据Offload到CPU内存或磁盘。
5. 其他训练设置
"gradient_accumulation_steps": "auto",
"gradient_clipping": "auto",
"steps_per_print": 100,
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"wall_clock_breakdown": false
- gradient_accumulation_steps:
"auto"
- 梯度累积步数,
"auto"
表示从训练参数中获取。梯度累积可以在显存有限的情况下增大有效批次大小。
- 梯度累积步数,
- gradient_clipping:
"auto"
- 梯度裁剪值,
"auto"
表示从训练参数中获取。梯度裁剪可以防止梯度爆炸。
- 梯度裁剪值,
- steps_per_print:
100
- 每进行多少步训练,DeepSpeed打印一次状态信息。
- train_batch_size:
"auto"
- 训练的总批次大小,
"auto"
表示由DeepSpeed根据其他参数和训练脚本自动计算。
- 训练的总批次大小,
- train_micro_batch_size_per_gpu:
"auto"
- 每个GPU上的微批次大小,
"auto"
表示自动确定。
- 每个GPU上的微批次大小,
- wall_clock_breakdown:
false
- 是否启用计时器,提供更详细的时间分析。设置为
true
可以在调试和性能分析时使用。
- 是否启用计时器,提供更详细的时间分析。设置为
总结
该配置文件配置了DeepSpeed的ZeRO Stage 3优化策略,主要特点如下:
- 使用ZeRO Stage 3进行优化:对模型参数、梯度和优化器状态进行全分片,最大限度地减少内存占用。
- 未启用Offload:参数和优化器状态都保留在GPU上,没有Offload到CPU或NVMe。
- 自动化参数设置:许多参数(如学习率、批次大小、梯度裁剪等)都设置为
"auto"
,以便从训练脚本中统一管理,简化配置。 - 混合精度训练:通过
fp16
或bf16
的自动设置,支持混合精度训练,以提高效率。 - 性能优化:启用了通信与计算重叠、连续梯度存储等选项,提高训练速度和效率。
使用该配置的注意事项:
- 显存需求:由于未启用Offload,所有数据都保存在GPU上,需要确保GPU有足够的显存。
- 调整Offload:如果遇到显存不足的问题,可以考虑启用Offload,将
offload_optimizer
或offload_param
的device
设置为"cpu"
或"nvme"
。 - 硬件支持:确保您的硬件支持混合精度训练(FP16或BF16)。例如,NVIDIA A100 GPU支持BF16。
- 参数调整:根据您的训练任务和模型规模,适当调整学习率、批次大小、梯度累积步数等超参数。
- 性能优化:可以根据实际训练情况,调整
reduce_bucket_size
、stage3_prefetch_bucket_size
等参数,以获得最佳性能。
补充说明
- 关于Offload:如果您将
offload_optimizer
或offload_param
的device
设置为"cpu"
,DeepSpeed将把优化器状态或模型参数Offload到CPU内存,这可以大幅降低GPU显存占用,但可能会增加CPU内存的压力和数据传输开销。 - 关于混合精度:如果您的硬件更适合FP16(例如V100 GPU),可以明确将
fp16
的enabled
设置为true
,并将bf16
的enabled
设置为false
。
示例:启用参数Offload到CPU
如果您希望将模型参数Offload到CPU,可以将配置修改为:
"offload_param": {
"device": "cpu",
"pin_memory": true
}
这将使模型参数保存在CPU内存中,减轻GPU显存压力。
总结建议
- 根据您的硬件和任务需求,调整配置文件中的参数。
- 对于新手,保持默认设置,并逐步修改参数,以了解其影响。
- 在更改配置后,监控训练过程中GPU和CPU的内存占用、训练速度,以及模型的性能,以评估配置的效果。
LoRA微调
脚本概述
这个Bash脚本旨在使用PyTorch的torchrun
工具和DeepSpeed,在多GPU环境下,对Qwen/Qwen-VL-Chat
模型进行微调。脚本设置了环境变量、分布式训练参数,并调用finetune.py
脚本进行训练。
与全参数微调相比,本脚本使用了LoRA方法进行微调,即在保留原始模型参数的基础上,只对新增的低秩适配器参数进行训练,从而减少了显存占用和计算资源需求。
脚本详细解释
Shebang行
#!/bin/bash
指定脚本使用Bash解释器。
设置CUDA设备最大连接数
export CUDA_DEVICE_MAX_CONNECTIONS=1
- 限制每个CUDA设备的最大连接数,有助于减少多GPU训练时的GPU内存占用。
获取当前工作目录
DIR=`pwd`
- 获取当前工作目录并赋值给
DIR
变量。 - 在此脚本中,
DIR
未被使用,可能是为了将来扩展。
设置分布式训练参数
GPUS_PER_NODE=8
NNODES=1
NODE_RANK=0
MASTER_ADDR=localhost
MASTER_PORT=6001
- GPUS_PER_NODE: 每个节点的GPU数量,这里设置为8。
- NNODES: 节点总数,这里为1,表示单节点训练。
- NODE_RANK: 当前节点的排名(从0开始),单节点情况下为0。
- MASTER_ADDR: 主节点地址,单节点情况下为
localhost
。 - MASTER_PORT: 主节点通信端口,设置为6001,可以根据需要更改。
指定模型路径
MODEL="Qwen/Qwen-VL-Chat" #"Qwen/Qwen-VL-Chat"/"Qwen/Qwen-VL" Set the path if you do not want to load from huggingface directly
- MODEL: 使用的预训练模型,这里指定为
Qwen/Qwen-VL-Chat
。 - 注释提示可以将模型路径设置为本地路径,而不是从Hugging Face直接加载。
指定训练数据路径
# ATTENTION: specify the path to your training data, which should be a json file consisting of a list of conversations.
# See the section for finetuning in README for more information.
DATA="path_to_data"
- DATA: 训练数据文件的路径,需要替换为实际的数据路径。
- 数据应为包含对话列表的JSON文件,详见README中的微调部分。
构建分布式训练参数
DISTRIBUTED_ARGS="
--nproc_per_node $GPUS_PER_NODE \
--nnodes $NNODES \
--node_rank $NODE_RANK \
--master_addr $MASTER_ADDR \
--master_port $MASTER_PORT
"
- DISTRIBUTED_ARGS: 包含分布式训练所需的参数,传递给
torchrun
。
运行训练脚本
torchrun $DISTRIBUTED_ARGS finetune.py \
--model_name_or_path $MODEL \
--data_path $DATA \
--bf16 True \
--fix_vit True \
--output_dir output_qwen \
--num_train_epochs 5 \
--per_device_train_batch_size 2 \
--per_device_eval_batch_size 1 \
--gradient_accumulation_steps 8 \
--evaluation_strategy "no" \
--save_strategy "steps" \
--save_steps 1000 \
--save_total_limit 10 \
--learning_rate 1e-5 \
--weight_decay 0.1 \
--adam_beta2 0.95 \
--warmup_ratio 0.01 \
--lr_scheduler_type "cosine" \
--logging_steps 1 \
--report_to "none" \
--model_max_length 2048 \
--lazy_preprocess True \
--use_lora \
--gradient_checkpointing \
--deepspeed finetune/ds_config_zero2.json
-
使用
torchrun
启动分布式训练,传入DISTRIBUTED_ARGS
参数。 -
关键参数解释如下:
模型与数据路径
--model_name_or_path $MODEL
:指定预训练模型,这里使用变量$MODEL
。--data_path $DATA
:指定训练数据路径,使用变量$DATA
。
混合精度训练
--bf16 True
:启用bfloat16混合精度训练。- 注意:需要硬件支持bf16,如NVIDIA A100 GPU。
模型设置
--fix_vit True
:在训练过程中固定视觉Transformer(ViT)的参数,只训练语言模型部分。- 如果需要微调视觉编码器,将其设置为
False
。
- 如果需要微调视觉编码器,将其设置为
输出设置
--output_dir output_qwen
:指定模型和检查点的输出目录。
训练超参数
--num_train_epochs 5
:训练5个Epoch。--per_device_train_batch_size 2
:每个设备的训练批次大小为2。--per_device_eval_batch_size 1
:每个设备的评估批次大小为1。--gradient_accumulation_steps 8
:梯度累积步数为8。- 有效批次大小:
有效批次大小 = per_device_train_batch_size * gradient_accumulation_steps * GPU数量 = 2 * 8 * 8 = 128
- 有效批次大小:
- 其他超参数如学习率、权重衰减等,根据需要设置。
检查点与保存策略
--evaluation_strategy "no"
:不在训练过程中进行评估。--save_strategy "steps"
:按训练步数保存模型。--save_steps 1000
:每1000步保存一次模型。--save_total_limit 10
:最多保留10个检查点。
日志记录与报告
--logging_steps 1
:每个训练步骤记录日志。--report_to "none"
:不将日志报告到外部工具。
模型与数据处理
--model_max_length 2048
:模型的最大序列长度。--lazy_preprocess True
:延迟数据预处理。
LoRA设置
--use_lora
:启用LoRA微调。
梯度检查点
--gradient_checkpointing
:启用梯度检查点,减少显存占用。
DeepSpeed配置
--deepspeed finetune/ds_config_zero2.json
:使用DeepSpeed的ZeRO Stage 2配置。
LoRA和全参数微调的差异点分析
1. 是否使用LoRA
- LoRA微调脚本:包含参数
--use_lora
,启用了LoRA微调。 - 全参数微调脚本:不包含
--use_lora
参数,进行全参数微调。
差异点:
- LoRA微调:只训练插入的低秩适配器参数,冻结原始模型的权重。
- 全参数微调:更新模型的所有参数,包括原始模型参数。
2. DeepSpeed配置
- LoRA微调脚本:
--deepspeed finetune/ds_config_zero2.json
- 使用了ZeRO Stage 2配置。
- 全参数微调脚本:
--deepspeed finetune/ds_config_zero3.json
- 使用了ZeRO Stage 3配置。
差异点:
- ZeRO Stage 2(ZeRO-2):分片优化器状态和梯度,但不分片模型参数。
- 适用于需要更新的参数较少的情况,如LoRA微调。
- ZeRO Stage 3(ZeRO-3):分片优化器状态、梯度和模型参数。
- 适用于全参数微调,减少显存占用。
3. 训练参数差异
-
LoRA微调脚本:
--per_device_train_batch_size 2
--gradient_accumulation_steps 8
-
全参数微调脚本:
--per_device_train_batch_size 1
--gradient_accumulation_steps 16
差异点:
-
LoRA微调:
- 由于更新的参数较少,显存占用更低,可以使用更大的批次大小。
- 有效批次大小计算:
有效批次大小 = 2 * 8 * 8 = 128
-
全参数微调:
- 需要更新所有参数,显存占用更高,需要使用较小的批次大小和更多的梯度累积步数。
- 有效批次大小计算:
有效批次大小 = 1 * 16 * 8 = 128
-
结论:
- 两者的有效批次大小相同,但设置的批次大小和梯度累积步数不同,以适应显存限制。
4. 混合精度训练
-
LoRA微调脚本:
--bf16 True
:启用bfloat16混合精度训练。
-
全参数微调脚本:
- 同样使用
--bf16 True
。
- 同样使用
差异点:
- 无明显差异:在硬件支持的情况下,LoRA和全参数微调都可以使用bf16训练。
5. 模型部分参数冻结
-
LoRA微调脚本:
--fix_vit True
:固定视觉Transformer(ViT)部分参数。
-
全参数微调脚本:
- 通常也会包含
--fix_vit True
。
- 通常也会包含
差异点:
- 可能需要调整:在LoRA微调中,通常只对语言模型部分进行适配器插入,如果希望对ViT部分进行LoRA微调,需要相应调整。
6. 参数更新量
- LoRA微调:只更新LoRA适配器参数,占模型总体参数的极小部分(通常小于0.1%)。
- 全参数微调:更新模型的所有参数,涉及数十亿的参数量。
7. 训练速度与资源占用
-
LoRA微调:
- 显存占用低,计算量小,训练速度更快。
- 可以在单机多卡甚至单卡上进行较大模型的微调。
-
全参数微调:
- 显存占用高,计算量大,训练速度相对较慢。
- 需要更强大的硬件资源,如多机多卡、更多的内存和存储。
LoRA与全参数微调的核心区别
-
参数更新策略:
-
LoRA微调:通过在Transformer的注意力层或其他指定层插入低秩瓶颈矩阵(适配器),只更新这些新增加的参数,冻结原有模型参数。
-
全参数微调:直接对所有模型参数进行更新,包括嵌入层、注意力层、前馈网络等。
-
-
资源需求:
-
LoRA微调:显著降低了内存和计算需求,适合资源有限的环境。
-
全参数微调:需要大量的GPU显存和计算资源,适合资源充足的环境。
-
-
训练效率:
-
LoRA微调:由于参数更新量小,训练速度更快,可以使用更大的批次大小。
-
全参数微调:训练时间更长,可能需要减少批次大小或增加梯度累积步数以适应显存限制。
-
-
模型性能:
-
LoRA微调:在许多任务中,LoRA微调可以达到与全参数微调相近的性能,但可能在某些任务上略有差距。
-
全参数微调:理论上可以达到最佳性能,但需要权衡资源消耗和收益。
-
-
部署与参数量:
-
LoRA微调:仅需存储和加载适配器参数,可以大幅减小模型部署的存储需求。
-
全参数微调:需要存储完整的模型参数。
-
DeepSpeed配置的选择
-
LoRA微调使用
ds_config_zero2.json
(ZeRO Stage 2)配置:-
由于需要更新的参数量较小,使用ZeRO-2即可满足需求。
-
ZeRO-2只分片优化器状态和梯度,通信和同步开销更小。
-
-
全参数微调使用
ds_config_zero3.json
(ZeRO Stage 3)配置:-
需要分片模型参数、优化器状态和梯度,以便在多GPU上训练大型模型。
-
ZeRO-3可以极大地降低单个GPU的显存占用,但通信和同步开销更大。
-
总结与建议
-
选择微调方法的依据:
-
资源有限、需要快速迭代:建议使用LoRA微调,能够在较短时间内完成训练,对硬件要求较低。
-
资源充足、追求最佳性能:可以选择全参数微调,充分利用硬件资源,可能在性能上获得略微提升。
-
-
LoRA微调的优势:
-
效率高:显存占用小,训练速度快。
-
灵活性:只需存储适配器参数,方便部署和管理。
-
效果好:在大多数任务中,性能接近全参数微调。
-
注意事项
-
调整训练参数:根据实际硬件资源和任务需求,调整批次大小、梯度累积步数等参数,以达到最佳性能。
-
混合精度训练:确保硬件支持bf16或fp16,选择合适的混合精度训练方式。
-
模型保存与加载:LoRA微调只保存适配器参数,加载时需要先加载基础模型,再加载适配器。
-
DeepSpeed配置:根据微调方式,选择合适的ZeRO Stage配置,平衡显存占用和通信开销。
ZeRO-2
1. ZeRO Stage 2(ZeRO-2)是什么?
**ZeRO(Zero Redundancy Optimizer)**是 DeepSpeed 提出的系列优化技术,旨在减少大规模模型训练中的内存冗余,从而实现高效的分布式训练。ZeRO 分为三个阶段:
- ZeRO Stage 1:将优化器状态在数据并行过程中进行分片,减少内存占用。
- ZeRO Stage 2:在 Stage 1 的基础上,进一步将梯度进行分片,进一步降低内存需求。
- ZeRO Stage 3:在前两个阶段的基础上,再将模型参数进行分片,实现优化器状态、梯度和模型参数的全分片,达到最大化的内存节约。
ZeRO Stage 2(ZeRO-2) 主要特点是:
- 优化器状态分片:优化器状态在 GPU 之间分片存储,每个 GPU 只保存自己负责的部分。
- 梯度分片:梯度在 GPU 之间分片存储,减少梯度累积时的内存占用。
- 完整的模型参数:每个 GPU 都保留完整的模型参数,以便于前向和反向传播。
适用场景:
- 参数较少需要更新:例如使用 LoRA 微调时,只需要更新少量的适配器参数,绝大部分模型参数是冻结的。
- 网络通信开销较小:由于模型参数未分片,避免了频繁的通信,适合网络带宽有限的环境。
2. ds_config_zero2.json
配置文件解析
下面,我们逐节详细解释您提供的 ds_config_zero2.json
配置文件,说明每个参数的含义和作用。
{
"fp16": {
"enabled": "auto",
"loss_scale": 0,
"loss_scale_window": 1000,
"initial_scale_power": 16,
"hysteresis": 2,
"min_loss_scale": 1
},
"bf16": {
"enabled": "auto"
},
"optimizer": {
"type": "AdamW",
"params": {
"lr": "auto",
"betas": "auto",
"eps": "auto",
"weight_decay": "auto"
}
},
"scheduler": {
"type": "WarmupLR",
"params": {
"warmup_min_lr": "auto",
"warmup_max_lr": "auto",
"warmup_num_steps": "auto"
}
},
"zero_optimization": {
"stage": 2,
"offload_optimizer": {
"device": "none",
"pin_memory": true
},
"allgather_partitions": true,
"allgather_bucket_size": 2e8,
"overlap_comm": true,
"reduce_scatter": true,
"reduce_bucket_size": 2e8,
"contiguous_gradients": true
},
"gradient_accumulation_steps": "auto",
"gradient_clipping": "auto",
"steps_per_print": 100,
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"wall_clock_breakdown": false
}
2.1 混合精度设置
fp16 设置:
"fp16": {
"enabled": "auto",
"loss_scale": 0,
"loss_scale_window": 1000,
"initial_scale_power": 16,
"hysteresis": 2,
"min_loss_scale": 1
}
- enabled:
"auto"
- 根据训练脚本参数自动启用或禁用 FP16 训练。
- loss_scale:
0
- 当设置为
0
时,使用自动损失缩放。
- 当设置为
- loss_scale_window:
1000
- 损失缩放调整的窗口大小。
- initial_scale_power:
16
- 初始损失缩放值为
2^16
。
- 初始损失缩放值为
- hysteresis:
2
- 在降低损失缩放之前,允许连续出现的溢出次数。
- min_loss_scale:
1
- 最小损失缩放值。
bf16 设置:
"bf16": {
"enabled": "auto"
}
- enabled:
"auto"
- 根据训练脚本参数自动启用或禁用 BF16 训练。
说明:
- 使用
"auto"
使得 DeepSpeed 可以根据训练脚本的参数(如--bf16
或--fp16
)自动选择混合精度模式。 - 混合精度训练减少显存占用,提高计算效率,需要硬件支持。
2.2 优化器设置
"optimizer": {
"type": "AdamW",
"params": {
"lr": "auto",
"betas": "auto",
"eps": "auto",
"weight_decay": "auto"
}
}
- type:
"AdamW"
- 使用 AdamW 优化器。
- params:
- lr:
"auto"
- 学习率,将从训练脚本参数中获取。
- betas:
"auto"
- Adam 优化器的 β 参数,
"auto"
表示使用默认值或从训练参数中获取。
- Adam 优化器的 β 参数,
- eps:
"auto"
- Adam 优化器的 epsilon 参数,
"auto"
表示使用默认值或从训练参数中获取。
- Adam 优化器的 epsilon 参数,
- weight_decay:
"auto"
- 权重衰减系数,将从训练脚本参数中获取。
- lr:
说明:
- 使用
"auto"
使得优化器参数可以统一在训练脚本中管理,保持配置一致性。
2.3 学习率调度器设置
"scheduler": {
"type": "WarmupLR",
"params": {
"warmup_min_lr": "auto",
"warmup_max_lr": "auto",
"warmup_num_steps": "auto"
}
}
- type:
"WarmupLR"
- 使用 Warmup 学习率调度器。
- params:
- warmup_min_lr:
"auto"
- 预热阶段的最小学习率,从训练脚本参数中获取。
- warmup_max_lr:
"auto"
- 预热阶段的最大学习率,从训练脚本参数中获取。
- warmup_num_steps:
"auto"
- 预热阶段的步数,从训练脚本参数中获取。
- warmup_min_lr:
说明:
- 学习率调度器在训练初期逐步增加学习率,可提高训练稳定性。
- 使用
"auto"
统一管理超参数,方便调整。
2.4 ZeRO 优化设置
"zero_optimization": {
"stage": 2,
"offload_optimizer": {
"device": "none",
"pin_memory": true
},
"allgather_partitions": true,
"allgather_bucket_size": 2e8,
"overlap_comm": true,
"reduce_scatter": true,
"reduce_bucket_size": 2e8,
"contiguous_gradients": true
}
- stage:
2
- 启用 ZeRO Stage 2 优化。
- offload_optimizer:
- device:
"none"
- 不进行优化器状态的 Offload,优化器状态保留在 GPU 上。
- pin_memory:
true
- 如果启用了 Offload,是否使用固定内存,提高数据传输效率。
- device:
- allgather_partitions:
true
- 启用分块 AllGather,将数据分块收集,减少通信开销。
- allgather_bucket_size:
2e8
- AllGather 操作的分块大小,单位字节,设置为 200MB。
- overlap_comm:
true
- 启用通信与计算重叠,提升训练速度。
- reduce_scatter:
true
- 启用 ReduceScatter 操作,减少通信量。
- reduce_bucket_size:
2e8
- ReduceScatter 操作的分块大小,单位字节,设置为 200MB。
- contiguous_gradients:
true
- 使用连续的内存空间存储梯度,提高内存访问效率。
说明:
- ZeRO Stage 2(ZeRO-2) 主要分片优化器状态和梯度,不分片模型参数。
- 不启用参数的 Offload,所有模型参数保留在 GPU 上。
- 启用 overlap_comm、allgather_partitions 和 reduce_scatter,以减少通信开销,提高训练效率。
- 适用于 LoRA 微调:
- LoRA 微调只更新少量的适配器参数,大部分原始模型参数是冻结的。
- 由于需要更新的参数较少,显存占用较低,使用 ZeRO-2 已足够,无需使用 ZeRO-3。
2.5 其他训练设置
"gradient_accumulation_steps": "auto",
"gradient_clipping": "auto",
"steps_per_print": 100,
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"wall_clock_breakdown": false
- gradient_accumulation_steps:
"auto"
- 梯度累积步数,
"auto"
表示从训练脚本参数中获取。
- 梯度累积步数,
- gradient_clipping:
"auto"
- 梯度裁剪值,
"auto"
表示从训练脚本参数中获取。
- 梯度裁剪值,
- steps_per_print:
100
- 每进行多少步训练,DeepSpeed 打印一次状态信息。
- train_batch_size:
"auto"
- 训练的总批次大小,
"auto"
表示由 DeepSpeed 根据训练脚本参数自动计算。
- 训练的总批次大小,
- train_micro_batch_size_per_gpu:
"auto"
- 每个 GPU 上的微批次大小,
"auto"
表示由 DeepSpeed 根据训练脚本参数自动计算。
- 每个 GPU 上的微批次大小,
- wall_clock_breakdown:
false
- 是否启用计时器,提供更详细的时间分析。
说明:
- 使用
"auto"
使得参数可以统一在训练脚本中设置,简化配置文件的管理。 - gradient_accumulation_steps 和 train_micro_batch_size_per_gpu 的配置对于控制有效批次大小和显存占用至关重要。
3. 配置文件的整体作用与适用场景
适用于 LoRA 微调的原因:
- 参数更新量少:LoRA 微调只更新少量的适配器参数,大部分模型参数是冻结的。
- 显存占用低:由于需要更新的参数较少,显存占用较低,不需要 ZeRO-3 的全参数分片。
- 通信开销小:模型参数未分片,避免了频繁的参数同步,降低了通信开销。
- 训练效率高:启用了通信与计算重叠、AllGather 和 ReduceScatter 操作,优化了通信模式,提高了训练速度。
与 ZeRO-3 的对比:
- ZeRO-3 主要用于全参数微调,需要对模型参数进行分片,以减少显存占用。
- ZeRO-2 适用于参数更新量较小的情况,如 LoRA 微调,避免了 ZeRO-3 的额外通信开销。
4. 使用此配置的注意事项
- 硬件支持:确保您的硬件支持混合精度训练(FP16 或 BF16)。如果硬件不支持 BF16,请使用 FP16。
- 参数调整:根据实际情况,调整训练脚本中的超参数,如学习率、批次大小、梯度累积步数等。
- 优化器状态 Offload:默认未启用优化器状态的 Offload,如果显存不足,可以考虑将
offload_optimizer.device
设置为"cpu"
,将优化器状态 Offload 到 CPU。 - 通信优化:启用了通信与计算重叠和通信优化策略,需确保网络带宽和拓扑适合高效通信。
5. 总结与建议
- LoRA 微调:此配置文件针对 LoRA 微调进行了优化,充分利用 ZeRO-2 的特点,减少了显存占用和通信开销,提高了训练效率。
- 全参数微调:如果需要全参数微调,建议使用 ZeRO-3 配置,以便对模型参数进行分片,减轻显存压力。
- 配置管理:使用
"auto"
参数,使得大部分超参数可以直接在训练脚本中统一管理,便于调整和实验。 - 性能调优:可以根据训练过程中观察到的性能和资源利用情况,调整
allgather_bucket_size
、reduce_bucket_size
等参数,进一步优化训练效率。
示例:如果需要调整优化器状态 Offload
如果在训练过程中发现显存不足,可以考虑将优化器状态 Offload 到 CPU,修改配置如下:
"offload_optimizer": {
"device": "cpu",
"pin_memory": true
}
注意:
- 将优化器状态 Offload 到 CPU 会增加 CPU 内存消耗和数据传输开销,可能会影响训练速度。
- 需要根据实际情况权衡显存占用和训练速度。
补充说明
- LoRA 微调的优势:通过仅训练少量的适配器参数,可以在有限的资源下,对大模型进行高效的微调,适用于资源受限的场景。
- DeepSpeed 的作用:利用 DeepSpeed 的 ZeRO 优化技术,可以进一步提高训练效率,支持大规模模型的训练。