Transformers 是一個強大的架構,但模型因其采用的自注意力機制,雖然能夠有效地處理序列數據并捕獲長距離依賴關系,但同時也容易導致在訓練過程中出現OOM(Out of Memory,內存不足)或者達到GPU的運行時限制。
主要是因為
- 參數數量龐大:Transformer模型通常包含大量的參數,尤其是在模型層面進行擴展時(例如,增加層數或頭數)。這些參數需要大量的內存來存儲權重和梯度。
- 自注意力計算:自注意力機制需要對輸入序列的每個元素與其他所有元素計算其相互關系,導致計算復雜度和內存需求隨著輸入長度的增加而顯著增加。對于非常長的序列,這一點尤其突出。
- 激活和中間狀態存儲:在訓練過程中,需要存儲前向傳播中的中間激活狀態,以便于反向傳播時使用。這增加了額外的內存負擔。
為了解決這些問題,我們今天來總結以下一些常用的加速策略
固定長度填充
在處理文本數據時,由于文本序列的長度可能各不相同,但許多機器學習模型(尤其是基于Transformer的模型)需要輸入數據具有固定的尺寸,因此需要對文本序列進行固定長度填充(padding)。
在使用Transformer模型時,填充部分不應影響到模型的學習。因此通常需要使用注意力掩碼(attention mask)來指示模型在自注意力計算時忽略這些填充位置。通過這種固定長度填充和相應的處理方法,可以使得基于Transformer的模型能夠有效地處理不同長度的序列數據。在實際應用中,這種方法是處理文本輸入的常見策略。
def fixed_pad_sequences(sequences, max_length, padding_value=0):padded_sequences = []for sequence in sequences:if len(sequence) >= max_length:padded_sequence = sequence[:max_length] # Trim the sequence if it exceeds max_lengthelse:padding = [padding_value] * (max_length - len(sequence)) # Calculate paddingpadded_sequence = sequence + padding # Pad the sequencepadded_sequences.append(padded_sequence)return padded_sequences
這種方式會將所有的序列填充成一個長度,這樣雖然長度相同了,但是因為序列的實際大小本來就不同,同一批次很可能出現有很多填充的情況,所以就出現了動態填充策略。
動態填充是在每個批處理中動態填充輸入序列到最大長度。與固定長度填充不同,在固定長度填充中,所有序列都被填充以匹配整個數據集中最長序列的長度,動態填充根據該批中最長序列的長度單獨填充每個批中的序列。
這樣雖然每個批次的長度是不同的,但是批次內部的長度是相同的,可以加快處理速度。
def pad_sequences_dynamic(sequences, padding_value=0):max_length = max(len(seq) for seq in sequences) # Find the maximum length in the sequencespadded_sequences = []for sequence in sequences:padding = [padding_value] * (max_length - len(sequence)) # Calculate paddingpadded_sequence = sequence + padding # Pad the sequencepadded_sequences.append(padded_sequence)return padded_sequences
等長匹配
等長匹配是在訓練或推理過程中將長度相近的序列分組成批處理的過程。等長匹配通過基于序列長度將數據集劃分為桶,然后從這些桶中采樣批次來實現的。
從上圖可以看到,通過等長匹配的策略,減少了填充量,這樣也可以加速計算
def uniform_length_batching(sequences, batch_size, padding_value=0):# Sort sequences based on their lengthssequences.sort(key=len)# Divide sequences into buckets based on lengthbuckets = [sequences[i:i+batch_size] for i in range(0, len(sequences), batch_size)]# Pad sequences within each bucket to the length of the longest sequence in the bucketpadded_batches = []for bucket in buckets:max_length = len(bucket[-1]) # Get the length of the longest sequence in the bucketpadded_bucket = []for sequence in bucket:padding = [padding_value] * (max_length - len(sequence)) # Calculate paddingpadded_sequence = sequence + padding # Pad the sequencepadded_bucket.append(padded_sequence)padded_batches.append(padded_bucket)return padded_batches
自動混合精度
自動混合精度(AMP)是一種通過使用單精度(float32)和半精度(float16)算法的組合來加速深度學習模型訓練的技術。它利用了現代gpu的功能,與float32相比,使用float16數據類型可以更快地執行計算,同時使用更少的內存。
import torchfrom torch.cuda.amp import autocast, GradScaler# Define your modelmodel = YourModel()# Define optimizer and loss functionoptimizer = torch.optim.Adam(model.parameters(), lr=1e-3)criterion = torch.nn.CrossEntropyLoss()# Create a GradScaler object for gradient scalingscaler = GradScaler()# Inside the training loopfor inputs, targets in dataloader:# Clear previous gradientsoptimizer.zero_grad()# Cast inputs and targets to the appropriate deviceinputs, targets = inputs.to(device), targets.to(device)# Enable autocasting for forward passwith autocast():# Forward passoutputs = model(inputs)loss = criterion(outputs, targets)# Backward pass# Scale the loss valuescaler.scale(loss).backward()# Update model parametersscaler.step(optimizer)# Update the scale for next iterationscaler.update()
AMP在訓練過程中動態調整計算精度,允許模型在大多數計算中使用float16,同時自動將某些計算提升為float32,以防止下流或溢出等數值不穩定問題。
Fp16 vs Fp32
雙精度(FP64)消耗64位。符號值為1位,指數值為11位,有效精度為52位。
單精度(FP32)消耗32位。符號值為1位,指數值為8位,有效精度為23位。
半精度(FP16)消耗16位。符號值為1位,指數值為5位,有效精度為10位。
所以Fp16可以提高內存節省,并可以大大提高模型訓練的速度。考慮到Fp16的優勢和它在模型使用方面的主導區域,它非常適合推理任務。但是fp16會產生數值精度的損失,導致計算或存儲的值不準確,考慮到這些值的精度至關重要。
另外就是這種優化師針對于分類任務的,對于回歸這種需要精確數值的任務Fp16的表現并不好。
總結
以上這些方法,可以在一定程度上緩解內存不足和計算資源的限制,但是對于大型的模型我們還是需要一個強大的GPU。
https://avoid.overfit.cn/post/7240bee210cd408a90ca04279830040e