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>)