PEFT documentation

PEFT 检查点格式

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v0.21.0).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

PEFT 检查点格式

本文档介绍了 PEFT 检查点文件的结构,以及如何将 PEFT 格式与其他格式进行相互转换。

PEFT 文件

PEFT(参数高效微调)方法仅更新模型参数的一小部分,而非全部参数。这很好,因为检查点文件通常比原始模型文件小得多,更易于存储和共享。然而,这也意味着要加载 PEFT 模型,您还需要拥有原始模型。

当您对 PEFT 模型调用 save_pretrained() 时,PEFT 模型会保存以下三个文件:

  1. adapter_model.safetensors 或 adapter_model.bin

默认情况下,模型以 safetensors 格式保存,这是一种 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_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 格式时,必须添加这些前缀。

最后一点对于提示微调(prompt tuning)等前缀微调技术并不成立。在这些技术中,额外的嵌入会直接存储在 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,则应创建一个包含 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"
}

不过,建议尽可能多地添加条目,例如秩 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 模型时才有效。由于其他适配器未在 Transformers 中实现,此方法仅适用于 LoRA。

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>)
Update on GitHub