past_key_values在P-TuningV2中的巧用

2023-11-07

背景

目前HuggingFace发布了关于微调LLMs的方法包——Parameter-Efficient Fine-Tuning(PEFT),其中包含下面6种方法:

  1. LoRA: LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS
  2. Prefix Tuning: Prefix-Tuning: Optimizing Continuous Prompts for Generation, P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks
  3. P-Tuning: GPT Understands, Too
  4. Prompt Tuning: The Power of Scale for Parameter-Efficient Prompt Tuning
  5. AdaLoRA: Adaptive Budget Allocation for Parameter-Efficient Fine-Tuning
  6. LLaMA-Adapter: Efficient Fine-tuning of Language Models with Zero-init Attention

此外也列出了该包对不同的任务中,不同方法和模型的支持情况(我只列出了关于NLP的,还有部分图像的):

image-20230607154231676 image-20230607154941828 image-20230607155331943 image-20230607155407529

但是还没有P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks的方法,因此我就看源码是怎么处理的。

在研究和阅读其他人blog期间,发现有些人对P-Tuning描述不准确。下面是一些存在的不准确描述情况❎:

  • 把Prompt Tuning称作是P-Tuning ❎;
  • 使用P-Tuning V2的时候,直接称作是P-Tuning❎;
  • 使用了P-Tuning,但是却说是使用了P-TuningV2❎
  • ……

因此需要注意甄别(主要是P-Tuning和Prompt-Tuning的方法提出时间就差了一个月,并且在方法上有一定的相似性,都是在Embedding中使用了continuous prompt)

Prompt-Tuning一文中对两者的不同做了说明:

P-tuning” where learnable continuous prompts are interleaved throughout the embedded input, using patterns based on human design. Prompt tuning approach removes this complication by simply prepending the prompt to the input. To achieve strong SuperGLUE results, P-tuning has to be used in conjunction with model tuning, that is, models jointly update both the prompt and the main model parameters【也就是全量微调,但是文章的论点是GPT在NLU任务上的表现,并且还突出few-shot场景下效果】, whereas our approach keeps the original language model frozen” “As another difference, P-tuning requires the addition of “anchor” tokens in the input (e.g. a question mark following the hypothesis in the RTE task) to achieve strong performance, while prompt tuning leaves inputs untouched.”

P-Tuning V2源码定位

这里以run_script/run_rte_roberta.sh为例,下面是代码:

export TASK_NAME=superglue
export DATASET_NAME=rte
export CUDA_VISIBLE_DEVICES=0

bs=32
lr=5e-3
dropout=0.1
psl=128
epoch=100

python3 run.py \
  --model_name_or_path roberta-large \
  --task_name $TASK_NAME \
  --dataset_name $DATASET_NAME \
  --do_train \
  --do_eval \
  --max_seq_length 128 \
  --per_device_train_batch_size $bs \
  --learning_rate $lr \
  --num_train_epochs $epoch \
  --pre_seq_len $psl \
  --output_dir checkpoints/$DATASET_NAME-roberta/ \
  --overwrite_output_dir \
  --hidden_dropout_prob $dropout \
  --seed 11 \
  --save_strategy no \
  --evaluation_strategy epoch \
  --prefix

通过查看arguments.py文件可以查看到prefix参数是如下定义的:

 prefix: bool = field(
        default=False,
        metadata={
            "help": "Will use P-tuning v2 during training"
        }
    )

【为什么非要用prefix呢?我一开始以为这个参数是使用prefix Tuning呢QAQ】

因为shell脚本中的task=superglue,可以看到第96行中引用了get_trainer

if data_args.task_name.lower() == "superglue":
  assert data_args.dataset_name.lower() in SUPERGLUE_DATASETS
  from tasks.superglue.get_trainer import get_trainer

最终定位到了BertPrefixForQuestionAnswering类,可以看到P-Tuning V2的关键入口代码:

class BertPrefixForQuestionAnswering(BertPreTrainedModel):
    def __init__(self, config):
        super().__init__(config)
        self.num_labels = config.num_labels

        self.pre_seq_len = config.pre_seq_len
        self.n_layer = config.num_hidden_layers
        self.n_head = config.num_attention_heads
        self.n_embd = config.hidden_size // config.num_attention_heads

        self.bert = BertModel(config, add_pooling_layer=False)
        self.qa_outputs = torch.nn.Linear(config.hidden_size, config.num_labels)
        self.dropout = torch.nn.Dropout(config.hidden_dropout_prob)
        self.prefix_encoder = PrefixEncoder(config)
        self.prefix_tokens = torch.arange(self.pre_seq_len).long()

        for param in self.bert.parameters(): //冻结网络
            param.requires_grad = False

可以看到P-Tuning V2是冻结了整个PLM的参数,仅添加了可训练的PrefixEncoder网络,下面就是PrefixEncoder网络:

import torch
class PrefixEncoder(torch.nn.Module):
    r'''
    The torch.nn model to encode the prefix

    Input shape: (batch-size, prefix-length)

    Output shape: (batch-size, prefix-length, 2*layers*hidden)
    '''
    def __init__(self, config):
        super().__init__()
        self.prefix_projection = config.prefix_projection
        if self.prefix_projection:
            # Use a two-layer MLP to encode the prefix
            self.embedding = torch.nn.Embedding(config.pre_seq_len, config.hidden_size)
            self.trans = torch.nn.Sequential(
                torch.nn.Linear(config.hidden_size, config.prefix_hidden_size),
                torch.nn.Tanh(),
                torch.nn.Linear(config.prefix_hidden_size, config.num_hidden_layers * 2 * config.hidden_size)
            )
        else:
            self.embedding = torch.nn.Embedding(config.pre_seq_len, config.num_hidden_layers * 2 * config.hidden_size)

    def forward(self, prefix: torch.Tensor):
        if self.prefix_projection:
            prefix_tokens = self.embedding(prefix)
            past_key_values = self.trans(prefix_tokens)
        else:
            past_key_values = self.embedding(prefix)
        return past_key_values

past_key_values的出现定位

model/question_answering.py文件中的第168行中,使用了这个参数:

 outputs = self.bert(
            input_ids,
            attention_mask=attention_mask,
            token_type_ids=token_type_ids,
            position_ids=position_ids,
            head_mask=head_mask,
            inputs_embeds=inputs_embeds,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=return_dict,
            past_key_values=past_key_values,
        )

至此,终于定位到了PrefixEncoder网络中的使用了past_key_values关键词。但是这里有两个问题我当时没有理解:

  1. 那么这个关键词是怎么使用呢 ?
  2. 为什么 torch.nn.Linear(dim_ebd, num_layer * 2 * dim_ebd)要乘2 ?

接下来我们从HuggingFace源码的角度来解读这个问题。

HuggingFace Transformers中的BertModel

BertModel的类在HuggingFace的源码中是这样定义的:

def __init__(self, config, add_pooling_layer=True):
    super().__init__(config)
    self.config = config

    self.embeddings = BertEmbeddings(config)
    self.encoder = BertEncoder(config)
    self.pooler = BertPooler(config) if add_pooling_layer else None

那么BertEncoder就是模型的主干了,因此继续深挖BertEncoder这个类

def __init__(self, config):
    super().__init__()
    self.config = config
    self.layer = nn.ModuleList([BertLayer(config) for _ in range(config.num_hidden_layers)])

可以看到BertLayer就是Bert原文中的Layer了(base和lager版本在这个地方是不同的,由config.num_hidden_layers来决定),其是由Pytorch的ModuleList来封装的,那么最终定位到了BertAttention注意力机制的源码中:

 def __init__(self, config, position_embedding_type=None):
        super().__init__()
        self.self = BertSelfAttention(config, position_embedding_type=position_embedding_type)
        self.output = BertSelfOutput(config)
        self.pruned_heads = set()

BertSelfAttention就是最终past_key_values使用的地方。

past_key_values的使用

past_key_values参数是这样定义的: (tuple(tuple(torch.FloatTensor)) of length config.n_layers with each tuple having 4 tensors of shape (batch_size, num_heads, sequence_length - 1, embed_size_per_head)) — Contains precomputed key and value hidden states of the attention blocks. Can be used to speed up decoding.

那么就需要看看在Bert源码中这个参数是怎么使用的,通过定位HuggingFace中的BertSelfAttention类,看到了具体的使用

def transpose_for_scores(self, x):
        new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size) # self.num_attention_heads * self.attention_head_size = hidden_size
        x = x.view(*new_x_shape)
        return x.permute(0, 2, 1, 3) #  (bs, num_attention_heads, length, attention_head_size)
def forward(
        self,
        hidden_states, # shape = (bs, length, hidden_size);length是包含past_key_value长度的
        attention_mask=None,
        head_mask=None,
        encoder_hidden_states=None,
        encoder_attention_mask=None,
        past_key_value=None,
        output_attentions=False,
    ):
        mixed_query_layer = self.query(hidden_states)

        # If this is instantiated as a cross-attention module, the keys
        # and values come from an encoder; the attention mask needs to be
        # such that the encoder's padding tokens are not attended to.
        is_cross_attention = encoder_hidden_states is not None

        if is_cross_attention and past_key_value is not None:
            # reuse k,v, cross_attentions
            key_layer = past_key_value[0]
            value_layer = past_key_value[1]
            attention_mask = encoder_attention_mask
        elif is_cross_attention:
            key_layer = self.transpose_for_scores(self.key(encoder_hidden_states))
            value_layer = self.transpose_for_scores(self.value(encoder_hidden_states))
            attention_mask = encoder_attention_mask
        elif past_key_value is not None: # 这个地方使用!!!
            key_layer = self.transpose_for_scores(self.key(hidden_states)) #  (bs, num_attention_heads, length, attention_head_size);self.key = nn.Linear(config.hidden_size, self.all_head_size),这个就是自注意力机制里面的训练参数W;
            value_layer = self.transpose_for_scores(self.value(hidden_states)) #  (bs, num_attention_heads, length, attention_head_size);self.value = nn.Linear(config.hidden_size, self.all_head_size),同上;
            key_layer = torch.cat([past_key_value[0], key_layer], dim=2) # 在length这个维度上concat
            value_layer = torch.cat([past_key_value[1], value_layer], dim=2)# 同上
        else:
            key_layer = self.transpose_for_scores(self.key(hidden_states))
            value_layer = self.transpose_for_scores(self.value(hidden_states))

        query_layer = self.transpose_for_scores(mixed_query_layer)

那么当传入past_key_value这个参数的时候就会执行下面的代码(部分注释我已经在上面写好了):

key_layer = self.transpose_for_scores(self.key(hidden_states))
value_layer = self.transpose_for_scores(self.value(hidden_states))
key_layer = torch.cat([past_key_value[0], key_layer], dim=2)
value_layer = torch.cat([past_key_value[1], value_layer], dim=2)

至此,我们就可以回答这个关键词是怎么使用呢?这个问题了,就是通过在length这个维度上concat,从而实现了下图中黄色部分的训练。

那么还剩下一个上面提到的问题:为什么 torch.nn.Linear(dim_ebd, num_layer * 2 * dim_ebd)要乘2?

past_key_values的继续深究(shape层面)

BertModel的类在HuggingFace的源码第954行中获取了past_key_values_length

# past_key_values_length
past_key_values_length = past_key_values[0][0].shape[2] if past_key_values is not None else 0

我们不去讨论这个参数是怎么用的(源码中是在self.embedding中使用的),只看它的获取方式是从past_key_values第0个维度的第0个维度shape中的第3个值,因此结合past_key_values的定义:

(tuple(tuple(torch.FloatTensor)) of length config.n_layers with each tuple having 4 tensors of shape (batch_size, num_heads, sequence_length - 1, embed_size_per_head))

可以确定past_key_values的shape是**[(num_hidden_layers, batch_size, num_heads, sequence_length - 1, embed_size_per_head), (num_hidden_layers, batch_size, num_heads, sequence_length - 1, embed_size_per_head)]**,其中past_key_value[0]是past_key;past_key_value[1]是past_value。

保险起见,再验证一次,我们再回看P-Tuning V2源码的BertPrefixForQuestionAnswering类的forward方法:

 def forward(
        self,
        input_ids=None,
        attention_mask=None,
        token_type_ids=None,
        position_ids=None,
        head_mask=None,
        inputs_embeds=None,
        start_positions=None,
        end_positions=None,
        output_attentions=None,
        output_hidden_states=None,
        return_dict=None,
    ):
        r"""
        start_positions (:obj:`torch.LongTensor` of shape :obj:`(batch_size,)`, `optional`):
            Labels for position (index) of the start of the labelled span for computing the token classification loss.
            Positions are clamped to the length of the sequence (:obj:`sequence_length`). Position outside of the
            sequence are not taken into account for computing the loss.
        end_positions (:obj:`torch.LongTensor` of shape :obj:`(batch_size,)`, `optional`):
            Labels for position (index) of the end of the labelled span for computing the token classification loss.
            Positions are clamped to the length of the sequence (:obj:`sequence_length`). Position outside of the
            sequence are not taken into account for computing the loss.
        """
        return_dict = return_dict if return_dict is not None else self.config.use_return_dict

        batch_size = input_ids.shape[0]
        past_key_values = self.get_prompt(batch_size=batch_size)
        prefix_attention_mask = torch.ones(batch_size, self.pre_seq_len).to(self.bert.device)
        attention_mask = torch.cat((prefix_attention_mask, attention_mask), dim=1)

        outputs = self.bert(
            input_ids,
            attention_mask=attention_mask,
            token_type_ids=token_type_ids,
            position_ids=position_ids,
            head_mask=head_mask,
            inputs_embeds=inputs_embeds,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=return_dict,
            past_key_values=past_key_values,
        )

直接定位与P-Tuning有关的地方,就是这个get_prompt方法,那么我们可以再看看这个方法是怎么定义的

def get_prompt(self, batch_size):
        prefix_tokens = self.prefix_tokens.unsqueeze(0).expand(batch_size, -1).to(self.bert.device)
        past_key_values = self.prefix_encoder(prefix_tokens) # 这个就是PrefixEncoder网络
        bsz, seqlen, _ = past_key_values.shape
        past_key_values = past_key_values.view(
            bsz,
            seqlen,
            self.n_layer * 2, 
            self.n_head,
            self.n_embd
        )
        past_key_values = self.dropout(past_key_values)
        past_key_values = past_key_values.permute([2, 0, 3, 1, 4]).split(2)# 划分past_key和past_value, 构成一个元组
        return past_key_values

至此,验证了我们的想法,其shape也确定了下来。可以说作者非常巧妙地使用了这个参数,妙~。

扩展内容

use_cache 字段

在使用past_key_values的时候,无意中看到user_cache这个字段,下面是官方给出的字段说明

If set to True, past_key_values key value states are returned and can be used to speed up decoding (see past_key_values).

也就是说这个参数有两个作用:返回存储和加速解码

我们先来看返回存储是怎么做的。

BertEncoder类中第一次使用了这个参数,其用它定义了一个参数next_decoder_cache

 next_decoder_cache = () if use_cache else None

接下来在源码next_decoder_cache用来存储layer_outputs的结果,并用作数据类BaseModelOutputWithPastAndCrossAttentions的结果。其他地方就没有用到了。

  if use_cache:
    next_decoder_cache += (layer_outputs[-1],) #layer_outputs就是每层Attention之后的结果
  ……
   return BaseModelOutputWithPastAndCrossAttentions(
            last_hidden_state=hidden_states,
            past_key_values=next_decoder_cache,
            hidden_states=all_hidden_states,
            attentions=all_self_attentions,
            cross_attentions=all_cross_attentions,
        )

可以看到代码中是将next_decoder_cache赋给了past_key_values。这样就与BaseModelOutputWithPastAndCrossAttentions这个数据流的说明保持了一致。下面是官方的说明:

past_key_values (tuple(tuple(torch.FloatTensor)), optional, returned when use_cache=True is passed or when config.use_cache=True) — Tuple of tuple(torch.FloatTensor) of length config.n_layers, with each tuple having 2 tensors of shape (batch_size, num_heads, sequence_length, embed_size_per_head)) and optionally if config.is_encoder_decoder=True 2 additional tensors of shape (batch_size, num_heads, encoder_sequence_length, embed_size_per_head).

Contains pre-computed hidden-states (key and values in the self-attention blocks and optionally if config.is_encoder_decoder=True in the cross-attention blocks) that can be used (see past_key_values input) to speed up sequential decoding.

这里也就可以说明其另一个作用是加速解码了。但是需要继续探讨当BERT作为decoder的时候,cross-attention的计算了。这不在本次的讨论内,给自己埋个坑。

总结来说,这个参数如果不在encoder-decode这个框架中,那么是用不到的,除非你非要想得到这个数值看看。

GPT2Model

在查阅相关使用的时候,我发现了另一篇的相近的blog内容GPT2模型源码阅读系列(二)一GPT2Model。该博主也是使用的HuggingFace,从而讨论了past_key_values在GPT2中的使用。这里就仅探讨past_key_values这个参数。

GPT2Model类中最主要的部分便是循环ModuleList层中的12个Block模块以及past_key_values元组中12个layer_past张量进行运算,这部分即为GPT2模型主体结构部分的运算过程。
past_key_values保存了上次迭代过程中的key和value(attention运算中的键值对)用于加速运算,因此在第一次迭代时为长度为12,值为None的list,在之后past_length为1

if past_key_values is None:
  past_length = 0
  # 若此时为GPT2模型第一次迭代, 则不存在上一次迭代返回的past_key_values列表(包含12个present的列表,
  # 也就是代码中的presents列表), 则此时past_key_values列表为一个包含12个None值的列表.
  past_key_values = [None] * len(self.h)
else:
	past_length = past_key_values[0][0].size(-2)

这部分内容其实就很像是我们前面讲到的内容,几乎是如出一辙。

参考

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

past_key_values在P-TuningV2中的巧用 的相关文章

随机推荐

  • 基于组合双向拍卖的共享储能机制研究(Matlab代码实现)

    目录 1 概述 2 运行结果 2 1 算例数据 2 2 买家中标 2 3 卖家中标 3 文献来源 4 Matlab代码实现 1 概述 文献来源 摘要 为满足共享储能中储能用户的互补性和替代性需求 解决常规单向拍卖中可能存在的垄断竞争问题 提
  • 怎样才算一个好的产品经理?

    虽然是小公司全能型人员 但是随着实际写代码变得越来越少 产品的规划和运营越来越多 自己的主要职能也在向产品经理上转变 所以最近几年比较关注产品经理方面的功能 那么怎么才能算是一个好的产品经理呢 周鸿伟的观点 这个名字相信让很多人又爱有恨 但
  • Vue.js模板语法

    模板语法 Vue js使用了基于HTML的模板语法 允许开发者声明式地将DOM绑定至底层Vue实例的数据 所有vue js的模板都是合法的HTML 所以能被遵循规范的浏览器和HTML解析器解析 在底层的实现上 Vue将模板编译成虚拟DOM渲
  • Python与OpenCV(一)——基于帧差法的运动目标检测程序分析

    OpenCV提供了强大的图像处理功能 与Python的结合堪称完美 这一次 我们试一下用帧差法来完成对运动目标的检测与跟踪 帧差法的原理是这样的 由于摄像机采集的视频序列具有连续性的特点 所以如果所采集场景内没有运动目标的时候 连续帧的变化
  • git本地删除后如何再pull下来_实践总结——Git 常见错误及解决方法

    Git 是当下最常用的代码管理库 是程序员日常工作中使用频率最高的工具 在频繁的使用过程中 难免会遇到各种各样的问题 今天跟大家分享 Git 常见的错误和解决方法 问题一 执行Git add somefile 的时候 出现如下错误 If n
  • 如何利用Parsec+Zerotier+moon实现远程电脑云电脑、云办公、云游戏

    前期说明 RD Client 没法串流 向日葵带宽受限 同时也没办法打游戏 那么可不可以自己实现一台云电脑来实现云游戏呢 答案当然是可以的了 下面文章将进行介绍Parsec 远程电脑的三种方案 快速跳转链接 利用IPv6实现公网访问远程桌面
  • Python爬虫时遇到SSL证书验证错误解决办法汇总

    在进行Python爬虫任务时 遇到SSL证书验证错误是常见的问题之一 SSL证书验证是为了确保与服务器建立的连接是安全和可信的 但有时候可能会由于证书过期 不匹配或未受信任等原因导致验证失败 为了解决这个问题 本文将提供一些实用的解决办法
  • 大数据采集、清洗、处理:使用MapReduce进行离线数据分析完整案例

    1 大数据处理的常用方法 大数据处理目前比较流行的是两种方法 一种是离线处理 一种是在线处理 基本处理架构如下 在互联网应用中 不管是哪一种处理方式 其基本的数据来源都是日志数据 例如对于web应用来说 则可能是用户的访问日志 用户的点击日
  • R语言练习题(1)

    关注公众号凡花花的小窝 收获更多的考研计算机专业编程相关的资料 R语言练习 打开Rgui 1 在R控制台中提示符后依次输入下列语句 理解R的交互过程 2 3 45 6 x lt 3 4 5 6 x lt x 1 x 1 2 如何打开R的帮助
  • Python小白项目体系练习500例(附源代码),练完可就业

    1 有一个jsonline格式的文件file txt大小约为10K 2 补充缺失的代码 3 输入日期 判断这一天是这一年的第几天 4 打乱一个排好序的list对象alist 5 现有字典 d a 24 g 52 i 12 k 33 请按va
  • SpringBoot框架下,前后端实现文件交互——文件上传

    pc端实现文件上传与下载 前后端进行文件交互 主要形式为文件的上传与下载 1 上传的主要形式为前端通过文件上传插件将文件通过文件流形式传递到后端 后端进行处理 保存至目标地址 2 下载的主要形式为前端访问后台下载地址 实现文件下载 然后弹出
  • java多选下拉列表框_Java+Selenium3自动化入门4---Select多选框下拉列表

    在做自动化的过程中我们会遇到很多的控件 有的控件在WebDriver中都有封装好的API 我们使用这些方法来操作会提高我们的测试用例编写效率和准确性 今天我就来介绍下关于select多选框的操作方法 在Selenium中 针对html的标签
  • VTK:显式结构化网格实战详解

    VTK 显式结构化网格实战详解 本篇文章将详细介绍如何使用 VTK 库对显式结构化网格进行建模 可视化等操作 我们将通过 Python 语言的代码实现对显式结构化网格的读取与显示 并对其中的数据进行处理和分析 首先 我们需要导入必要的 Py
  • iOS项目统计总代码行数

    快速统计Xcode工程项目代码量 步骤一 打开终端 用cd命令 定位到工程所在的目录 然后调用以下命名即可把每个源代码文件行数及总数统计出来 find name m or name mm or name cpp or name h or n
  • JAVA后台接收前台传过来的json字符串并解析获得key 和value

    前台代码 ajax type post url project updateProject data formdata JSON stringify formdata tabname tabname val id proid val suc
  • Ubuntu16.04中python2.7、python3.5和python3.7共存

    Ubuntu中默认安装了python2 7和python3 5 由于一次测试 我需要python3 7的环境 所以进行了安装 其中涉及到 python源码安装 软链接的增添 删除修改 对应版本的pip安装和查看 一 python源码安装 1
  • IT项目管理八

    作业一 假定你的组织想聘用新教师来教授项目管理课程 制定一个质量标准清单 可用于做出此次聘用决定 质量标准项 质量标准度量 学历要求 985硕士及以上学历 资格要求 具有教室资格证 专业要求 计算机或者软件工程专业 从业经历 任教5年以上
  • Qt一步一步实现插件调用(附源码)

    最近手里几个项目都采用插件的方式进行开发工作 这里记录一下实现方法 给需要的同学一个参考 在linux系统和window系统都能成功编译通过 不废话直接步骤 第一步 建立插件原型 新建一个Qt项目 实现一个一个实时刷新当前时间这这么一个功能
  • C++ 比较浮点型数据(float,double)

    学习来源 C Lern bool approximatelyEqualAbsRel double a double b double absEpsilon double relEpsilon Check if the numbers are
  • past_key_values在P-TuningV2中的巧用

    背景 目前HuggingFace发布了关于微调LLMs的方法包 Parameter Efficient Fine Tuning PEFT 其中包含下面6种方法 LoRA LORA LOW RANK ADAPTATION OF LARGE L