写点什么

LLM 大模型学习必知必会系列 (七):掌握分布式训练与 LoRA/LISA 微调:打造高性能大模型的秘诀进阶实战指南

  • 2024-05-28
    浙江
  • 本文字数:6458 字

    阅读完需:约 21 分钟

LLM 大模型学习必知必会系列(七):掌握分布式训练与LoRA/LISA微调:打造高性能大模型的秘诀进阶实战指南

LLM 大模型学习必知必会系列(七):掌握分布式训练与 LoRA/LISA 微调:打造高性能大模型的秘诀进阶实战指南

1.微调(Supervised Finetuning)

指令微调阶段使用了已标注数据。这个阶段训练的数据集数量不会像预训练阶段那么大,最多可以达到几千万条,最少可以达到几百条到几千条。指令微调可以将预训练的知识“涌现”出来,进行其他类型的任务,如问答类型的任务。一般指令微调阶段对于在具体行业上的应用是必要的,但指令微调阶段一般不能灌注进去新知识,而是将已有知识的能力以某类任务的形式展现出来。


指令微调任务有多种场景,比较常用的有:


  • 风格化:特定的问答范式

  • 自我认知:自我认知改变

  • 能力增强:模型本身能力不够,对具体行业的数据理解不良

  • Agent:支持 Agent 能力,比如程序编写、API 调用等


上述只是举了几个例子,一般来说距离用户最近的训练方式就是指令微调。


一般来说,LLM 中指的 base 模型是指经过了预训练(以及进行了一部分通用指令的微调)的模型。Chat 模型是经过了大量通用数据微调和人类对齐训练的模型。


如何选择 base 模型和 chat 模型进行微调呢?


  • 数据量较少的时候(比如小于 1w 条)建议使用 chat 模型微调

  • 数据量较多、数据较为全面的时候,建议使用 base 模型微调


当然,如果硬件允许,建议两个模型都进行尝试,选择效果较好的。需要注意的是,chat 模型有其独特的输入格式,在微调时一定要遵循。base 模型的输入格式一般比较简单(但也需要遵守该格式),而且一般该格式不支持多轮数据集。


如果需要用 base 模型训练多轮对话,一般需要使用一个支持多轮对话的 template。在 SWIFT 中,可以指定为default,在训练时只需要指定--template_type default 即可。


  • 重要概念


  1. loss 代表模型求解的 y 和实际的 y 值的差异。该值会进行 loss.backward(),这个方法会求解梯度,并将对应梯度值记录在每个参数上

  2. loss 可以理解为根据模型计算出来的值和正确值的偏差(也就是残差)。 例如,回归任务中计算的值是 1.0,而实际的值应当为 2.0,那么 loss 为 2.0-1.0=1.0。上述 loss 类型为 MAE,除此外,还有 MSE,Hinge 等各类 loss。一般分类任务的 loss 为交叉熵(Cross-Entropy),这也是目前 LLM 最常用的 loss。

  3. loss 计算出来后(这个过程也就是 forward,即前向推理),经过 backward 过程即可计算出梯度。

  4. 梯度:光滑的曲面上导数变化最大的方向

  5. loss 可以经过 PyTorch 的 loss.backward()将每个算子、每个步骤的梯度都计算出来(复杂微分方程的链式求导过程),当有了梯度后,可以将参数往负梯度方向更新,学习率(lr)就是这时候起作用的,由于直接加上负梯度太大,可能直接产生震荡,即值从一个点瞬间跑到了曲线上的另一个点,导致在这两点反复震荡不收敛,因此乘以一个 lr,让 loss 一点点下降。

  6. epoch 代表对数据集训练多少轮次

  7. iter 对输入数据的每次 forward+backward 代表一个 iter

  8. batch_size 批处理大小。在一次前向推理中,同时处理多少行数据。由于同一批数据会并行求解梯度,因此 batch_size 越大,梯度越稳定。在 SFT 时较为合适的梯度一般选择为 16/32/64 等值

  9. batch_size 越大,并行计算消耗的显存越高。因此在低显存情况下,可以选用 batch_size=1,gradient_accumulation_steps=16。训练会在 iter%gradient_accumulation_steps==0 时集中进行一次参数更新。在 iter%gradient_accumulation_steps!=0 时,会将梯度值不断累加到参数上,这样就相当于将 batch_size 扩大了 gradient_accumulation_steps 倍

  10. learning_rate 学习率 训练将负梯度值乘以该值加到原参数上。换句话说,每次只将参数更新一个小幅度,避免向错误的更新方向移动太多。

  11. 一般 LoRA 的学习率可以比全参数训练的学习率稍高一点,因为全参数训练会完全重置所有参数,训练时需要学习率更低。LLM 训练的学习率一般设置在 1e-4~1e-5 不等

  12. max_length 输入句子的最大长度。比如设置为 4096,那么句子加答案转换为 token 后最大长度为 max_length。这个值会影响显存占用,需要按照自己的实际需求设置。

  13. 当 batch_size 大于 1 时,意味着不同句子的长度可能不同。data_collator 的作用就是按照固定 max_length 或者 batch 中的最大长度对其他句子的 token 进行补齐。补齐的部分不参与模型的 loss 计算,但仍然会占用计算量

  14. flash_attention flash attention 是一种针对 attention 结构高效计算的组件,该组件主要原理利用了显卡的高速缓存。flash attention 会节省约 20%~40%训练显存并提高训练速度,对训练精度没有不良影响。在显卡支持的情况下建议开启。

  15. optimizer

  16. optimizer 是深度学习中的优化器,负责将负梯度值累加到原来需要更新的参数上,类似于:

  17. Vanilla SGD

    weights = weights - learning_rate * grad

  18. 实际的原理会比较复杂,比如常用的 AdamW 实际上是一个复杂的滑动平均的算法。

  19. lr_scheduler

  20. 一般来说,训练各个阶段的学习率是不一样的,有时候需要越来越小(因为训练到最后需要更精细的调节),有时候需要先有个 warmup(先将 lr 从 0 增大到指定值,再慢慢减小),lr_scheduler 就是用来动态调整 lr 使用的组件。

  21. gradient_checkpointing 梯度检查点。该方法的原理是将训练时的中间变量在前向过程中暂时丢弃,并在后向过程中重新计算。该方法可以有效节省训练显存,但属于时间换空间的做法,因此训练时间会变长。对显存的节省可以达到 30%-70%不等。训练速度会减慢 20%-40%不等。


训练有很多超参数,它们的含义和设置技巧可以参考这里

2.分布式训练(Distributed Training)

由于较大模型可能在单张显卡上显存溢出,或者训练速度不够,因此单机多卡或多机多卡训练是必要的。在训练过程中的分布式训练有以下几种模式:


  • DDP 分布式数据并行。将训练集的数据分段拆分到不同的进程中,这种训练方式相当于增加了 batch_size。比如四个进程,每个进程 batch_size=1,则总体 batch_size=4。在计算梯度时,torch 框架会自动将四个进程的梯度进行累加平均。该方法会提高训练速度,但如果模型在单张显卡上显存溢出,DDP 方式也无法运行。

  • MP 模型并行。模型并行分为多种方式,如 tensor 并行、device_map、流水线并行、FSDP 等。

  • tensor 并行:将矩阵拆分到多张显卡上,比如,将一个 2048x2048 的矩阵,拆分为两个 1024x2048 的矩阵,在前向推理时在显卡间通讯,完成一次推理,这样一个模型的显存需求就被平均拆分到两个显卡上。tensor 并行最知名的框架是 Megatron。

  • device_map 并行:自动计算如何将模型拆分到多个显卡上。比如一个模型按照顺序分为 embedder、layer0~95、output,device_map 可能将这些参数均分到两张显卡上,比如 embedder、layer0~48 分配到显卡 1 上,layer49~95、output 分配到显卡 2 上。相比 Megatron,device_map 方式较为低效,因为使用该方法训练或推理时,显卡 1 计算时显卡 2 是空闲的,计算效率较低;而 Megatron 是同时使用两个显卡计算,效率较高

  • 流水线并行:类似于 device_map,将模型按照 layer 拆分到不同显卡上

  • FSDP,在讲 FSDPqian 需要先讲解 DeepSpeed 的 ZeRO 优化方式

  • ZeRO-1:类似 DDP,但是将 Optimizer 的 state 均分维护到不同的进程中,每次更新参数后对所有进程的参数进行同步更新

  • ZeRO-2:在 ZeRO-1 的基础上,将不同层的梯度值均分维护到不同的进程中,每次每个进程同步梯度后更新自己负责的梯度对应的参数部分,并在更新后对所有的进程的参数进行同步

  • ZeRO-3:在 ZeRO-2 的基础上,将不同层的模型参数也均分到不同的进程中。每个进程在计算某层结果时,从其他进程中获得对应的层的参数,计算完后抛弃该层参数;backward 时,也从其他进程获得对应层的参数并同步梯度信息,计算完后抛弃该层参数。这样每个进程就在仅保存某些层的参数的条件下完成了数据并行计算

  • FSDP 就是 ZeRO-3 的并行策略

3.LoRA

LoRA 是一个非常重要的可调优结构,简单来说,就是增加了一个额外可训练部分,比如原来的 Linear 的矩阵是 MxN 维,增加一个 LoRA,该 LoRA 会包含两个参数量较少的矩阵:Mxd, dxN,这两个矩阵相乘后仍然是 MxN 维的,训练时原 MxN 矩阵冻结,只训练 LoRA 的两个矩阵,参数量就会大大减少。



为什么模型本身的矩阵不使用这种形式?


一般大规模矩阵的非零特征值数量会远远小于矩阵的维度,这个非零特征值的数量叫做矩阵的秩(rank),秩决定了这个矩阵如何影响被乘的向量,为 0 或较小的特征值对传入 tensor 的影响也比较小,丢弃这些信息对精度的影响不大。


一个模型包含了多个大矩阵,这些大矩阵的秩不相等而且难以预测,因此不能对原模型应用 LoRA,但在 sft 时使用 LoRA 相对安全,虽然有精度损失,但可以使一个大模型在一个消费级显卡上进行训练。


也就是说,LoRA 的原理是假设所有矩阵的秩都是 d,进行了一定的有损压缩。基于 LoRA 也有很多升级版技术,如 AdaLoRA、SoRA 等,这些组件方案都是基于 LoRA,对不同算子的 LoRA 的 rank 进行动态调节以达到更好的效果。


LoRA 目前已经是训练 SD 模型和 LLM 模型的最常用技术。LoRA 的 weights 也非常小,只有几十兆,因此加载和使用都非常方便,且 LoRA 本身可以合并回原模型,推理时可以做到兼容原模型结构。


如果涉及到对模型的知识编辑,比如自我认知任务,LoRA 的目标 module 一般需要设置为ALL,因为 MLP 层对模型的知识获取是至关重要的,需要参与训练过程。

3.1 训练过程

在前序的文章中,我们讲述了如何进行数据的前处理。结合上面讲解的基本概念,我们就可以运行一个完整的训练过程。


pip install ms-swift -U
复制代码


安装好 SWIFT 后,可以直接启动界面运行训练和推理:


swift web-ui
复制代码


  • 官方链接:https://modelscope.cn/studios/iic/Scalable-lightWeight-Infrastructure-for-Fine-Tuning/summary





在框架中,一个最小的训练过程代码如下:


#Experimental environment: A10, 3090, V100, ...#20GB GPU memoryimport osos.environ['CUDA_VISIBLE_DEVICES'] = '0'
import torch
from swift.llm import ( DatasetName, InferArguments, ModelType, SftArguments, infer_main, sft_main, app_ui_main, merge_lora_main)
model_type = ModelType.qwen_1_8bsft_args = SftArguments( model_type=model_type, train_dataset_sample=2000, dataset=[DatasetName.blossom_math_zh], output_dir='output')result = sft_main(sft_args)best_model_checkpoint = result['best_model_checkpoint']print(f'best_model_checkpoint: {best_model_checkpoint}')torch.cuda.empty_cache()
infer_args = InferArguments( ckpt_dir=best_model_checkpoint, load_dataset_config=True, show_dataset_sample=10)#merge_lora_main(infer_args)result = infer_main(infer_args)torch.cuda.empty_cache()
app_ui_main(infer_args)
复制代码

3.2 自定义一个训练过程

上面我们构建了一个最小的训练和推理流程。大多数时候开发者需要自定义一个训练流程和对应的数据集。在这种情况可以参考下面的步骤:


  1. 选择一个启动训练的方式,界面方式可以使用上述的 web-ui 命令(swift web-ui),命令行方式可以参考:


CUDA_VISIBLE_DEVICES=0 \swift sft \    --model_id_or_path qwen/Qwen-7B-Chat \    --dataset blossom-math-zh \    --output_dir output \
复制代码


注意命令行具有很多可调节参数,可以查看文档来查看这些参数的具体意义。


​ 如果想要了解训练流程可以查看训练代码


​ 了解超参数的拼接和处理可以查看超参数的处理代码


​ 了解所有支持的模板可以查看模板的拼接


  1. 选择一个需要参与训练的模型,可以参考支持的模型列表

  2. 选择一个或若干个自己的数据集参与训练,注意这些数据集有一定的格式要求。或者也可以使用一个自己的模型训练,只需要注册自定义模型即可。


CUDA_VISIBLE_DEVICES=0 \swift sft \    --model_id_or_path qwen/Qwen-7B-Chat \    --dataset blossom-math-zh \    --output_dir output \    --custom_train_dataset_path xxx.jsonl zzz.jsonl \    --custom_val_dataset_path yyy.jsonl aaa.jsonl \
复制代码

4.LISA

  • 背景介绍


LISA 是 Layerwise Importance Sampling for Memory-Efficient Large Language Model Fine-Tuning 的简写。这个技术可以把全参训练的显存使用降低到之前的三分之一左右,而使用的技术方法却是非常简单的。例如,全参训练一个 7b 模型大约需要 80G 显存(相当于一张完整的 A100 显卡),但使用 LISA 训练后却可以使显存降低到 30G 左右,这使得使用 40G A100 显卡甚至是 24G A10 或者 RTX 3090 成为可能,且它的显存占用更低、训练速度更快。


论文地址:https://arxiv.org/abs/2403.17919

4.1 技术解析

LISA 使用的技术原理相对简单。作者首先对 LoRA 训练和全参训练每个 layer 不同 step 时的 L2 范数的平均和进行了对比,结果如下:



作者训练了 GPT2 和 LLaMA-2-7B 两个模型,发现它们自身不同 layers 的 parameters 的 LoRA 训练和全参训练的 L2 范数不同,可以间接说明 LoRA 训练中由于低秩矩阵的存在,因此其参数更新的重点和全参数更新重点完全不同。可以看出,在权重更新时,除底层和顶层外其它层的 L2 范数都较小,因此作者假设可以在全参数训练时通过冻结大部分层的参数来模拟 LoRA 更新的行为,使其最后的参数迭代范数达到类似的效果。


完整的算法迭代可以用下图表示:


4.2 实验

在官方实验中,作者对比了 LISA 和 LoRA 训练以及全参数的显存占用:


![img]



可以看到 LISA 的显存占用要小于 LoRA。在训练速度上面:



官方实验结果,LISA 的 Forward 和 Backward 时间要显著短于 LoRA 训练。在训练方面,作者进行不同尺寸的微调和大规模微调,均证明了 LISA 的效果要强于 LoRA:




如何调节 LISA 的超参数呢?LISA 的超参数包含两个值:


  1. LISA 采样的有效层数γ

  2. LISA 的更新频率 K


消融实验对这两个值的对比如下:



可以看到 LISA 的性能在γ=8,采样频率 K=5 的时候达到最好。作者也证明,LISA 对于不同的随机种子的鲁棒性很强,在此不列举表格。

4.3 本次实验

为了验证 LISA 在实际测试中的效果,我们对 LISA 进行了一定的实验。我们使用了魔搭社区提供的 SWIFT 框架(https://github.com/modelscope/swift),该框架支持 LISA 训练方式,且支持 LoRA 等通用训练方式。我们可以设置 LISA 的两个值:


  • lisa_activated_layers 上文的γ

  • lisa_step_interval 上文的 K


我们使用如下命令进行训练:


#pip install ms-swift -Usft.py \ --model_type qwen-7b-chat \ --dataset ms-agent \ --train_dataset_mix_ratio 2.0 \ --batch_size 1 \ --max_length 2048 \ --use_loss_scale True \ --gradient_accumulation_steps 16 \ --learning_rate 5e-05 \ --use_flash_attn True \ --eval_steps 2000 \ --save_steps 2000 \ --train_dataset_sample -1 \ --val_dataset_sample 5000 \ --num_train_epochs 2 \ --check_dataset_strategy none \ --gradient_checkpointing True \ --weight_decay 0.01 \ --warmup_ratio 0.03 \ --save_total_limit 2 \ --logging_steps 10 \ --sft_type full \ --lisa_activated_layers 2 \ --lisa_step_interval 20
复制代码


同时,我们将--lisa_activated_layers 置为 0,进行全参数训练,并且使用 r=8 进行了 LoRA 训练,得到的效果如下:



从我们的实验中可以看到下面的结论:


  1. 在显存占用中,全参数几乎是其他轻量训练方式显存占用的两倍,但是在 loss 中也是最低的,这说明全参数在模型训练的基础指标中仍然是最优的;

  2. LISA 的显存使用比 r=8(这是个常用值)的显存占用要低,其中 lisa_activated_layers 越低显存越低

  3. 训练速度上 LISA 的训练速度也比 LoRA 要快一些,并且该指标也受到 lisa_activated_layers 的影响

  4. 在评估指标上,LoRA 更为优秀,然而评估指标受到数据集的强烈影响,由于训练主要内容是 Agent 数据集,因此说明 LoRA 在防止灾难性遗忘上具有一定的优势



LISA lisa_activated_layers=2 训练的 loss



LoRA r=8 训练的 loss


可以观察到 LISA 的训练 loss 较 LoRA 曲线更为抖动一些,猜测可能是 LISA 随机挑选 layer 进行反向传播的随机性造成的。

结论

可以看到 LISA 作为 2024 年的新晋 tuner,使用一个非常简单的方式做到了部分数据集的 SOTA,同时显存使用和训练速度也是很优秀的,且没有额外的使用条件。然而 LISA 仍然存在着一些可以分析讨论的问题,比如:是否可以通过参数范数或者参数矩阵特征值判断哪些 layers 应该被反向传播?或者是否可以在更细粒度上(qkv/mlp/layernorm)层面上控制反向传播?如果有做过实验的同学欢迎留言讨论。

发布于: 刚刚阅读数: 4
用户头像

本博客将不定期更新关于NLP等领域相关知识 2022-01-06 加入

本博客将不定期更新关于机器学习、强化学习、数据挖掘以及NLP等领域相关知识,以及分享自己学习到的知识技能,感谢大家关注!

评论

发布
暂无评论
LLM 大模型学习必知必会系列(七):掌握分布式训练与LoRA/LISA微调:打造高性能大模型的秘诀进阶实战指南_大模型微调_汀丶人工智能_InfoQ写作社区