極市導讀
對煉丹界的當紅炸子雞LoRA的「大拷問」!結合源碼解析深入了解LoRA。>>加入極市CV技術交流群,走在計算機視覺的最前沿
前言
自 ChatGPT 掀起了大模型(LLM)風潮后,一大波 LLMs(GPT-4, LLaMa, BLOOM, Alpaca, Vicuna, MPT ..) 百花齊放。知識問答、文章撰寫、代碼編寫和糾錯、報告策劃等等,它們都會,也能夠交互式地和你玩文字游戲,甚至還有些很有才的朋友將 LLM 作為交互的接口,同時連接到其它各種模態(e.g. 視覺 & 語音)的模型,從而創造了炸裂的多模態效果,炫~!
這么炫,難免人人都想打造一個自己專屬的 LLM(怎么有種回到了小時候玩寵物馴養游戲的趕腳..)。但是,大多數像 CW 這種“平民”玩家,并沒有能夠玩得起 LLM 的資源(主要是 GPU),別說成百上千億參數量的模型了,就算是幾十億級別的模型,玩得起的朋友可能也不多。
大多數人對于 LLM 的“親密度”,可能最多就是拉個開源的 demo 跑下推理過程,得到個“意料之中”的結果,然后很諷刺地自 high 一把:WOW~ 好膩害喲!我們離 AGI 又更近了一步!至于你讓他訓一個?他會說:呵呵.. 別想太多,洗洗睡吧!
一項技術通常都不會在它誕生之初就得以被廣泛使用,和人一樣,它也需要機遇。正是現在這種背景,加劇了我們大多數平民在大模型時代煉丹的矛盾。于是,本文的主角 LoRA(Low-Rank Adaptation of Large Language Models) ,一個于2021年就出生的家伙,順勢成為煉丹界的當紅炸子雞,成功出圈。
本文會先介紹 LoRA 的概念與優勢、講述其 motivation 和 以往方法存在的問題,然后以提問的形式從七個方面切入去認識與理解 LoRA(結合源碼解析),接著進一步深入思考 LoRA 的一些方面,最后給出一個應用 LoRA 進行微調的例子。對 LoRA 已經有基本了解的帥哥靚女們,可以直接跳到“LoRA 七問”與“進擊的 LoRA”這兩章。
快給我講講 LoRA 是什么
如今快節奏生活下的人們都比較浮躁,你們看我吹水那么多水還沒講 LoRA 到底是什么,肯定已經饑渴難耐。哦?你說你不會,那很好,CW 為你點贊!不過,我也不磨嘰,該進入正題了。
LoRA 的全稱是 "Low-Rank Adaption", 看到 "low-rank",線性代數玩家們應該會神經反射性地聯系到低秩矩陣。Binggo! 這里就是這個意思。你問我 LoRA 的中文名?Em.. 就叫它“低秩(自)適應”吧,雖然英文里沒有 "self", 但根據 LoRA 的思想和做法及其帶來的效果,它就是自適應的意思。
概括地來說,LoRA 是一項主要用于微調 LLMs 的技術,它額外引入了可訓練的低秩分解矩陣,同時固定住預訓練權重。這個玩法的重點在于:預訓練權重不需訓練,因此沒有梯度,僅訓練低秩矩陣那部分的參數。
有一點 CW 一定要告訴你們:引入的低秩矩陣那部分的參數量比起預訓練權重來說,少炒雞多! 這就意味著,比起全量 fine-tune 的玩法,可訓練的參數量少了很多,于是就不需要那么多顯存(GPU)資源了。這對于我等平(貧)民來說,簡直不要太香了~!
利用 LoRA,我們可以享受到諸多福利,比如以下幾點:
- 在面對不同的下游任務時,僅需訓練參數量很少的低秩矩陣,而預訓練權重可以在這些任務之間共享;
- 省去了預訓練權重的梯度和相關的 optimizer states,大大增加了訓練效率并降低了硬件要求;
- 訓練好的低秩矩陣可以合并(merge)到預訓練權重中,多分支結構變為單分支,從而達到沒有推理延時的效果;
- 與之前的一些參數高效的微調方法(如 Adapter, Prefix-Tuning 等)互不影響,并且可以相互結合
注:參數高效的微調方法 即 PEFT(Parameter-Efficient Fine-Tuning),這類方法僅需微調少量參數(可以是額外引入的),而無需微調預訓練模型的所有參數,從而能夠降低計算和存儲資源。
靈感來源
對于一項技術,CW 往往會好奇它是基于怎樣的想法被發明出來的。也就是,發明者的靈感來源究竟源自于哪里。可惜無法親自采訪作者,不然我肯定讓他“口若懸河”hhh!沒辦法咯,我只能通過 paper 來為自己找答案。
CW 發現,作者在 paper 中提到:以往的一些工作表明,模型通常是“過參數化”(over-parametrized)的,它們在優化過程中參數更新的部分通常“駐扎”(reside)在低維子空間中。基于此,作者就順理成章地提出假設:預訓練模型在下游任務中微調而更新參數時,也符合這樣的規律。
另外,以往的 PEFT 方法存在一系列問題,如:加大了推理延時、增加了模型深度、限制了輸入句長 等,更重要的是,它們大多數都打不過全量 fine-tune,也就是最終訓完的模型性能沒有全量 fine-tune 來得好。
結合自己的假設與時代背景,作者就搞出了 LoRA,在這種玩法下訓出的模型,最終在性能上能和全量 fine-tune 對飆,甚至在一些任務上還更加出色。
以往的 PEFT 跪在哪里
在上一章,CW 淺淺地提到了以往的 PEFT 方法存在的一些問題,如今在本章,我再稍稍展開來談一下。
在 LoRA 出生之前,比較有代表性的 PEFT 方法主要有額外引入適配下游任務的 adapter layers 和 優化靠近模型輸入層的激活(activations) 兩種。對于后者,比較有代表性的有 prefix tuning 等。其實,降低下要求,這些方法也算是 good 的,畢竟在一定程度上算是 work 了,只是不 good enough ..
對于引入 adapter layers 這類方法,它們的不足在于:
- 新增的 adapter layers 必須串行處理,從而增加了推理延時;
- 在分布式訓練時需要更多的 GPU 同步操作(如 All-Reduce & Broadcast)
至于另一類方法,以 prefix tuning 為例,它們則跪在了:
- 該方法本身很難優化;
- 這種方法需要在模型的輸入序列中預留一部分用作可微調的 prompt,從而限制了原始輸入文本的句長
LoRA 七問
這一章,CW 會向大家詳細說明 LoRA 的玩法,主要從七個方面切入,分別對應以下每一小節的標題,這其實也是我自己在剛接觸 LoRA 時所產生的疑問。可以把它們當作一個個 target 去攻破,待全部攻破之后,對 LoRA 應該就算是有一定的理解了。
前四節主要是理論分析,結合了 paper 中的公式和實驗結果。后三節的內容則會結合源碼解析,這樣會有更深刻的認識。
為何可以引入低秩矩陣
作者說他之前看到了一篇 paper: Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning,這篇 paper 的結論是:預訓練語言模型在下游任務微調后,權重矩陣其實具有很低的 "intrinsic rank"F-。
【關于 intrinsic rank 的理解】
"intrinsic" 直譯是“內在的”、“固有的”,因此我看到有人直接喊 instrinsic rank 為“內在秩”、“固有秩”。(⊙o⊙)… 對于這種叫法,我是覺得很別扭,而且有點不明所以。
CW 覺得,在這里,"intrinsic" 應該理解為“本質上的”、“最具代表性的”會比較恰當。于是,"intrinsic rank" 就應當理解為最能體現數據本質的維度(特征)數目,我們也因此可以美其名曰:“本征秩”。其實,在信號處理里也有相應的概念——intrinsic dimension,它代表能夠表示信號的最少特征數,它們所對應的特征是最能體現信號本質的特征。
也就是說,模型在適配下游任務后,權重矩陣的本征秩變得很低,這就代表著其實并不需要這么高的維度數就能夠進行表征了,在高維度的權重矩陣中存在冗余。
基于此,作者就自信地認為:模型在微調過程中,權重更新的那部分(參數矩陣)肯定也低秩(low rank)的。
Inspired by this, we hypothesize the updates to the weights also have a low “intrinsic rank” during adaptation.
你問:“權重更新的那部分”具體指什么?
CW 答:模型在微調過程中,權重的變化可以表示為 。其中 就是更新前的權重(在一開始就是預訓練權重),而 就是更新的那部分,也就是經過反向傳播得到梯度后計算出來的需要更新的量。
LoRA 改變了什么
假設 , 由于梯度與權重參數是一對一的, 因此 。如今, 既然認為 的本征秩很低,那么不妨對其做低秩分解:
, 其中 , 并且 。
這個 就是所謂的 low rank,因為它遠小于 和 。
由此可知, 經過低秩分解后, 這部分的參數量是遠小于預訓練權重 的。
按理來說, 是在反向傳播階段才會出現的, 但我們可以將其 “提前拿出來” :讓它和 一起做好朋友參與前向過程。這樣一來, 反向傳播時梯度就只會傳導到 這部分, 因其本身就是待更新量, 而 初始是預訓練權重, 被固定了, 無需接收梯度。
經過 LoRA 的“洗禮”, 現在如果你喂給模型一個輸入 , 前向過程就會表示為:
另外,這里還有兩點需要提一下:
- 低秩矩陣 B,AB,A 的初始化
經過 式的低秩分解后, 為了在一開始讓模型的輸出保持為原來預訓練模型的輸出(也就是沒有 那部分), 于是將 初始化為全0, 而 則采用隨機高斯初始化。
- 對低秩部分的輸出 進行 scale
作者在 paper 中還提到, 對于 這部分, 會乘上一個 scale 系數 。其中, 相對于 保持一個常數倍的關系。作者認為調節這個 大致相當于調節學習率, 于是干脆固定為常數(這樣就可以偷懶了 )。
降低了哪部分的顯存需求
由于 的參數量遠小于 , 因此, 相比于全量 fine-tune 的玩法, LoRA 降低了 optimizer states 這部分對于顯存資源的需求。
這是因為 optimizer 對于需要更新的模型參數會保存一份副本, 在全量 fine-tune 的玩法下, 要全量更新, 于是 optimizer 保存的副本參數量為 ; 而我們的小可愛 LoRA 僅需更新 這部分, 所以 optimizer 保存的副本參數量僅為 , 其中 是遠小于 的。
另外, 我們可能很容易理所當然地認為 LoRA 對于梯度部分的顯存需求也遠小于全量 fine-tune, 實際真的是這樣嗎? 嘿嘿不妨一起來分析下 的梯度是如何計算的。
假設模型經過如公式 所示的前向過程后得到了輸出 , 并且我們進一步計算出了損失 ,現在我們是來求 的梯度。根據鏈式求導法則,易得:
注意 這部分, 它和全量 fine-tune 時是一樣的, 這部分梯度的 shape 和權重矩陣的 shape 一致,都是 。
OMG!這就是說, 實際在計算 的梯度過程中, 所需的顯存并沒有比全量 fine-tune 來得少, 樣也需要算出 shape 為 的梯度矩陣。更尷尬的是, 由于 的存在, 因此在梯度的計算過程中, 所需的顯存和計算量甚至比全量 fine-tune 還來得多.. 幸運的是, 在計算完成后, 這個中間狀態量所占的顯存會被釋放掉, 僅需存儲 這部分 shape 為 的梯度矩陣。
所以說,對于梯度部分,LoRA 在嚴格意義上并不能降低其對于顯存資源的需求,甚至比起全量 fine-tune 來說計算量還更大了,只不過降低了最終存儲的需求。
對模型的哪些部分做低秩分解
可是,在如今的 202x 年代,模型通常有 N 個權重矩陣,那么應該對其中的哪些做低秩分解呢?還是說,應該暴力地、一個不拉地通殺?
對于這個問題,作者選擇了偷懶,他僅將 LoRA 應用于 self-attention 層中的 projection matrices(如),而其中的 MLP 模塊以及 self-attention 層以外的結構均“不受待見”。
In the Transformer architecture, there are four weight matrices in the self-attention module (Wq, Wk, Wv, Wo) and two in the MLP module.
We limit our study to only adapting the attention weights for downstream tasks and freeze the MLP modules.
We leave the empirical investigation of adapting the MLP layers, LayerNorm layers, and biases to a future work.
作者可能也猜到了你們可能會打破砂鍋問到底:應該對 self-attention 層中的哪個或哪幾個 projection matrices 應用 LoRA 呢?于是,對于這個問題,他倒是下了番功夫去做實驗進行探究。
在實驗中,作者以 175B 的 GPT-3 為研究對象,并設置了參數量為 18M 的 budget,也就是應用了 LoRA 部分的可微調參數量不能超過 18M。在這種設置下,當每層僅對 的其中一個應用 LoRA 時,rank 則等于8;而如果每層都對 的其中兩個應用 LoRA,則 rank 等于4。
通過上表可以看出,模型更傾向于我們對更多類型的 projection matrices 應用 LoRA(如上表顯示,對4個 projection matrices 都應用 LoRA 時效果是最好的),盡管 rank 很低(如上表中最右一列 ), 也足夠讓 捕獲足夠的信息。
在代碼中如何實現
假設 是一個線性層(Linear Layer),我們一起來看看對其應用 LoRA 是如何實現的。
(麻煩認真看下代碼中的注釋,謝謝~)
classMergedLinear(nn.Linear,LoraLayer):
#Loraimplementedinadenselayer
def__init__(
self,
in_features:int,
out_features:int,
r:int=0,
lora_alpha:int=1,
lora_dropout:float=0.0,
enable_lora:List[bool]=[False],
fan_in_fan_out:bool=False,
merge_weights:bool=True,
**kwargs,
):
nn.Linear.__init__(self,in_features,out_features,**kwargs)
LoraLayer.__init__(self,r=r,lora_alpha=lora_alpha,lora_dropout=lora_dropout,merge_weights=merge_weights)
# enable_lora 是一個布爾類型的列表,用于指示對權重矩陣的哪些“子部分”做低秩分解。
#比如W是shape為(out_features,in_features)的矩陣,
#那么enable_lora=[True,False,True]就表示將W在out_features這個維度上按序均分成三部分W1,W2,W3,
# shape 均為(out_features // 3, in_features),然后僅對 W1 和 W3 做低秩分解。
#其中 W1 的第一個維度取值范圍是[0, out_features // 3),W3 則是[2 * out_features // 3, out_features)。
#同理,若 enable_lora =[True],就表示對整個 W 都做低秩分解。
ifout_features%len(enable_lora)!=0:
raiseValueError("Thelengthofenable_loramustdivideout_features")
self.enable_lora=enable_lora
self.fan_in_fan_out=fan_in_fan_out
#Actualtrainableparameters
ifr>0andany(enable_lora):
#僅enable_lora=True的部分應用低秩分解,每部分的low-rank是r
self.lora_A=nn.Linear(in_features,r*sum(enable_lora),bias=False)
#注意下這里B是用一維的分組卷積實現的
self.lora_B=nn.Conv1d(
r*sum(enable_lora),
out_features//len(enable_lora)*sum(enable_lora),
kernel_size=1,
groups=2,
bias=False,
)
#scale系數,對低秩矩陣的輸出(BAx)做縮放
self.scaling=self.lora_alpha/self.r
#Freezingthepre-trainedweightmatrix
#固定住預訓練權重
self.weight.requires_grad=False
#Computetheindices
#記錄權重矩陣中,做了低秩分解的是哪些“子矩陣”
self.lora_ind=self.weight.new_zeros((out_features,),dtype=torch.bool).view(len(enable_lora),-1)
self.lora_ind[enable_lora,:]=True
self.lora_ind=self.lora_ind.view(-1)
self.reset_parameters()
iffan_in_fan_out:
#fan_in_fan_out是針對GPT-2的Conv1D模塊的,
#該模塊和Linear的區別就是維度互為轉置
self.weight.data=self.weight.data.T
defreset_parameters(self):
nn.Linear.reset_parameters(self)
ifhasattr(self,"lora_A"):
#initializeAthesamewayasthedefaultfornn.LinearandBtozero
nn.init.kaiming_uniform_(self.lora_A.weight,a=math.sqrt(5))
nn.init.zeros_(self.lora_B.weight)
以上這個類叫作 MergedLinear, 顧名思義就是低秩分解的部分 可以合并到原來的預訓練權重 中。
以上代碼中, 比較繞的是與 enable_lora 這個參數相關的內容, 該參數可以用來靈活地指定預訓練權重 中,哪些部分要做低秩分解。
關于這個參數的設計由來, 猜想這是因為在某些模型的實現中, Attention 層中的 projection matrix 是用一個共享的線性層實現的(比如 GPT-2, BLOOM, etc.), 而有了這個 enable_lora 參數, 就可以靈活地指定要對這三者中的哪一個做低秩分解。
所有需要進行低秩分解的層都會繼承 LoraLayer 這個父類,這個類沒有什么特別,也就是設置一些 LoRA 該有的屬性:
classLoraLayer:
def__init__(
self,
r:int,
lora_alpha:int,
lora_dropout:float,
merge_weights:bool,
):
self.r=r
self.lora_alpha=lora_alpha
#Optionaldropout
iflora_dropout>0.0:
self.lora_dropout=nn.Dropout(p=lora_dropout)
else:
self.lora_dropout=lambdax:x
#Marktheweightasunmerged
#標記低秩分解部分是否已經合并至預訓練權重
self.merged=False
#指定是否要將低秩分解部分合并至預訓練權重中
self.merge_weights=merge_weights
#是否要禁用低秩分解的部分,如果是,則僅使用預訓練權重部分
self.disable_adapters=False
現在來介紹下 MergedLinear 這個層的前向過程:
- 先用預訓練權重 對輸入 實施前向過程, 得到 ;
- 再將輸入 喂給低秩分解矩陣 , 得到輸出 ;
- 接著對 這部分作 "零填充"使其與 的 shape 一致, 并且進行縮放(scale);
- 最后再將這部分的結果加回至 中, 如前面的公式 。
defforward(self,x:torch.Tensor):
result=F.linear(x,transpose(self.weight,self.fan_in_fan_out),bias=self.bias)
ifself.r>0:
after_A=self.lora_A(self.lora_dropout(x))
after_B=self.lora_B(after_A.transpose(-2,-1)).transpose(-2,-1)
result+=self.zero_pad(after_B)*self.scaling
returnresult
第3點中的“零填充"對應以上代碼中的 zero_pad(), 前面 CW 在介紹 enable_lora 這個參數時說過, 由于不一定會對整個預訓練權重矩陣做低秩分解, 于是 的 shape 不一定等同于 , 因此需要對前者進行 padding, 使其與后者的 shape 一致, 從而才可讓兩者進行 element-wise add 。
現在放出這個填充的邏輯:
defzero_pad(self,x):
"""將低秩矩陣的輸出BAx與原權重矩陣輸出Wx在維度上對應起來,維度不足的部分用0填充"""
result=x.new_zeros((*x.shape[:-1],self.out_features))
result=result.view(-1,self.out_features)
#將BAx“塞到”與Wx相對應的正確位置
result[:,self.lora_ind]=x.reshape(-1,self.out_features//len(self.enable_lora)*sum(self.enable_lora))
returnresult.view((*x.shape[:-1],self.out_features))
怎么做到無推理延時
CW 在前面提到過,LoRA 的優勢之一就是推理無延時(相比預訓練模型),這是因為低秩分解的部分可以合并至原預訓練權重中。比如,這時候模型需要推理,那么你會先調用 model.eval(),這時等同于調用了 model.train(mode=False),接著再將低秩分解部分合并至預訓練權重中,過程如下:
deftrain(self,mode:bool=True):
nn.Linear.train(self,mode)
self.lora_A.train(mode)
self.lora_B.train(mode)
#注:當調用 model.eval()時就會調用 train(mode=False)
#將低秩矩陣A,B合并至原權重矩陣W
ifnotmodeandself.merge_weightsandnotself.merged:
#Mergetheweightsandmarkit
ifself.r>0andany(self.enable_lora):
#delta_W=BA
delta_w=(
#這里使用1維卷積將低秩矩陣 A, B 進行“融合”:
# A(r * k)作為輸入,r 看作是其 channel,k 看作是空間維度上的大小;
# B(d * r * 1)作為卷積權重,d 是 output channel, r 是 input channel, 1 是 kernel size(注意B本身就是用1維分組卷積實現的)。
#由于是卷積,因此二維的 A 需要增加一維給 mini-batch:r * k -> 1 * r * k。
#卷積后,輸入(1*r*k)->輸出(1*d*k)
F.conv1d(
self.lora_A.weight.data.unsqueeze(0),
self.lora_B.weight.data,
groups=sum(self.enable_lora),
)
.squeeze(0)#1*d*k->d*k
.transpose(-2,-1)#d*k->k*d
)
#zero_pad()是對低秩分解矩陣delta_W進行0填充,因為原權重矩陣W中可能有些部分沒有進行低秩分解,
#從而得到一個和原權重矩陣 W 的 shape 對齊的結果,以便進行加和。k * d -> k * D(假設 D 是原權重矩陣 W 的 out features)
#對于原權重矩陣 W 是 Linear 層的情況,fan_in_fan_out = False,于是這里會進行 transpose: k * D -> D * k;
#而對于原權重矩陣W是GPT-2的Conv1D的情況,fan_in_fan_out=True,于是不需要transpose,它的outfeatures就是放在第二維的
#W=W+#delta_W
self.weight.data+=transpose(self.zero_pad(delta_w*self.scaling),notself.fan_in_fan_out)
elifxxx:
...
merge 完之后,在進行前向過程時就無需再像上一節展示的那樣分步進行,而是一步到位(見以下第二個分支):
defforward(self,x:torch.Tensor):
#此部分先省略,下一節再介紹
ifxxx:
...
#低秩分解部分已合并
elifself.merged:
returnF.linear(x,transpose(self.weight,self.fan_in_fan_out),bias=self.bias)
#低秩分解部分未合并
else:
result=F.linear(x,transpose(self.weight,self.fan_in_fan_out),bias=self.bias)
ifself.r>0:
after_A=self.lora_A(self.lora_dropout(x))
after_B=self.lora_B(after_A.transpose(-2,-1)).transpose(-2,-1)
result+=self.zero_pad(after_B)*self.scaling
returnresult
如何在下游任務中靈活地切換
LoRA 還有一點很吸引人,就是模型在某個下游任務 A 微調后,可以將低秩矩陣那部分的參數解耦出來,還原出預訓練權重,從而繼續在另一個下游任務 B 中進行微調。
deftrain(self,mode:bool=True):
nn.Linear.train(self,mode)
self.lora_A.train(mode)
self.lora_B.train(mode)
ifxxx:
...
#前一個分支是代表mode=False,進入該分支說明mode=True,即調用了model.train(),
#那么當低秩矩陣 A, B 已經合并至原權重矩陣 W 中時,就需要將它們分解出來,以便進行訓練(預訓練權重 W 無需訓練)。
elifself.merge_weightsandself.merged:
#Makesurethattheweightsarenotmerged
ifself.r>0andany(self.enable_lora):
#delta_W=BA
delta_w=(
F.conv1d(
self.lora_A.weight.data.unsqueeze(0),
self.lora_B.weight.data,
groups=sum(self.enable_lora),
)
.squeeze(0)
.transpose(-2,-1)
)
#W=W-delta_W
self.weight.data-=transpose(self.zero_pad(delta_w*self.scaling),notself.fan_in_fan_out)
self.merged=False
還原了預訓練權重后,如果你不想使用低秩矩陣那部分的參數,也可以(見以下第一個分支):
defforward(self,x:torch.Tensor):
#當指定不需要使用adapters部分(在這里即低秩分解矩陣delta_W=BA這部分),
#則將已經合并到預訓練權重W中的delta_W解耦出來,僅用預訓練權重W對輸入x進行前向操作
ifself.disable_adapters:
ifself.r>0andself.mergedandany(self.enable_lora):
delta_w=(
F.conv1d(
self.lora_A.weight.data.unsqueeze(0),
self.lora_B.weight.data,
groups=sum(self.enable_lora),
)
.squeeze(0)
.transpose(-2,-1)
)
#W=W-delta_W
self.weight.data-=transpose(self.zero_pad(delta_w*self.scaling),notself.fan_in_fan_out)
self.merged=False
returnF.linear(x,transpose(self.weight,self.fan_in_fan_out),bias=self.bias)
#當使用 adapters 并且低秩分解矩陣delta_W=BA 已經合并至預訓練權重 W 中,則直接進行前向過程即可。
elifself.merged:
...
#當使用 adapters 但低秩分解矩陣delta_W=BA 未合并到預訓練權重 W 中,則“分步”進行前向過程:
#先用預訓練權重 W 對輸入 x 實施前向過程,得到 Wx;
#再將輸入 x 喂給低秩分解矩陣delta_W=BA,得到 adapters 的輸出(delta_W)x;
#接著對 adapters 部分的輸出作零填充使得其與 Wx 的 shape 一致,并且進行縮放(scale);
#最后再將這部分的結果加回至 Wx 中。
else:
...
進擊的 LoRA
攻破了 LoRA 七問之后,是時候來些更深層次的思想活動了。
Rank r 的設置
一個很直接的問題就是:在實踐中,rank 應該設為多少比較合適呢?
作者做了幾組實驗進行比較,結果發現 rank 可以很低,不超過8就很 OK 了,甚至是1也挺好..
Low Rank 的有效性
看到前面這個實驗現象,作者“忍不住”認為: 擁有很低的本征秩(intrinsic rank),增加 rr 并不能使其覆蓋到更多有意義的子空間,low rank 萬歲!
不過, 他并非是個嘴炮, 對于這一直覺, 他還是仔仔細細地做了個實驗去驗證的。具體做法就是: 以同樣的預訓練模型, 分別用 和 兩種 rank 設置去應用 LoRA 進行微調, 然后將訓好的低秩矩陣 拿出來做奇異值分解, 得到它們的右奇異單位矩陣 , 最后去比較它們的 top 奇異值向量所張成(span)的子空間的重合程度(使用 Grassmann Distance 度量), 公式表示為(對應 paper 中的公式4):
其中 對應于 的 top-i 奇異值向量的列, 同理。 的取值范圍是 , 越大代表兩個子空間重合度越高。
根據上圖的實驗結果顯示,兩者在最 top 的那些奇異值向量所張成的空間重合度最高,特別是在 top-1 處,這也對前面一節“ 效果也不錯”提供了一種解釋。
由于在兩種 rank 的設置下使用的是同一個預訓練模型,并且經過同樣的下游訓練后,兩者在 top 奇異值向量的方向上比較一致(其余方向上則相關度較小),因此這說明 top 奇異值向量所指的方向對于下游任務i 來說是最有用的,而其它方向可能更多地是一些隨機噪聲的方向,這可能是訓練過程中被潛在地積累下來的。
于是乎,low rank 對于 才是正解。
低秩矩陣與預訓練權重矩陣的關系
剛接觸 LoRA 時, CW 就很好奇訓出來的這個 與原來的預訓練權重 到底有沒有“血緣關系", 它與 的相關度是怎么樣的呢?
作者也對這個問題進行了探究, 他將 映射到 的 維子空間, 得到 ,其中 分別是 左、右奇異向量矩陣。然后計算 的 Frobenius 范數 (后續簡稱為 范數, 即所有元素的平方和再開方) 。作為對照組, 作者還將 分別映射到了自身和一個隨機矩陣的 top-r 奇異值向量空間。
由實驗結果可以看出, 預訓練權重矩陣 與低秩矩陣 還算是“近親":比起隨機矩陣, 映射到 的子空間后的數值會大一些。
低秩矩陣的優化方向
以上實驗結果還揭示出兩點:
- 低秩矩陣在經過下游訓練后放大了那些在預訓練時沒有被強調的向量方向;
- 對于上一點, 較低時放大程度更強 (
咋一看實驗結果,可能無法 get 到以上第一點,那么該如何理解呢?
還是得看回上面那張實驗結果圖, 無論是 還是 的情況, 映射到 的 維子空間后其 F2 范數都非常小(0.32 & 1.90), 說明這些子空間的向量方向并非是 中重要程度比較高的方向, 而是在預訓練時顯得不那么重要的、沒有被強調的方向。但可以看到, 卻不那么小(6.91 & 3.57), 說明在經過下游微調后, 那些原本存在感不強的方向被重視起來了。
由此可知, 在下游訓練過程中, 低秩矩陣 并非簡單地重復預訓練權重矩陣的 top 奇異值向量方向,而是去放大原本在預訓練中沒有被強調的方向。
至于第二點, 我們分別在 和 兩種情況下計算 , 其中 取 那一列的值, 這個計算結果衡量了低秩矩陣在第2點中的放大效應。通過計算, 我們可以發現在秩比較的低的情況下 放大效應更強, 那么這又意味著什么呢?
我們有理由認為, 低秩矩陣 包含著大部分與下游任務相關的向量方向(畢竟是朝著下游最優的方向而進行優化的), 于是以上計算結果就意味著適配下游任務的矩陣的本征秩是低秩的。
巧了! 不小心再次證明了 low rank 對于 才是正解~
低秩是萬能的嗎
CW 在以上多次喊道“low rank 對于 是正解”其實有點過于夸張了。首先,作者的實驗場景十分有限,沒有在更廣泛的 case 上進行驗證;其次,我們也不應該無腦地認為以一個很小的 值就能夠在所有任務和數據集上 work。
想象下,當下游任務與預訓練任務的差異(gap)巨大時(比如在英文上預訓練、而在中文上微調),使用很小的 值應該不會有好效果,這時候去微調模型所有的參數(可以令 )應該會得到更好的效果,畢竟中英文的向量空間重合度應該不那么高,我們需要讓更多的參數“調頭”,轉向到適配中文的空間中去。
Example: 在單卡 12G 左右的顯存下微調 BLOOM-7B
最后這部分給一個利用 LoRA 進行微調的例子,這個 demo 基于 Huggingface 的 PEFT 庫,使用了 LoRA + 8bit 訓練,其中 8bit 訓練需要安裝 bitsandbytes。在單卡的條件下,12G 左右的顯存即可玩起 7B 的 BLOOM。
一些雜碎
先來做一些瑣碎小事:導入模塊、設置數據集相關參數、訓練參數 以及 隨機種子等。
importgc
importos
importsys
importpsutil
importargparse
importthreading
importtorch
importtorch.nnasnn
importnumpyasnp
fromtqdmimporttqdm
fromtorch.utils.dataimportDataLoader
fromdatasetsimportload_dataset
fromaccelerateimportAccelerator
fromtransformersimport(
AutoModelForCausalLM,
AutoTokenizer,
default_data_collator,
get_linear_schedule_with_warmup,
set_seed,
)
frompeftimportLoraConfig,TaskType,get_peft_model,prepare_model_for_int8_training
defset_seed(seed:int):
"""
Helperfunctionforreproduciblebehaviortosettheseedin`random`,`numpy`,`torch`and/or`tf`(ifinstalled).
Args:
seed(`int`):Theseedtoset.
"""
random.seed(seed)
np.random.seed(seed)
ifis_torch_available():
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
#^^safetocallthisfunctionevenifcudaisnotavailable
ifis_tf_available():
tf.random.set_seed(seed)
defmain():
accelerator=Accelerator()
dataset_name="twitter_complaints"
text_column="Tweettext"
label_column="text_label"
#樣本的最大句長
max_length=64
lr=1e-3
batch_size=8
num_epochs=20
#設置隨機種子(42yyds!)
seed=42
set_seed(seed)
數據加載和預處理
這里使用的數據集是 RAFT(The Real-world Annotated Few-shot Tasks) 任務中的 Twitter Complaints,有50個訓練樣本和3399個測試樣本。
下面給出數據加載和預處理的邏輯,代碼本身簡單明了,無需啰嗦。
'''DatsetandDataloader'''
dataset=load_dataset("ought/raft",dataset_name,cache_dir=args.data_cache_dir)
classes=[k.replace("_","")forkindataset["train"].features["Label"].names]
dataset=dataset.map(
lambdax:{"text_label":[classes[label]forlabelinx["Label"]]},
batched=True,
num_proc=4
)
#Preprocessing
tokenizer=AutoTokenizer.from_pretrained(args.model_name_or_path,cache_dir=args.model_cache_dir)
defpreprocess_function(examples):
#注:這里的 batch size 并非訓練時的 batch size,
#在這個數據預處理的過程中,也是批處理的,所以這里也有個batchsize的概念
batch_size=len(examples[text_column])
#Addprompt'Label'toinputtext
inputs=[f"{text_column}:{x}Label:"forxinexamples[text_column]]
targets=[str(x)forxinexamples[label_column]]
model_inputs=tokenizer(inputs)
labels=tokenizer(targets)
#依次處理每個樣本
foriinrange(batch_size):
sample_input_ids=model_inputs["input_ids"][i]
label_input_ids=labels["input_ids"][i]+[tokenizer.pad_token_id]
#將輸入文本(model_inputs)與標簽(labels)“對齊”(設置成一樣),然后將標簽中對應輸入文本的部分設為-100,
#這樣在計算 loss 時就不會計算這部分,僅計算真實標簽文本的那部分。
#Addlabeltexttoinputtext
model_inputs["input_ids"][i]=sample_input_ids+label_input_ids
#Letthelabelvaluewhichcorrespondtotheinputtextwordtobe-100
labels["input_ids"][i]=[-100]*len(sample_input_ids)+label_input_ids
model_inputs["attention_mask"][i]=[1]*len(model_inputs["input_ids"][i])
#Putpadtokensatthefrontoftheinputs,andtruncateto'max_length'
foriinrange(batch_size):
sample_input_ids=model_inputs["input_ids"][i]
label_input_ids=labels["input_ids"][i]
pad_length=max_length-len(sample_input_ids)
labels["input_ids"][i]=[-100]*pad_length+label_input_ids
model_inputs["input_ids"][i]=[tokenizer.pad_token_id]*pad_length+sample_input_ids
model_inputs["attention_mask"][i]=[0]*pad_length+model_inputs["attention_mask"][i]
#Totensor
model_inputs["input_ids"][i]=torch.tensor(model_inputs["input_ids"][i][:max_length])
model_inputs["attention_mask"][i]=torch.tensor(model_inputs["attention_mask"][i][:max_length])
labels["input_ids"][i]=torch.tensor(labels["input_ids"][i][:max_length])
model_inputs["labels"]=labels["input_ids"]
returnmodel_inputs
deftest_preprocess_function(examples):
batch_size=len(examples[text_column])
inputs=[f"{text_column}:{x}Label:"forxinexamples[text_column]]
model_inputs=tokenizer(inputs)
foriinrange(batch_size):
sample_input_ids=model_inputs["input_ids"][i]
pad_length=max_length-len(sample_input_ids)
model_inputs["input_ids"][i]=[tokenizer.pad_token_id]*pad_length+sample_input_ids
model_inputs["attention_mask"][i]=[0]*pad_length+model_inputs["attention_mask"][i]
#Totensor
model_inputs["input_ids"][i]=torch.tensor(model_inputs["input_ids"][i][:max_length])
model_inputs["attention_mask"][i]=torch.tensor(model_inputs["attention_mask"][i][:max_length])
returnmodel_inputs
withaccelerator.main_process_first():
processed_datasets=dataset.map(
preprocess_function,
batched=True,
num_proc=4,
remove_columns=dataset["train"].column_names,
load_from_cache_file=True,
desc="Runningtokenizerondataset",
)
accelerator.wait_for_everyone()
train_dataset=processed_datasets["train"]
withaccelerator.main_process_first():
processed_datasets=dataset.map(
test_preprocess_function,
batched=True,
num_proc=4,
remove_columns=dataset["train"].column_names,
load_from_cache_file=False,
desc="Runningtokenizerondataset",
)
eval_dataset=processed_datasets["train"]
test_dataset=processed_datasets["test"]
#Dataloaders
train_dataloader=DataLoader(
train_dataset,shuffle=True,collate_fn=default_data_collator,
batch_size=batch_size,pin_memory=True,num_workers=4
)
eval_dataloader=DataLoader(
eval_dataset,collate_fn=default_data_collator,
batch_size=batch_size,pin_memory=True,num_workers=4
)
test_dataloader=DataLoader(
test_dataset,collate_fn=default_data_collator,
batch_size=batch_size,pin_memory=True,num_workers=4
)
print(f"The1sttrainbatchsample:{next(iter(train_dataloader))}
")
Model, Optimizer & Lr scheduler
必備套裝:模型、優化器、學習率的調度。
'''Model,Optimizer,LrScheduler'''
#creatingmodel
model=AutoModelForCausalLM.from_pretrained(
args.model_name_or_path,
cache_dir=args.model_cache_dir,
load_in_8bit=args.load_in_8bit,
device_map='auto'#Adevicemapneedstobepassedtorunconvertmodelsintomixed-int8format
)
'''Post-processingonthemodel,includes:
1-Castthelayernorminfp32;
2-makingoutputembeddinglayerrequiregrads;
3-Anablegradientcheckpointingformemoryefficiency;
4-Addtheupcastingofthelmheadtofp32
'''
model=prepare_model_for_int8_training(model)
#配置LoRA的一些參數
peft_config=LoraConfig(task_type=TaskType.CAUSAL_LM,inference_mode=False,r=8,lora_alpha=32,lora_dropout=0.1)
#對模型應用LoRA
model=get_peft_model(model,peft_config)
#打印出可訓練的參數個數
model.print_trainable_parameters()
#optimizer
optimizer=torch.optim.AdamW(model.parameters(),lr=args.lr)
#lrscheduler
lr_scheduler=get_linear_schedule_with_warmup(
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=(len(train_dataloader)*num_epochs),
)
model,train_dataloader,eval_dataloader,test_dataloader,optimizer,lr_scheduler=accelerator.prepare(
model,train_dataloader,eval_dataloader,test_dataloader,optimizer,lr_scheduler
)
accelerator.print(f"Model:{model}
")
Preparation for 8bit Training
這一節進一步解析上一節中的 model = prepare_model_for_int8_training(model) 這部分,它是為了讓訓練過程更穩定,以便得到更好的效果,下面來看看其具體做了些什么。
defprepare_model_for_int8_training(
model,output_embedding_layer_name="lm_head",use_gradient_checkpointing=True,layer_norm_names=["layer_norm"]
):
r"""
Thismethodwrappstheentireprotocolforpreparingamodelbeforerunningatraining.Thisincludes:
1-Castthelayernorminfp322-makingoutputembeddinglayerrequiregrads3-Addtheupcastingofthelm
headtofp32
Args:
model,(`transformers.PreTrainedModel`):
Theloadedmodelfrom`transformers`
"""
loaded_in_8bit=getattr(model,"is_loaded_in_8bit",False)
# 1. 固定預訓練的權重;
#2.將LayerNorm的參數轉換為fp32,這是為了訓練的穩定性
forname,paraminmodel.named_parameters():
#freezebasemodel'slayers
param.requires_grad=False
ifloaded_in_8bit:
#castlayernorminfp32forstabilityfor8bitmodels
ifparam.ndim==1andany(layer_norm_nameinnameforlayer_norm_nameinlayer_norm_names):
param.data=param.data.to(torch.float32)
#讓Embedding層接受梯度,通過對Embedding層注冊forwardhook實現,
# forward hook 的內容會在模型前向過程完成后被調用。
#通過以下可以看到,這里hook的內容是使Embedding層的輸出接受梯度,
#從而梯度可以傳導到 Embedding 層。
ifloaded_in_8bitanduse_gradient_checkpointing:
#Forbackwardcompatibility
ifhasattr(model,"enable_input_require_grads"):
model.enable_input_require_grads()
else:
defmake_inputs_require_grad(module,input,output):
output.requires_grad_(True)
model.get_input_embeddings().register_forward_hook(make_inputs_require_grad)
#enablegradientcheckpointingformemoryefficiency
#對前向過程的中間激活部分的梯度做優化,以優化內存所需。
model.gradient_checkpointing_enable()
#將模型頭部的輸出轉換為fp32以穩定訓練
ifhasattr(model,output_embedding_layer_name):
output_embedding_layer=getattr(model,output_embedding_layer_name)
input_dtype=output_embedding_layer.weight.dtype
classCastOutputToFloat(torch.nn.Sequential):
r"""
Manuallycasttotheexpecteddtypeofthelm_headassometimesthereisafinallayernormthatiscasted
infp32
"""
defforward(self,x):
#這里之所以要先將輸入(x)轉換成該層參數的精度(dtype)是因為上一層可能是LayerNorm,
#而由上可知,我們對LayerNorm的輸出精度轉換成了fp32,因此在這種情況下,就需要先將
#上一層的輸出(也就是該層的輸入 x)先轉成與該層參數同樣的精度。
returnsuper().forward(x.to(input_dtype)).to(torch.float32)
setattr(model,output_embedding_layer_name,CastOutputToFloat(output_embedding_layer))
returnmodel
通過以上的源碼實現并結合 CW 的注釋,可以知道,這部分主要做了以下四件事情:
- 將 LayerNorm 的參數轉換成 fp32 類型;
- 令 Embedding 層的輸出接收梯度,從而使得梯度得以傳遞至該層;
- 優化前向過程產生的中間激活部分的梯度,以減少顯存消耗;
- 將模型頭部(Head)的輸出精度轉換成 fp32 類型
PEFT Model
在這一節,CW 引領大家來看看從普通的 model 轉換成 peft model:_model = get_peft_model(model, peft_config)_ 是怎么做的,以下針對 BLOOM 模型的情況進行解析。
defget_peft_model(model,peft_config):
"""
ReturnsaPeftmodelobjectfromamodelandaconfig.
Args:
model([`transformers.PreTrainedModel`]):Modeltobewrapped.
peft_config([`PeftConfig`]):ConfigurationobjectcontainingtheparametersofthePeftmodel.
"""
model_config=model.config.to_dict()
peft_config.base_model_name_or_path=model.__dict__.get("name_or_path",None)
ifpeft_config.task_typenotinMODEL_TYPE_TO_PEFT_MODEL_MAPPING.keys():
peft_config=_prepare_lora_config(peft_config,model_config)
returnPeftModel(model,peft_config)
ifnotisinstance(peft_config,PromptLearningConfig):
#BLOOM會進入到這個分支
peft_config=_prepare_lora_config(peft_config,model_config)
else:
peft_config=_prepare_prompt_learning_config(peft_config,model_config)
#在我們這個例子里,peft_config.task_type是CAUSAL_LM,
#MODEL_TYPE_TO_PEFT_MODEL_MAPPING[peft_config.task_type]則是PeftModelForCausalLM,
#它是PeftModel的子類,它就是在原模型基礎上對目標模塊做了LoRA轉換的結果
returnMODEL_TYPE_TO_PEFT_MODEL_MAPPING[peft_config.task_type](model,peft_config)
進一步來看看 peft_config = _prepare_lora_config(peft_config, model_config) 這里面的實現,它決定了要對模型的哪些模塊應用 LoRA 。
def_prepare_lora_config(peft_config,model_config):
ifpeft_config.target_modulesisNone:
ifmodel_config["model_type"]notinTRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING:
raiseValueError("Pleasespecify`target_modules`in`peft_config`")
#設置需要進行LoRA轉換的目標模塊,通常是Attention層中的一個或幾個映射矩陣(LinearLayer)
#對于BLOOM,這里返回的是["query_key_value"],對應的是其模型實現中BloomAttention中的QKV映射矩陣
peft_config.target_modules=TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING[model_config["model_type"]]
iflen(peft_config.target_modules)==1:
#這個僅對GPT-2里用到的Conv1D有效
peft_config.fan_in_fan_out=True
#這三個值分別代表Q,K,V映射矩陣是否要應用LoRA
#對于 BLOOM 來說,這里僅對 Q, V 的映射矩陣做轉換,而 K 不做。
peft_config.enable_lora=[True,False,True]
ifpeft_config.inference_mode:
#如果是推理模式,則將低秩矩陣A,B合并到Linear層原來的權重W中
peft_config.merge_weights=True
returnpeft_config
結合 CW 在上面代碼中的注釋可知,對于 BLOOM,peft_config.target_modules是 ["query_key_value"],這對應的是其子模塊 BloomAttention 中的 Q, K, V 映射矩陣:
classBloomAttention(nn.Module):
def__init__(self,config:BloomConfig):
super().__init__()
#省略部分
...
self.hidden_size=config.hidden_size
self.num_heads=config.n_head
self.head_dim=self.hidden_size//self.num_heads
self.split_size=self.hidden_size
self.hidden_dropout=config.hidden_dropout
#省略部分
...
#["query_key_value"]指的就是這個模塊
self.query_key_value=nn.Linear(self.hidden_size,3*self.hidden_size,bias=True)
self.dense=nn.Linear(self.hidden_size,self.hidden_size)
self.attention_dropout=nn.Dropout(config.attention_dropout)
這個 target_modules 也支持自定義,只要和模型實現里的關鍵字匹配得上就行。
訓練
其實就是常規的訓練迭代,只不過這里的特色是利用了 TorchTracemalloc 上下文管理器,它可以方便地計算出 GPU 和 CPU 的消耗(以 MB 計)。
forepochinrange(num_epochs):
withTorchTracemalloc()astracemalloc:
model.train()
total_loss=0
forstep,batchinenumerate(tqdm(train_dataloader)):
#Forward
outputs=model(**batch)
loss=outputs.loss
total_loss+=loss.detach().float()
#Backward
accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
ifstep%3==0:
accelerator.print(f"epoch{epoch+1} step{step+1} loss{loss.item()}")
epoch_loss=total_loss/len(train_dataloader)
epoch_ppl=torch.exp(epoch_loss)
accelerator.print(f"[Epoch{epoch+1}] totalloss:{epoch_loss} perplexity:{epoch_ppl}
")
#PrintingtheGPUmemoryusagedetailssuchasallocatedmemory,peakmemory,andtotalmemoryusage
accelerator.print("GPUMemorybeforeenteringthetrain:{}".format(b2mb(tracemalloc.begin)))
accelerator.print("GPUMemoryconsumedattheendofthetrain(end-begin):{}".format(tracemalloc.used))
accelerator.print("GPUPeakMemoryconsumedduringthetrain(max-begin):{}".format(tracemalloc.peaked))
accelerator.print(
"GPUTotalPeakMemoryconsumedduringthetrain(max):{}
".format(
tracemalloc.peaked+b2mb(tracemalloc.begin)
)
)
accelerator.print("CPUMemorybeforeenteringthetrain:{}".format(b2mb(tracemalloc.cpu_begin)))
accelerator.print("CPUMemoryconsumedattheendofthetrain(end-begin):{}".format(tracemalloc.cpu_used))
accelerator.print("CPUPeakMemoryconsumedduringthetrain(max-begin):{}".format(tracemalloc.cpu_peaked))
accelerator.print(
"CPUTotalPeakMemoryconsumedduringthetrain(max):{}
".format(
tracemalloc.cpu_peaked+b2mb(tracemalloc.cpu_begin)
)
)
train_epoch_loss=total_loss/len(eval_dataloader)
train_ppl=torch.exp(train_epoch_loss)
accelerator.print(f"{epoch=}:{train_ppl=}{train_epoch_loss=}
")
順便秀一波訓練期間 GPU 和 CPU 的資源消耗情況(以下單位均為 MB):
某個 epoch 的訓練資源消耗
哦?你說你好奇 TorchTracemalloc 是如何實現的?OK,CW 也不吝嗇,這就為您獻上:
defb2mb(x):
"""ConvertingBytestoMegabytes."""
returnint(x/2**20)
classTorchTracemalloc:
"""Thiscontextmanagerisusedtotrackthepeakmemoryusageoftheprocess."""
def__enter__(self):
gc.collect()
torch.cuda.empty_cache()
#Resetthepeakgaugetozero
torch.cuda.reset_max_memory_allocated()
#返回當前的顯存占用
self.begin=torch.cuda.memory_allocated()
self.process=psutil.Process()
self.cpu_begin=self.cpu_mem_used()
self.peak_monitoring=True
peak_monitor_thread=threading.Thread(target=self.peak_monitor_func)
peak_monitor_thread.daemon=True
peak_monitor_thread.start()
returnself
defcpu_mem_used(self):
"""Getresidentsetsizememoryforthecurrentprocess"""
returnself.process.memory_info().rss
defpeak_monitor_func(self):
self.cpu_peak=-1
whileTrue:
self.cpu_peak=max(self.cpu_mem_used(),self.cpu_peak)
#can'tsleeporwillnotcatchthepeakright(thiscommentishereonpurpose)
#time.sleep(0.001)#1msec
ifnotself.peak_monitoring:
break
def__exit__(self,*exc):
self.peak_monitoring=False
gc.collect()
torch.cuda.empty_cache()
self.end=torch.cuda.memory_allocated()
self.peak=torch.cuda.max_memory_allocated()
self.used=b2mb(self.end-self.begin)
self.peaked=b2mb(self.peak-self.begin)
self.cpu_end=self.cpu_mem_used()
self.cpu_used=b2mb(self.cpu_end-self.cpu_begin)
self.cpu_peaked=b2mb(self.cpu_peak-self.cpu_begin)
評估
評估與訓練的玩法基本類似,只不過前向過程需要調用的是模型的 generate() 方法,而非 forward(),前者是 auto-regressive 的方式。
model.eval()
eval_preds=[]
withTorchTracemalloc()astracemalloc:
forbatchintqdm(eval_dataloader):
batch={k:vfork,vinbatch.items()ifk!="labels"}
withtorch.no_grad():
#注:推理過程用的是 auto-regressive 的方式,調用的是模型的 generate()方法
outputs=accelerator.unwrap_model(model).generate(**batch,max_new_tokens=10)
outputs=accelerator.pad_across_processes(outputs,dim=1,pad_index=tokenizer.pad_token_id)
preds=accelerator.gather(outputs)
#Thepartbefore'max_length'belongstoprompts
preds=preds[:,max_length:].detach().cpu().numpy()
#'skip_special_tokens=True'willignorethosesspecialtokens(e.g.padtoken)
eval_preds.extend(tokenizer.batch_decode(preds,skip_special_tokens=True))
#PrintingtheGPUmemoryusagedetailssuchasallocatedmemory,peakmemory,andtotalmemoryusage
accelerator.print("GPUMemorybeforeenteringtheeval:{}".format(b2mb(tracemalloc.begin)))
accelerator.print("GPUMemoryconsumedattheendoftheeval(end-begin):{}".format(tracemalloc.used))
accelerator.print("GPUPeakMemoryconsumedduringtheeval(max-begin):{}".format(tracemalloc.peaked))
accelerator.print(
"GPUTotalPeakMemoryconsumedduringtheeval(max):{}
".format(
tracemalloc.peaked+b2mb(tracemalloc.begin)
)
)
accelerator.print("CPUMemorybeforeenteringtheeval:{}".format(b2mb(tracemalloc.cpu_begin)))
accelerator.print("CPUMemoryconsumedattheendoftheeval(end-begin):{}".format(tracemalloc.cpu_used))
accelerator.print("CPUPeakMemoryconsumedduringtheeval(max-begin):{}".format(tracemalloc.cpu_peaked))
accelerator.print(
"CPUTotalPeakMemoryconsumedduringtheeval(max):{}
".format(
tracemalloc.cpu_peaked+b2mb(tracemalloc.cpu_begin)
)
)
assertlen(eval_preds)==len(dataset["train"][label_column]),
f"{len(eval_preds)}!={len(dataset['train'][label_column])}"
correct=total=0
forpred,trueinzip(eval_preds,dataset["train"][label_column]):
ifpred.strip()==true.strip():
correct+=1
total+=1
accuracy=correct/total*100
accelerator.print(f"{accuracy=}
")
accelerator.print(f"Predofthefirst10samples:
{eval_preds[:10]=}
")
accelerator.print(f"Truthofthefirst10samples:
{dataset['train'][label_column][:10]=}
")
推理期間,GPU 和 CPU 的消耗情況如下(以下單位均為 MB):
某個 epoch 訓練后,推理的資源消耗
End
LoRA 作為當今大模型時代最火的技術之一,是否算得上是微調 LLMs 的正確姿勢由你們決定。比起正確與否,合不合適才是最重要的。于我而言,只是覺得它好玩而不是無聊的風格而已~
-
模型
+關注
關注
1文章
3226瀏覽量
48809 -
LoRa
+關注
關注
349文章
1689瀏覽量
231910 -
ChatGPT
+關注
關注
29文章
1558瀏覽量
7596 -
LLM
+關注
關注
0文章
286瀏覽量
327
原文標題:當紅炸子雞 LoRA,是當代微調 LLMs 的正確姿勢?
文章出處:【微信號:GiantPandaCV,微信公眾號:GiantPandaCV】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論