PEFT 文档

PEFT 检查点格式

Hugging Face's logo
加入 Hugging Face 社区

并获得增强型文档体验

开始使用

PEFT 检查点格式

本文档描述了 PEFT 的检查点文件是如何结构化的,以及如何在 PEFT 格式和其他格式之间进行转换。

PEFT 文件

PEFT(参数高效微调)方法只更新模型参数的一个小子集,而不是所有参数。这样做的好处是,检查点文件通常比原始模型文件小得多,并且更容易存储和共享。但是,这也意味着要加载 PEFT 模型,您还需要原始模型可用。

当您在 PEFT 模型上调用 save_pretrained() 时,PEFT 模型会保存三个文件,如下所述

  1. adapter_model.safetensorsadapter_model.bin

默认情况下,模型以 safetensors 格式保存,它是 bin 格式的安全替代方案,bin 格式已知容易受到 安全漏洞 的影响,因为它在底层使用了 pickle 实用程序。但是,这两种格式都存储相同的 state_dict,并且可以互换使用。

state_dict 仅包含适配器模块的参数,不包含基础模型的参数。为了说明尺寸差异,一个普通的 BERT 模型需要约 420MB 的磁盘空间,而在此 BERT 模型之上添加的 IA³ 适配器仅需要约 260KB。

  1. 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³ 适配器相关。

  1. 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_Alora_Blora_embedding_Alora_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 应用于注意力模块的 queryvalue 层。这就是为什么在每层的键名中看到 attention.self.queryattention.self.value 的原因。
  • LoRA 将权重分解为两个低秩矩阵,lora_Alora_B。这就是键名中 lora_Alora_B 的来源。
  • 这些 LoRA 矩阵实现为 nn.Linear 层,因此参数存储在 .weight 属性中(lora_A.weightlora_B.weight)。
  • 默认情况下,LoRA 不应用于 BERT 的嵌入层,因此没有lora_A_embeddinglora_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.ModuleDictnn.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,则应为 DoRA 创建一个带有额外条目的 LoRA 检查点。您可以在前面 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"
}

但是,建议尽可能多地添加条目,例如秩 rbase_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>)
< > 更新 在 GitHub 上