參數高效微調PEFT(三)快速入門LoRA、AdaLoRA
- 我們已經了解了HuggingFace中peft庫的幾種高效微調方法。
參數高效微調PEFT(一)快速入門BitFit、Prompt Tuning、Prefix Tuning
參數高效微調PEFT(二)快速入門P-Tuning、P-Tuning V2
- 今天我們繼續了解大火的高效微調方法LoRA以及改進的AdaLoRA。
- 另外,QLoRA是模型量化 (Quantilization) 與LoRA的結合,我們會在后面低精度微調時(微調Chat GLM3的7B模型)進行介紹。
注意:我們到目前都是以單精度FP32加載模型,模型本身占用的顯存大小并沒有改變。
1 LoRA
- 將小型網絡模塊添加到PLMs中,保持基礎模型保持不變的情況下僅針對每個任務微調這些模塊,可以用于所有任務。
- 這樣,只需引入和更新少量任務特定的參數,就可以適配下游的任務,大大提高了預訓練模型的實用性。如:Adapter tuning、Prefix tuning、Prompt Tuning等。
- 這類方法雖然大大減少了內存消耗,但是這些方法存在一些問題。
- 比如:Adapter tuning引入了推理延時;
- Prefix tuning或Prompt tuning直接優化Prefix和Prompt是非單調的,比較難收斂,并且消耗了輸入的token。
- LoRA訓練完成后,可以將兩個低秩矩陣與原始模型中的權重進行合并,合并后的模型與原始模型無異,
避免了推理期間Prompt系列方法帶來的額外計算量
。
1.1 LoRA簡介
-
論文地址:LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS (2106)
-
LORA的核心思想就是通過低秩分解來模擬參數的改變量,從而以極小的參數量來實現大模型的間接訓練。
- 預訓練模型中存在一個極小的內在維度,這個內在維度是發揮核心作用的地方。
- 在微調的過程中,權重的更新依然也有如此特點,即也存在一個內在維度 (內在秩)。
-
如下圖所示,可訓練層維度和預訓練模型層維度一致為d,先將維度d通過全連接層降維至r,再從r通過全連接層映射回d維度。其中,r<<d,r是矩陣的秩,這樣矩陣計算就從 d × d d × d d×d變為 d × r + r × d d × r + r × d d×r+r×d,參數量減少很多。
- 在下游任務訓練時,固定模型的其他參數,只優化新增的兩個矩陣的權重參數,將PLM跟新增的通路兩部分的結果加起來作為最終的結果(兩邊通路的輸入、輸出維度都是一致的),即 h = W x + B A x h=Wx+BAx h=Wx+BAx。
- 第一個矩陣的A的權重參數會通過高斯函數初始化(注意:A矩陣不能初始化為零矩陣);
- 第二個矩陣的B的權重參數則會初始化為零矩陣,這樣能保證訓練開始時新增的通路 B A = 0 BA=0 BA=0,從而對模型結果沒有影響。
-
訓練完成后,可以將兩個低秩矩陣與原始模型中的權重進行合并,合并后的模型與原始模型無異,
避免了推理期間Prompt系列方法帶來的額外計算量
。 -
我們知道Transformer的權重矩陣包括Attention模塊里用于計算query,key,value的Wq、Wk、Wv、多頭attention的Wo以及MLP層的權重矩陣。如下圖,LoRA只應用于Attention模塊中的4種權重矩陣,而且通過消融實驗發現同時調整 Wq 和 Wv 會產生最佳結果。
- 如下圖所示,可以發現保證權重矩陣的種類的數量比起增加隱藏層維度r更為重要,增加r并不一定能覆蓋更加有意義的子空間。
- 通常情況下,r選擇為4,8,16即可,peft庫中默認為8。
- 在眾多數據集上LoRA在只訓練極少量參數的前提下,最終在性能上能和全量微調匹配,甚至在某些任務上優于全量微調。
1.2 LoRA源碼分析
我們在peft庫默認配置下(LoraConfig(task_type=TaskType.CAUSAL_LM)
),進行源碼分析。
from peft import LoraConfig, TaskType, get_peft_model
config = LoraConfig(task_type=TaskType.CAUSAL_LM)
model = get_peft_model(model, config)
LoRA的代碼主要分為三個部分:初始化、推理和參數合并。
1.2.1 LoRA初始化分析
我們先看下LoRA初始化,默認設置下,LoRA為線性層class Linear(nn.Linear, LoRALayer)
。
-
LoRA僅支持nn.Linear、nn.Embedding和nn.Conv2d,微調線性層是常見的做法。
-
通過MAPPING找到PeftModelForCausalLM進行初始化
# peft\mapping.pydef get_peft_model(model: PreTrainedModel, peft_config: PeftConfig, adapter_name: str = "default") -> PeftModel:......# 1、判斷任務類型 如果不是['SEQ_CLS', 'SEQ_2_SEQ_LM', 'CAUSAL_LM', 'TOKEN_CLS', 'QUESTION_ANS', 'FEATURE_EXTRACTION']里類型 而且 不是prompt_learningif peft_config.task_type not in MODEL_TYPE_TO_PEFT_MODEL_MAPPING.keys() and not peft_config.is_prompt_learning:return PeftModel(model, peft_config, adapter_name=adapter_name)# 2、prompt_learningif peft_config.is_prompt_learning:peft_config = _prepare_prompt_learning_config(peft_config, model_config)# 3、通過mapping找到peft.peft_model.PeftModelForCausalLMreturn MODEL_TYPE_TO_PEFT_MODEL_MAPPING[peft_config.task_type](model, peft_config, adapter_name=adapter_name)
- 然后我們會進入PeftModelForCausalLM的初始化,此時會調用父類PeftModel的初始化方法
# peft\peft_model.py
class PeftModelForCausalLM(PeftModel):def __init__(self, model, peft_config: PeftConfig, adapter_name="default"):# 調用父類PeftModel的初始化方法super().__init__(model, peft_config, adapter_name)self.base_model_prepare_inputs_for_generation = self.base_model.prepare_inputs_for_generation......
- 父類PeftModel的初始化中,會進行LoraModel的初始化;在LoraModel中,會進行父類BaseTuner的初始化
# peft\peft_model.py
class PeftModel(PushToHubMixin, torch.nn.Module):def __init__(self, model: PreTrainedModel, peft_config: PeftConfig, adapter_name: str = "default"):super().__init__()self.base_model = modelself.config = getattr(self.base_model, "config", {"model_type": "custom"})self.modules_to_save = Noneself.peft_config = {}self.active_adapter = adapter_nameself.peft_type = peft_config.peft_typeif not peft_config.is_prompt_learning:# 不是prompt_learning,我們進入此分支self.peft_config[adapter_name] = peft_config# 通過PEFT_TYPE_TO_MODEL_MAPPING我們獲取peft.tuners.lora.LoraModel# 然后會進入LoraModel的初始化self.base_model = PEFT_TYPE_TO_MODEL_MAPPING[peft_config.peft_type](self.base_model, self.peft_config, adapter_name)self.set_additional_trainable_modules(peft_config, adapter_name)else:self.add_adapter(adapter_name, peft_config)......# peft\tuners\lora.py
class LoraModel(BaseTuner):def __init__(self, model, config, adapter_name) -> None:# 進行BaseTuner的初始化super().__init__(model, config, adapter_name) .....
- 在父類BaseTuner中,會調用inject_adapter方法。
- inject_adapter中遍歷每一個key,調用_create_and_replace,
- target默認為線性層,會調用_create_new_module方法,返回一個new_module,然后將舊的module替換為new_module ,即加了LoRA后的module。
- create_new_module會進行
new_module = Linear(adapter_name, in_features, out_features, bias=bias, **kwargs)
,即peft\tuners\lora.py中Linear的初始化。
# peft\tuners\tuners_utils.py
class BaseTuner(nn.Module, ABC):def __init__(self, model, peft_config: Union[PeftConfig, dict[str, PeftConfig]], adapter_name: str) -> None:super().__init__()self.model = model......# 會掉用inject_adapter方法self.inject_adapter(self.model, adapter_name)# Copy the peft_config in the injected model.self.model.peft_config = self.peft_config...... # peft\tuners\lora.pydef inject_adapter(self, model: nn.Module, adapter_name: str):peft_config = self.peft_config[adapter_name]is_target_modules_in_base_model = Falsekey_list = [key for key, _ in model.named_modules()]model_config = getattr(model, "config", {"model_type": "custom"})if hasattr(model_config, "to_dict"):model_config = model_config.to_dict()# 獲取高效微調的默認設置項peft_config = self._prepare_adapter_config(peft_config, model_config)# 遍歷每一個keyfor key in key_list:is_target_modules_in_base_model = True# 例如,key= 'transformer.h.0.self_attention.query_key_value'# parent = BloomAttention(# (query_key_value): Linear(in_features=64, out_features=192, bias=True)# (dense): Linear(in_features=64, out_features=64, bias=True)# (attention_dropout): Dropout(p=0.0, inplace=False)# )# target = Linear(in_features=64, out_features=192, bias=True)# target_name = 'query_key_value'parent, target, target_name = _get_submodules(model, key)optionnal_kwargs = {"loaded_in_8bit": getattr(model, "is_loaded_in_8bit", False),"loaded_in_4bit": getattr(model, "is_loaded_in_4bit", False),"current_key": key,}# self._create_and_replace(peft_config, adapter_name, target, target_name, parent, **optionnal_kwargs)......# peft\tuners\lora.pydef _create_and_replace(self,lora_config,adapter_name,target,target_name,parent,**optionnal_kwargs,):......# TODO: better deal with thatif isinstance(target, LoraLayer) and isinstance(target, torch.nn.Conv2d):target.update_layer_conv2d(adapter_name,lora_config.r,lora_config.lora_alpha,lora_config.lora_dropout,lora_config.init_lora_weights,)......else:# target默認為線性層,會進入此分支new_module = self._create_new_module(lora_config, adapter_name, target, **kwargs) # 此方法會將原始module進行替換,替換為加上lora后的self._replace_module(parent, target_name, new_module, target)
# 高效微調的默認設置項
LoraConfig(peft_type=<PeftType.LORA: 'LORA'> # peft的類型, auto_mapping=None, base_model_name_or_path='', revision=None, task_type=<TaskType.CAUSAL_LM: 'CAUSAL_LM'> # 任務類型, inference_mode=False, r=8 # 秩大小,一般4,8,16,小數據集一般設置更小的r(1,2), target_modules=['query_key_value'] # 指定應用lora的目標模塊,Bloom模型默認為query_key_value,可以使用正則, lora_alpha=8 # 尺度縮放參數,ΔW按α/r縮放,即scaling[adapter_name]=lora_alpha/r,默認為1, lora_dropout=0.0 # lora層的dropout比率, fan_in_fan_out=False, bias='none' # 偏差可以是“無”、“全部”或“lora_only”。如果是“all”或“lora_only”,則相應的偏差將在訓練期間更新, modules_to_save=None # 全量微調時,模型的layer名稱, init_lora_weights=True, layers_to_transform=None, layers_pattern=None
)
-
Linear初始化中,重要的代碼就是update_layer
-
update_layer中,就會初始化秩為r的可訓練參數A和B
-
new_module = Linear(in_features=64, out_features=192, bias=True(lora_dropout): ModuleDict((default): Identity())(lora_A): ModuleDict((default): Linear(in_features=64, out_features=8, bias=False))(lora_B): ModuleDict((default): Linear(in_features=8, out_features=192, bias=False))(lora_embedding_A): ParameterDict()(lora_embedding_B): ParameterDict() )
-
上面就是Linear初始化后的模塊,即new_module。初始化后,就會將之前的module進行替換。
# 替換前
BloomAttention((query_key_value): Linear(in_features=64, out_features=192, bias=True)(dense): Linear(in_features=64, out_features=64, bias=True)(attention_dropout): Dropout(p=0.0, inplace=False)
)# 通過下面代碼進行替換
# peft\tuners\lora.py中的_create_and_replace方法
new_module = self._create_new_module(lora_config, adapter_name, target, **kwargs)
self._replace_module(parent, target_name, new_module, target)# 替換后
BloomAttention((query_key_value): Linear(in_features=64, out_features=192, bias=True(lora_dropout): ModuleDict((default): Identity())(lora_A): ModuleDict((default): Linear(in_features=64, out_features=8, bias=False))(lora_B): ModuleDict((default): Linear(in_features=8, out_features=192, bias=False))(lora_embedding_A): ParameterDict()(lora_embedding_B): ParameterDict())(dense): Linear(in_features=64, out_features=64, bias=True)(attention_dropout): Dropout(p=0.0, inplace=False)
)
- 上面就是替換前后BloomAttention模塊的結構。
# peft\tuners\lora.py
class Linear(nn.Linear, LoraLayer):# Lora implemented in a dense layerdef __init__(self,adapter_name: str,in_features: int,out_features: int,r: int = 0,lora_alpha: int = 1,lora_dropout: float = 0.0,fan_in_fan_out: bool = False, # Set this to True if the layer to replace stores weight like (fan_in, fan_out)is_target_conv_1d_layer: bool = False,**kwargs,):init_lora_weights = kwargs.pop("init_lora_weights", True)nn.Linear.__init__(self, in_features, out_features, **kwargs)LoraLayer.__init__(self, in_features=in_features, out_features=out_features)# Freezing the pre-trained weight matrixself.weight.requires_grad = Falseself.fan_in_fan_out = fan_in_fan_outif fan_in_fan_out:self.weight.data = self.weight.data.Tnn.Linear.reset_parameters(self)# 重要代碼self.update_layer(adapter_name, r, lora_alpha, lora_dropout, init_lora_weights)self.active_adapter = adapter_nameself.is_target_conv_1d_layer = is_target_conv_1d_layer......
def update_layer(self, adapter_name, r, lora_alpha, lora_dropout, init_lora_weights):self.r[adapter_name] = rself.lora_alpha[adapter_name] = lora_alphaif lora_dropout > 0.0:# dropoutlora_dropout_layer = nn.Dropout(p=lora_dropout)else:# 默認設置lora_dropout_layer = nn.Identity()self.lora_dropout.update(nn.ModuleDict({adapter_name: lora_dropout_layer}))# Actual trainable parametersif r > 0:# 如果秩大于0,就初始化秩為R的可訓練參數A和Bself.lora_A.update(nn.ModuleDict({adapter_name: nn.Linear(self.in_features, r, bias=False)}))self.lora_B.update(nn.ModuleDict({adapter_name: nn.Linear(r, self.out_features, bias=False)}))# 指定lora_alpha參數,用于平衡LORA 和 基礎模型的貢獻# 這里scaling默認為1self.scaling[adapter_name] = lora_alpha / rif init_lora_weights:self.reset_lora_parameters(adapter_name)self.to(self.weight.device)
1.2.2 前向過程
- 前向過程中,最終會調用BloomForCausalLM的前向傳播方法。
LoraModel((model): BloomForCausalLM((transformer): BloomModel((word_embeddings): Embedding(250880, 64)(word_embeddings_layernorm): LayerNorm((64,), eps=1e-05, elementwise_affine=True)(h): ModuleList((0-1): 2 x BloomBlock((input_layernorm): LayerNorm((64,), eps=1e-05, elementwise_affine=True)(self_attention): BloomAttention(# 在BloomAttention的query_key_value中,使用LoRA進行微調(query_key_value): Linear(in_features=64, out_features=192, bias=True(lora_dropout): ModuleDict((default): Identity())(lora_A): ModuleDict((default): Linear(in_features=64, out_features=8, bias=False))(lora_B): ModuleDict((default): Linear(in_features=8, out_features=192, bias=False))(lora_embedding_A): ParameterDict()(lora_embedding_B): ParameterDict())(dense): Linear(in_features=64, out_features=64, bias=True)(attention_dropout): Dropout(p=0.0, inplace=False))(post_attention_layernorm): LayerNorm((64,), eps=1e-05, elementwise_affine=True)(mlp): BloomMLP((dense_h_to_4h): Linear(in_features=64, out_features=256, bias=True)(gelu_impl): BloomGelu()(dense_4h_to_h): Linear(in_features=256, out_features=64, bias=True))))(ln_f): LayerNorm((64,), eps=1e-05, elementwise_affine=True))(lm_head): Linear(in_features=64, out_features=250880, bias=False))
)
- 在BloomAttention中,會調用
peft\tuners\lora.py
中Linear的前向傳播(如下代碼)。 - 注意:self.lora_A和self.lora_B類型為ModuleDict,這是因為LoRA有高級用法,我們以后再介紹:
- 一個主模型,可以使用多個適配器(
默認self.active_adapter=default
) - 可以切換適配器
- 可以禁用適配器,獲取原始結果
- 一個主模型,可以使用多個適配器(
# peft\tuners\lora.pydef forward(self, x: torch.Tensor):previous_dtype = x.dtypeif self.active_adapter not in self.lora_A.keys():return F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)if self.disable_adapters:if self.r[self.active_adapter] > 0 and self.merged:self.unmerge()result = F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)# 不禁用adapters,且秩r > 0,且不合并 elif self.r[self.active_adapter] > 0 and not self.merged:# 1、result = torch.Size([1, 48, 192])result = F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)x = x.to(self.lora_A[self.active_adapter].weight.dtype)# 2、核心代碼# x = torch.Size([1, 48, 64])# self.lora_A['default'] = Linear(in_features=64, out_features=8, bias=False)# x經過self.lora_A,torch.Size([1, 48, 8])# self.lora_B['default'] = Linear(in_features=8, out_features=192, bias=False)# x經過self.lora_B,torch.Size([1, 48, 192])# 然后result相加合并,即h = Wx + BAxresult += (self.lora_B[self.active_adapter](self.lora_A[self.active_adapter](self.lora_dropout[self.active_adapter](x)))* self.scaling[self.active_adapter])else:result = F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)result = result.to(previous_dtype)return result
1.3 LoRA輕量微調bloom模型
同樣,我們只需要在加載原模型后、配置訓練器前加peft的代碼即可。
from peft import LoraConfig, TaskType, get_peft_modelconfig = LoraConfig(task_type=TaskType.CAUSAL_LM# , target_modules=".*\.1.*query_key_value" 指定要添加LoRA的目標模塊(支持正則)# , modules_to_save=["word_embeddings"] 全量微調時,模型的layer名稱, modules_to_save=None
)model = get_peft_model(model, config)# 打印可訓練參數
model.print_trainable_parameters()trainable params: 786,432 || all params: 346,555,392 || trainable%: 0.22692822508443325
- 配置訓練器、模型訓練及推理和參數高效微調PEFT(一)快速入門BitFit、Prompt Tuning、Prefix Tuning中2.1一樣。
- 顯存消耗情況:
(base) root@autodl-container-adbc11ae52-f2ebff02:~# nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.89.02 Driver Version: 525.89.02 CUDA Version: 12.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 NVIDIA GeForce ... On | 00000000:41:00.0 Off | N/A |
| 35% 59C P2 143W / 250W | 2810MiB / 11264MiB | 31% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------++-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
+-----------------------------------------------------------------------------+
1.4 模型保存
LoRA訓練完成后,可以將兩個低秩矩陣與原始模型中的權重進行合并,合并后的模型與原始模型無異,避免了推理期間Prompt系列方法帶來的額外計算量
。
from transformers import AutoModelForCausalLM, AutoTokenizerfrom peft import PeftModel# 1、加載基礎模型
model_path = r'/root/autodl-fs/models/langboat/bloom-389m-zh'model = AutoModelForCausalLM.from_pretrained(model_path, low_cpu_mem_usage=True)
tokenizer = AutoTokenizer.from_pretrained(model_path)# 2、加載PeftModel
p_model = PeftModel.from_pretrained(model, model_id="./chatbot/checkpoint-500/")# 3、模型合并
merge_model = p_model.merge_and_unload()# 4、完整模型保存,此時大小就很大了,下次加載時候可以用AutoModelForCausalLM直接加載
merge_model.save_pretrained("./chatbot/merge_model")
2 AdaLoRA
2.1 AdaLoRA簡介
-
論文地址:ADAPTIVE BUDGET ALLOCATION FOR PARAMETER EFFICIENT FINE-TUNING(23.03)
-
LoRA可以達到與完全微調幾乎相當的性能,但是也存在一些問題。
- LoRA需要預先指定每個增量矩陣的本征秩 r 相同,忽略了在微調預訓練模型時,權重矩陣的重要性在不同模塊和層之間存在顯著差異
- 并且只訓練了Attention,沒有訓練FFN,事實上FFN更重要。
-
如下圖a所示,將可微調參數全部放在FFN的效果要好于放在attention矩陣中的效果;如下圖b所示,同時微調高層參數的效果會比微調底層參數的效果更好。因此,作者提出了AdaLoRA,它根據權重矩陣的重要性得分,在權重矩陣之間自適應地分配參數預算。
-
AdaLORA主要包含兩個模塊:
- (i) SVD形式參數更新(SVD-based adaptation):直接將增量矩陣Δ參數化為SVD的形式,避免了在訓練過程中進行SVD計算帶來的資源消耗;
-
(ii) 基于重要程度的參數分配(Importance-aware rank allocation): 裁剪一些冗余的奇異值。
具體原理可以參考:AdaLoRA(Adaptive LoRA)詳解
2.2 AdaLoRA輕量微調bloom模型
-
這里我們沒有分析AdaLoRA的源碼,直接進行輕量微調,有興趣的可以分析下源碼。
-
同樣,我們只需要在加載原模型后、配置訓練器前加peft的代碼即可。
from peft import get_peft_model, TaskType, AdaLoraConfigconfig = AdaLoraConfig(task_type=TaskType.CAUSAL_LM, r=8, lora_alpha=8, lora_dropout=0, target_modules=["query_key_value"])model = get_peft_model(model, config)# 打印可訓練參數
model.print_trainable_parameters()trainable params: 1,179,936 || all params: 346,948,920 || trainable%: 0.3400892557901607
- 配置訓練器、模型訓練及推理和參數高效微調PEFT(一)快速入門BitFit、Prompt Tuning、Prefix Tuning中2.1一樣。
- 顯存消耗情況:
(base) root@autodl-container-adbc11ae52-f2ebff02:~/autodl-tmp/transformers-code/03-PEFT# nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.89.02 Driver Version: 525.89.02 CUDA Version: 12.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 NVIDIA GeForce ... On | 00000000:41:00.0 Off | N/A |
| 33% 54C P2 119W / 250W | 2816MiB / 11264MiB | 32% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------++-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
+-----------------------------------------------------------------------------+