LoRA: Low-Rank Adaptation of Large Language Models动机

大模型的参数量都在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架构的每一层。

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

代码

源码: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上微调迄今为止最大的公开可用模型。

推荐内容