🤗 PEFT 欢迎新的合并方法
模型合并已迅速成为推动大型语言模型性能极限的事实标准。在 Open LLM 排行榜上,我们不断注意到合并模型位居榜首。我们自己的 Omar Sanseviero 对模型合并进行了一次小小的冲刺,并发现了有趣的发现。
到目前为止,模型合并的典型方式是取一组模型并将其合并。这篇博文提供了关于此主题的很好的入门介绍。通常,对于合并多个模型,我们首先下载它们的检查点,然后执行合并。根据合并算法和底层模型的大小,此过程可能非常占用内存。mergekit
库提供了处理此问题的优化方法,使得在有限内存下也能管理该过程。
但是,如果我们要合并从*同一个*模型获得的“不同适配器”怎么办?您可能有从同一个基础模型获得的四个不同的 LoRA 检查点,并且您想尝试不同的合并技术。最终,您希望选择最佳合并,为您的任务提供最佳结果。在实现这种开发体验时,有几点变得显而易见
- 在处理 LoRA 等适配器时,用户通常会来回切换不同的适配器,甚至将它们组合起来。适配器可以被激活、停用,或完全从内存中移除。因此,我们需要即时进行“合并”部分(与上述方法相反),以便为用户提供无缝体验。
- 不同的适配器可能对合并有不同的要求。例如,LoRA 的合并算法可能无法同样适用于 IA3。
考虑到这些方面,我们在 🤗 PEFT 中发布了针对流行 LoRA 适配器的新合并方法。在这篇博文中,我们想向您介绍可用的方法、帮助您入门的代码示例、令人印象深刻的结果以及我们未来的计划。让我们开始吧 🚀
目录
组合/合并 LoRA 适配器的方法
拼接 (cat
)
在此方法中,LoRA 矩阵被拼接。例如,如果我们有两个 LoRA 适配器 和 以及权重 和 用于这两个适配器的加权合并,则合并如下所示
其中 。
现在,这个新合并的 LoRA 层的输出将如同原始的 2 个 LoRA 以权重 和 分别应用于第一个和第二个适配器时那样。
这里,我们可以观察到
set_adapters()
时,它也通过 Diffusers 的 PEFT 集成提供,其中不是创建新的合并适配器,而是按顺序组合活动适配器,如上述方程的右侧所示。当使用此方法时,它允许参与的 LoRA 适配器具有不同的秩。
线性/任务算术 (linear
)
在该方法中,LoRA 矩阵进行加权求和。这就是任务算术论文在任务权重上实现的内容。在任务算术中,首先计算任务权重,即微调权重和基础模型权重之间的差异,然后对这些任务权重进行加权求和。在这里,所考虑的 delta 权重是单独的矩阵 和 ,而不是它们的乘积 。此方法仅适用于所有参与的 LoRA 适配器具有相同秩的情况。
让我们举个例子。考虑 2 个 LoRA 适配器 和 以及权重 和 用于这两个适配器的加权合并,则合并如下所示
欲了解更多详情,请参阅论文:使用任务算术编辑模型。
SVD (svd
)
不将单独的矩阵 和 视为任务权重,而是将其乘积 (即 delta 权重)视为任务权重。
我们继续使用上一小节的例子。在这里,首先计算合并组合的 delta 权重,如下所示
在获得上述合并的 delta 权重后,应用 SVD(奇异值分解)来获得近似值 和

cat
方法类似,此方法也允许使用不同秩的 LoRA 适配器。此外,可以为结果合并的 LoRA 适配器选择秩,该秩默认为参与 LoRA 适配器中的最大秩。此方法的一个限制是,执行 SVD 操作需要大量 GPU 内存。
TIES (ties
, ties_svd
)
这建立在 linear
和 svd
方法的基础上,通过改变合并适配器从任务权重计算的方式,分别产生了 ties
和 ties_svd
方法。在 TIES (TRIM, ELECT SIGN & MERGE) 中,首先计算任务权重,在我们的例子中,对于非 svd 变体将是 LoRA 适配器 和 ,对于 svd 变体将是它们的乘积 。之后,您根据指定的分数 density
修剪任务权重中最小的值并保留 top-k 值。然后,您从参与的修剪任务权重中计算多数符号掩码,将任务张量与用户提供的权重相乘,然后根据多数符号掩码进行不相交合并。对于多数符号掩码计算,您有两个选项
total
同时考虑大小和符号以获得多数符号,即,将所有相应权重求和;frequency
只考虑权重符号以获得多数符号,即,将所有相应权重的符号求和。
欲了解更多详情,请参阅论文:TIES-Merging: Resolving Interference When Merging Models。
DARE (dare_linear
, dare_ties
, dare_linear_svd
, dare_ties_svd
)
这也建立在 linear
和 svd
方法的基础上,其中任务权重是 LoRA 适配器 和 用于非 SVD 变体,以及它们的乘积 用于 SVD 变体。DARE
方法在 语言模型是超级马里奥:从同源模型中吸收能力作为免费午餐中提出,首先根据指定的分数 1-density
随机修剪任务权重的值,然后通过 1/density
重新缩放修剪后的任务权重。DARE
是一个通用的插件,可以应用于任何现有的模型合并方法。我们已经使用线性/任务算术(*_linear*
)和 TIES(*_ties*
)实现了 DARE
。
对于 DARE
的 *_linear*
变体,我们首先使用 DARE
随机修剪任务权重,然后根据用户指定的参与 LoRA 适配器权重执行任务张量的加权和。
对于 DARE
的 *_ties*
变体,我们首先使用 DARE
获取修剪后的任务权重,然后采用 ties
的最后 2 个步骤,即计算多数符号掩码并使用该掩码执行任务权重的分离合并。
幅度修剪 (magnitude_prune
, magnitude_prune_svd
)
这也建立在 linear
和 svd
方法的基础上,其中任务权重是 LoRA 适配器 、(对于非 svd 变体)及其乘积 (对于 svd 变体)。在此方法中,您首先修剪任务权重的最小值,并根据指定的比例 density
保留前 k 个值。然后,您根据用户指定的参与 LoRA 适配器的权重执行任务张量的加权和。
如何合并我的 LoRA 适配器?
在 PEFT 中,使用 LoRA 时,您可以使用类方法 add_weighted_adapter()
尝试不同的组合方法。例如,下面您可以看到我们如何使用 ties
方法组合三个 LoRA 适配器,以及新合并适配器生成的输出。我们可以观察到合并后的适配器能够保留各个适配器的能力。
您可以在 PEFT 仓库的 示例 中找到上述示例。
让我们再举一个例子,如下图所示,使用 magnitude_prune
方法及其生成的输出。
现在,如果我们要使用合并后的适配器能力来回答印地语+英语(Hinglish)中与心理健康相关的查询呢?这将需要使用两个适配器的能力。下面我们可以看到查询“Sad feelings ko kaise dur kare?”(翻译:如何摆脱悲伤情绪?)的结果。当所有适配器都禁用并使用基础模型时,响应以“我是 AI”开头,然后是通用建议。当启用 Hinglish 适配器时,响应是 Hinglish 并且简短,遵循微调数据,但在提供具体的建议以帮助克服悲伤方面做得不好。当启用 mental_health 适配器时,响应类似于人类会说的话,但遗憾的是它不是 Hinglish。当启用合并适配器时,我们可以看到响应是 Hinglish 并且简短,同时提供了 mental_health 适配器响应中可以找到的具体建议,例如锻炼、与朋友共度时光、阅读、冥想和专注于积极思考。因此,我们可以观察到合并适配器可以结合它们的个体能力来支持新的用例。
最后,让我们以 dare_linear
为例,并检查生成的输出。
我们在 PEFT 中为这些合并方法提供了专门的开发者指南,您可以在此处找到。
扩展到文本到图像生成
在本节中,我们将向您展示如何利用这些合并方法通过 🤗 Diffusers 进行文本到图像生成。请注意,Diffusers 已经依赖 PEFT 进行所有 LoRA 相关操作,包括训练和推理。但是,目前,在 Diffusers 管道上调用 set_adapters()
时,无法利用新的合并方法。这就是为什么我们正在与社区公开讨论如何最好地在 Diffusers 内部原生支持它。
但多亏了 PEFT,总有一种方法可以规避这个问题。我们将为此使用 add_weighted_adapter()
功能。具体来说,我们将采取以下步骤来组合 “toy-face” LoRA 和 “Pixel-Art” LoRA,并尝试不同的合并技术
- 从这些 LoRA 检查点获取
PeftModel
。 - 使用
add_weighted_adapter()
方法和我们选择的合并方法合并PeftModel
。 - 将合并后的模型分配给底层
DiffusionPipeline
的相应组件。
让我们看看它的实际应用。下面各部分中显示的所有代码都来自 此 Colab Notebook。
由于这两个 LoRA 检查点都使用 SDXL UNet 作为其基础模型,我们首先加载 UNet
from diffusers import UNet2DConditionModel
import torch
unet = UNet2DConditionModel.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16,
use_safetensors=True,
variant="fp16",
subfolder="unet",
).to("cuda")
然后我们加载实际的 SDXL 管道和 LoRA 检查点。我们从“CiroN2022/toy-face” LoRA 开始
from diffusers import DiffusionPipeline
import copy
sdxl_unet = copy.deepcopy(unet)
pipe = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
variant="fp16",
torch_dtype=torch.float16,
unet=unet
).to("cuda")
pipe.load_lora_weights("CiroN2022/toy-face", weight_name="toy_face_sdxl.safetensors", adapter_name="toy")
现在,从加载的 LoRA 检查点获取 PeftModel
from peft import get_peft_model, LoraConfig
toy_peft_model = get_peft_model(
sdxl_unet,
pipe.unet.peft_config["toy"],
adapter_name="toy"
)
original_state_dict = {f"base_model.model.{k}": v for k, v in pipe.unet.state_dict().items()}
toy_peft_model.load_state_dict(original_state_dict, strict=True)
💡 您可以使用:toy_peft_model.push_to_hub("toy_peft_model", token=TOKEN)
选择性地将 toy_peft_model
推送到 Hub。
接下来,我们对“nerijs/pixel-art-xl” LoRA 做同样的事情
pipe.delete_adapters("toy")
sdxl_unet.delete_adapters("toy")
pipe.load_lora_weights("nerijs/pixel-art-xl", weight_name="pixel-art-xl.safetensors", adapter_name="pixel")
pipe.set_adapters(adapter_names="pixel")
pixel_peft_model = get_peft_model(
sdxl_unet,
pipe.unet.peft_config["pixel"],
adapter_name="pixel"
)
original_state_dict = {f"base_model.model.{k}": v for k, v in pipe.unet.state_dict().items()}
pixel_peft_model.load_state_dict(original_state_dict, strict=True)
现在,我们已经具备了加权适配器推理所需的一切!我们首先加载所有必需的东西
from peft import PeftModel
from diffusers import UNet2DConditionModel, DiffusionPipeline
import torch
base_unet = UNet2DConditionModel.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16,
use_safetensors=True,
variant="fp16",
subfolder="unet",
).to("cuda")
toy_id = "sayakpaul/toy_peft_model"
model = PeftModel.from_pretrained(base_unet, toy_id, use_safetensors=True, subfolder="toy", adapter_name="toy")
model.load_adapter("sayakpaul/pixel_peft_model", use_safetensors=True, subfolder="pixel", adapter_name="pixel")
现在,组合 LoRA 适配器——我们都期待的时刻!
model.add_weighted_adapter(
adapters=["toy", "pixel"],
weights=[0.7, 0.3],
combination_type="linear",
adapter_name="toy-pixel"
)
model.set_adapters("toy-pixel")
在这里,我们只是从“linear”合并策略开始,但将尝试其他异域合并算法,例如 TIES。我们最终将 model
分配给我们的 DiffusionPipeline
并执行推理
model = model.to(dtype=torch.float16, device="cuda")
pipe = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0", unet=model, variant="fp16", torch_dtype=torch.float16,
).to("cuda")
prompt = "toy_face of a hacker with a hoodie, pixel art"
image = pipe(prompt, num_inference_steps=30, generator=torch.manual_seed(0)).images[0]
image
让我们尝试 ties_svd
方法。您可以在此处找到示例 notebook。
pipe.unet.add_weighted_adapter(
["teapot","watercolour"],
[1.0, 1.0],
"merge",
combination_type="ties_svd",
density=0.5
)
现在,让我们尝试使用 dare_linear
组合两个风格 LoRA
model.add_weighted_adapter(
adapters=["toy", "pixel"],
weights=[1.0, 1.0],
combination_type="dare_linear",
adapter_name="merge",
density=0.7
)
现在,让我们尝试使用 majority_sign_method="frequency"
的 ties
方法
model.add_weighted_adapter(
adapters=["toy", "sticker"],
weights=[1.0, 1.0],
combination_type="ties",
adapter_name="merge",
density=0.5,
majority_sign_method="frequency"
)
观察
- 在大多数情况下,
cat
方法会给出很好的结果。所以,从它开始。但是,请注意,如果您组合许多适配器,由于连接,生成的合并适配器可能尺寸很大,导致 OOM。因此,在探索少数适配器时,cat
将是一个很好的起点。 - 如果您想探索或者
cat
不起作用,请按顺序尝试linear
、maginuted_prune
和dare_linear
。对于maginuted_prune
和dare_linear
,我们发现较高的density
值(约 0.7-0.8)效果更好。 - 在使用
ties
时,我们发现在许多情况下majority_sign_method="frequency"
的表现优于majority_sign_method="total"
(total
是当前的默认值)。对于ties
,density
的一个很好的默认值是 0.5。然后您可以根据合并适配器后的观察结果,尝试将其调低或调高。 dare_ties
没有给出好结果。- 在使用具有不同等级的 Stable Diffusion LoRA 适配器时,您可以尝试
*svd
系列方法。请注意,这些方法需要更多的 GPU 内存,并且由于昂贵的 SVD 操作,创建合并适配器大约需要 1.5 分钟。ties_svd
在组合subject
+style
LoRA 时给出了很好的结果,如上例所示。在组合 2 个style
适配器时,dare_linear
具有高density
或ties
具有majority_sign_method="frequency"
似乎效果更好,如上例所示。
致谢
我们非常感谢 DARE 和 TIES 的作者 Le Yu 和 Prateek Yadav 在 PR 中提供的宝贵反馈和指导。为了表彰他们的努力,我们已将他们列为 PR 的共同作者。感谢 Prateek 和 Le 审阅了博客草稿。
有用链接
- 使用任务算术编辑模型
- TIES-Merging:解决模型合并时的干扰
- 语言模型是超级马里奥:免费吸收同源模型的能力
- mergekit:用于合并预训练大型语言模型的工具。
- Diffusers 中的 PEFT 集成
- PEFT 用户的模型合并指南
引用
@inproceedings{
ilharco2023editing,
title={Editing models with task arithmetic},
author={Gabriel Ilharco and Marco Tulio Ribeiro and Mitchell Wortsman and Ludwig Schmidt and Hannaneh Hajishirzi and Ali Farhadi},
booktitle={The Eleventh International Conference on Learning Representations },
year={2023},
url={https://openreview.net/forum?id=6t0Kwf8-jrj}
}
@inproceedings{
yadav2023tiesmerging,
title={{TIES}-Merging: Resolving Interference When Merging Models},
author={Prateek Yadav and Derek Tam and Leshem Choshen and Colin Raffel and Mohit Bansal},
booktitle={Thirty-seventh Conference on Neural Information Processing Systems},
year={2023},
url={https://openreview.net/forum?id=xtaX3WyCj1}
}
@misc{yu2023language,
title={Language Models are Super Mario: Absorbing Abilities from Homologous Models as a Free Lunch},
author={Le Yu and Bowen Yu and Haiyang Yu and Fei Huang and Yongbin Li},
year={2023},
eprint={2311.03099},
archivePrefix={arXiv},
primaryClass={cs.CL}
}
@misc{
mergekit,
author = {Charles O. Goddard and contributors},
title = {mergekit},
year = {2023},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/arcee-ai/mergekit}}
}