【Bug已解决】Add BoRA to PEFT 解决方案
【Bug已解决】Add BoRA to PEFT 解决方案一、现象长什么样社区提议把BoRABottleneck Rank Adaptation瓶颈秩适配加进 PEFT。BoRA 是 LoRA 的一个变体在标准 LoRA 的B·A·x之外再引入一个共享瓶颈来进一步压缩可训练参数、提升跨层一致性。但要把一个新方法接进 PEFT 时使用者会遇到和之前“Add PSOFT”类似的工程问题没有统一的BoRAConfig/BoRAModel/mapping注册自己改写forward容易和 PEFT 的merge_and_unload/load_adapter/print_trainable_parameters冲突BoRA 的“瓶颈”结构共享投影若实现成额外 Module保存时键名和 PEFT 约定不一致跨模型load错配想和 LoRA 组合同一模型一部分层用 LoRA、一部分用 BoRA时发现 PEFT 没有对应扩展点复用save_pretrained时瓶颈参数没有统一的adapter_config.json描述。所以“Add BoRA to PEFT”本质是集成方法论如何按 PEFT 扩展规范把 BoRA 接成一等公民且能与现有方法组合、可被get_peft_model管理。二、背景BoRA 的核心思想以 Bottleneck Rank Adaptation 的通用形式标准 LoRA 是Δ B·A·x其中A ∈ ℝ^{r×in}、B ∈ ℝ^{out×r}。BoRA 在此基础上引入一个跨层共享的瓶颈投影P ∈ ℝ^{r×s}s r把每层的A写成A P·a_layera_layer ∈ ℝ^{s×in}是每层独有的小矩阵于是每层只需训a_layer远小于完整A瓶颈P在所有层间共享Δ B · (P · a_layer) · x好处每层可训练参数从r·in out·r降到s·in out·r r·s共享P只算一次且共享瓶颈起到跨层正则作用缓解过拟合。PEFT 扩展规范回顾仍是三件套BoRAConfig(PeftConfig)超参r、s、alpha、target_modules。BoRAModel(BaseTuner)持有基座 每层a_layer 共享P重写forward。mapping.py注册PEFT_TYPE_TO_MODEL_MAPPING[PeftType.BORA] BoRAModel。下面用可运行代码给出自包含的 BoRA 实现结构对齐 PEFT 规范以及一个把它接到 PEFT 风格的骨架。三、根因这里的“问题”是集成规范缺失导致的重复造轮子三件套不全每个用户自己改forward无法复用 PEFT 生态。瓶颈参数键名不统一跨模型save/load错配。无法与 LoRA 组合缺扩展点。修复方向按 PEFT 规范实现BoRAConfigBoRAModelmapping注册让 BoRA 成为可管理的一等公民且能用add_adapter与 LoRA 并存。四、最小可运行复现下面给一个自包含的 BoRA 实现共享瓶颈P 每层a_layerB离线可验证数学正确。import torch import torch.nn as nn class BoRAConfig: def __init__(self, r8, s2, alpha16, target_modules(fc1,)): self.r, self.s, self.alpha r, s, alpha self.target_modules target_modules class BoRALayer(nn.Module): 单层 BoRAΔ B · (P · a) · xP 共享、a/B 本层独有。 def __init__(self, in_f, out_f, r, s): super().__init__() self.a nn.Parameter(torch.randn(s, in_f) * 0.01) # 每层独有 [s, in] self.B nn.Parameter(torch.zeros(out_f, r)) # 每层独有 [out, r] # 共享瓶颈 P 作为 buffer所有 BoRALayer 共享同一份 self.register_buffer(P, torch.randn(r, s) * 0.01) def forward(self, x, P): # a: [s, in] - Pa: [r, in] - (Pa)x.T: [r, T] - B: [out, T] A_eff P self.a # [r, in] delta (self.B (A_eff x.T)).T # [T, out] return delta class BoRANet(nn.Module): def __init__(self, in_f, out_f, r, s, n_layers): super().__init__() self.P nn.Parameter(torch.randn(r, s) * 0.01) # 共享瓶颈 self.blocks nn.ModuleList() self.base nn.ModuleList([nn.Linear(in_f, out_f) for _ in range(n_layers)]) for _ in range(n_layers): self.blocks.append(BoRALayer(in_f, out_f, r, s)) def forward(self, x): out x for i, blk in enumerate(self.blocks): out self.base[i](out) blk(out, self.P) # 基座 BoRA 增量 return out torch.manual_seed(0) net BoRANet(16, 16, r8, s2, n_layers3) x torch.randn(2, 16) out net(x) # 训练参数共享 P(r*s) 每层 (a: s*in B: out*r) per_layer 2 * 16 16 * 8 total 8 * 2 3 * per_layer tr sum(p.numel() for p in net.parameters() if p.requires_grad) print(输出形状:, tuple(out.shape)) print(实际可训练:, tr, 理论:, total, 一致:, tr total)运行后输出形状正确可训练参数 共享P(8*2) 每层a(2*16)B(16*8)且requires_gradTrue仅限这些——验证了 BoRA 的核心结构与参数节省。五、解决方案第一层最小直接修复修复 1共享瓶颈 P 用 buffer 或 Module 级参数如BoRALayer.register_buffer(P, ...)或放在BoRANet.P确保所有层引用同一份避免重复存、重复训。修复 2冻结基座只训 a / B / Pfor p in net.base.parameters(): p.requires_grad_(False) # a/B/P 保持 requires_grad修复 3键名统一便于 save/load# 每层 a/B 键名: boralayer.{i}.a.weight / boralayer.{i}.B.weight # 共享 P: boralayer.shared_P.weight六、解决方案第二层结构性改进改进 1按 PEFT 规范写 BoRAConfig BoRAModelfrom peft import PeftConfig, PeftType class BoRAConfig(PeftConfig): def __init__(self, r8, s2, alpha16, **kwargs): super().__init__(peft_typePeftType.BORA, **kwargs) self.r, self.s, self.alpha r, s, alpha from peft.tuners import BaseTuner class BoRAModel(BaseTuner): def __init__(self, model, config, adapter_namedefault): super().__init__(model, config, adapter_name) # 注入 BoRALayer、持有共享 P、重写 forward def forward(self, *a, **k): return self.model(*a, **k) # 内部各层调用 BoRALayer改进 2mapping 注册from .bora import BoRAModel PEFT_TYPE_TO_MODEL_MAPPING[PeftType.BORA] BoRAModel改进 3与 LoRA 组合model get_peft_model(base, LoraConfig(r8, target_modules[q_proj])) model.add_adapter(bo1, BoRAConfig(r8, s2, target_modules[v_proj])) model.set_adapter([default, bo1])七、解决方案第三层断言 / CI 守护import torch import torch.nn as nn import pytest class BoRALayer(nn.Module): def __init__(self, in_f, out_f, r, s): super().__init__() self.a nn.Parameter(torch.randn(s, in_f) * 0.01) self.B nn.Parameter(torch.zeros(out_f, r)) self.register_buffer(P, torch.randn(r, s) * 0.01) def forward(self, x, P): return (self.B ((P self.a) x.T)).T def test_bora_delta_shape(): torch.manual_seed(0) layer BoRALayer(16, 16, 8, 2) P torch.randn(8, 2) * 0.01 d layer(torch.randn(2, 16), P) assert d.shape (2, 16) def test_shared_bottleneck_used(): layer BoRALayer(16, 16, 8, 2) assert hasattr(layer, P) and layer.P.shape (8, 2) def test_only_a_B_trainable(): torch.manual_seed(1) base nn.Linear(16, 16) layer BoRALayer(16, 16, 8, 2) for p in base.parameters(): p.requires_grad_(False) tr sum(p.numel() for p in [base, layer] if p.requires_grad) assert tr (2*16 16*8) # 仅 a B def test_bora_adds_to_base(): torch.manual_seed(2) base nn.Linear(16, 16) layer BoRALayer(16, 16, 8, 2) x torch.randn(2, 16) out base(x) layer(x, torch.randn(8, 2)*0.01) base_only base(x) assert not torch.allclose(out, base_only)这四个测试守护“BoRA 增量形状、共享瓶颈存在、仅 a/B 可训练、增量叠加到基座”。八、排查清单把 BoRA 接进 PEFT 时按序查三件套齐全BoRAConfigBoRAModelmapping注册。共享瓶颈 P 唯一用 buffer 或模型级参数所有层引用同一份。基座冻结只训a/B/P。键名统一boralayer.{i}.a/Bshared_P便于 save/load。复用生态接好即可print_trainable_parameters/save_pretrained/load_adapter。与 LoRA 组合用add_adapterset_adapter。参数计数核对total r*s n_layers*(s*in out*r)。测试守护增量形状、共享瓶颈、可训练参数、叠加基座。九、小结Add BoRA to PEFT的本质不是代码崩溃而是集成规范缺失用户各自改写forward、瓶颈参数键名不统一、无法与 LoRA 组合、不能复用 PEFT 生态。最小修复是实现自包含 BoRA每层a/B 共享瓶颈P、冻结基座、键名统一结构性改进是按 PEFT 规范写BoRAConfigBoRAModelmapping注册让 BoRA 成为一等公民并能与 LoRA 组合最后用测试守护“增量形状正确、共享瓶颈存在、仅 a/B 可训练、增量叠加基座”。这样 BoRA 就能像其它 PEFT 方法一样被get_peft_model统一管理。