AI 文摘

PEFTLoRA实现及核心源码解读





作者: AINLP 来源: AINLP

1. PEFT 介绍

HuggingFace的PEFT(Parameter-Efficient Fine-Tuning)中提供了模型微调加速的方法,参数高效微调(PEFT)方法能够使预先训练好的语言模型(PLMs)有效地适应各种下游应用,而不需要对模型的所有参数进行微调。

对大规模的PLM进行微调往往成本过高,在这方面,PEFT方法只对少数(额外的)模型参数进行微调,基本思想在于仅微调少量 (额外) 模型参数,同时冻结预训练 LLM 的大部分参数,从而大大降低了计算和存储成本,这也克服了灾难性遗忘的问题,这是在 LLM 的全参数微调期间观察到的一种现象,同时PEFT 方法也显示出在低数据状态下比微调更好,可以更好地泛化到域外场景,参数高效微调有如下好处

(1)由于在训练时只更新少量参数,可以大大减少GPU显存的使用量,让大语言模型可以在消费级GPU进行训练。

(2)大模型的参数存在冗余,通过参数有效微调可以达到甚至比全参数微调的效果还要好

(3)无需全量保存参数,减少下游子任务参数存储成本

**2.**LoRA 介绍

LoRA的原理比较简单,原始全量微调就是在原始模型参数上通过微调加入增量W=W0+ΔW,那我们可以通过冻结原始参数W0,并且把增量部分通过低秩分解方式进一步降低参数量级ΔW=AB,原始参数的维度是dd, 则低秩分解后的参数量级是2rd,因为这里的r«d,因此可以起到大幅降低微调参数量级的效果,如下图

总结Lora特点:

(1)给原模型增加旁路,通过低秩分解(先降维再升维)来模拟参数的更新量;

(2)训练时,原模型固定,只训练降维矩阵A和升维矩B;

(3)推理时,可将BA加到原参数上,不引入额外的推理延迟;

(4)初始化,A采用高斯分布初始化,B初始化为全0,保证训练开始时旁路为0矩阵(严格讲,对不同算子采用不同的A初始化方式,例如,Linear算子采用kaiming_uniform,对于Embedding算子采用normal高斯分布);

(5)可插拔式的切换任务,当前任务W0+B1A1,将lora部分减掉,换成B2A2,即可实现任务切换。

3. PEFT 实现****LoRA

基于PEFT框架实现指定模型的LoRA封装非常方便,仅需实例化模型,设置LoRA的config文件,调用get_peft_model方法即可。基于Transformer结构,LoRA一般只对每层的Self-Attention的部分进行微调,即对Wq、Wk、Wv、Wo四个映射层参数进行微调。消融实验显示只微调Wq效果略差,微调Wq、Wv的效果和微调Wq、Wk、Wv、Wo的效果相似(如下所示),以下示例微调Wq、Wv。

3.1 PEFT核心类

截至发文,PEFT支持LoRA、Prefix Tuning、P-Tuning、Prompt Tuning、AdaLoRA、Adaption Prompt共六种对大模型高效调参的方法,分别对应框架六个核心方法类即LoraModel、PrefixEncoder、PromptEncoder、PromptEmbedding、AdaLoraModel、AdaptionPromptModel,每个核心方法对应配置文件类为LoraConfig、PrefixTuningConfig、PromptEncoderConfig、PromptTuningConfig、AdaLoraConfig、AdaptionPromptConfig,如下

PeftModel类支持上述六种方法配置文件,对PromptLearning类 (PrefixTuning、PromptEncoder、PromptTuning)和非PromptLearning类(LORA、ADALORA、ADAPTION_PROMPT)不同分支进行处理,得到对应的base_model,其__init__函数如下


def __init__(self, model: PreTrainedModel, peft_config: PeftConfig, adapter_name: str = "default"):
    super().__init__()
    self.base_model = model
    self.config = self.base_model.config
    self.modules_to_save = None
    self.peft_config = {}
    self.active_adapter = adapter_name
    self.peft_type = peft_config.peft_type
    self.base_model_torch_dtype = getattr(model, "dtype", None)
    if not isinstance(peft_config, PromptLearningConfig):
        self.peft_config[adapter_name] = peft_config
        self.base_model = PEFT_TYPE_TO_MODEL_MAPPING[peft_config.peft_type](
            self.base_model, self.peft_config, adapter_name
        )
        self.set_additional_trainable_modules(peft_config, adapter_name)
    else:
        self.add_adapter(adapter_name, peft_config)
  

    if getattr(model, "is_gradient_checkpointing", True):
        model = self._prepare_model_for_gradient_checkpointing(model)

根据不同高效调参方法适配PeftModel类得到base_model,不同下游子任务基于PeftModel类构造子任务类,PEFT框架当前支持五种下游子任务,即PeftModelForSequenceClassification、PeftModelForSeq2SeqLM、PeftModelForCausalLM、PeftModelForTokenClassification、PeftModelForQuestionAnswering,这些子任务类就是借助PEFT框架形成的最终的peft_model,用于后续训练与推理。

3.2 LoraModel类的实现

LoraModel类实现对模型Lora化的方法封装,以下显式调用LoraModel实现Lora


from transformers import AutoModelForSeq2SeqLM, LoraConfig
from peft import LoraModel, LoraConfig
# step1. Lora配置
config = LoraConfig(peft_type="LORA", task_type="SEQ_2_SEQ_LM", r=8, lora_alpha=32, target_modules=["q", "v"], lora_dropout=0.01,...)
# step2. 预训练模型加载
model = AutoModelForSeq2SeqLM.from_pretrained("t5-base")
# step3. 显示生成Lora模型
lora_model = LoraModel(config, model)

LoraModel是如何实现的呢?其__init__函数如下


class LoraModel(torch.nn.Module):
    def __init__(self, model, config, adapter_name):
        super().__init__()
        self.model = model
        self.forward = self.model.forward
        self.peft_config = config
        self.add_adapter(adapter_name, self.peft_config[adapter_name])

主要实现对预训练模型add_adapter操作,该操作由_find_and_replace和mark_only_lora_as_trainable组成,_find_and_replace找到所有需要加入lora策略的层,例如q_proj,把它们替换成lora模式,mark_only_lora_as_trainable保留lora部分的参数可训练,其余参数全都固定下来不动。


  def add_adapter(self, adapter_name, config=None):
      if config is not None:
          model_config = self.model.config.to_dict() if hasattr(self.model.config, "to_dict") else self.model.config
          config = self._prepare_lora_config(config, model_config)
          self.peft_config[adapter_name] = config
      self._find_and_replace(adapter_name)
      if len(self.peft_config) > 1 and self.peft_config[adapter_name].bias != "none":
          raise ValueError(
              "LoraModel supports only 1 adapter with bias. When using multiple adapters, set bias to 'none' for all adapters."
          )
      mark_only_lora_as_trainable(self.model, self.peft_config[adapter_name].bias)
      if self.peft_config[adapter_name].inference_mode:
          _freeze_adapter(self.model, adapter_name)

3.3 LoraLayer层的实现

定义低秩r、缩放参数alpha、归一化尺度字典scaling、A、B矩阵等,如下


class LoraLayer:
    def __init__(self, in_features: int, out_features: int,**kwargs):
        self.r = {}
        self.lora_alpha = {}
        self.scaling = {} # scaling[adapter_name] = lora_alpha / r
        self.lora_dropout = nn.ModuleDict({})
        self.lora_A = nn.ModuleDict({})
        self.lora_B = nn.ModuleDict({})
        # For Embedding layer
        self.lora_embedding_A = nn.ParameterDict({})
        self.lora_embedding_B = nn.ParameterDict({})
        # Mark the weight as unmerged
        self.merged = False
        self.disable_adapters = False
        self.in_features = in_features
        self.out_features = out_features
        self.kwargs = kwargs
  

    def update_layer(self, adapter_name, r, lora_alpha, lora_dropout, init_lora_weights):
        ...
  

    def update_layer_conv2d(self, adapter_name, r, lora_alpha, lora_dropout, init_lora_weights):
        ...
  

    def update_layer_embedding(self, adapter_name, r, lora_alpha, lora_dropout, init_lora_weights):
        ...
  

    def reset_lora_parameters(self, adapter_name):
        ...

PEFT框架通过直接继承LoraLayer类已内置实现对nn.Linear、nn.Embedding、nn.Conv2d、bnb.nn.Linear8bitLt、bnb.nn.Linear4bit的Lora化支持,代码基本逻辑如下,每个模块可通过LoraLayer.disable_adapters字段决定forward推理中是否使用ΔW参数,使用则是Lora模型参数,否则为原模型参数。


class Linear(nn.Linear, LoraLayer): 
  ...
  def forward(self, x: torch.Tensor):
      ...
      if self.disable_adapters:
          if self.r[self.active_adapter] > 0 and self.merged:
              self.unmerge()
      ...
  ...
class Embedding(nn.Embedding, LoraLayer):
  ...
class Conv2d(nn.Conv2d, LoraLayer):
  ...
class Linear8bitLt(bnb.nn.Linear8bitLt, LoraLayer):
  ...
class Linear4bit(bnb.nn.Linear4bit, LoraLayer):
  ...

3.4 三步实现LoRA及参数解释


# step1. 参数配置
R = 8
LORA_ALPHA = 16
LORA_DROPOUT = 0.04
TARGET_MODULES = ["q_proj", "v_proj"]
  

config = LoraConfig(
    r=R,
    lora_alpha=LORA_ALPHA,
    target_modules=TARGET_MODULES,
    lora_dropout=LORA_DROPOUT,
    bias="none",
    task_type="CAUSAL_LM",
)
  

# step2. 加载transformer预训练模型
model = AutoModelForSeq2SeqLM.from_pretrained(model_name_or_path)
# step3. 获取LoRA模型
model = get_peft_model(model, config)

PEFT源码中LoraConfig参数描述如下


class LoraConfig(PeftConfig):
    """
    This is the configuration class to store the configuration of a [`LoraModel`].
  

    Args:
        r (`int`): Lora attention dimension.
        target_modules (`Union[List[str],str]`): The names of the modules to apply Lora to.
        lora_alpha (`int`): The alpha parameter for Lora scaling.
        lora_dropout (`float`): The dropout probability for Lora layers.
        fan_in_fan_out (`bool`): Set this to True if the layer to replace stores weight like (fan_in, fan_out).
        For example, gpt-2 uses `Conv1D` which stores weights like (fan_in, fan_out) and hence this should be set to `True`.:
        bias (`str`): Bias type for Lora. Can be 'none', 'all' or 'lora_only'
        modules_to_save (`List[str]`):List of modules apart from LoRA layers to be set as trainable
            and saved in the final checkpoint.
        layers_to_transform (`Union[List[int],int]`):
            The layer indexes to transform, if this argument is specified, it will apply the LoRA transformations on
            the layer indexes that are specified in this list. If a single integer is passed, it will apply the LoRA
            transformations on the layer at this index.
        layers_pattern (`str`):
            The layer pattern name, used only if `layers_to_transform` is different from `None` and if the layer
            pattern is not in the common layers pattern.
    """

参数名

含义

r

lora的秩,矩阵A和矩阵B相连接的宽度,r«d

lora_alpha

尺度缩放参数,lora参数ΔWx乘以 α/r 尺度归一化 ,本质和learning rate相同

lora_dropout

lora层的dropout比率

bias

是否可训练bias,none:均不可;all:均可;lora_only:只有lora部分的bias可训练

modules_to_save

除了lora部分之外,还有哪些层可以被训练,并且需要保存

fan_in_fan_out

只有应用在Conv1D层时置为True,其他情况False

target_modules 指定应用lora的目标模块

**4.**LoRA 模型微调范式示例

前述得到Lora的模型peft model,接下来如何实现模型存储加载、混合精度训练?这里我们给出一个有代表性的范例,基于该范例根据具体任务做适当调整即可。


import datasets
from transformers import Trainer, DataCollatorForSeq2Seq
  

if resume_from_checkpoint:
    lora_weight = torch.load(ckpt_name)
    set_peft_model_state_dict(model, lora_weight)
  

train_data = datasets.load_from_disk(dataset_path)
  

class ModifiedTrainer(Trainer):
    def save_model(self, output_dir=None, _internal_call=False):
        # 改写trainer的save_model,在checkpoint的时候只存lora权重
        from transformers.trainer import TRAINING_ARGS_NAME
  

        os.makedirs(output_dir, exist_ok=True)
        torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
        saved_params = {
            k: v.to("cpu") for k, v in self.model.named_parameters() if v.requires_grad
        }
        torch.save(saved_params, os.path.join(output_dir, "adapter_model.bin"))
  

trainer = ModifiedTrainer(
    model=model,
    train_dataset=train_data,
        args=transformers.TrainingArguments(
            per_device_train_batch_size=8,
            gradient_accumulation_steps=16,
            num_train_epochs=10,
            learning_rate=3e-4,
            fp16=True,
            logging_steps=10,
            save_steps=200,
            output_dir=output_dir
        ),
    data_collator=DataCollatorForSeq2Seq(
        tokenizer, pad_to_multiple_of=8, return_tensors="pt", padding=True
    ),
)
trainer.train()
model.save_pretrained(train_args.output_dir)

因为peft model重写了原始model的save_pretrained函数,只把lora层的权重及配置文件进行存储,因此model.save_pretrained只会存储lora权重,这里trainer的save_model函数没有做相应的重写,因此我们重写下对应的function,避免checkpoint写入原始模型全部参数。

总结,本文对PEFT框架、LoRA原理做了简单介绍,也通过代码对PEFT中不同算子Lora化的逻辑和细节进行分析,对这些分析的理解将有助于开发者对不同transformer实现更灵活的Lora模型,助力实现对大模型的微调及应用落地。

*—-END—- *

附录:

https://github.com/huggingface/peft

https://github.com/huggingface/safetensors

https://huggingface.co/docs/transformers/model_doc/rag

https://github.com/microsoft/LoRA

https://cloud.tencent.com/developer/article/2276508

进技术交流群请添加AINLP小助手微信(id: ainlp2)

请备注具体方向+所用到的相关技术点

![](https://api.allorigins.win/raw?url=https://mmbiz.qpic.cn/mmbiz_jpg/nW2ZPfuYqSJADkmZ2IX6Z23znAibuEevotDMq9iaMxiapK7jfMibiauGFkycicAJEs6x5U9SGyDJZ0S1tRed9TPNUUDQ/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1)

关于AINLP

AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括LLM、预训练模型、自动生成、文本摘要、智能问答、聊天机器人、机器翻译、知识图谱、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLP小助手微信(id:ainlp2),备注工作/研究方向+加群目的。

  


  


![](https://api.allorigins.win/raw?url=https://mmbiz.qpic.cn/mmbiz_jpg/nW2ZPfuYqSKABHCqVVQkVYPrM4XY1vsd0iaeuXzyJnoFc8cibd5mYb4wdA3WMQtiaPVmr0XLZHMuVibqWncibpnTSnQ/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1)

阅读至此了,分享、点赞、在看三选一吧🙏

更多AI工具,参考Github-AiBard123国内AiBard123

可关注我们的公众号:每天AI新工具