数百次 LLM 微调实验的核心成果,包括 LoRA 超参数、数据集规模、训练效率等关键优化建议。对模型微调工程师直接可用。
过去几个月里,我进行了数百次、甚至可能数千次涉及 LoRA 的实验。几周前,我专门花了一些时间,深入研究了部分超参数的选择。
本文更像是一本按时间顺序呈现的实验日志。希望它能对一些读者有所帮助。具体来说,我希望解答以下问题:QLoRA 是否有价值、是否应该用 SGD 替换 AdamW、是否可以使用调度器,以及如何调整 LoRA 的超参数。
实验方面有很多内容值得讨论,因此我会尽量简要介绍 LoRA。
简而言之,LoRA 是 Low-Rank Adaptation(低秩适配,Hu 等人,2021)的缩写。它会在模型中加入少量可训练参数,同时冻结原始模型参数。
如下图所示,LoRA 将一个权重矩阵分解为两个较小的权重矩阵,从而以参数效率更高的方式近似完整的监督微调。
有关 LoRA 的更多细节,请参阅我的深度文章《使用低秩适配(LoRA)进行参数高效的 LLM 微调》。
本文将涵盖以下主题:
本文的重点是选择最优设置。为了将范围控制在合理程度,我会固定数据集,只关注 LLM 的监督式指令微调。(数据集修改或针对分类任务的微调,可能会在未来的文章中讨论。)
在模型评估方面,我从 Eleuther AI 的 Evaluation Harness 中选择了一小部分任务,包括 TruthfulQA、BLiMP Causative、MMLU Global Facts,以及包含两位数(arithmetic 2ds)和四位数(arithmetic 4ds)的简单算术任务。
在每项基准测试中,模型性能分数都会归一化到 0 和 1 之间,其中 1 代表满分。TruthfulQA 会报告两个分数,其定义如下:
MC1(单一正确答案):给定一个问题和 4~5 个候选答案,选出唯一正确的答案。模型选择的是这样一个候选答案:在不考虑其他候选答案的情况下,模型为该答案作为问题后续补全结果所分配的对数概率最高。该分数是所有问题的简单准确率。
MC2(多个正确答案):给定一个问题以及多个正确或错误的参考答案,分数为模型分配给所有正确答案的归一化总概率。
作为参考,参数量为 175B 的 GPT-3 模型在 TruthfulQA 上的 MC1 和 MC2 分数分别为 0.21 和 0.33。
下面用两个示例说明 arithmetic 2ds 和 arithmetic 4ds 的区别:
Arithmetic 2ds:“59 减去 38 等于多少”。“21”。
Arithmetic 4ds:“2762 加上 2751 等于多少”。“5513”。
如前所述,我固定了数据集,使用经过广泛研究——或者更准确地说,是较为常用的 Alpaca 数据集进行监督式指令微调。当然,还有许多其他数据集可用于指令微调,包括 LIMA、Dolly、LongForm、FLAN 等。不过,探索多个数据集以及混合数据集上的训练,将会是未来研究中一个有趣的主题。
Alpaca 数据集包含大约 5 万组用于训练的指令—回答对。使用 Llama 2 SentencePiece tokenizer 计算时,其输入长度的中位数为 110 个 token,如下面的直方图所示。
数据集中的任务本身可以按照下图所示的方式组织。
本文使用的自定义 LLM 微调代码基于开源 Lit-GPT 仓库。为了保持本文前言简洁,我不会详细介绍使用方法,但你可以在 Lit-GPT 教程部分找到更详细的指南。
简要来说,使用方法如下:
1)克隆仓库并安装依赖
git clone https://github.com/Lightning-AI/lit-gpt
cd lit-gpt
pip install -r requirements.txt
2)下载并准备模型检查点
python scripts/download.py \
--repo_id mistralai/Mistral-7B-Instruct-v0.1
# there are many other supported models
python scripts/convert_hf_checkpoint.py \
--checkpoint_dir checkpoints/mistralai/Mistral-7B-Instruct-v0.1
python scripts/prepare_alpaca.py \
--checkpoint_dir checkpoints/mistralai/Mistral-7B-Instruct-v0.1
# or from a custom CSV file
python scripts/prepare_csv.py \
--csv_dir MyDataset.csv \
--checkpoint_dir checkpoints/mistralai/Mistral-7B-Instruct-v0.1
python finetune/lora.py \
--checkpoint_dir checkpoints/mistralai/Mistral-7B-Instruct-v0.1/ \
--precision bf16-true
5)合并 LoRA 权重
python scripts/merge_lora.py \
--checkpoint_dir "checkpoints/mistralai/Mistral-7B-Instruct-v0.1" \
--lora_path "out/lora/alpaca/Mistral-7B-Instruct-v0.1/lit_model_lora_finetuned.pth" \
--out_dir "out/lora_merged/Mistral-7B-Instruct-v0.1/"
cp checkpoints/mistralai/Mistral-7B-Instruct-v0.1/*.json \
out/lora_merged/Mistral-7B-Instruct-v0.1/
python eval/lm_eval_harness.py \
--checkpoint_dir "out/lora_merged/Mistral-7B-Instruct-v0.1/" \
--eval_tasks "[arithmetic_2ds, ..., truthfulqa_mc]" \
--precision "bf16-true" \
--batch_size 4 \
--num_fewshot 0 \
--save_filepath "results.json"
python chat/base.py \
--checkpoint_dir "out/lora_merged/Mistral-7B-Instruct-v0.1/"
第一项任务是为 LoRA 实验选择一个能力出色的基础模型。为此,我将重点放在尚未经过指令微调的模型上:phi-1.5 1.3B、Mistral 7B、Llama 2 7B、Llama 2 13B 和 Falcon 40B。请注意,所有实验都在单张 A100 GPU 上运行。
从上表可以看出,Mistral 7B 模型在数学基准测试中的表现极为出色。与此同时,考虑到 phi-1.5 1.3B 模型的规模相对较小,它在 TruthfulQA MC2 上的表现也令人印象深刻。由于某种原因,Llama 2 13B 在算术基准测试中表现不佳,而规模更小的 Llama 2 7B 在这方面明显优于它。
由于研究人员和从业者目前推测 phi-1.5 1.3B 与 Mistral 7B 的训练数据中可能包含基准测试数据,因此我决定不在实验中使用它们。此外,我认为在剩余模型中选择最小的一个,既能留出最大的性能提升空间,又能降低硬件要求。因此,本文其余部分将重点讨论 Llama 2 7B。
首先,我使用以下默认设置评估了 LoRA 微调效果(这些设置可以在 finetune/lora.py 脚本中修改):
# Hyperparameters
learning_rate = 3e-4
batch_size = 128
micro_batch_size = 1
max_iters = 50000 # train dataset size
weight_decay = 0.01
lora_r = 8
lora_alpha = 16
lora_dropout = 0.05
lora_query = True
lora_key = False
lora_value = True
lora_projection = False
lora_mlp = False
lora_head = False
warmup_steps = 100
(请注意,批次大小为 128,但为了节省内存,我们使用了微批次大小为 1 的梯度累积;其产生的训练轨迹等价于常规批次大小为 128 的训练。如果你想了解梯度累积的工作原理,请参阅我的文章《使用梯度累积在单张 GPU 上微调 LLM》。)
该配置在总计 6,738,415,616 个参数中训练了 4,194,304 个 LoRA 参数。在我的机器上使用单张 A100 训练大约耗时 1.8 小时,最大内存占用为 21.33 GB。
为了衡量方差,我将实验重复了三次,以观察不同运行之间的性能波动。
从上表可以看出,各次运行的性能非常一致且稳定。还值得注意的是,采用 LoRA 默认设置微调后的模型,其算术能力变得非常差。不过,这可能在意料之中,因为据我所知,Alpaca 并不包含多少算术任务,甚至可能完全不包含。
此外,我还查看了由 Meta 通过 RLHF 进行指令微调的 Llama 2 7B 版本。正如下表所示,Meta 的 Llama 2 Chat 模型在算术性能上也表现更差。不过,Chat 模型在其他基准测试上有了很大改进(除了 BLiMP),这可以作为参考,指导我们用 LoRA 微调想要达到的方向。
在开始调整 LoRA 超参数之前,我想探索一下 QLoRA(Dettmers 等人开发的常用量化 LoRA 技术)在建模性能和内存节省之间的权衡。
在 Lit-GPT 中,我们可以通过 –quantize 标志(这里使用 4 位标准浮点格式)来启用 QLoRA,如下所示:
此外,我还尝试了 4 位浮点精度作为对照。下面是对训练时间和最大内存使用的影响:
默认 LoRA(使用 bfloat-16):
QLoRA via –-quantize "bnb.nf4":
QLoRA via –quantize "bnb.fp4":
我们可以看到 QLoRA 的内存需求减少了将近 6 GB。然而,代价是训练时间慢了 30%,这是由于额外的量化和反量化步骤导致的,这也在预期之内。
接下来,让我们看看 QLoRA 训练如何影响模型性能:
从上表可以看出,与常规 LoRA 相比,QLoRA 对模型性能确实有小小的影响。该模型在算术基准测试上有所改进,但在 MMLU 全局事实基准测试上有所下降。
由于内存节省相当可观(通常超过更长的训练时间的代价,因为它允许用户在更小的 GPU 上运行模型),我将在本文的其余部分使用 QLoRA。
我在之前的所有实验中都使用了 AdamW 优化器,因为它是 LLM 训练的常见选择。然而,众所周知 Adam 优化器可能会相当占用内存。这是因为它为每个模型参数引入并跟踪两个额外参数(矩 m 和 v)。大语言模型 (LLM) 有很多模型参数;例如,我们的 Llama 2 模型有 70 亿个模型参数。
本节探索是否值得将 AdamW 替换为 SGD 优化器。不过,对于 SGD 优化器,引入学习率调度器也尤其重要。我选择了余弦退火调度,它在每次批次更新后降低学习率。
如果你对在 PyTorch 中使用学习率调度器的更多细节感兴趣,我有一个相关的讲座。
不幸的是,将 AdamW 替换为 SGD 只能节省很小的内存。
这可能是因为大部分内存消耗在大矩阵乘法上,而不是保持额外参数在内存中。
但这个小差异也许是预期的。在当前选择的 LoRA 配置(r=8)下,我们有 4,194,304 个可训练参数。如果 Adam 为每个模型参数添加 2 个额外值,这些值以 16 位浮点数存储,我们有 4,194,304 * 2 * 16 bit = 134.22 megabits = 16.78 megabytes。
当我们将 LoRA 的 r 增加到 256 时,我们会看到更大的差异,我们稍后会这样做。在 r=256 的情况下,我们有 648,871,936 个可训练参数,按照相同的计算方式等于 2.6 GB。实际测量的结果是 3.4 GB 的差异,可能是由于存储和复制优化器状态的一些额外开销。
底线是,对于少量可训练参数(如 LoRA 和低秩(rank)值的情况),从 AdamW 交换到 SGD 获得的内存收益可能很小,与此相反,在预训练中,我们训练更多数量的参数。
尽管 SGD 在这里没有提供显著的内存节省,让我们仍然快速看一下生成的模型性能:
SGD 优化器的性能似乎与 AdamW 相当。有趣的是,当向 AdamW 添加调度器时,TruthfulQA MC2 和 MMLU 全局事实性能有所改进,但算术性能有所下降。(注:TruthfulQA MC2 是一个广为认可的基准测试,出现在其他公开排行榜中。)目前,我们不会过度强调算术性能,而是继续本文中的其余实验,使用带有调度器的 AdamW。
如果你想重现这些实验,我发现最佳的 AdamW 学习率是 3e-4,衰减率为 0.01。最佳的 SGD 学习率是 0.1,动量为 0.9。在两种情况下我都使用了额外的 100 步学习率预热。
(基于这些实验,余弦调度器已被添加到 Lit-GPT 中,现在默认启用。)
到目前为止,我使用 50k 次迭代训练了所有模型——Alpaca 数据集有 50k 个训练示例。一个显而易见的问题是,我们是否可以通过多次迭代训练集来改进模型性能,所以我用 100k 次迭代运行了之前的实验,这是 2 倍的增加:
有趣的是,增加的迭代次数导致了全面的性能下降。算术基准测试的下降最为明显。我的假设是 Alpaca 数据集不包含任何相关的算术任务,当模型更专注于其他任务时,它会主动遗忘基本算术。
不管怎样,如果我说这个结果不是意料之中的话,那我就是在撒谎。这样,我可以在本文的其余部分继续使用较短的 50k 次迭代实验。
现在我们已经探索了 LoRA 微调脚本周围的基本设置,让我们把注意力转向 LoRA 超参数本身。默认情况下,LoRA 仅对多头自注意力块中的 Key 和 Query 矩阵启用。现在,我们也为 Value 矩阵、投影层和线性层启用它:
最重要的 LoRA 参数之一是"r",它决定了 LoRA 矩阵的秩或维度,直接影响模型的复杂性和容量。较高的"r"意味着更强的表达力,但可能导致过拟合,而较低的"r"可以以牺牲表达力为代价减少过拟合。保持所有层都启用 LoRA,让我们将 r 从 8 增加到 16,看看这对性能有什么影响:
我们可以看到仅仅增加 r 本身会使结果变差,那么发生了什么?让我们在下一节中找出答案。
在上一节中,我们增加了矩阵秩 r,同时保持 LoRA 的 alpha 参数不变。较高的"alpha"会更强调低秩结构或正则化,而较低的"alpha"会减少其影响,使模型更多地依赖原始参数。调整"alpha"有助于在拟合数据和通过正则化防止过拟合之间找到平衡。
作为一条经验法则,在微调 LLM 时,通常倾向于选择一个秩的两倍大的 alpha(注意这与使用扩散模型时不同)。让我们试试这个,看看当我们将 alpha 增加两倍时会发生什么:
如我们所见,将 alpha 增加到 32 现在产生了我们迄今为止最好的模型!但同样,我们用更多要训练的参数来换取了这个改进:
alpha=16 情况:
alpha=32 情况:
不过,可训练参数的数量仍然足够小,它不会明显影响峰值内存需求。
无论如何,我们现在终于开始取得一些进展,并在更明显的范围内改进模型性能。那么,让我们继续前进,看看通过增加秩和 alpha 能走多远:
我还用特别大的秩(512、1024 和 2048)进行了额外的实验,但这些导致了更差的结果。有些运行甚至没有在训练期间收敛到接近零的损失,这就是为什么我没有将它们添加到表中。
迄今为止,我们可以注意到上一行中 r=256 和 alpha=512 的模型总体上表现最好。作为额外的对照实验,我用 alpha 值为 1 重复了运行,并注意到了确实需要大的 alpha 值来获得良好的性能:
我也重复了 alpha 值为 16 和 32 的实验,我观察到了相同的性能恶化情况,与选择两倍秩的 alpha 值相比。
在本文最后的调优实验中,我希望进一步优化上一节中最佳模型(r=256,最后一行)的 alpha 值,因为我怀疑这个值可能有些过大。
从上表可以看出,增大秩时,选择较大的 alpha 值似乎至关重要。
对于 r=256、a=512 的 QLoRA 模型,可以明显看出,我们的模型相较基础模型取得了显著提升。微调模型唯一不如基础模型的领域是四位数算术。不过这是可以理解的,因为 Alpaca 数据集很可能不包含这类训练样本。
前面我们已经看到,通常建议将 alpha 设为秩的两倍(例如 r=256、alpha=512),这确实取得了最佳结果,而较小的 alpha 值会导致更差的结果。但如果让 alpha 超过“秩的两倍”这一建议值,情况又会怎样呢?
根据上表中的结果,选择超过“秩的两倍”建议值的 alpha,同样会使基准测试结果变差。
我们知道,在机器学习中不应多次使用测试集,否则就有可能对某个特定任务过度优化。因此,建议在最终的独立数据集上验证模型。
恰巧,目前 NeurIPS LLM Efficiency 挑战赛正在进行,该挑战聚焦于在单块 GPU 上微调 LLM。由于我很好奇 Llama-2 7B 基础模型与我们在 Alpaca 上微调得到的最佳 LoRA 模型相比表现如何,因此我将基础模型和微调模型都提交到了他们的排行榜。
我们可以看到,在 r=256 的设置下,(Q)LoRA 微调耗时 10522.77s(约 3 小时),需要 19.24 GB GPU 显存。它提升了模型在多项基准测试中的表现,但并非所有基准测试都有所改善。若考虑使用 Alpaca 之外的其他微调数据集,并采用 RLHF 等对齐技术,模型性能可能还会进一步提升;我在这里对这些技术进行了更详细的说明。
本文探讨了使用 LoRA 训练定制 LLM 时可以调节的各种参数。我们发现,尽管 QLoRA 会增加运行时间,但它非常节省内存。此外,虽然学习率调度器可能有所帮助,但在 AdamW 和 SGD 优化器之间进行选择几乎没有区别。对数据集进行多轮迭代甚至可能使结果变得更差。优化包括秩在内的 LoRA 设置,可以获得最高的投入产出比。提高秩会增加可训练参数的数量,这可能导致更严重的过拟合并增加运行时间成本。不过,在提高秩时,选择合适的 alpha 值非常重要。
本文绝非面面俱到,因为我没有足够的时间和资源探索所有可能的配置。此外,未来还可以通过考虑其他数据集和模型来进一步改进。
希望你能从中获得一些可以应用到自己项目中的启发。为了避免本文变得长得不合理,我尽可能精简了 LoRA、学习率调度器、梯度累积等各种概念的背景信息和解释。不过,如果你有任何问题或疑虑,我非常乐意与你交流。你可以通过 X/Twitter 或 LinkedIn 联系我,也可以联系 @LightningAI。
如果你觉得本文有用,希望你能将它分享给同事,我将不胜感激。
对于有关 Lit-GPT 的一般反馈、建议或改进意见,请随时使用 GitHub issue tracker。