关于RaLo、RDP LoRA、GateRA 各自同细分领域的开源论文
2026/9/24 17:25:09 网站建设 项目流程

难点:确认各自对应的开源“平替”。

RaLo、RDP LoRA、GateRA 均属于PEFT方向,但各自的开源替代方案定位不同:

三个方法都属 PEFT,但能对上的开源方案不是一类东西。RaLo 是动态调秩,对应 AdaLoRA;RDP LoRA 是挑层,对应 DropLoRA;GateRA 是 token 级门控,按理说 X-LoRA 更贴,但 DoRA 是这块更经典的东西,所以先看 DoRA。

RaLo动态秩分配

论文原文对 AdaLoRA 进行对比,所以选择看看这个。

AdaLoRa (Adaptive LoRa)

1. 核心问题:秩分配不均

LoRa 的问题:对所有权重矩阵统一分配相同的秩,忽略了不同层/模块对任务的贡献差异。

关键洞察:不同权重矩阵的重要性差异很大——FFN层和顶层对任务贡献更大,底层需要更多参数预算。(本论文采取了单个部分删除,判断结果差异的方法,确定重要层)

方法对比

LoRa(均等分配):每层分配相同的;重要层与不重要层获得相同预算;参数利用效率不高;低预算时性能下降明显。

AdaLoRa(自适应分配):根据重要性动态分配;重要层获得更大秩;不需要层获得较小秩;参数用在关键点上。

2. SVD 参数化

公式对比

LoRa

AdaLoRa

矩阵含义

:左奇异向量矩阵

:对角奇异值矩阵

:右奇异向量矩阵

为什么用 SVD 而非 AB 分解

奇异值天然表达每个分量的重要性,裁剪奇异值就和平滑减小秩,比直接删除AB列更能进行更细粒度的控制。

奇异值本身可以表示为不同方向上的分量,且能进行特定方向的裁剪。

3. 重要性评估与预算调度

重要性得分(综合三个因素)

  1. 奇异值大小:值越大贡献越多。

  2. 梯度敏感度:该参数对 Loss 的影响。

  3. 指数移动平均 (EMA):平滑时段波动。

三阶段预算调度

  1. Warmup 阶段:初始较大,不裁剪,让模型充分探索参数空间。

  2. 裁剪阶段:每隔评估重要性,裁剪低重要性的奇异值。

  3. 收敛阶段:预算固定,精调剩余参数。

4. 论文实验结果与核心发现

核心发现在不同任务上的表现(如 SST-2, MNLI, CoLA, SQuADv2 F1 等)

  1. 5.预算下优势显著:在极少参数下(如 0.08% 参数),AdaLoRa 相比 LoRa 表现出极高的参数利用率和准确率。

  2. FFN 和顶层权重的贡献更高:实验证明 FFN 层和高层对大模型微调至关重要。

  3. SVD 正交正则化:对性能有着显著贡献。

  4. 裁剪奇异值比直接删除 AB 列更稳定:通过逐步裁剪能够更平滑地调整模型容量。

5.代码解释

代码:AdaLoRA-main/AdaLoRA-main/loralib/loralib/adalora.py,355 行。SVDLinear在 14–99 行,RankAllocator在 102–323 行。

四个分块很清晰:

换层在SVDLinear,打分在update_ipt,动刀在mask_to_target_rank,预算在schedule_threshold。互不纠缠,这是它代码好读的原因。

核心片段一:前向只有一行
# adalora.py:91-97 if self.r > 0 and not self.merged: result = F.linear(x, T(self.weight), bias=self.bias) result += ( self.lora_dropout(x) @ (self.lora_A * self.lora_E).T @ self.lora_B.T ) * self.scaling / (self.ranknum+1e-5)

self.lora_A * self.lora_Elora_A(r, in)lora_E(r, 1),这是广播的逐元素乘,不是矩阵乘。效果是 A 的第 k 行整体乘上 E[k]——这就是"r 个刻度盘"。

核心片段二:重要性怎么打分
# adalora.py:219-232 with torch.no_grad(): self.ipt[n] = (p * p.grad).abs().detach() # 敏感度 |θ·g| self.exp_avg_ipt[n] = self.beta1 * self.exp_avg_ipt[n] + \ (1-self.beta1)*self.ipt[n] # 平滑 self.exp_avg_unc[n] = self.beta2 * self.exp_avg_unc[n] + \ (1-self.beta2)*(self.ipt[n]-self.exp_avg_ipt[n]).abs() # 不确定度 ipt_score = self.exp_avg_ipt[n] * self.exp_avg_unc[n] # 两者相乘

论文写得很复杂的"综合奇异值 + 梯度敏感度 + EMA",代码里就是这三行。

核心片段三:动刀是全局切一刀
# adalora.py:284-292 mask_threshold = torch.kthvalue(torch.cat(all_is), (self.total_rank-curr_rank))[0].item() with torch.no_grad(): for n,p in model.named_parameters(): if "lora_E" in n: p.data.masked_fill_(is_dict[n]<=mask_threshold, 0.0)

这是"动态分配"真正的实现方式。所有矩阵的分数拼成一条长向量,取第 k 小的值当门槛,全局统一。没有"每层配额"这回事——预算自己流向分数高的矩阵。三个矩阵最后保留 3、2、1 而不是平均分,就是这么来的。

学到了什么

1. 论文的 P、Q 和代码的 B、A 是反的。论文写 ΔW = PΛQ,代码里 B 在前、A 在后(第 72、84、95 行三处)。对着论文读代码第一眼就会卡在这。

2. 正交正则根本不在 forward 里,是外挂的。

# adalora.py:345-354 def compute_orth_regu(model, regu_weight=0.1): for n,p in model.named_parameters(): if "lora_A" in n or "lora_B" in n: para_cov = p @ p.T if "lora_A" in n else p.T @ p I = torch.eye(*para_cov.size(), out=torch.empty_like(para_cov)) regu_loss += torch.norm(para_cov-I, p="fro") return regu_weight*regu_loss/num_param

训练时必须自己写loss + compute_orth_regu(model, 0.1)。忘了加,AdaLoRA 退化成普通的 SVD 版 LoRA,而且不报错,只是悄悄变差

作用也值得记:它逼 A 的行正交、B 的列正交,也就是让 P、Q 接近正交。为什么要逼?因为真实训练里"第 k 块积木的大小"是|E[k]| × ‖B[:,k]‖ × ‖A[k,:]‖,多出来那两个范数是噪声——E 很小但那一列特别长,这块积木其实不小。正交化在压这份噪声。论文措辞是参数化 mimics(模拟)SVD,因为它一开始真的只是模拟。

3.ranknum是死代码。

# adalora.py:43-46 self.ranknum = nn.Parameter(self.weight.new_zeros(1), requires_grad=False) self.ranknum.data.fill_(float(self.r))

它在整个库里从不被更新,但出现在 forward(第 96 行)和 merge(第 73、85 行)的分母里。名字暗示"按实际秩自适应缩放",实际恒等于float(r)。这套机制没实现。

4. final 期是真冻结,衰减期不是。

# adalora.py:310-311 def update_and_mask(self, model, global_step): if global_step < self.total_step-self.final_warmup: self.update_ipt(model) # 只有非 final 期才更新分数

final 期不更新分数 → 每步掩的是同一批位置 → 冻结。衰减期分数一直在变,而被剪的lora_E[k]自己的梯度不是零(第 221 行里 E 的梯度不带 E 因子),所以它会从 0 自己长回来。论文说"保留将来恢复的可能性",真正管用的是这个周期性重掩,不是它给的理由。

RDP LoRA关键层筛选

DropLoRADropLoRA: Sparse Low-Rank Adaptation for Parameter-Efficient Fine-Tuning

在 PEFT(参数高效微调)领域中,为了解决传统 LoRA 静态低rank(Low-rank)子空间限制及确定关键层/关键维度的需求,DropLoRA提供了一种轻量级、无需复杂搜索的动态稀疏化方案,可以作为 RDP LoRA 在结构稀疏和子空间管理上的优秀开源替代。

1. 技术动机与核心原理

传统 LoRA 的局限:标准 LoRA 在整个微调过程中依赖一个静态的低秩子空间(),这导致它在表达能力上与全量微调(Full Fine-Tuning)存在性能差距。

DropLoRA 的核心思想:它引入了动态子空间学习的概念,通过在 LoRA 的两个低秩矩阵之间创新性地集成一个基于剪枝/Dropout 的掩码模块,在训练过程中沿着秩的维度动态地对中间激活或矩阵列/行进行稀疏化。

训练与推理的差异

训练阶段:动态随机将一部分秩的维度归零(即每次只激活一部分子空间),让模型在多个不同的子空间中进行“类似集成”的学习,从而捕获更本质的参数变化。

推理阶段:该剪枝/Dropout 模块会被停用,恢复完整的矩阵进行推理,不引入任何额外的推理延迟或计算开销

2. 数学公式与前向传播

标准 LoRA 的前向传播公式为:

而在DropLoRA中,通过在低秩投影的中间步骤引入掩码向量或特征丢弃机制(设为掩码/Dropout 操作),其核心更新过程可以表示为:

(注:在实际代码实现中,它等价于在中间激活值上施加一个类似 Feature Dropout 的掩码层,再与相乘。通过动态改变每次迭代中被激活的 rank 维度,迫使模型学习到更具鲁棒性的低秩表征。)

3. 论文实验结论

根据 DropLoRA 官方论文的实验评估,其在多个主流大模型(如 LLaMA 系列)和生成任务中表现优异:

全面超越标准 LoRA

在 LLaMA3-8B 的常识推理(Commonsense Reasoning)任务上,DropLoRA 比标准 LoRA 平均准确率高出+0.83个百分点,并在所有 8 个子数据集上均取得了最优表现。

数学推理代码生成任务中,相比标准 LoRA 也取得了显著提升(例如代码任务在 LLaMA2-7B 上提升了+2.28个百分点)。

高效的参数利用率(消融实验发现)

实验表明,即使将 DropLoRA 的有效秩或剪枝率进行折减(例如设置 rank=32,剪枝率 p=0.5,其实际生效的参数量等同于 rank=16 的 LoRA),其表现依然能够超越常规的 LoRA-16 甚至 LoRA-32。

最优的剪枝/稀疏率(Drop Rate)通常在0.3 左右表现最好,既保证了稀疏子空间的动态探索,又不会过度损伤模型的表达容量。

零额外开销

由于 DropLoRA 的动态掩码仅在训练计算图内生效,训练完成后可无缝合入原权重,训练和推理阶段均不产生额外的显存或时间负担

4.代码解释

代码在DropLoRA-main/peft-0.14.0/src/peft/tuners/droplora/——这是一个改过的 PEFT fork,DropLoRA 作为独立 tuner 塞进了 PEFT。训练入口是 ms-swift 的--train_type droplora,配置透传在swift/llm/train/tuner.py:279-295

核心片段一:门控夹在秩维度上
# droplora/layer.py:706-707 (Linear.forward) if not self.use_dora[active_adapter]: result = result + lora_B(inner_dropout(lora_A(dropout(x)))) * scaling

链条是x → dropout → lora_A → inner_dropout → lora_Binner_dropout作用在 A 和 B 中间那层 r 维激活上,不是作用在输入上。这一行就是 DropLoRA 的全部改动。

核心片段二:两种模式,不是一种
# droplora/layer.py:160-163 if not dynamic_pruning: self.lora_inner_dropout[adapter_name] = nn.Dropout(p=inner_dropout) else: self.lora_inner_dropout[adapter_name] = LearnablePruning(r)

默认走的是第一条:普通随机 dropout。第二条才是"可学习结构化剪枝":

# droplora/layer.py:35-55 class LearnablePruning(nn.Module): def __init__(self, dim, temperature=1.0, eps=1e-8): self.logits = nn.Parameter(torch.zeros(dim), requires_grad=True) def forward(self, x): if self.training: mask_prob = torch.sigmoid(self.logits) # 保留概率 mask_hard = (mask_prob >= 0.5).to(self.logits.dtype) mask = (mask_hard - mask_prob).detach() + mask_prob # STE 技巧 p = mask_hard.sum() / mask_hard.size()[0] scale_factor = 1.0 / (p.detach() + self.eps) # 缩放补偿 output = x * mask * scale_factor else: output = x # 推理时直接放行 return output

三个细节:mask 是硬 0/1(阈值 0.5),不是软的;梯度靠 STE(straight-through estimator)穿过二值化;输出乘了1/p做缩放补偿,保证期望不变

学到了什么

1. 它的稀疏化粒度是"秩维度",不是"层"。这一点很关键。看训练脚本:

# scripts/llama2_7b/run_cms.sh --train_type droplora \ --lora_rank 32 --lora_alpha 64 --lora_dropout 0.05 \ --target_modules q_proj,k_proj,v_proj,up_proj,down_proj

五个目标模块一个不落,全都挂上了 LoRA。DropLoRA 从来不跳过任何层,它做的是让每一层内部的 r 个秩分量按概率/按学习被置零。 RDP LoRA 定义是"稀疏层适配、精准挑出关键层",这跟 DropLoRA 的机制不同——同一个大方向,但不是同一件事。:一个在层间做选择,一个在秩维度上做选择。

2. "有效参数量等于一半秩"这个说法要打个折扣。因为scale_factor = 1/(p+eps)在做期望补偿,被保留的那部分分量被放大了1/p倍。所以 drop_rate=0.5 时,参数量确实是 rank 16 的量级,但每一维的数值尺度是被补偿过的,和直接训 LoRA-16 不是一回事。

3. 推理确实干净。nn.Dropout在 eval 下自动变恒等;LearnablePruningself.training=False时直接output = x。两条路都不留额外算子。合并走get_mask()layer.py:666-667)拿硬 mask 再折叠。

GateRAToken 级动态门控调制

GateRAX-LoRA的核心本质都是面向输入或 Token 级的动态适配,更贴合,但是DoRA是更经典的同领域内容,所以先看 DoRA。

DoRA (Weight-Decomposed Low-Rank Adaptation)

1. DoRA 动机:全量微调(FT)与 LoRA 的学习差异

关键发现:将权重分解为magnitude(幅度)direction(方向)后,全量微调(FT)和 LoRA 呈现截然不同的学习模式。

核心痛点:FT 的幅度和方向变化呈负相关(耦合),而 LoRA 的两者呈正相关(耦合)——这限制了 LoRA 的学习能力。

对比

全量微调 (FT):幅度与方向呈负相关(解耦学习);方向大幅度调整,幅度变化小;具有更灵活的参数更新能力。

LoRA:幅度与方向呈正相关(耦合学习);两者同增同减,缺乏灵活性;学习能力不如 FT。

2. DoRA 技术原理

核心公式,其中= 幅度(magnitude),= 方向(direction)。

核心步骤

  1. 权重分解:将预训练权重分解为幅度分量和方向分量(灵感来源于 Weight Normalization)。
  2. 方向应用 LoRA 更新(低秩更新),方向分量参数最大,用 LoRA 高效更新。
  3. 幅度独立训练作为独立可训练参数,参数量极小(仅一维向量),可直接全量更新。

优势:解耦幅度和方向的学习,使 LoRA 的学习更为接近全量微调,同时保持零推理延迟。

3. 论文实验结果

在各项任务上的表现(如 LLaMA-7B 等模型)

常识推理任务:DoRA 相比标准 LoRA 提升明显(平均提升约 2.0% 至 2.5% 点)。

视觉指令微调与理解:在 LLaVA、VL-BART 等多模态任务上也展现出持续的性能提升。

结论:DoRA 在多任务和模型上一致优于 LoRA,且训练后可合并权重,零推理延迟。

4.代码解释

DoRA 官方仓库 PEFT 里:peft-0.14.0/src/peft/tuners/lora/dora.py,189 行。

核心片段一:前向那行"反直觉"的展开
# dora.py:86-98 mag_norm_scale = (magnitude / weight_norm).view(1, -1) lora_result = lora_B(lora_A(x)) ... result_dora = (mag_norm_scale - 1) * base_result + mag_norm_scale * lora_result * scaling

论文公式是W' = m · (W₀ + BA)/‖W₀ + BA‖。但代码从来不真的去构造W₀ + BA这个矩阵,而是展开成上面这行。

代数是等价的:(m/n − 1)·W₀x + (m/n)·BAx·s = (m/n)·(W₀x + BAx·s)。妙处在那个− 1——它让W₀x(也就是base_result)能复用base_layer已经算出来的输出,省掉一次完整的矩阵乘。这是"零额外推理开销"在代码层面的真实来源。

核心片段二:magnitude 怎么初始化
# dora.py:30-35, 59-63 def get_weight_norm(self, weight, lora_weight, scaling): weight = transpose(weight, self.fan_in_fan_out) weight = weight + scaling * lora_weight weight_norm = torch.linalg.norm(weight, dim=1).to(weight.dtype) # 按列取 L2 范数 return weight_norm weight_norm = self.get_weight_norm(weight.to(lora_A.device), lora_weight, scaling) self.weight = nn.Parameter(weight_norm, requires_grad=True)

nn.Parameter(weight_norm)——magnitude 初始化为基座权重的列范数,不是零、也不是一。范数按dim=1取,即每个输出通道一个标量,所以这个向量的长度是out_features

核心片段三:那个藏在 §4.3 里的 detach
# dora.py:78-85 weight_norm = self.get_weight_norm(weight, lora_weight.detach(), scaling) # see section 4.3 of DoRA # "[...] we suggest treating ||V +∆V ||_c in Eq. (5) as a constant, thereby # detaching it from the gradient graph." weight_norm = weight_norm.detach()

‖W₀+BA‖这个归一化项在反向时被当成常数切断了。主公式里完全看不出来,它藏在论文第 4.3 节的一句建议里。不 detach 会怎样,代码没写——但这属于"复现时最容易漏掉、漏了效果就不同"的那一类。

核心片段四:merge 时才真把幅度折进去
# lora/layer.py:524-535 weight_norm = ( self.lora_magnitude_vector[active_adapter] .get_weight_norm(orig_weights, transpose(delta_weight, self.fan_in_fan_out), scaling=1) .detach() ) # We need to cache weight_norm because it has to be based on the original weights. self._cache_store(f"{active_adapter}-weight_norm", weight_norm) dora_factor = self.lora_magnitude_vector[active_adapter].weight / weight_norm orig_weights = dora_factor * (orig_weights + delta_weight)

最后一行dora_factor * (orig_weights + delta_weight)就是m·(W₀+BA)/‖W₀+BA‖落地的样子。这才是"训练完可以 merge、推理零延迟"成真的地方——不在 forward 里,在 merge 里。另外注意那个_cache_store:weight_norm 必须缓存,因为 unmerge 时基座权重已经变了,再算就是另一个值。

学到了什么

1. 论文的主公式和实现之间隔着三层优化。复用 base_result(−1那一手)、identity 前向替代B@A、norm 的 detach。论文一句W' = m·(W₀+BA)/‖W₀+BA‖,代码里拆成了四处改动。这三层全是性能/工程考虑,不是数学考虑。

2. 算lora_weight用单位阵推,不用B@A

# dora.py:70-73 # Don't use `lora_weight = lora_B.weight @ lora_A.weight` because this causes errors with FSDP. x_eye = torch.eye(lora_A.weight.shape[1], device=lora_A.weight.device, dtype=x.dtype) lora_weight = lora_B(lora_A(x_eye)).T

代价是一个(in × in)的矩阵乘(7B 模型里in可以到上万),换来的是 FSDP/DDP 下梯度图正确。读论文没想到。

3. DoRA 不是 token 级的。magnitude是每个输出通道一个标量,和输入是什么 token 无关。你想对标 GateRA 的"token 级动态门控",DoRA 只能提供一个"通道级静态解耦"的近似——它解决的是幅度和方向的学习耦合,不是逐 token 的权重调节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询