上一章介紹了如何基于APE+SELF自動化構建指令微調樣本。這一章咱就把微調跑起來,主要介紹以Lora為首的低參數微調原理,環(huán)境配置,微調代碼,以及大模型訓練中顯存和耗時優(yōu)化的相關技術細節(jié)
標題這樣寫是因為上周突然收到了一周內上線一版chatbo的命令,原因無它領導們都刷到了《一個小時你也可以擁有ChatGPT》,《100美金訓練ChatGPT》,《僅訓練3小時超越ChatGPT》,《人人都可以擁有ChatGPT》。。。領導說人人都有了為啥我沒有呀???!真誠呼吁標題黨們求手下留情,留人一命!于是這里我換個標題來Debuff!Debuff!
看到這里本文最重要的部分已經說完了,累了的小伙伴可以撤退了,五一快樂~
低參數微調原理
- LORA:LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS
- 原理:INTRINSIC DIMENSIONALITY EXPLAINS THE EFFECTIVENESS
OF LANGUAGE MODEL FINE-TUNING- 前人的肩膀:Adapter: Parameter-Efficient Transfer Learning for NLP
我們之前在解密Prompt系列3. 凍結LM微調Prompt介紹過一些soft-prompt,包括P-Tunning和Prompt-Tunning也屬于低參數微調。這些方案是通過參數拼接的方案引入額外參數。這里介紹另一類方案,同樣是凍結LLM的參數,通過參數相加的方案引入額外參數, 相較soft-prompt最明顯的優(yōu)勢,就是不會占用輸入token的長度。
LoRA的原理比較簡單,原始全量微調其實就是在原始模型參數上通過微調加入增量\(W = W_0+\Delta W\),那我們可以通過凍結原始參數\(W_0\),并且把增量部分通過低秩分解方式進一步降低參數量級\(\Delta W=A*B^T\),原始參數的維度是\(d*d\), 則低秩分解后的參數量級是\(2*r*d\),因為這里的r<<d,因此可以起到大幅降低微調參數量級的效果,如下圖
核心代碼如下
## 初始化低秩矩陣A和B
self.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)}))
self.scaling[adapter_name] = lora_alpha / r
## 向前計算
result = F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)
result += (
self.lora_B[self.active_adapter](
self.lora_A[self.active_adapter](self.lora_dropout[self.active_adapter](x))
)
* self.scaling[self.active_adapter]
)
論文測試了在多數場景下適當的LORA微調和全量微調的效果不相上下。一個可能原因是INTRINSIC DIMENSIONALITY論文中提出,雖然語言模型整體參數空間很大,但具體到每個任務其實有各自的隱表征空間(intrisic dimension),這個隱表征空間的維度并不高, 因此在微調過程中加入低秩分解并不一定會影響微調效果。使用LORA微調有以下幾個細節(jié)
- 對哪些參數進行微調:基于Transformer結構,LORA只對每層的Self-Attention的部分進行微調,有\(W_q, W_k, W_v, W_O\)四個映射層參數可以進行微調。消融實驗顯示只微調\(W_q\)效果略差,微調\(W_q, W_v\)的效果和微調\(W_q, W_k, W_v, W_O\)的效果相似。需要注意不同模型參數名稱不同,像chatglm對應的參數名稱就是query_key_value
- Rank的選?。篟ank的取值作者對比了1-64,效果上Rank在4-8之間最好,再高并沒有效果提升。不過論文的實驗是面向下游單一監(jiān)督任務的,因此在指令微調上根據指令分布的廣度,Rank選擇還是需要在8以上的取值進行測試。
- alpha參數:alpha其實是個縮放參數,本質和learning rate相同,所以為了簡化我默認讓alpha=rank,只調整lr,這樣可以簡化超參
- 初始化:A和Linear層的權重相同Uniform初始化,B是zero初始化,這樣最初的Lora權重為0。所以Lora參數是從頭學起,并沒有那么容易收斂。
Lora的優(yōu)點很明顯,低參數,適合小樣本場景;可以拔插式的使用,快速針對不同下游任務訓練不同的lora權重;完全沒有推理延時,這個在后面代碼中會提到推理時,可以預先把lora權重merge到原始權重上。
但Lora微調雖好,個人在嘗試中感受到的局限性就是adapter類的微調方案可能更適合下游單一任務類型/生成風格。至于是否適合作為通用指令微調的解決方案,有個問題我也沒有搞懂,就是通用的指令樣本是否真的有統(tǒng)一的低秩空間表征?這個表征又是什么含義?因為指令微調階段的樣本其實是混合的多任務指令樣本,這種情況下lora是否合適,感覺需要更全面的評估(當前出來的眾多LLama們都缺少合理統(tǒng)一全面可比的Evaluation),當前就我們的嘗試情況lora的效果并不及預期。
環(huán)境配置
- GPU 云服務廠商對比
我用了featurize和攬睿星舟。云服務廠商的選擇主要看是否有jupyter,存儲夠大,下載快,能連git,有高配torch環(huán)境。這兩家在眾多小廠里脫穎而出,4090的卡一個小時也就3塊錢,來來來盆友辛苦把推廣費結一下~
強調下環(huán)境配置,想跑通微調,搞定環(huán)境你就成功了80%!運氣好1分鐘,運氣差1天都在原地打轉
- 實例環(huán)境:TRX4090 + py38 + torch2.0 + CUDA12
- python環(huán)境:主要坑在transforemrs和peft,幾個相關issue包括:llama tokenizer special token有問題,peft adapter.bin微調不更新,Bug with fan_in_fan_out。我一個不差都踩中了。。。
# 以下配置可能會隨時間變化,出了問題就去issue里面刨吧
# 要相信你不是唯一一個大冤種!
accelerate
appdirs
loralib
bitsandbytes
black
black[jupyter]
datasets
fire
transformers>=4.28.0
git+https://github.com/huggingface/peft.git
sentencepiece
gradio
wandb
cpm-kernel
模型初始化
以下代碼主要整合自alpaca-lora和chatglm-finetune。其實lora微調的代碼本身并不復雜,相反是如何加速大模型訓練,降低顯存占用的一些技巧大家可能不太熟悉。模型初始化代碼如下,get_peft_model會初始化PeftModel把原模型作為base模型,并在各個self-attention層加入lora層,同時改寫模型forward的計算方式。
主要說下load_in_8bit和prepare_model_for_int8_training,這里涉及到2個時間換空間的大模型顯存壓縮技巧。
from peft import get_peft_model, LoraConfig, prepare_model_for_int8_training, set_peft_model_state_dict
from transformers import AutoTokenizer, AutoModel
model = AutoModel.from_pretrained("THUDM/chatglm-6b", load_in_8bit=True, torch_dtype=torch.float16, trust_remote_code=True, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True)
model = prepare_model_for_int8_training(model)
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
r=8,
lora_alpha=8,
lora_dropout=0.05,
)
model = get_peft_model(model, lora_config)
model.config.use_cache = False
模型顯存占用分成兩個部分,一部分是靜態(tài)顯存基本由模型參數量級決定,另一部分是動態(tài)顯存在向前傳播的過程中每個樣本的每個神經元都會計算激活值并存儲,用于向后傳播時的梯度計算,這部分和batchsize以及參數量級相關。以下8bit量化優(yōu)化的是靜態(tài)顯存,而梯度檢查優(yōu)化的是動態(tài)顯存。
1. 8bit Quantization
https://huggingface.co/blog/hf-bitsandbytes-integration
from_pretrained中的load_in_8bit參數是bitsandbytes庫賦予的能力,會把加載模型轉化成混合8bit的量化模型,注意這里的8bit模型量化只用于模型推理,通過量化optimizer state降低訓練時顯存的時8bit優(yōu)化器是另一個功能不要搞混喲~
模型量化本質是對浮點參數進行壓縮的同時,降低壓縮帶來的誤差。 8-bit quantization是把原始FP32(4字節(jié))壓縮到Int8(1字節(jié))也就是1/4的顯存占用。如上加載后會發(fā)現(xiàn)除lora層外的多數層被轉化成int類型如下
當然壓縮方式肯定不是直接四舍五入,那樣會帶來巨大的精度壓縮損失。常見的量化方案有absolute-maximum和zero-point,它們的差異只是rescale的方式不同,這里簡單說下absmax,如下
先尋找tensor矩陣的絕對值的最大值,并計算最大值到127的縮放因子,然后使用該縮放因子對整個tensor進行縮放后,再round到整數。這樣就把浮點數映射到了INT8,逆向回到float的原理相同。
當然以上的縮放方案依舊存在精度損失,以及當矩陣中存在outlier時,這個精度損失會被放大,例如當tensor中絕大部分取值在1以下,有幾個值在100+,則縮放后,所有1以下的tensor信息都會被round抹去。因此LLM.int8()的實現(xiàn)對outlier做了進一步的優(yōu)化,把outlier和非outlier的矩陣分開計算,再把結果進行合并來降低outlier對精度的影響。
prepare_model_for_int8_training是對在Lora微調中使用LLM.int8()進行了適配用來提高訓練的穩(wěn)定性,主要包括
- layer norm層保留FP32精度
- 輸出層保留FP32精度保證解碼時隨機sample的差異性
2. gradient checkpoint
https://medium.com/tensorflow/fitting-larger-networks-into-memory-583e3c758ff9
prepare_model_for_int8_training函數還做了一件事就是設置gradient_checkpointing=True,這是另一個時間換空間的技巧。
gradient checkpoint的實現(xiàn)是在向前傳播的過程中使用torch.no_grad()不去存儲中間激活值,降低動態(tài)顯存的占用。而只是保存輸入和激活函數,當進行反向傳播的時候,會重新獲取輸入和激活函數計算激活值用于梯度計算。因此向前傳播會計算兩遍,所以需要更多的訓練時間。
use_cache設置為False,是因為和gradient checkpoint存在沖突。因為use_cache是對解碼速度的優(yōu)化,在解碼器解碼時,存儲每一步輸出的hidden-state用于下一步的輸入,而因為開啟了gradient checkpoint,中間激活值不會存儲,因此use_cahe=False。其實#21737已經加入了參數檢查,這里設置只是為了不輸出warning。
模型訓練
訓練基本和常規(guī)訓練基本相同,代碼如下。主要說下模型存儲和加載以及混合精度訓練
import datasets
from transformers import Trainer, DataCollatorForSeq2Seq
if resume_from_checkpoint:
lora_weight = torch.load(ckpt_name)
set_peft_model_state_dict(model, lora_weight)
train_data = datasets.load_from_disk(dataset_path)
class ModifiedTrainer(Trainer):
def save_model(self, output_dir=None, _internal_call=False):
# 改寫trainer的save_model,在checkpoint的時候只存lora權重
from transformers.trainer import TRAINING_ARGS_NAME
os.makedirs(output_dir, exist_ok=True)
torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
saved_params = {
k: v.to("cpu") for k, v in self.model.named_parameters() if v.requires_grad
}
torch.save(saved_params, os.path.join(output_dir, "adapter_model.bin"))
trainer = ModifiedTrainer(
model=model,
train_dataset=train_data,
args=transformers.TrainingArguments(
per_device_train_batch_size=8,
gradient_accumulation_steps=16,
num_train_epochs=10,
learning_rate=3e-4,
fp16=True,
logging_steps=10,
save_steps=200,
output_dir=output_dir
),
data_collator=DataCollatorForSeq2Seq(
tokenizer, pad_to_multiple_of=8, return_tensors="pt", padding=True
),
)
trainer.train()
model.save_pretrained(train_args.output_dir)
1. 模型的存儲和加載
因為peftModel重寫了原始model的save_pretrained函數,只把lora層的權重進行存儲,因此model.save_pretrained只會存儲lora權重。而trainer的save_model函數沒有做相應的重寫,因此我們重寫下對應的function,避免checkpoint寫入原始模型全部參數。
相應的如果你從ckpt加載lora權重去繼續(xù)訓練的話,也是對PeftModel中的Lora權重進行加載。
2. 混合精度訓練
https://huggingface.co/docs/transformers/main/en/perf_train_gpu_one#fp16-training
除了默認的全精度FP32,參數精度還有半精度FP16,以及BF16和TF32。最常用也是這里使用的是FP16的混合精度。
實現(xiàn)原理是并非所有變量都需要全精度存儲,如果把部分中間變量轉化成半精度,則計算效率會大幅提升,加上一些GPU對FP16計算做了優(yōu)化,吞吐上比全精度會快2~5倍。
不過只使用半精度訓練同樣會帶來量化誤差,主要包括:數據溢出因為半精度比全精度的范圍更小,訓練到后期因為梯度越來越小可能會下溢出;舍入誤差梯度變小后,因為精度有限,導致梯度更新被四舍五入,更新了個寂寞。
為了解決以上的問題引入了混合精度訓練。簡單來說就是向前傳遞時,模型權重、激活值和梯度都使用FP16進行存儲,同時會拷貝一份模型權重以FP32存儲,向后傳播optimizer更新時會更新FP32的參數。因此混合精度訓練并不會節(jié)省內存,只會提高模型訓練速度。
模型推理
推理有兩個方案,一個和訓練相同,直接加入Lora層,不過會增加推理延時因為多了lora層的計算,適合線下測評用,如下
from peft import PeftModel
from transformers import AutoModel, AutoTokenizer
model = AutoModel.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True, load_in_8bit=True, device_map='auto')
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True)
model = PeftModel.from_pretrained(model, "./lora_ckpt")
model.half().to(device)
model.eval()
另一個沒有推理延時的方案,是先把lora權重和原始模型權重進行合并,把合并后的參數存儲成新的bin文件,然后和加載常規(guī)模型一樣加載合并后的模型參數進行推理。權重合并的代碼如下
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True)
# when merging disable int8
model = AutoModel.from_pretrained(
"THUDM/chatglm-6b", load_in_8bit=False, torch_dtype=torch.float16,
trust_remote_code=True, device_map={"": "cpu"},
)
## 用來檢查權重是否合并成功,合并成功weight會改變
first_weight = model.base_model.layers[0].attention.query_key_value.weight
first_weight_old = first_weight.clone()
# 返回的不是新的模型,而是在原始模型上加了adapter層
lora_model = PeftModel.from_pretrained(
model,
"./lora_ckpt",
device_map={"": "cpu"},
torch_dtype=torch.float16,
)
# 報錯:A*B shape mismatch,大概率是get_peft_model錯誤修改了peft_config里面的fan_in_fan_out參數,某個peft的revision有這個bug
lora_model = lora_model.merge_and_unload()
lora_model.train(False)
# 報錯:大概率peft訓練有問題,檢查adapter.bin大小
assert not torch.allclose(first_weight_old, first_weight), 'Weight Should Change after Lora Merge'
# lora模型權重把原模型權重加了prefix,這里移除恢復原始key
deloreanized_sd = {
k.replace("base_model.model.", ""): v
for k, v in lora_model.state_dict().items()
if "lora" not in k
}
# 保存合并后的模型權重
lora_model.save_pretrained(output_dir, state_dict=deloreanized_sd)
更多Prompt相關論文·教程,開源數據·模型,以及AIGC相關玩法戳這里DecryptPrompt文章來源:http://www.zghlxwxcb.cn/news/detail-429031.html
Reference文章來源地址http://www.zghlxwxcb.cn/news/detail-429031.html
- https://blog.csdn.net/anycall201/article/details/129959567
- 蘇劍林. (Jun. 20, 2022). 《Ladder Side-Tuning:預訓練模型的“過墻梯” 》[Blog post]. Retrieved from https://kexue.fm/archives/9138
- 蘇劍林. (Apr. 17, 2023). 《梯度視角下的LoRA:簡介、分析、猜測及推廣 》[Blog post]. Retrieved from https://kexue.fm/archives/9590
4.https://github.com/huggingface/blog/blob/main/notebooks/HuggingFace_int8_demo.ipynb - ChatGLM-Finetune
- Alpaca-lora
到了這里,關于解密Prompt系列6. lora指令微調扣細節(jié)-請冷靜,1個小時真不夠~的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!