基于SWIFT和Qwen1.5-14B-Chat进行大模型全参微调测试
基于SWIFT和Qwen1.5-14B-Chat进行大模型全参微调测试
环境准备
基础环境
- 操作系统:Ubuntu 18.04.5 LTS (GNU/Linux 3.10.0-1127.el7.x86_64 x86_64)
- Anaconda3:Anaconda3-2023.03-1-Linux-x86_64
- 根据服务器网络情况配置好conda源和pip源,此处使用的是超算山河源
- 服务器硬件配置:CPU 96核;GPU 8×NVIDIA A100 40GB
环境安装
通过源代码安装SWIFT:
创建一个新的conda环境:
conda create --name swift python=3.8
激活刚刚创建的conda环境:
conda activate swift
下载SWIFT源码:
git clone https://github.com/modelscope/swift.git
SWIFT(ms-swift) 1.8.0
切换到SWIFT路径:
cd /yldm0226/swift
安装SWIFT:
pip install -e .[llm]
非必要步骤:检查服务器cuda版本是否与当前安装的pytorch对应,详见:搭建一个大模型API服务
数据集准备
对于数据集,我们均采用json或jsonl的格式。
在做大模型SFT(Supervised Fine-Tuning)时,可以准备两种数据:
- 单轮问答
- 多轮对话
对于单轮问答数据,其格式可以为:
{"query": "11111", "response": "22222"}
对于多轮对话数据,其格式可以为:
{"query": "eeeee", "response": "fffff", "history": []}
{"query": "EEEEE", "response": "FFFFF", "history": [["AAAAA", "BBBBB"], ["CCCCC", "DDDDD"]]}
同时,也可以用以下两种格式的数据:
{"conversations": [{"from": "user", "value": "11111"}, {"from": "assistant", "value": "22222"}]}
{"conversations": [{"from": "user", "value": "aaaaa"}, {"from": "assistant", "value": "bbbbb"}, {"from": "user", "value": "ccccc"}, {"from": "assistant", "value": "ddddd"}]}
{"conversations": [{"from": "user", "value": "AAAAA"}, {"from": "assistant", "value": "BBBBB"}, {"from": "user", "value": "CCCCC"}, {"from": "assistant", "value": "DDDDD"}]}
{"messages": [{"role": "user", "content": "11111"}, {"role": "assistant", "content": "22222"}]}
{"messages": [{"role": "user", "content": "aaaaa"}, {"role": "assistant", "content": "bbbbb"}, {"role": "user", "content": "ccccc"}, {"role": "assistant", "content": "ddddd"}]}
{"messages": [{"role": "user", "content": "AAAAA"}, {"role": "assistant", "content": "BBBBB"}, {"role": "user", "content": "CCCCC"}, {"role": "assistant", "content": "DDDDD"}]}
在本文中,共使用了9个数据集,数据集的详细信息如下:
序号 | 数据集 | 简介 | 数据量 |
---|---|---|---|
1 | Chinese_medical_dialogue_six_department | 中文医疗问答数据集,包括男科、内科、妇产科、肿瘤科、儿科、外科六个科室的问题。 | 792K |
2 | HuatuoGPT2_sft_instruct_GPT4 | 华佗GPT(HuatuoGPT)第二版训练数据集。 | 50K |
3 | ChatMed_Consult-v0.3 | 中文医疗在线问诊数据集ChatMed_Consult_Dataset的50w+在线问诊+ChatGPT回复。 | 500K |
4 | ChatMed_TCM-v0.2 | 以开源的中医药知识图谱为基础,采用以实体为中心的自指令方法(entity-centric self-instruct),调用ChatGPT得到11w+的围绕中医药的指令数据。 | 110K |
5 | QiZhen_sft_20k | 包含20k训练数据(该数据集来自于启真医学知识库收集整理的真实医患知识问答数据以及在启真医学知识库的药品文本知识基础上,通过对半结构化数据设置特定的问题模板构造的指令数据)。 | 20K |
6 | Huatuo_Lite | Huatuo-Lite 是在Huatuo26M数据集的基础上经过多次提纯和重写而精炼优化的数据集。它包含了18万个高质量的医疗问答对,并具有医院科室和相关疾病两个额外的数据维度。 | 180K |
7 | ZhongJing_CMtMedQA | 仲景SFT训练集。 | 70K |
8 | DISC-Med-SFT_released | 包含了超过47万个衍生于现有的医疗数据集重新构建得到的样本。采用了目标导向的策略,通过对于精心选择的几个数据源进行重构来得到SFT数据集。这些数据的作用在于帮助模型学习医疗领域知识,将行为模式与人类偏好对齐,并对齐真实世界在线医疗对话的分布情况。 | 514K |
9 | SZY_TCM_QA | 私有数据集。 | 12K |
以下是加载后的数据集信息:
[INFO:swift] train_dataset: Dataset({
features: ['query', 'response', 'history'],
num_rows: 2223540
})
[INFO:swift] val_dataset: Dataset({
features: ['query', 'response', 'history'],
num_rows: 22460
})
数据总量为2,246,000,从中抽取出约1%作为验证集,其余的作为训练集。
通过max_lengt=4096进行过滤后的数据集信息如下:
[INFO:swift] Dataset Token Length: 224.276768±159.001432, min=25.000000, max=4089.000000, size=2223411
[INFO:swift] Dataset Token Length: 224.254464±157.600093, min=28.000000, max=3086.000000, size=22459
编写微调脚本
SWIFT框架提供了部分大模型的微调脚本,可以在我们下载的源码中的swift/examples/pytorch/llm/scripts路径中找到这些脚本。如果这些脚本能够满足我们大部分的微调需求,我们可以选择直接对这些脚本进行修改。如果找不到我们需要的脚本,需要我们根据swift/docs/source/LLM中的命令行参数文档自行编写训练脚本。
以下是对Qwen1.5-14B-Chat进行全参微调的一个训练脚本:
# Experimental environment: 8 * A100 40GB
nproc_per_node=1
NPROC_PER_NODE=$nproc_per_node \
MASTER_PORT=29500 \
CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \
swift sft \
--model_type qwen1half-14b-chat \
--model_id_or_path /yldm0226/models/Qwen1.5-14B-Chat \
--model_revision master \
--sft_type full \
--tuner_backend swift \
--template_type AUTO \
--dtype AUTO \
--output_dir /yldm0226/llm_sft_output \
--ddp_backend nccl \
--custom_train_dataset_path /yldm0226/data/1-Chinese_medical_dialogue_six_department.jsonl /yldm0226/data/2-HuatuoGPT2_sft_instruct_GPT4.jsonl /yldm0226/data/3-ChatMed_Consult-v0.3.jsonl /yldm0226/data/4-ChatMed_TCM-v0.2.jsonl /yldm0226/data/5-QiZhen_sft_20k.jsonl /yldm0226/data/6-Huatuo_Lite.jsonl /yldm0226/data/7-ZhongJing_CMtMedQA.jsonl /yldm0226/data/8-DISC-Med-SFT_released.jsonl /yldm0226/data/9-SZY_TCM_QA.jsonl \
--train_dataset_sample -1 \
--num_train_epochs 1 \
--max_length 4096 \
--check_dataset_strategy warning \
--gradient_checkpointing true \
--batch_size 1 \
--weight_decay 0.01 \
--learning_rate 1e-4 \
--gradient_accumulation_steps $(expr 8 / $nproc_per_node) \
--max_grad_norm 0.5 \
--warmup_ratio 0.03 \
--eval_steps 100 \
--save_steps 100 \
--save_total_limit 3 \
--logging_steps 10 \
--use_flash_attn false \
--save_only_model true \
下面对该脚本中的一些重要参数作出解释:
nproc_per_node
:在多机分布式训练中,每个主机被当做一个node;nproc_per_node代表的是每个node中有几个线程;以该脚本为例,该脚本运行在单机环境中,因此nproc_per_node就代表着我们使用的单台服务器有几个线程去同时训练模型;假如nproc_per_node设置为8,那么将有8个线程同时训练模型,速度会提高很多,但是这样每块GPU都要负责存储完整的模型权重,显存会受到很大的挑战;假如nproc_per_node设置为4,那么将有4个线程同时训练模型,每个线程中有两块GPU,这两块GPU共同负责存储模型权重,这样虽然速度降低了,但是能够得到更宽裕的GPU显存;但此时我们的显存还是不足以训练14B的Qwen1.5-Chat,我们只能舍弃时间以换取空间,所以将nproc_per_node设置为1,此时该服务器只有1个线程去训练模型,模型被切分到8块GPU卡上。请注意,模型被切分到8块GPU卡上时,并不代表着这8块GPU只需要承担模型部分权重的显存,还需要承担优化器中各种梯度的存储,这也是相当大的一部分显存开销。所以如果显存允许,我们应该尽可能的提高nproc_per_node的值,提高显卡的利用率。CUDA_VISIBLE_DEVICES
:服务器中如果有多张显卡,可以通过该参数指定使用哪几张显卡。显卡的序号可以通过nvidia-smi查看。model_type
:model_type指定我们要微调的大模型的类型,这些类型必须是SWIFT框架所支持大模型类型的一种,具体有哪些支持的模型可以在swift源码的swift/docs/source/LLM路径中的支持的模型和数据集文档中查看。model_id_or_path
:model_id_or_path用于指定大模型权重的本地路径。sft_type
: sft_type表示微调的方式, 默认是lora。可以选择的值包括: 'lora', 'full', 'longlora', 'qalora'。此处使用的是full,即全参微调。output_dir
:output_dir用于指定大模型微调过程中输出日志的存储路径。custom_train_dataset_path
:用于指定我们数据集的存放路径,每个数据集之间用空格分隔。train_dataset_sample
:对训练集进行采样, 默认是20000, 用于加快训练的速度。 该参数是为了避免数据集过大, 单个epoch训练时间过长的问题。 如果你指定为-1, 则使用完整的训练集进行训练。max_length
:token的最大长度, 默认为2048。该参数可以避免个别过长的数据样本造成OOM的问题。当指定--truncation_strategy delete时, 如果某数据样本长度超过max_length, 我们会删除该数据样本。如果指定--truncation_strategy truncation_left时, 我们会切除最前面的token: input_ids[-max_length:]。如果设置为-1, 则无限制。该参数很重要,要根据显存情况选择合适的max_length,不然在训练中会出现OOM的情况,导致训练终止。
对于其他参数,这里不做过多讲解。此外,这个脚本只涉及到了部分参数,如果需要进一步的定制化,需根据文档自行修改。
测试
运行脚本,可以得到以下信息:
数据集预处理所需要的时间大概30分钟:
703283/2223540 [09:23<20:31, 1234.07it/s]
max_length设置为2048的情况下,估算训练时间:
100/277856 [05:38<255:02:02, 3.31s/it]
8596/22452 [10:15<17:42, 13.04it/s]
训练一个epoch大约需要255小时;进行一次验证大约需要27分钟,我们设置每100步进行一次验证,总步数为277856,需要进行2778次验证,预计用时为1250小时;当然,设置100步进行一次验证有些太频繁了,在实际进行训练时可以设置为10000步进行一次验证,预计用时为12.6小时。(这里的时间只是一个大概值,在训练时,不同数据的处理速度不同,花费的总时间会一直变化)
max_length设置为4096的情况下,训练所需的大概时间如下:
100/277926 [05:50<259:11:42, 3.36s/it]
1512/22459 [01:43<24:31, 14.24it/s]
情况与max_length设置为2048的情况差距不大。
max_length设置为8192的情况下,显存不够了,出现溢出:
torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 9.95 GiB (GPU 0; 39.45 GiB total capacity; 26.79 GiB already allocated; 8.68 GiB free; 29.46 GiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation. See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF
最后看一下显存占用情况: