PEFT 文档
PEFT 检查点格式
并获得增强的文档体验
开始使用
PEFT 检查点格式
本文档描述了 PEFT 检查点文件的结构以及如何在 PEFT 格式和其他格式之间进行转换。
PEFT 文件
PEFT(参数高效微调)方法仅更新模型参数的一个小子集,而不是全部参数。这很好,因为检查点文件通常比原始模型文件小得多,更易于存储和共享。然而,这也意味着要加载 PEFT 模型,您还需要有原始模型可用。
当你在 PEFT 模型上调用 save_pretrained() 时,PEFT 模型会保存以下三个文件:
adapter_model.safetensors
或adapter_model.bin
默认情况下,模型以 safetensors
格式保存,这是 bin
格式的一个安全替代方案,后者因在底层使用 pickle 工具而存在已知的安全漏洞。不过,两种格式都存储相同的 state_dict
,并且可以互换。
state_dict
只包含适配器模块的参数,不包含基础模型的参数。为了说明大小上的差异,一个普通的 BERT 模型需要约 420MB 的磁盘空间,而在这个 BERT 模型之上的 IA³ 适配器只需要约 260KB。
adapter_config.json
adapter_config.json
文件包含适配器模块的配置,这对于加载模型是必需的。以下是一个应用于 BERT 模型的 IA³ 适配器标准设置的 adapter_config.json
示例:
{
"auto_mapping": {
"base_model_class": "BertModel",
"parent_library": "transformers.models.bert.modeling_bert"
},
"base_model_name_or_path": "bert-base-uncased",
"fan_in_fan_out": false,
"feedforward_modules": [
"output.dense"
],
"inference_mode": true,
"init_ia3_weights": true,
"modules_to_save": null,
"peft_type": "IA3",
"revision": null,
"target_modules": [
"key",
"value",
"output.dense"
],
"task_type": null
}
配置文件包含:
- 存储的适配器模块类型,
"peft_type": "IA3"
- 关于基础模型的信息,如
"base_model_name_or_path": "bert-base-uncased"
- 模型的修订版本(如果有的话),
"revision": null
如果基础模型不是预训练的 Transformers 模型,后两个条目将为 null
。除此之外,这些设置都与用于微调模型的特定 IA³ 适配器相关。
README.md
生成的 README.md
是 PEFT 模型的模型卡,其中包含一些预填充的条目。其目的是为了更容易地与他人共享模型,并提供一些关于模型的基本信息。加载模型时不需要此文件。
转换为 PEFT 格式
当从其他格式转换为 PEFT 格式时,我们需要 adapter_model.safetensors
(或 adapter_model.bin
)文件和 adapter_config.json
文件。
adapter_model
对于模型权重,为了让 PEFT 能加载文件,使用正确的参数名称到值的映射非常重要。正确获取此映射需要检查实现细节,因为 PEFT 适配器没有普遍认可的格式。
幸运的是,对于常见的基础情况,弄清楚这个映射并不太复杂。让我们看一个具体的例子,LoraLayer
# showing only part of the code
class LoraLayer(BaseTunerLayer):
# All names of layers that may contain (trainable) adapter weights
adapter_layer_names = ("lora_A", "lora_B", "lora_embedding_A", "lora_embedding_B")
# All names of other parameters that may contain adapter-related parameters
other_param_names = ("r", "lora_alpha", "scaling", "lora_dropout")
def __init__(self, base_layer: nn.Module, **kwargs) -> None:
self.base_layer = base_layer
self.r = {}
self.lora_alpha = {}
self.scaling = {}
self.lora_dropout = nn.ModuleDict({})
self.lora_A = nn.ModuleDict({})
self.lora_B = nn.ModuleDict({})
# For Embedding layer
self.lora_embedding_A = nn.ParameterDict({})
self.lora_embedding_B = nn.ParameterDict({})
# Mark the weight as unmerged
self._disable_adapters = False
self.merged_adapters = []
self.use_dora: dict[str, bool] = {}
self.lora_magnitude_vector: Optional[torch.nn.ParameterDict] = None # for DoRA
self._caches: dict[str, Any] = {}
self.kwargs = kwargs
在 PEFT 中所有 LoraLayer
类使用的 __init__
代码中,有许多用于初始化模型的参数,但只有少数与检查点文件相关:lora_A
、lora_B
、lora_embedding_A
和 lora_embedding_B
。这些参数在类属性 adapter_layer_names
中列出,并包含可学习的参数,因此它们必须包含在检查点文件中。所有其他参数,如秩 r
,都从 adapter_config.json
派生,并且必须包含在那里(除非使用默认值)。
让我们检查一个应用于 BERT 的 PEFT LoRA 模型的 state_dict
。使用默认 LoRA 设置打印前五个键(其余键相同,只是层号不同),我们得到:
base_model.model.encoder.layer.0.attention.self.query.lora_A.weight
base_model.model.encoder.layer.0.attention.self.query.lora_B.weight
base_model.model.encoder.layer.0.attention.self.value.lora_A.weight
base_model.model.encoder.layer.0.attention.self.value.lora_B.weight
base_model.model.encoder.layer.1.attention.self.query.lora_A.weight
- 等等。
让我们来分析一下:
- 默认情况下,对于 BERT 模型,LoRA 会应用于注意力模块的 `query` 和 `value` 层。这就是为什么你会在每一层的键名中看到 `attention.self.query` 和 `attention.self.value`。
- LoRA 将权重分解为两个低秩矩阵,
lora_A
和lora_B
。这就是键名中lora_A
和lora_B
的来源。 - 这些 LoRA 矩阵被实现为
nn.Linear
层,因此参数存储在.weight
属性中(lora_A.weight
,lora_B.weight
)。 - 默认情况下,LoRA 不应用于 BERT 的嵌入层,因此没有
lora_A_embedding
和lora_B_embedding
的条目。 state_dict
的键总是以"base_model.model."
开头。原因是在 PEFT 中,我们将基础模型包装在一个特定于微调器的模型(本例中为LoraModel
)中,该模型本身又被包装在一个通用的 PEFT 模型(PeftModel
)中。因此,这些键会加上这两个前缀。在转换为 PEFT 格式时,必须添加这些前缀。
最后一点对于像提示调整这样的前缀调整技术并不适用。在那里,额外的嵌入直接存储在 `state_dict` 中,键名没有添加任何前缀。
在检查加载的模型中的参数名称时,你可能会惊讶地发现它们看起来有点不同,例如 base_model.model.encoder.layer.0.attention.self.query.lora_A.default.weight
。不同之处在于倒数第二段中的 .default
部分。这部分存在是因为 PEFT 通常允许一次性添加多个适配器(使用 nn.ModuleDict
或 nn.ParameterDict
来存储它们)。例如,如果你添加了另一个名为“other”的适配器,那么该适配器的键将是 base_model.model.encoder.layer.0.attention.self.query.lora_A.other.weight
。
当你调用 save_pretrained() 时,适配器名称会从键中剥离。原因在于适配器名称并不是模型架构的重要部分;它只是一个任意的名称。加载适配器时,你可以选择一个完全不同的名称,模型仍然会以同样的方式工作。这就是为什么适配器名称不存储在检查点文件中的原因。
如果你调用 save_pretrained("some/path")
并且适配器名称不是 "default"
,那么适配器将存储在与适配器同名的子目录中。因此,如果名称是“other”,它将被存储在 some/path/other
中。
在某些情况下,决定向检查点文件中添加哪些值可能会变得更复杂。例如,在 PEFT 中,DoRA 是作为 LoRA 的一种特殊情况实现的。如果你想将 DoRA 模型转换为 PEFT,你应该创建一个 LoRA 检查点,并为 DoRA 添加额外的条目。你可以在之前的 LoraLayer
代码的 __init__
中看到这一点:
self.lora_magnitude_vector: Optional[torch.nn.ParameterDict] = None # for DoRA
这表示对于 DoRA,每一层都有一个可选的额外参数。
adapter_config
加载 PEFT 模型所需的其他所有信息都包含在 adapter_config.json
文件中。让我们检查一个应用于 BERT 的 LoRA 模型的这个文件:
{
"alpha_pattern": {},
"auto_mapping": {
"base_model_class": "BertModel",
"parent_library": "transformers.models.bert.modeling_bert"
},
"base_model_name_or_path": "bert-base-uncased",
"bias": "none",
"fan_in_fan_out": false,
"inference_mode": true,
"init_lora_weights": true,
"layer_replication": null,
"layers_pattern": null,
"layers_to_transform": null,
"loftq_config": {},
"lora_alpha": 8,
"lora_dropout": 0.0,
"megatron_config": null,
"megatron_core": "megatron.core",
"modules_to_save": null,
"peft_type": "LORA",
"r": 8,
"rank_pattern": {},
"revision": null,
"target_modules": [
"query",
"value"
],
"task_type": null,
"use_dora": false,
"use_rslora": false
}
这包含了很多条目,乍一看,要弄清楚所有该填入的正确值可能会让人感到不知所措。然而,大多数条目对于加载模型来说并不是必需的。这要么是因为它们使用了默认值而不需要添加,要么是因为它们只影响 LoRA 权重的初始化,而这在加载模型时是无关紧要的。如果你发现你不知道某个特定参数的作用,比如 "use_rslora",
不要添加它,通常不会有问题。另外请注意,随着未来添加更多选项,这个文件中的条目会越来越多,但它应该是向后兼容的。
至少,您应该包含以下条目:
{
"target_modules": ["query", "value"],
"peft_type": "LORA"
}
然而,建议尽可能多地添加条目,比如秩 `r` 或 `base_model_name_or_path`(如果它是一个 Transformers 模型)。这些信息可以帮助他人更好地理解和分享模型。要检查期望的键和值,请查看 PEFT 源代码中的 config.py 文件(例如,这是 LoRA 的配置文件)。
模型存储
在某些情况下,您可能希望存储整个 PEFT 模型,包括基础权重。例如,如果尝试加载 PEFT 模型的用户无法访问基础模型,这可能是必要的。您可以先合并权重或将其转换为 Transformer 模型。
合并权重
存储整个 PEFT 模型最直接的方法是将适配器权重合并到基础权重中:
merged_model = model.merge_and_unload() merged_model.save_pretrained(...)
然而,这种方法也有一些缺点:
- 一旦调用 merge_and_unload(),你将得到一个没有任何 PEFT 特定功能的基础模型。这意味着你不能再使用任何 PEFT 特定的方法。
- 您将无法取消合并权重、一次性加载多个适配器、禁用适配器等。
- 并非所有 PEFT 方法都支持合并权重。
- 有些 PEFT 方法可能通常允许合并,但在特定设置下不支持(例如,当使用某些量化技术时)。
- 整个模型将比 PEFT 模型大得多,因为它也将包含所有的基础权重。
但是,使用合并后的模型进行推理应该会稍快一些。
转换为 Transformers 模型
另一种保存整个模型的方法是,假设基础模型是 Transformers 模型,可以使用这种取巧的方法直接将 PEFT 权重插入到基础模型中并保存,这只有在“欺骗”Transformers 让它相信 PEFT 模型不是 PEFT 模型时才有效。这只适用于 LoRA,因为其他适配器在 Transformers 中没有实现。
model = ... # the PEFT model
...
# after you finish training the model, save it in a temporary location
model.save_pretrained(<temp_location>)
# now load this model directly into a transformers model, without the PEFT wrapper
# the PEFT weights are directly injected into the base model
model_loaded = AutoModel.from_pretrained(<temp_location>)
# now make the loaded model believe that it is _not_ a PEFT model
model_loaded._hf_peft_config_loaded = False
# now when we save it, it will save the whole model
model_loaded.save_pretrained(<final_location>)
# or upload to Hugging Face Hub
model_loaded.push_to_hub(<final_location>)