大模型的参数量都在100B级别,由于算力的吃紧,在这个基础上进行所有参数的微调变得不可能。LoRA正是在这个背景下提出的解决方案。

1|2原理

虽然模型的参数众多,但其实模型主要依赖低秩维度的内容(low intrinsic dimension),由此引出低秩自适应方法lora,通过低秩分解来模拟参数的改变量,从而以极小的参数量来实现大模型的间接训练。

LoRA的思想也很简单,在原始PLM旁边增加一个旁路,做一个降维再升维的操作,来模拟所谓的 intrinsic rank 。

训练的时候固定PLM的参数,只训练降维矩阵A与升维矩阵B。而模型的输入输出维度不变,输出时将BA与PLM的参数叠加。

用随机高斯分布初始化A,用0矩阵初始化B,保证训练的开始此旁路矩阵依然是0矩阵。

这种思想有点类似于残差连接,同时使用这个旁路的更新来模拟full finetuning的过程。并且,full finetuning可以被看做是LoRA的特例(当r等于k时)

1|3 LoRA详细过程

  • 在原模型旁边增加一个旁路,通过低秩分解(先降维再升维)来模拟参数的更新量;
  • 训练时,原模型固定,只训练降维矩阵A和升维矩阵B;
  • 推理时,可将BA加到原参数上,不引入额外的推理延迟;
  • 初始化,A采用高斯分布初始化,B初始化为全0,保证训练开始时旁路为0矩阵;
  • 可插拔式的切换任务,当前任务W0+B1A1,将lora部分减掉,换成B2A2,即可实现任务切换;
  • 秩的选取:对于一般的任务,rank=1,2,4,8足矣,而对于一些领域差距比较大的任务可能需要更大的rank。

总的来说,lora就是冻结预先训练的模型权重,并将可训练的秩分解矩阵注入Transformer架构的每一层。

目前对于大多数实验只在 Wq 和 Wv使用LoRA,可训练参数的数量由秩r和原始权值的形状决定。

1|4 代码

源码:GitHub - microsoft/LoRA: Code for loralib, an implementation of "LoRA: Low-Rank Adaptation of Large Language Models"

LoRALayer层

 

class LoRALayer(): def __init__( self, r: int, lora_alpha: int, lora_dropout: float, merge_weights: bool, ): self.r = r self.lora_alpha = lora_alpha # Optional dropout if lora_dropout > 0.: self.lora_dropout = nn.Dropout(p=lora_dropout) else: self.lora_dropout = lambda x: x # Mark the weight as unmerged self.merged = False self.merge_weights = merge_weights

Linear层

 

class Linear(nn.Linear, LoRALayer): # LoRA implemented in a dense layer def __init__( self, in_features: int, out_features: int, r: int = 0, lora_alpha: int = 1, lora_dropout: float = 0., fan_in_fan_out: bool = False, # Set this to True if the layer to replace stores weight like (fan_in, fan_out) 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) self.fan_in_fan_out = fan_in_fan_out # Actual trainable parameters if r > 0: self.lora_A = nn.Parameter(self.weight.new_zeros((r, in_features))) self.lora_B = nn.Parameter(self.weight.new_zeros((out_features, r))) self.scaling = self.lora_alpha / self.r # Freezing the pre-trained weight matrix self.weight.requires_grad = False self.reset_parameters() if fan_in_fan_out: self.weight.data = self.weight.data.transpose(0, 1) def reset_parameters(self): nn.Linear.reset_parameters(self) if hasattr(self, 'lora_A'): # initialize A the same way as the default for nn.Linear and B to zero nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5)) nn.init.zeros_(self.lora_B) def train(self, mode: bool = True): def T(w): return w.transpose(0, 1) if self.fan_in_fan_out else w nn.Linear.train(self, mode) if mode: if self.merge_weights and self.merged: # Make sure that the weights are not merged if self.r > 0: self.weight.data -= T(self.lora_B @ self.lora_A) * self.scaling self.merged = False else: if self.merge_weights and not self.merged: # Merge the weights and mark it if self.r > 0: self.weight.data += T(self.lora_B @ self.lora_A) * self.scaling self.merged = True def forward(self, x: torch.Tensor): def T(w): return w.transpose(0, 1) if self.fan_in_fan_out else w if self.r > 0 and not self.merged: result = F.linear(x, T(self.weight), bias=self.bias) if self.r > 0: result += (self.lora_dropout(x) @ self.lora_A.transpose(0, 1) @ self.lora_B.transpose(0, 1)) * self.scaling return result else: return F.linear(x, T(self.weight), bias=self.bias)

Peft实现

 

from peft import LoraConfig, get_peft_model, prepare_model_for_int8_training, TaskType # Define LoRA Config lora_config = LoraConfig( r=16, lora_alpha=32, target_modules=["q", "v"], lora_dropout=0.05, bias="none", task_type=TaskType.SEQ_2_SEQ_LM ) # prepare int-8 model for training model = prepare_model_for_int8_training(model) # add LoRA adaptor model = get_peft_model(model, lora_config) model.print_trainable_parameters() # trainable params: 18874368 || all params: 11154206720 || trainable%: 0.16921300163961817

 参考链接:

[速读经典]LoRA-给大语言模型做Low-Rank低秩改造 - 知乎

高效微调llm-LORA 原理、代码解读、实战 - 知乎

LoRA: Low-Rank Adaptation of Large Language Models 简读 - 知乎

2|0QLoRA:Efficient Finetuning of Quantized LLMs

2|1动机

微调非常大的模型的成本过高;对650亿参数的LLaMA模型进行进行16位微调需要超过780GB的GPU内存,QLORA使用一种新的高精度技术将预训练模型量化为int4,然后添加一小组可学习的低秩适配器权重。它是通过量化权重反向传播梯度来调整的。QLORA将65B参数模型进行微调的平均内存需求从 >780GB 的 GPU 内存减少到 <48GB,而不会降低运行时间或预测性能。这标志着LLM微调可访问性的显著转变:现在最大的公开可用的模型,迄今为止在单个GPU上进行微调。

2|2创新

首先分析下LoRA微调中的痛点:

  1. 参数空间小:LoRA中参与训练的参数量较少,解空间较小,效果相比全量微调有一定的差距。

  2. 微调大模型成本高:对于上百亿参数量的模型,LoRA微调的成本还是很高。

  3. 精度损失:针对第二点,可以采用int8或int4量化,进一步对模型基座的参数进行压缩。但是又会引发精度损失的问题,降低模型性能。

今天的主角QLoRA优点:

  1. 4-bit NormalFloat:提出一种理论最优的4-bit的量化数据类型,优于当前普遍使用的FP4与Int4。对于正态分布权重而言,一种信息理论上最优的新数据类型,该数据类型对正态分布数据产生比 4 bit整数和 4bit 浮点数更好的实证结果。QLORA包含一种低精度存储数据类型(通常为4-bit)和一种计算数据类型(通常为BFloat16)。在实践中,QLORA权重张量使用时,需要将将张量去量化为BFloat16,然后在16位计算精度下进行矩阵乘法运算。模型本身用4bit加载,训练时把数值反量化到bf16后进行训练。

  2. Double Quantization:对第一次量化后的那些常量再进行一次量化,减少存储空间。相比于当前的模型量化方法,更加节省显存空间。每个参数平均节省0.37bit,对于65B的LLaMA模型,大约能节省3GB显存空间。

  3. Paged Optimizers:使用NVIDIA统一内存特性,该特性可以在在GPU偶尔OOM的情况下,进行CPU和GPU之间自动分页到分页的传输,以实现无错误的 GPU 处理。该功能的工作方式类似于 CPU 内存和磁盘之间的常规内存分页。使用此功能为优化器状态(Optimizer)分配分页内存,然后在 GPU 内存不足时将其自动卸载到 CPU 内存,并在优化器更新步骤需要时将其加载回 GPU 内存。

  4. 增加Adapter:4-bit的NormalFloat与Double Quantization,节省了很多空间,但带来了性能损失,作者通过插入更多adapter来弥补这种性能损失。在LoRA中,一般会选择在query和value的全连接层处插入adapter。而QLoRA则在所有全连接层处都插入了adapter,增加了训练参数,弥补精度带来的性能损失。

参考:

QLoRA介绍 - 知乎

大模型参数高效微调技术原理综述(五)-LoRA、AdaLoRA、QLoRA - 知乎

QLoRA: 4bit量化+LoRA训练=瞬间起飞 - 知乎

QLoRA:一种高效LLMs微调方法,48G内存可调65B 模型,调优模型Guanaco 堪比Chatgpt的99.3%! - 知乎

QLoRA:量化 LLM 的高效微调_AI生成式曾小健2的博客-CSDN博客

2|3总结

 QLORA 可以使用 4 位基础模型和低秩适配器 (LoRA) 复制 16 位完全微调性能。QLORA将微调65B参数模型的平均内存需求从>780GB的GPU内存降低到<48GB,与完全微调的16位基准相比,既不降低运行时间也不降低预测性能,这意味着可以在单个GPU上微调迄今为止最大的公开可用模型。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐