LoRA
LoRA 是一种低秩分解方法,用于减少可训练参数的数量,这加快了大型模型的微调速度并使用更少的内存。在 PEFT 中,使用 LoRA 就像设置 LoraConfig 并使用 get_peft_model() 包装它以创建可训练的 PeftModel 一样简单。
本指南更详细地探讨了使用 LoRA 的其他选项和功能。
初始化
LoRA 权重的初始化由 LoraConfig 中的参数 init_lora_weights
控制。默认情况下,PEFT 使用 Kaiming-uniform 初始化 LoRA 权重 A,并使用零初始化权重 B,从而导致恒等变换(与参考 实现 相同)。
还可以传递 init_lora_weights="gaussian"
。顾名思义,这将用高斯分布初始化权重 A,并将权重 B 初始化为零(这就是 Diffusers 初始化 LoRA 权重的方式)。
from peft import LoraConfig
config = LoraConfig(init_lora_weights="gaussian", ...)
还可以选择设置 init_lora_weights=False
,这对调试和测试很有用。这应该是你使用此选项的唯一情况。选择此选项时,LoRA 权重将被初始化,以确保它们不会导致恒等变换。
from peft import LoraConfig
config = LoraConfig(init_lora_weights=False, ...)
PiSSA
PiSSA 使用主奇异值和奇异向量初始化 LoRA 适配器。这种简单的修改使 PiSSA 比 LoRA 收敛更快,最终获得更好的性能。此外,PiSSA 减少了与 QLoRA 相比的量化误差,从而进一步增强了性能。
将初始化方法配置为“pissa”,这可能需要几分钟才能在预训练模型上执行 SVD。
from peft import LoraConfig
config = LoraConfig(init_lora_weights="pissa", ...)
或者,执行快速 SVD,这只需要几秒钟。迭代次数决定了误差和计算时间之间的权衡。
lora_config = LoraConfig(init_lora_weights="pissa_niter_[number of iters]", ...)
有关使用 PiSSA 的详细说明,请按照 这些说明。
OLoRA
OLoRA 利用 QR 分解来初始化 LoRA 适配器。OLoRA 通过其 QR 分解的因子来转换模型的基本权重,即它在执行任何训练之前会改变权重。这种方法显著提高了稳定性,加速了收敛速度,最终实现了更好的性能。
你只需要传递一个额外的选项来使用 OLoRA。
from peft import LoraConfig
config = LoraConfig(init_lora_weights="olora", ...)
有关更高级的用法,请参考我们的 文档。
LoftQ
标准方法
在对 QLoRA 训练的基本模型进行量化时,请考虑使用 LoftQ 初始化,该方法已被证明可以提高训练量化模型的性能。其理念是,LoRA 权重被初始化,以使量化误差最小化。要使用 LoftQ,请按照 这些说明。
一般来说,为了使 LoftQ 能够获得最佳效果,建议尽可能多地使用 LoRA 针对层,因为没有针对的层无法应用 LoftQ。这意味着传递 LoraConfig(..., target_modules="all-linear")
最有可能获得最佳结果。此外,在使用 4 位量化时,你应该在量化配置中使用 nf4
作为量化类型,即 BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4")
。
更便捷的方式
一种更简单但更受限的应用 LoftQ 初始化的方法是使用便捷函数 replace_lora_weights_loftq
。它接受量化的 PEFT 模型作为输入,并用其 LoftQ 初始化的对应项就地替换 LoRA 权重。
from peft import replace_lora_weights_loftq
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(load_in_4bit=True, ...)
base_model = AutoModelForCausalLM.from_pretrained(..., quantization_config=bnb_config)
# note: don't pass init_lora_weights="loftq" or loftq_config!
lora_config = LoraConfig(task_type="CAUSAL_LM")
peft_model = get_peft_model(base_model, lora_config)
replace_lora_weights_loftq(peft_model)
replace_lora_weights_loftq
还允许你传递一个 callback
参数,让你能够更好地控制哪些层应该被修改或不应该被修改,这在经验上可以显著提高结果。要查看更详细的示例,请查看 此笔记本。
replace_lora_weights_loftq
只实现 LoftQ 的一个迭代步骤。这意味着只有 LoRA 权重被更新,而不是迭代更新 LoRA 权重和量化的基本模型权重。这可能会导致性能降低,但它具有以下优点:我们可以使用从基本模型派生的原始量化权重,而不是必须保留修改后的量化权重的额外副本。这种权衡是否值得取决于具体情况。
目前,replace_lora_weights_loftq
具有以下其他限制:
- 模型文件必须存储为
safetensors
文件。 - 仅支持 bitsandbytes 4 位量化。
在 量化 指南中了解更多有关 PEFT 如何与量化一起工作的信息。
秩稳定的 LoRA
初始化 LoraConfig 的另一种方法是使用 秩稳定的 LoRA (rsLoRA) 方法。LoRA 架构在每次前向传递过程中都会通过一个固定的标量缩放每个适配器,该标量在初始化时设置,并取决于秩 r
。标量在原始实现中由 lora_alpha/r
给出,但 rsLoRA 使用 lora_alpha/math.sqrt(r)
,它可以稳定适配器并提高使用更高 r
所带来的性能潜力。
from peft import LoraConfig
config = LoraConfig(use_rslora=True, ...)
权重分解低秩适应(DoRA)
这种技术将权重更新分解为两个部分:幅度和方向。方向由普通的 LoRA 处理,而幅度由一个单独的可学习参数处理。这可以提高 LoRA 的性能,特别是在低秩情况下。有关 DoRA 的更多信息,请参阅 https://arxiv.org/abs/2402.09353。
from peft import LoraConfig
config = LoraConfig(use_dora=True, ...)
如果模型的部分或 DoRA 适配器被卸载到 CPU,你可以通过在 config.runtime_config
中使用 ephemeral_gpu_offload=True
来实现显著的加速,代价是会带来一些临时的(短暂的)VRAM 开销。
from peft import LoraConfig, LoraRuntimeConfig
config = LoraConfig(use_dora=True, runtime_config=LoraRuntimeConfig(ephemeral_gpu_offload=True), ...)
具有 DoRA 适配器的 PeftModel
也可以使用 from_pretrained
方法以及 load_adapter
方法加载,并使用 ephemeral_gpu_offload=True
标志。
from peft import PeftModel
model = PeftModel.from_pretrained(base_model, peft_model_id, ephemeral_gpu_offload=True)
注意事项
- 目前,DoRA 仅支持线性层和 Conv2d 层。
- DoRA 引入的开销比纯 LoRA 更大,因此建议将权重合并以进行推理,请参阅 LoraModel.merge_and_unload()。
- DoRA 应该适用于使用 bitsandbytes 量化的权重 (“QDoRA”)。但是,在将 QDoRA 与 DeepSpeed Zero2 结合使用时,已报告了一些问题。
QLoRA 风格训练
PEFT 中的默认 LoRA 设置将可训练权重添加到每个注意力块的查询层和值层。但 QLoRA 将可训练权重添加到 Transformer 模型的所有线性层,可以提供与完全微调模型相同的性能。要将 LoRA 应用于所有线性层,例如在 QLoRA 中,请设置 target_modules="all-linear"
(比根据名称指定单个模块更容易,单个模块名称可能因架构而异)。
config = LoraConfig(target_modules="all-linear", ...)
内存高效的 LoRA 层复制
一种用于提高模型性能的方法是通过复制模型中的层来扩展模型,从而从给定大小的预训练模型构建一个更大的模型。例如,将 7B 模型扩展到 10B 模型,如 SOLAR 论文中所述。PEFT LoRA 以内存高效的方式支持这种扩展,这种方式支持使用附加到层复制后的层的 LoRA 适配器进行进一步的微调。复制的层不会占用额外的内存,因为它们共享底层权重,因此唯一需要的额外内存是适配器权重的内存。要使用此功能,您可以使用 layer_replication
参数创建一个配置。
config = LoraConfig(layer_replication=[[0,4], [2,5]], ...)
假设原始模型有 5 层 [0, 1, 2 ,3, 4]
,这将创建一个具有 7 层的模型,排列方式为 [0, 1, 2, 3, 2, 3, 4]
。这遵循 mergekit 的通过合并约定,其中指定为起始包含和结束排除元组的层序列被堆叠以构建最终模型。最终模型中的每个层都获得一组独特的 LoRA 适配器。
Fewshot-Metamath-OrcaVicuna-Mistral-10B 是一个使用此方法训练的模型示例,它将 Mistral-7B 扩展到 10B。 adapter_config.json 展示了应用此方法进行微调的示例 LoRA 适配器配置。
优化器
LoRA 训练可以选择包含专用优化器。目前唯一的这种优化器是 LoRA+。
LoRA+ 优化 LoRA
可以使用 LoRA+ 来优化 LoRA 训练,LoRA+ 为适配器矩阵 A 和 B 使用不同的学习率,已证明可以将微调速度提高高达 2 倍,并将性能提高 1-2%。
from peft import LoraConfig, get_peft_model
from peft.optimizers import create_loraplus_optimizer
from transformers import Trainer
import bitsandbytes as bnb
base_model = ...
config = LoraConfig(...)
model = get_peft_model(base_model, config)
optimizer = create_loraplus_optimizer(
model=model,
optimizer_cls=bnb.optim.Adam8bit,
lr=5e-5,
loraplus_lr_ratio=16,
)
scheduler = None
...
trainer = Trainer(
...,
optimizers=(optimizer, scheduler),
)
将 LoRA 权重合并到基础模型中
虽然 LoRA 的训练速度明显更快,而且规模更小,但在推理过程中,由于单独加载基础模型和 LoRA 适配器,您可能会遇到延迟问题。要消除延迟,请使用 merge_and_unload() 函数将适配器权重与基础模型合并。这使您可以将新合并的模型用作独立模型。 merge_and_unload() 函数不会将适配器权重保留在内存中。
以下图表解释了 LoRA 适配器合并的直觉
我们在下面的代码片段中展示了如何使用 PEFT 运行它。
from transformers import AutoModelForCausalLM
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1")
peft_model_id = "alignment-handbook/zephyr-7b-sft-lora"
model = PeftModel.from_pretrained(base_model, peft_model_id)
model.merge_and_unload()
如果您需要保留权重的副本,以便以后取消合并适配器或删除并加载不同的适配器,您应该使用 merge_adapter() 函数。现在,您可以使用 unmerge_adapter() 来返回基础模型。
from transformers import AutoModelForCausalLM
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1")
peft_model_id = "alignment-handbook/zephyr-7b-sft-lora"
model = PeftModel.from_pretrained(base_model, peft_model_id)
model.merge_adapter()
# unmerge the LoRA layers from the base model
model.unmerge_adapter()
add_weighted_adapter() 函数对于根据 weights
参数中用户提供的加权方案将多个 LoRA 合并到新的适配器中非常有用。下面是一个端到端的示例。
首先加载基础模型
from transformers import AutoModelForCausalLM
from peft import PeftModel
import torch
base_model = AutoModelForCausalLM.from_pretrained(
"mistralai/Mistral-7B-v0.1", torch_dtype=torch.float16, device_map="auto"
)
然后我们加载第一个适配器
peft_model_id = "alignment-handbook/zephyr-7b-sft-lora"
model = PeftModel.from_pretrained(base_model, peft_model_id, adapter_name="sft")
然后加载不同的适配器并将其与第一个适配器合并
weighted_adapter_name = "sft-dpo"
model.load_adapter("alignment-handbook/zephyr-7b-dpo-lora", adapter_name="dpo")
model.add_weighted_adapter(
adapters=["sft", "dpo"],
weights=[0.7, 0.3],
adapter_name=weighted_adapter_name,
combination_type="linear"
)
model.set_adapter(weighted_adapter_name)
有几种支持的 combination_type
方法。有关更多详细信息,请参阅 文档。请注意,当使用 torch.float16
或 torch.bfloat16
作为数据类型时,不支持 combination_type
为“svd”。
现在,执行推理
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1")
prompt = "Hey, are you conscious? Can you talk to me?"
inputs = tokenizer(prompt, return_tensors="pt")
inputs = {k: v.to("cuda") for k, v in inputs.items()}
with torch.no_grad():
generate_ids = model.generate(**inputs, max_length=30)
outputs = tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
print(outputs)
加载适配器
可以使用 load_adapter() 将适配器加载到预训练模型上,这对于尝试权重未合并的不同适配器很有用。使用 set_adapter() 函数设置活动适配器权重。
from transformers import AutoModelForCausalLM
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1")
peft_model_id = "alignment-handbook/zephyr-7b-sft-lora"
model = PeftModel.from_pretrained(base_model, peft_model_id)
# load different adapter
model.load_adapter("alignment-handbook/zephyr-7b-dpo-lora", adapter_name="dpo")
# set adapter as active
model.set_adapter("dpo")
要返回基础模型,您可以使用 unload() 来卸载所有 LoRA 模块,或者使用 delete_adapter() 来完全删除适配器。
# unload adapter
model.unload()
# delete adapter
model.delete_adapter("dpo")
在同一个批次中使用不同的 LoRA 适配器进行推理
通常,每个推理批次必须在 PEFT 中使用相同的适配器。这有时会很烦人,因为我们可能会有包含打算使用不同 LoRA 适配器的样本的批次。例如,我们可能有一个在英语中表现良好的基础模型,以及两个额外的 LoRA 适配器,一个用于法语,另一个用于德语。通常,我们必须拆分批次,以使每个批次只包含一种语言的样本,我们不能在同一个批次中组合不同的语言。
值得庆幸的是,可以使用 adapter_name
参数在同一个批次中混合不同的 LoRA 适配器。下面,我们展示了如何在实践中实现这一点的示例。首先,让我们加载基础模型、英语和两个适配器,法语和德语,如下所示
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
model_id = ...
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id)
# load the LoRA adapter for French
peft_model = PeftModel.from_pretrained(model, <path>, adapter_name="adapter_fr")
# next, load the LoRA adapter for German
peft_model.load_adapter(<path>, adapter_name="adapter_de")
现在,我们希望在包含所有三种语言的样本上生成文本:前三个样本是英文,接下来的三个是法文,最后三个是德文。我们可以使用adapter_names
参数来指定每个样本使用哪个适配器。由于我们的基础模型用于英文,因此我们对这些样本使用特殊字符串"__base__"
。对于接下来的三个样本,我们指定法语 LoRA 微调的适配器名称,在本例中为"adapter_fr"
。对于最后三个样本,我们指定德语 LoRA 微调的适配器名称,在本例中为"adapter_de"
。这样,我们就可以在一个批次中使用基础模型和两个适配器。
inputs = tokenizer(
[
"Hello, my dog is cute",
"Hello, my cat is awesome",
"Hello, my fish is great",
"Salut, mon chien est mignon",
"Salut, mon chat est génial",
"Salut, mon poisson est super",
"Hallo, mein Hund ist süß",
"Hallo, meine Katze ist toll",
"Hallo, mein Fisch ist großartig",
],
return_tensors="pt",
padding=True,
)
adapter_names = [
"__base__", "__base__", "__base__",
"adapter_fr", "adapter_fr", "adapter_fr",
"adapter_de", "adapter_de", "adapter_de",
]
output = peft_model.generate(**inputs, adapter_names=adapter_names, max_new_tokens=20)
请注意,这里的顺序无关紧要,也就是说,批次中的样本不需要像上面示例那样按适配器分组。我们只需要确保adapter_names
参数与样本正确对齐。
此外,相同的方法也适用于modules_to_save
功能,该功能允许跨不同的 LoRA 适配器保存和重用特定的神经网络层,例如用于分类任务的自定义头部。
注意事项
使用此功能有一些缺点,即
- 它仅适用于推理,不适用于训练。
- 使用
with model.disable_adapter()
上下文禁用适配器优先于adapter_names
。 - 当一些适配器权重使用
merge_adapter
方法与基础权重合并时,您不能传递adapter_names
。请首先通过调用model.unmerge_adapter()
取消合并所有适配器。 - 出于显而易见的原因,这在调用
merge_and_unload()
之后不能使用,因为在这种情况下,所有 LoRA 适配器都将合并到基础权重中。 - 此功能目前不适用于 DoRA,因此如果您想使用它,请在您的
LoraConfig
中设置use_dora=False
。 modules_to_save
功能目前仅支持类型为Linear
、Embedding
、Conv2d
和Conv1d
的层。- 使用
adapter_names
进行推理预计会有开销,尤其是当批次中不同适配器的数量很多时。这是因为批次大小实际上减少到每个适配器的样本数量。如果运行时性能是您的首要任务,请尝试以下操作