0


使用GaLore在本地GPU进行高效的LLM调优

训练大型语言模型(llm),即使是那些“只有”70亿个参数的模型,也是一项计算密集型的任务。这种水平的训练需要的资源超出了大多数个人爱好者的能力范围。为了弥补这一差距,出现了低秩适应(LoRA)等参数高效方法,可以在消费级gpu上对大量模型进行微调。

GaLore是一种新的方法,它不是通过直接减少参数的数量,而是通过优化这些参数的训练方式来降低VRAM需求,也就是说GaLore是一种新的模型训练策略,可让模型使用全部参数进行学习,并且比LoRA更省内存。

GaLore将这些梯度投影到低秩空间上,显著减少了计算负荷,同时保留了训练所需的基本信息。与传统的优化器在反向传播后同时更新所有层的方法不同,GaLore在反向传播期间实现逐层更新。这种方法进一步减少了整个训练过程中的内存占用。

就像LoRA一样,GaLore可以让我们在具有24 GB VRAM的消费级GPU上微调7B模型。结果模型的性能与全参数微调相当,并且似乎优于LoRA。

优于目前Hugging Face还没有官方代码,我们就来手动使用论文的代码进行训练,并与LoRA进行对比

安装依赖

首先就要安装GaLore

  1. pip install galore-torch

然后我们还要一下这些库,并且请注意版本

  1. datasets==2.18.0
  2. transformers==4.39.1
  3. trl==0.8.1
  4. accelerate==0.28.0
  5. torch==2.2.1

调度器和优化器的类

Galore分层优化器是通过模型权重挂钩激活的。由于我们使用Hugging Face

  1. Trainer

,还需要自己实现一个优化器和调度器的抽象类。这些类的结构不执行任何操作。

  1. from typing import Optional
  2. import torch
  3. # Approach taken from Hugging Face transformers https://github.com/huggingface/transformers/blob/main/src/transformers/optimization.py
  4. class LayerWiseDummyOptimizer(torch.optim.Optimizer):
  5. def __init__(self, optimizer_dict=None, *args, **kwargs):
  6. dummy_tensor = torch.randn(1, 1)
  7. self.optimizer_dict = optimizer_dict
  8. super().__init__([dummy_tensor], {"lr": 1e-03})
  9. def zero_grad(self, set_to_none: bool = True) -> None:
  10. pass
  11. def step(self, closure=None) -> Optional[float]:
  12. pass
  13. class LayerWiseDummyScheduler(torch.optim.lr_scheduler.LRScheduler):
  14. def __init__(self, *args, **kwargs):
  15. optimizer = LayerWiseDummyOptimizer()
  16. last_epoch = -1
  17. verbose = False
  18. super().__init__(optimizer, last_epoch, verbose)
  19. def get_lr(self):
  20. return [group["lr"] for group in self.optimizer.param_groups]
  21. def _get_closed_form_lr(self):
  22. return self.base_lrs

加载GaLore优化器

GaLore优化器的目标是特定的参数,主要是那些在线性层中以attn或mlp命名的参数。通过系统地将函数与这些目标参数挂钩,GaLore 8位优化器就会开始工作。

  1. from transformers import get_constant_schedule
  2. from functools import partial
  3. import torch.nn
  4. import bitsandbytes as bnb
  5. from galore_torch import GaLoreAdamW8bit
  6. def load_galore_optimizer(model, lr, galore_config):
  7. # function to hook optimizer and scheduler to a given parameter
  8. def optimizer_hook(p, optimizer, scheduler):
  9. if p.grad is not None:
  10. optimizer.step()
  11. optimizer.zero_grad()
  12. scheduler.step()
  13. # Parameters to optimize with Galore
  14. galore_params = [
  15. (module.weight, module_name) for module_name, module in model.named_modules()
  16. if isinstance(module, nn.Linear) and any(target_key in module_name for target_key in galore_config["target_modules_list"])
  17. ]
  18. id_galore_params = {id(p) for p, _ in galore_params}
  19. # Hook Galore optim to all target params, Adam8bit to all others
  20. for p in model.parameters():
  21. if p.requires_grad:
  22. if id(p) in id_galore_params:
  23. optimizer = GaLoreAdamW8bit([dict(params=[p], **galore_config)], lr=lr)
  24. else:
  25. optimizer = bnb.optim.Adam8bit([p], lr = lr)
  26. scheduler = get_constant_schedule(optimizer)
  27. p.register_post_accumulate_grad_hook(partial(optimizer_hook, optimizer=optimizer, scheduler=scheduler))
  28. # return dummies, stepping is done with hooks
  29. return LayerWiseDummyOptimizer(), LayerWiseDummyScheduler()

HF Trainer

准备好优化器后,我们开始使用Trainer进行训练。下面是一个简单的例子,使用TRL的SFTTrainer (Trainer的子类)在Open Assistant数据集上微调llama2-7b,并在RTX 3090/4090等24 GB VRAM GPU上运行。

  1. from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, set_seed, get_constant_schedule
  2. from trl import SFTTrainer, setup_chat_format, DataCollatorForCompletionOnlyLM
  3. from datasets import load_dataset
  4. import torch, torch.nn as nn, uuid, wandb
  5. lr = 1e-5
  6. # GaLore optimizer hyperparameters
  7. galore_config = dict(
  8. target_modules_list = ["attn", "mlp"],
  9. rank = 1024,
  10. update_proj_gap = 200,
  11. scale = 2,
  12. proj_type="std"
  13. )
  14. modelpath = "meta-llama/Llama-2-7b"
  15. model = AutoModelForCausalLM.from_pretrained(
  16. modelpath,
  17. torch_dtype=torch.bfloat16,
  18. attn_implementation = "flash_attention_2",
  19. device_map = "auto",
  20. use_cache = False,
  21. )
  22. tokenizer = AutoTokenizer.from_pretrained(modelpath, use_fast = False)
  23. # Setup for ChatML
  24. model, tokenizer = setup_chat_format(model, tokenizer)
  25. if tokenizer.pad_token in [None, tokenizer.eos_token]:
  26. tokenizer.pad_token = tokenizer.unk_token
  27. # subset of the Open Assistant 2 dataset, 4000 of the top ranking conversations
  28. dataset = load_dataset("g-ronimo/oasst2_top4k_en")
  29. training_arguments = TrainingArguments(
  30. output_dir = f"out_{run_id}",
  31. evaluation_strategy = "steps",
  32. label_names = ["labels"],
  33. per_device_train_batch_size = 16,
  34. gradient_accumulation_steps = 1,
  35. save_steps = 250,
  36. eval_steps = 250,
  37. logging_steps = 1,
  38. learning_rate = lr,
  39. num_train_epochs = 3,
  40. lr_scheduler_type = "constant",
  41. gradient_checkpointing = True,
  42. group_by_length = False,
  43. )
  44. optimizers = load_galore_optimizer(model, lr, galore_config)
  45. trainer = SFTTrainer(
  46. model = model,
  47. tokenizer = tokenizer,
  48. train_dataset = dataset["train"],
  49. eval_dataset = dataset['test'],
  50. data_collator = DataCollatorForCompletionOnlyLM(
  51. instruction_template = "<|im_start|>user",
  52. response_template = "<|im_start|>assistant",
  53. tokenizer = tokenizer,
  54. mlm = False),
  55. max_seq_length = 256,
  56. dataset_kwargs = dict(add_special_tokens = False),
  57. optimizers = optimizers,
  58. args = training_arguments,
  59. )
  60. trainer.train()

GaLore优化器带有一些需要设置的超参数如下:

target_modules_list:指定GaLore针对的层

rank:投影矩阵的秩。与LoRA类似,秩越高,微调就越接近全参数微调。GaLore的作者建议7B使用1024

update_proj_gap:更新投影的步骤数。这是一个昂贵的步骤,对于7B来说大约需要15分钟。定义更新投影的间隔,建议范围在50到1000步之间。

scale:类似于LoRA的alpha的比例因子,用于调整更新强度。在尝试了几个值之后,我发现scale=2最接近于经典的全参数微调。

微调效果对比

给定超参数的训练损失与全参数调优的轨迹非常相似,表明GaLore分层方法确实是等效的。

用GaLore训练的模型得分与全参数微调非常相似。

GaLore可以节省大约15 GB的VRAM,但由于定期投影更新,它需要更长的训练时间。

上图为2个3090的内存占用对比

训练事件对比,微调:~58分钟。GaLore:约130分钟

最后我们再看看GaLore和LoRA的对比

上图为LoRA微调所有线性层,rank64,alpha 16的损失图

从数值上可以看到GaLore是一种近似全参数训练的新方法,性能与微调相当,比LoRA要好得多。

总结

GaLore可以节省VRAM,允许在消费级GPU上训练7B模型,但是速度较慢,比微调和LoRA的时间要长差不多两倍的时间。

GaLore: Memory-Efficient LLM Training by Gradient Low-Rank Projection.

https://arxiv.org/abs/2403.03507

本文的完整代码

https://github.com/geronimi73/3090_shorts/blob/main/nb_galore_llama2-7b.ipynb

作者:Geronimo

“使用GaLore在本地GPU进行高效的LLM调优”的评论:

还没有评论