问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

OpenRLHF实践:Qwen2.5-0.5B模型微调指南

创作时间:
作者:
@小白创作中心

OpenRLHF实践:Qwen2.5-0.5B模型微调指南

引用
CSDN
1.
https://m.blog.csdn.net/qq_46325481/article/details/145748086

OpenRLHF是一个基于Ray、DeepSpeed和HF Transformers构建的高性能RLHF框架。本文将详细介绍如何使用OpenRLHF对Qwen2.5-0.5B模型进行微调,包括环境准备、模型下载、SFT训练等步骤。

OpenRLHF框架简介

OpenRLHF具有以下特点:

  • 简单易用:是目前可用的最简单的高性能RLHF库之一,无缝兼容Huggingface模型和数据集
  • 高性能:在RLHF训练中,80%的时间用于样本生成阶段。得益于使用Ray、Packing Samples以及vLLM生成加速的能力,OpenRLHF的性能是极致优化的DeepSpeedChat with Hybrid Engine的3~4倍以上
  • 分布式RLHF:使用Ray将Actor、Reward、Reference和Critic模型分布到不同的GPU上,同时将Adam优化器放在CPU上。这使得使用多个A100 80G GPU和vLLM可以全面微调超过70B+的模型以及在多个24GB RTX 4090 GPU上微调7B模型
  • 支持多种微调方式:同时支持全量SFT、QLoRA参数高效微调、KTO算法、PRM训练等

环境准备

本实验的基础设备是AutoDL租用的服务器。以下是几点建议:

  • 选择重庆A区,空闲GPU比较多,即使租用机器GPU不足也可以在同一地区克隆实例--非常快速
  • 租用机器的时候先选6卡,点进去后再租一卡,这样你的机器比较不容易GPU不足
  • 选择pytorch的环境自带miniconda

配置miniconda环境变量

vim ~/.bashrc
# 将这行添加再最下面
source /root/miniconda3/etc/profile.d/conda.sh
# 按Esc,再输入“:wq”退出
# 然后 重启终端:bash  

安装OpenRLHF环境

conda create -n openrlhf python=3.11
conda activate openrlhf
pip install openrlhf
pip install torch==2.5.1 torchvision==0.19.0 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu118
pip install openrlhf[vllm]
git clone https://github.com/OpenRLHF/OpenRLHF.git
cd OpenRLHF
pip install -e .
# 如果报ninja相关错
sudo apt update
sudo apt install ninja-build

因为是租用的服务器,建议使用miniconda的base环境去下载,以节省系统盘空间。

下载模型

这里下载的是Qwen2.5-0.5B的模型,一张4090显卡足以应对(省钱)。

在Hugging Face上找到模型,建议使用huggingface-cli或snapshot_download,具体命令如下:

huggingface-cli download --resume-download Qwen/Qwen2.5-0.5B --local-dir Qwen

启动训练脚本之前需要确定你的设备能不能直连Huggingface,如果不能的话请在环境变量中加入镜像站:

# Linux,最好写到~/.bashrc里面
export HF_ENDPOINT=https://hf-mirror.com
# Windows Power Shell
$env:HF_ENDPOINT = "https://hf-mirror.com"

为了节省训练时间,建议提前运行一个加载模型和数据集的脚本,这样模型和数据集会被下载下来并放到你的~/.cache/huggingface里面,后面在此运行训练脚本的时候可以直接读取模型和数据集:

from datasets import load_dataset
# 加载数据集的 firefly 文件夹
dataset = load_dataset("QingyiSi/Alpaca-CoT", data_dir="firefly")
# 打印数据集的一些信息
print(dataset)
print(dataset["train"][0])
from transformers import AutoModelForCausalLM, AutoTokenizer
# 加载模型和tokenizer
model_name = "Qwen/Qwen2-1.5B"
model = AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
# 打印模型的一些信息
print(model)

SFT训练

这里选择了Qwen2-0.5B作为pretrain model,Alpaca-CoT@firefly作为数据集进行SFT训练,下面详细解释一下训练脚本中的各个参数,脚本位置在OpenRLHF/examples/scripts/train_sft_llama.sh。复制一份为train_sft_qwen.sh来编辑:

set -x
read -r -d '' training_commands <<EOF
openrlhf.cli.train_sft \ #SFT训练的py文件
--max_len 2048 \ #最长序列长度
--dataset QingyiSi/Alpaca-CoT@firefly \ #数据集name或者path(本地)
--input_key instruction \ #数据集的输入字段
--output_key output \ #数据集输出字段
--train_batch_size 256 \
--micro_train_batch_size 2 \
--max_samples 500000 \ #最大训练样本数
--pretrain /root/Qwen/Qwen2-0.5B \ #pretrain模型name或者path
--save_path ./checkpoint/qwen2-0.5b-firefly-sft \
--save_steps 1000 \ #多少步保存一个checkpoint,-1表示不保存
--logging_steps 1 \
--eval_steps -1 \ #多少步evaluate一下,-1表示最后再evaluate一次
--zero_stage 2 \ #deepspeed Zero阶段,1,2,3,推荐2,优化器状态和梯度都分到每张卡上
--max_epochs 1 \ #训练epoch数量
--bf16 \ #是否用BF16精度训练,默认启用
--flash_attn \ #是否启用flash attention,默认启用
--learning_rate 5e-6 \
--load_checkpoint \ #是否load中间checkpoint
--gradient_checkpointing \ #是否启用梯度检查点技术
--use_wandb=[WANDB_TOKENS] \ #wandb的user token
--wandb_project OpenRLHF \
--wandb_run_name qwen2.5-0.5b-firefly-sft
EOF
# --wandb [WANDB_TOKENS]
# --packing_samples
# 判断命令第一个是不是slurm,如果不是启用deepspeed命令
if [[ ${1} != "slurm" ]]; then
deepspeed --module $training_commands
fi

特别注意:

--use_wandb=[WANDB_TOKENS] \ # 要修改为 wandb的user token
--wandb_project OpenRLHF \
--wandb_run_name qwen2.5-0.5b-firefly-sft

需要在wandb注册一个账号,方便记录和观察训练过程。

训练

使用nohup运行脚本train_sft_qwen.sh,即使终端关闭,脚本也会继续执行。将脚本的标准输出和标准错误都重定向到output.log文件中,并将命令放到后台运行,终端可以继续执行其他任务:

nohup bash examples/scripts/train_sft_qwen.sh > output.log 2>&1 &

可以在wandb里观察实验了。Qwen2.5-0.5B模型在4090上训练一个epoch,500000个样本,batch size为256,大约需要12小时,loss_mean曲线最后收敛在2.5左右。

训练期间的显存占用等信息可以在训练代码中查看,SFT训练代码路径为OpenRLHF/openrlhf/cli/train_sft.py,主要是train(args)函数:

# train
self.model.train()
for prompt_id_lens, inputs, attention_masks, infos in self.train_dataloader:
    if self.packing_samples:
        inputs = inputs.to(torch.cuda.current_device())
        attention_mask = attention_masks.to(torch.cuda.current_device())
    else:
        inputs = inputs.to(torch.cuda.current_device()).squeeze(1)
        attention_mask = attention_masks.to(torch.cuda.current_device()).squeeze(1)
    if self.strategy.ring_attn_group is None:
        output = self.model(inputs, attention_mask=attention_mask, return_output=True)
    else:
        output = self.model(
            inputs,
            attention_mask=attention_mask,
            return_output=True,
            ring_attn_group=self.strategy.ring_attn_group,
            packed_seq_lens=infos["input_length"],
        )
    # loss function
    labels = torch.where(
        attention_mask.bool(),
        inputs,
        self.loss_fn.IGNORE_INDEX,
    )
    # mixtral
    if self.aux_loss:
        aux_loss = output.aux_loss
    else:
        aux_loss = 0
    if not self.pretrain_mode:
        if self.packing_samples:
            # As response_ranges need to constrain the dataset organization strictly, we handle multiturn feature separately.
            if infos["response_ranges"]:
                dump_labels = torch.full(labels.size(), self.loss_fn.IGNORE_INDEX).to(labels.device)
                for response_ranges in infos["response_ranges"]:
                    for response_range in response_ranges:
                        dump_labels[0][response_range[0]: response_range[1]] = labels[0][response_range[0]: response_range[1]]
                labels = dump_labels
            else:
                index = 0
                for input_length, source_len in zip(infos["input_length"], prompt_id_lens):
                    labels[0][index : index + source_len] = self.loss_fn.IGNORE_INDEX
                    index += input_length
        else:
            for label, source_len in zip(labels, prompt_id_lens):
                label[:source_len] = self.loss_fn.IGNORE_INDEX
    gpt_loss = self.loss_fn(output.logits, labels)
    loss = gpt_loss + aux_loss * self.args.aux_loss_coef
    self.strategy.backward(loss, self.model, self.optimizer)
    self.strategy.optimizer_step(self.optimizer, self.model, self.scheduler)
    loss_sum += gpt_loss.item()
    logs_dict = {
        "gpt_loss": gpt_loss.item(),
        "lr": self.scheduler.get_last_lr()[0],
    }
    if self.aux_loss:
        logs_dict["aux_loss"] = aux_loss.item()
    # step bar
    logs_dict = self.strategy.all_reduce(logs_dict)
    step_bar.set_postfix(logs_dict)
    step_bar.update()
    # logs/checkpoints/evaluation
    if step % self.strategy.accumulated_gradient == 0:
        logs_dict["loss_mean"] = loss_sum / self.strategy.accumulated_gradient
        loss_sum = 0
        global_step = step // self.strategy.accumulated_gradient
        client_states = {"consumed_samples": global_step * args.train_batch_size}
        self.save_logs_and_checkpoints(args, global_step, step_bar, logs_dict, client_states)
    step += 1
epoch_bar.update()
if self._wandb is not None and self.strategy.is_rank_0():
    self._wandb.finish()
if self._tensorboard is not None and self.strategy.is_rank_0():
    self._tensorboard.close()
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号