使用GaLore在本地GPU進(jìn)行高效的LLM調(diào)優(yōu)
訓(xùn)練大型語言模型(llm),即使是那些“只有”70億個參數(shù)的模型,也是一項計算密集型的任務(wù)。這種水平的訓(xùn)練需要的資源超出了大多數(shù)個人愛好者的能力范圍。為了彌補(bǔ)這一差距,出現(xiàn)了低秩適應(yīng)(LoRA)等參數(shù)高效方法,可以在消費(fèi)級gpu上對大量模型進(jìn)行微調(diào)。
GaLore是一種新的方法,它不是通過直接減少參數(shù)的數(shù)量,而是通過優(yōu)化這些參數(shù)的訓(xùn)練方式來降低VRAM需求,也就是說GaLore是一種新的模型訓(xùn)練策略,可讓模型使用全部參數(shù)進(jìn)行學(xué)習(xí),并且比LoRA更省內(nèi)存。
GaLore將這些梯度投影到低秩空間上,顯著減少了計算負(fù)荷,同時保留了訓(xùn)練所需的基本信息。與傳統(tǒng)的優(yōu)化器在反向傳播后同時更新所有層的方法不同,GaLore在反向傳播期間實現(xiàn)逐層更新。這種方法進(jìn)一步減少了整個訓(xùn)練過程中的內(nèi)存占用。
就像LoRA一樣,GaLore可以讓我們在具有24 GB VRAM的消費(fèi)級GPU上微調(diào)7B模型。結(jié)果模型的性能與全參數(shù)微調(diào)相當(dāng),并且似乎優(yōu)于LoRA。
優(yōu)于目前Hugging Face還沒有官方代碼,我們就來手動使用論文的代碼進(jìn)行訓(xùn)練,并與LoRA進(jìn)行對比
安裝依賴
首先就要安裝GaLore
pip install galore-torch
然后我們還要一下這些庫,并且請注意版本
datasets==2.18.0
transformers==4.39.1
trl==0.8.1
accelerate==0.28.0
torch==2.2.1
調(diào)度器和優(yōu)化器的類
Galore分層優(yōu)化器是通過模型權(quán)重掛鉤激活的。由于我們使用Hugging Face Trainer,還需要自己實現(xiàn)一個優(yōu)化器和調(diào)度器的抽象類。這些類的結(jié)構(gòu)不執(zhí)行任何操作。
from typing import Optional
import torch
# Approach taken from Hugging Face transformers https://github.com/huggingface/transformers/blob/main/src/transformers/optimization.py
class LayerWiseDummyOptimizer(torch.optim.Optimizer):
def __init__(self, optimizer_dict=None, *args, **kwargs):
dummy_tensor = torch.randn(1, 1)
self.optimizer_dict = optimizer_dict
super().__init__([dummy_tensor], {"lr": 1e-03})
def zero_grad(self, set_to_none: bool = True) -> None:
pass
def step(self, closure=None) -> Optional[float]:
pass
class LayerWiseDummyScheduler(torch.optim.lr_scheduler.LRScheduler):
def __init__(self, *args, **kwargs):
optimizer = LayerWiseDummyOptimizer()
last_epoch = -1
verbose = False
super().__init__(optimizer, last_epoch, verbose)
def get_lr(self):
return [group["lr"] for group in self.optimizer.param_groups]
def _get_closed_form_lr(self):
return self.base_lrs
加載GaLore優(yōu)化器
GaLore優(yōu)化器的目標(biāo)是特定的參數(shù),主要是那些在線性層中以attn或mlp命名的參數(shù)。通過系統(tǒng)地將函數(shù)與這些目標(biāo)參數(shù)掛鉤,GaLore 8位優(yōu)化器就會開始工作。
from transformers import get_constant_schedule
from functools import partial
import torch.nn
import bitsandbytes as bnb
from galore_torch import GaLoreAdamW8bit
def load_galore_optimizer(model, lr, galore_config):
# function to hook optimizer and scheduler to a given parameter
def optimizer_hook(p, optimizer, scheduler):
if p.grad is not None:
optimizer.step()
optimizer.zero_grad()
scheduler.step()
# Parameters to optimize with Galore
galore_params = [
(module.weight, module_name) for module_name, module in model.named_modules()
if isinstance(module, nn.Linear) and any(target_key in module_name for target_key in galore_config["target_modules_list"])
]
id_galore_params = {id(p) for p, _ in galore_params}
# Hook Galore optim to all target params, Adam8bit to all others
for p in model.parameters():
if p.requires_grad:
if id(p) in id_galore_params:
optimizer = GaLoreAdamW8bit([dict(params=[p], **galore_config)], lr=lr)
else:
optimizer = bnb.optim.Adam8bit([p], lr = lr)
scheduler = get_constant_schedule(optimizer)
p.register_post_accumulate_grad_hook(partial(optimizer_hook, optimizer=optimizer, scheduler=scheduler))
# return dummies, stepping is done with hooks
return LayerWiseDummyOptimizer(), LayerWiseDummyScheduler()
HF Trainer
準(zhǔn)備好優(yōu)化器后,我們開始使用Trainer進(jìn)行訓(xùn)練。下面是一個簡單的例子,使用TRL的SFTTrainer (Trainer的子類)在Open Assistant數(shù)據(jù)集上微調(diào)llama2-7b,并在RTX 3090/4090等24 GB VRAM GPU上運(yùn)行。
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, set_seed, get_constant_schedule
from trl import SFTTrainer, setup_chat_format, DataCollatorForCompletionOnlyLM
from datasets import load_dataset
import torch, torch.nn as nn, uuid, wandb
lr = 1e-5
# GaLore optimizer hyperparameters
galore_config = dict(
target_modules_list = ["attn", "mlp"],
rank = 1024,
update_proj_gap = 200,
scale = 2,
proj_type="std"
)
modelpath = "meta-llama/Llama-2-7b"
model = AutoModelForCausalLM.from_pretrained(
modelpath,
torch_dtype=torch.bfloat16,
attn_implementation = "flash_attention_2",
device_map = "auto",
use_cache = False,
)
tokenizer = AutoTokenizer.from_pretrained(modelpath, use_fast = False)
# Setup for ChatML
model, tokenizer = setup_chat_format(model, tokenizer)
if tokenizer.pad_token in [None, tokenizer.eos_token]:
tokenizer.pad_token = tokenizer.unk_token
# subset of the Open Assistant 2 dataset, 4000 of the top ranking conversations
dataset = load_dataset("g-ronimo/oasst2_top4k_en")
training_arguments = TrainingArguments(
output_dir = f"out_{run_id}",
evaluation_strategy = "steps",
label_names = ["labels"],
per_device_train_batch_size = 16,
gradient_accumulation_steps = 1,
save_steps = 250,
eval_steps = 250,
logging_steps = 1,
learning_rate = lr,
num_train_epochs = 3,
lr_scheduler_type = "constant",
gradient_checkpointing = True,
group_by_length = False,
)
optimizers = load_galore_optimizer(model, lr, galore_config)
trainer = SFTTrainer(
model = model,
tokenizer = tokenizer,
train_dataset = dataset["train"],
eval_dataset = dataset['test'],
data_collator = DataCollatorForCompletionOnlyLM(
instruction_template = "<|im_start|>user",
response_template = "<|im_start|>assistant",
tokenizer = tokenizer,
mlm = False),
max_seq_length = 256,
dataset_kwargs = dict(add_special_tokens = False),
optimizers = optimizers,
args = training_arguments,
)
trainer.train()
GaLore優(yōu)化器帶有一些需要設(shè)置的超參數(shù)如下:
target_modules_list:指定GaLore針對的層
rank:投影矩陣的秩。與LoRA類似,秩越高,微調(diào)就越接近全參數(shù)微調(diào)。GaLore的作者建議7B使用1024
update_proj_gap:更新投影的步驟數(shù)。這是一個昂貴的步驟,對于7B來說大約需要15分鐘。定義更新投影的間隔,建議范圍在50到1000步之間。
scale:類似于LoRA的alpha的比例因子,用于調(diào)整更新強(qiáng)度。在嘗試了幾個值之后,我發(fā)現(xiàn)scale=2最接近于經(jīng)典的全參數(shù)微調(diào)。
微調(diào)效果對比
給定超參數(shù)的訓(xùn)練損失與全參數(shù)調(diào)優(yōu)的軌跡非常相似,表明GaLore分層方法確實是等效的。
用GaLore訓(xùn)練的模型得分與全參數(shù)微調(diào)非常相似。
GaLore可以節(jié)省大約15 GB的VRAM,但由于定期投影更新,它需要更長的訓(xùn)練時間。
上圖為2個3090的內(nèi)存占用對比
訓(xùn)練事件對比,微調(diào):~58分鐘。GaLore:約130分鐘
最后我們再看看GaLore和LoRA的對比
上圖為LoRA微調(diào)所有線性層,rank64,alpha 16的損失圖
從數(shù)值上可以看到GaLore是一種近似全參數(shù)訓(xùn)練的新方法,性能與微調(diào)相當(dāng),比LoRA要好得多。
總結(jié)
GaLore可以節(jié)省VRAM,允許在消費(fèi)級GPU上訓(xùn)練7B模型,但是速度較慢,比微調(diào)和LoRA的時間要長差不多兩倍的時間。