大模型的参数量都在100B级别,由于算力的吃紧,在这个基础上进行所有参数的微调变得不可能。LoRA正是在这个背景下提出的解决方案。
原理虽然模型的参数众多,但其实模型主要依赖低秩维度的内容(low intrinsic dimension
),由此引出低秩自适应方法lora,通过低秩分解来模拟参数的改变量,从而以极小的参数量来实现大模型的间接训练。
LoRA的思想也很简单,在原始PLM旁边增加一个旁路,做一个降维再升维的操作,来模拟所谓的intrinsic rank
。
(资料图片)
训练的时候固定PLM的参数,只训练降维矩阵A与升维矩阵B。而模型的输入输出维度不变,输出时将BA与PLM的参数叠加。
用随机高斯分布初始化A,用0矩阵初始化B,保证训练的开始此旁路矩阵依然是0矩阵。
这种思想有点类似于残差连接,同时使用这个旁路的更新来模拟full finetuning的过程。并且,full finetuning可以被看做是LoRA的特例(当r等于k时)
LoRA详细过程在原模型旁边增加一个旁路,通过低秩分解(先降维再升维)来模拟参数的更新量;训练时,原模型固定,只训练降维矩阵A和升维矩阵B;推理时,可将BA加到原参数上,不引入额外的推理延迟;初始化,A采用高斯分布初始化,B初始化为全0,保证训练开始时旁路为0矩阵;可插拔式的切换任务,当前任务W0+B1A1,将lora部分减掉,换成B2A2,即可实现任务切换;秩的选取:对于一般的任务,rank=1,2,4,8足矣,而对于一些领域差距比较大的任务可能需要更大的rank。总的来说,lora就是冻结预先训练的模型权重,并将可训练的秩分解矩阵注入Transformer架构的每一层。
目前对于大多数实验只在 Wq
源码:https://github.com/microsoft/LoRA
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 Configlora_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 trainingmodel = prepare_model_for_int8_training(model)# add LoRA adaptormodel = get_peft_model(model, lora_config)model.print_trainable_parameters()# trainable params: 18874368 || all params: 11154206720 || trainable%: 0.16921300163961817
参考链接:
https://zhuanlan.zhihu.com/p/631077870
https://zhuanlan.zhihu.com/p/636759194
https://zhuanlan.zhihu.com/p/514033873
QLoRA:Efficient Finetuning of Quantized LLMs动机微调非常大的模型的成本过高;对650亿参数的LLaMA模型进行进行16位微调需要超过780GB的GPU内存,QLORA使用一种新的高精度技术将预训练模型量化为int4,然后添加一小组可学习的低秩适配器权重。它是通过量化权重反向传播梯度来调整的。QLORA将65B参数模型进行微调的平均内存需求从 >780GB 的 GPU 内存减少到 <48GB,而不会降低运行时间或预测性能。这标志着LLM微调可访问性的显著转变:现在最大的公开可用的模型,迄今为止在单个GPU上进行微调。
创新首先分析下LoRA微调中的痛点:
参数空间小:LoRA中参与训练的参数量较少,解空间较小,效果相比全量微调有一定的差距。
微调大模型成本高:对于上百亿参数量的模型,LoRA微调的成本还是很高。
精度损失:针对第二点,可以采用int8或int4量化,进一步对模型基座的参数进行压缩。但是又会引发精度损失的问题,降低模型性能。
今天的主角QLoRA优点:
4-bit NormalFloat:提出一种理论最优的4-bit的量化数据类型,优于当前普遍使用的FP4与Int4。对于正态分布权重而言,一种信息理论上最优的新数据类型,该数据类型对正态分布数据产生比 4 bit整数和 4bit 浮点数更好的实证结果。QLORA包含一种低精度存储数据类型(通常为4-bit)和一种计算数据类型(通常为BFloat16)。在实践中,QLORA权重张量使用时,需要将将张量去量化为BFloat16,然后在16位计算精度下进行矩阵乘法运算。模型本身用4bit加载,训练时把数值反量化到bf16后进行训练。
Double Quantization:对第一次量化后的那些常量再进行一次量化,减少存储空间。相比于当前的模型量化方法,更加节省显存空间。每个参数平均节省0.37bit,对于65B的LLaMA模型,大约能节省3GB显存空间。
Paged Optimizers:使用NVIDIA统一内存特性,该特性可以在在GPU偶尔OOM的情况下,进行CPU和GPU之间自动分页到分页的传输,以实现无错误的 GPU 处理。该功能的工作方式类似于 CPU 内存和磁盘之间的常规内存分页。使用此功能为优化器状态(Optimizer)分配分页内存,然后在 GPU 内存不足时将其自动卸载到 CPU 内存,并在优化器更新步骤需要时将其加载回 GPU 内存。
增加Adapter:4-bit的NormalFloat与Double Quantization,节省了很多空间,但带来了性能损失,作者通过插入更多adapter来弥补这种性能损失。在LoRA中,一般会选择在query和value的全连接层处插入adapter。而QLoRA则在所有全连接层处都插入了adapter,增加了训练参数,弥补精度带来的性能损失。
参考:
https://zhuanlan.zhihu.com/p/632164305
https://zhuanlan.zhihu.com/p/636215898
https://zhuanlan.zhihu.com/p/634256206
https://zhuanlan.zhihu.com/p/632229856
https://blog.csdn.net/qq_39970492/article/details/131048994
总结QLORA 可以使用 4 位基础模型和低秩适配器 (LoRA) 复制 16 位完全微调性能。QLORA将微调65B参数模型的平均内存需求从>780GB的GPU内存降低到<48GB,与完全微调的16位基准相比,既不降低运行时间也不降低预测性能,这意味着可以在单个GPU上微调迄今为止最大的公开可用模型。