PEFT documentation
LoRA 转换
LoRA 转换
用于将非 LoRA PEFT 模型转换为 LoRA 模型的函数。
描述
PEFT 支持数十种不同的参数高效微调技术。其中,LoRA 无疑是最受欢迎的。这意味着许多其他包也支持 LoRA。例如,Diffusers 允许加载 LoRA 适配器以改变扩散模型的能力。vLLM 允许部署带有 LoRA 适配器的模型。这很好,但遗憾的是,其他非 LoRA 的 PEFT 方法很少得到支持。因此,即使另一种 PEFT 方法更适合您的特定用例,由于下游包不提供支持,您也可能无法使用它。
在此,我们提出一种潜在的解决方案。PEFT 提供了两个函数 save_as_lora() 和 convert_to_lora(),可将 PEFT 适配器转换为 LoRA 适配器。目前并非所有 PEFT 方法都支持此功能,但如果支持,则意味着您可以从最适合您的 PEFT 方法入手,之后将其当作 LoRA 适配器使用。
示例
转换后的适配器 LoRA 秩可以通过向 rank 参数传入一个大于 0 的整数来设置为固定秩,或者通过向 rank 参数传入 0 到 1 之间的浮点数来设置为动态秩,动态秩会根据每一层进行自适应调整。动态秩可能更高效(以更少的参数实现相同的性能)。
固定的 LoRA 秩
save_as_lora() 的用法相对简单:
from peft import get_peft_model, save_as_lora
# first load and train your non-LoRA PEFT model as normal
base_model = ...
non_lora_config = ...
model = get_peft_model(base_model, non_lora_config)
# check that this PEFT method can indeed be converted to LoRA
assert model.supports_lora_conversion()
... # train the model
# the rank of the LoRA adapter that you want to convert to
target_rank = 64
# save as a LoRA checkpoint
save_as_lora(output_path, model, rank=target_rank)这将在 output_path 创建一个 LoRA 检查点,您可以像加载其他 LoRA 适配器一样加载它,或在 Diffusers 或 vLLM 等下游包中使用。
如果你不想保存转换后的 LoRA 适配器,而是希望立即使用转换后的权重(例如用于执行评估),那么 convert_to_lora() 函数会很有用:
from peft import convert_to_lora, get_peft_model, set_peft_model_state_dict
base_model = ...
non_lora_config = ...
model = get_peft_model(base_model, non_lora_config)
... # train the model
# get the lora config and state dict of the converted lora model
lora_config, lora_state_dict = convert_to_lora(model, rank=target_rank)
# reload the base model, or use model.unload()
base_model = ...
# apply the lora config to the base model
lora_model = get_peft_model(base_model, lora_config)
# load the LoRA weights onto the base model
set_peft_model_state_dict(lora_model, state_dict)动态 LoRA 秩
在上面的示例中,我们使用了固定的 LoRA 秩进行转换。然而,可以设想某些层不需要高秩即可被准确转换,而其他层则需要更高的秩。为了适应这种情况,PEFT 提供了将 0 到 1 之间的浮点数作为 rank 参数传入的选项。假设你传入 rank=0.5。这意味着对于每一层,LoRA 适配器的秩被选择为使得该 LoRA 适配器能够解释原始适配器引入的权重方差的 50%。用更技术性的术语来说,在底层我们对适配器的权重贡献执行 奇异值分解,然后选取前若干个奇异值,使得这些奇异值归一化后的总和等于传入的值。
# set a dynamic rank by passing a float
threshold = 0.7
# save as a LoRA checkpoint
save_as_lora(output_path, model, rank=threshold)
# get the lora config and state dict directly:
lora_config, lora_state_dict = convert_to_lora(model, rank=threshold)
# inspect the different ranks per layer:
print(lora_config.rank_pattern)如果不同层的贡献差异很大,使用这种动态 LoRA 秩可能很有用。其缺点在于,某些层的 LoRA 秩可能会非常高,从而导致内存峰值。请测试哪种方法最适合您的使用场景。
编译模型
对于大型模型,执行转换可能需要一些时间;例如,每个 PEFT 模块都必须经过一次 SVD 计算。通过向 save_as_lora() 或 convert_to_lora() 传递 compile_kwargs,你可以对转换函数应用 torch.compile,从而可能加快处理速度。compile_kwargs 是一个字典,其中包含传递给 torch.compile 的关键字参数(空字典也可以)。以下是一个示例:
compile_kwargs = {"dynamic": True, "mode": "max-autotune-no-cudagraphs", "fullgraph": True}
save_as_lora(output_path, model, rank=rank, compile_kwargs=compile_kwargs)LoRA 到 LoRA 的转换
也可以将一个 LoRA 适配器转换为另一个 LoRA 适配器。为什么要这样做?原因只有一个,即当你希望降低 LoRA 适配器的秩(rank)时。如果在训练后想要缩小 LoRA 适配器,请使用 save_as_lora() 或 convert_to_lora() 并传入更小的秩。这将生成一个新的 LoRA 适配器,其内存和存储占用更小。
指标
非 LoRA 到 LoRA 的转换
当然,将一个 PEFT 适配器转换为另一个适配器是一个有损过程。新适配器的性能很可能不如初始适配器。因此,强烈建议评估转换后的 LoRA 适配器。这样,您可以确保转换后的适配器足以满足您的使用场景。一般规律是,LoRA 适配器的秩越高,它对初始适配器的近似效果就越好。这意味着,转换后的 LoRA 适配器可能需要比原始适配器更多的参数才能达到相似的性能。
举个例子,以下是基于 PEFT MetaMathQA 基准 得出的一些数据。为此,我们使用 LoHa 适配器在 MetaMathQA 上对 meta-llama/Llama-3.2-3B 进行微调,并在 GSM8K 上进行评估。初始 LoKr 适配器的秩为 32,产生了 18,350,080 个可训练参数,测试准确率为 41.85%。评估需要 12.25 GB 的内存。该检查点被转换为具有不同 rank 值的 LoRA。结果如下:
| rank | 可训练参数 | 测试准确率(%) | accuracy 变化 | 预留内存(最大值,GB) | 内存增加 |
|---|---|---|---|---|---|
| 8 | 2293760 | 37.60 | -4.25 | 12.41 | 0.16 |
| 16 | 4587520 | 38.89 | -2.96 | 12.15 | -0.10 |
| 32 | 9175040 | 40.11 | -1.74 | 12.41 | 0.16 |
| 64 | 18350080 | 39.20 | -2.65 | 12.18 | -0.07 |
| 0.4 | 2428928 | 37.60 | -4.25 | 12.41 | 0.16 |
| 0.5 | 4761600 | 40.18 | -1.67 | 12.41 | 0.16 |
| 0.6 | 8857600 | 39.42 | -2.43 | 12.41 | 0.16 |
| 0.7 | 16230400 | 39.04 | -2.81 | 12.15 | -0.10 |
如你所见,如果秩足够高,我们可以获得接近原始 LoHa 适配器的测试准确率。选择合适的秩是在模型性能和模型效率之间进行权衡。要复现此实验,请遵循位于 https://github.com/huggingface/peft/tree/main/scripts/evaluate-lora-conversion.py 的脚本。
请注意,可训练参数的数量无法与内存占用一一对应。即使可训练参数数量相同,某些 PEFT 方法所需的内存更多,而另一些则更少。因此,即使转换后 LoRA 适配器的参数数量多于原始适配器,其在服务部署时仍可能具有更高的内存效率。
LoRA 到 LoRA 的转换
与上述实验类似,我们同样可以评估 LoRA 到 LoRA 的转换(即 LoRA 压缩)。在这里,我们从使用 RS-LoRA 在相同设置下训练的秩为 64 的 LoRA 适配器开始。初始适配器拥有 18,350,080 个可训练参数,测试准确率为 52.92%,评估时需要 12.58 GB 的内存。下表展示了将此适配器转换为更小秩的 LoRA 适配器的结果:
| rank | 可训练参数 | 测试准确率(%) | accuracy 变化 | 预留内存(最大值,GB) | 内存增加 |
|---|---|---|---|---|---|
| 8 | 2293760 | 43.37 | -9.55 | 12.38 | -0.20 |
| 16 | 4587520 | 48.90 | -4.02 | 12.38 | -0.20 |
| 32 | 9175040 | 51.48 | -1.44 | 12.49 | -0.09 |
| 48 | 13762560 | 52.01 | -0.91 | 12.38 | -0.20 |
| 0.5 | 2150400 | 44.12 | -8.80 | 12.37 | -0.21 |
| 0.6 | 3082240 | 47.54 | -5.38 | 12.37 | -0.21 |
| 0.7 | 4448256 | 50.49 | -2.43 | 12.37 | -0.21 |
| 0.8 | 6510592 | 50.11 | -2.81 | 12.37 | -0.21 |
| 0.9 | 10022912 | 51.55 | -1.37 | 12.38 | -0.20 |
| 0.95 | 12976128 | 52.62 | -0.30 | 12.39 | -0.19 |
例如,对于秩 0.95,我们可以将准确率差距缩小至仅 0.3 个百分点,同时将参数量减少 30%。另外请注意,这些压缩后的 LoRA 可能比直接在较低秩上训练的效果更好——例如,对于秩 32,直接训练的测试准确率为 48.22%,而从秩 64 转换得到的结果则为 51.48%。
注意事项
LoRA 转换存在一些局限性。如上所述,预期会出现性能下降,且转换后的 LoRA 在参数效率上很可能低于原始适配器。此外,LoRA 转换还具有以下限制:
- 目前,只有应用于线性层的适配器可以被转换。
- 并非所有 PEFT 方法目前都支持 LoRA 转换。
如果大家对扩展 LoRA 转换有强烈需求,请通过创建 GitHub 讨论 告知我们,我们将使其支持更多层类型和 PEFT 方法。
API
将非 LoRA 模型转换为 LoRA 模型,返回 LoraConfig 和 state_dict
peft.convert_to_lora
< source >( model: Modulerank: floatadapter_name: str = 'default'progressbar: bool = Falsecompile_kwargs = None )
参数
- model — 要转换的模型。应为一个包含支持转换的 PEFT 层的模型。
- rank (
int或float) — 返回的 LoRA 适配器所需的秩。秩越高,LoRA 适配器对原始适配器的镜像就越准确。但这也需要更多的内存、计算资源和磁盘空间。因此,请选择最适合您使用场景的权衡值,并验证最终的适配器。如果传入浮点数,则将其解释为解释方差/能量阈值:我们选择最小的秩 k,使得前 k 个奇异值至少占总平方奇异值之和的该比例。如果少数奇异值就能捕获该层的适配,这实际上会导致分配较低的秩。浮点数越低,秩越低,反之亦然。请注意,动态秩可能导致每层的秩非常不均衡,这意味着某些层可能需要不成比例的大量内存用于激活值。选择固定的(int)秩有助于实现可预测的内存需求。 - adapter_name (
str, 可选) — 要转换的适配器名称。一次只能转换一个适配器。默认为"default"。 - progressbar (
bool) — 是否显示进度条以指示转换进度(对于大型模型,可能需要几分钟)。 - compile_kwargs (
dict, 可选) — 如果提供,则编译该函数,以使用给定的 kwargs 将各个模块转换为 LoRA,这些 kwargs 会传递给torch.compile。这可能会加速大型模型的转换。
将带有 PEFT 层的非 LoRA 模型转换为 LoRA 检查点。
此功能仅支持部分允许等效转换的特定 PEFT 方法。本质上,这指的是那些通过用增量权重更新基础权重来工作的 PEFT 方法。此外,目前仅支持线性层。
LoRA 适配器将尽可能接近地近似初始适配器。秩越高,近似效果越好。预计该近似永远无法达到原始适配器的完整性能,并且 LoRA 适配器的参数效率将低于原始适配器(即,在性能相似的情况下,它需要更多的参数)。这种转换在许多情况下仍然有用:
- 在 PEFT 中,LoRA 支持的功能比大多数其他方法更多,例如混合适配器批次。因此,转换后的适配器可以与这些功能一起使用。
- 某些下游包支持 LoRA 适配器,但不支持其他 PEFT 方法,例如 Diffusers。转换功能允许在这些包中使用非 LoRA 适配器。
LoRA 缩放因子已经内置于 LoRA 权重中,因此缩放始终为一(即 rank 和 alpha 被设置为相同的值)。
注意:此函数(目前)尚不支持分片模型。
Raises
TypeError:
如果提供的模型没有任何可以转换为 LoRA 的层,则会引发 TypeError。
ValueError:
如果选择了无效的秩(过高或过低)。
将非 LoRA 模型转换为 LoRA 模型,并在指定路径保存适配器检查点和配置
peft.save_as_lora
< source >( path: str | os.PathLikemodel: Modulerank: floatadapter_name: str = 'default'progressbar: bool = Falsecompile_kwargs = None )
参数
- model — 要转换的模型。应为一个包含支持转换的 PEFT 层的模型。
- rank (
int或float) — 返回的 LoRA 适配器所需的秩。秩越高,LoRA 适配器对原始适配器的镜像就越准确。但这也需要更多的内存、计算资源和磁盘空间。因此,请选择最适合您使用场景的权衡值,并验证最终的适配器。如果传入浮点数,则将其解释为解释方差/能量阈值:我们选择最小的秩 k,使得前 k 个奇异值至少占总平方奇异值之和的该比例。如果少数奇异值就能捕获该层的适配,这实际上会导致分配较低的秩。浮点数越低,秩越低,反之亦然。请注意,动态秩可能导致每层的秩非常不均衡,这意味着某些层可能需要不成比例的大量内存用于激活值。选择固定的(int)秩有助于实现可预测的内存需求。 - adapter_name (
str, 可选) — 要转换的适配器名称。一次只能转换一个适配器。默认为"default"。 - progressbar (
bool) — 是否显示进度条以指示转换进度(对于大型模型,可能需要几分钟)。 - compile_kwargs (
dict, 可选) — 如果提供,则编译该函数,以使用给定的 kwargs 将各个模块转换为 LoRA,这些 kwargs 会传递给torch.compile。这可能会加速大型模型的转换。
将带有 PEFT 层的非 LoRA 模型转换为 LoRA,然后保存检查点文件和 PEFT 配置。
此功能仅支持部分允许等效转换的特定 PEFT 方法。本质上,这指的是那些通过用增量权重更新基础权重来工作的 PEFT 方法。此外,目前仅支持线性层。
LoRA 适配器将尽可能接近地近似初始适配器。秩越高,近似效果越好。预计该近似永远无法达到原始适配器的完整性能,并且 LoRA 适配器的参数效率将低于原始适配器(即,在性能相似的情况下,它需要更多的参数)。这种转换在许多情况下仍然有用:
- 在 PEFT 中,LoRA 支持的功能比大多数其他方法更多,例如混合适配器批次。因此,转换后的适配器可以与这些功能一起使用。
- 某些下游包支持 LoRA 适配器,但不支持其他 PEFT 方法,例如 Diffusers。转换功能允许在这些包中使用非 LoRA 适配器。
LoRA 缩放因子已经内置于 LoRA 权重中,因此缩放始终为一(即 rank 和 alpha 被设置为相同的值)。
您可以按以下方式加载转换后的 LoRA 权重:
>>> lora_path = ...
>>> save_as_lora(lora_path, model, rank=...)
>>> base_model = AutoModel.from_pretrained(...)
>>> lora_model = PeftModel.from_pretrained(base_model, lora_path)注意:此函数(目前)尚不支持分片模型。
Raises
TypeError:
如果提供的模型没有任何可以转换为 LoRA 的层,则会引发 TypeError。
ValueError:
如果选择了无效的秩(过高或过低)。