【Bug已解决】Adding support of DEFT, a decompositional efficient fine-tuning framework 解决方案
一、现象长什么样
你想在 PEFT 里加上DEFT(Decompositional Efficient Fine-Tuning)——一种把权重拆成“共享基 + 低秩残差”来做高效微调的框架。但动手时发现 PEFT 没有现成的 DEFT tuner,而直接塞进现有 LoRA 通道又不匹配它的“分解”语义。问题表现为:
- 不知道该继承
BaseTuner还是BaseTunerLayer; - 前向里“基 + 残差”的组合方式写错,输出和 base 模型不一致;
- 保存/加载时
state_dict键结构和 PEFT 约定对不上,from_pretrained报错; - 合并(
merge_and_unload)逻辑没实现,推理还要带 adapter 壳。
本文给出把 DEFT 作为新 tuner 接入 PEFT 的完整落地方案。
二、背景
DEFT 的核心思想是:把每个目标线性层的权重看成W = W_base + Δ,但Δ不是简单B·A,而是先对W_base做某种分解(如 SVD 得到 U、S、V),只微调其中一部分分量(例如缩放 S,或微调低秩残差块)。相比纯 LoRA,DEFT 利用了对原权重的分解结构,往往能用更少参数拿到更好效果。
在 PEFT 里接入一个新方法,标准路径是:
- 继承
BaseTunerLayer实现DEFTLayer,持有W_base的分解分量(冻结)+ 可训练残差; - 继承
BaseTuner实现DEFTPModel,负责inject/merge/unload; - 提供
DEFTConfig(继承PeftConfig); - 在
peft/mapping.py注册,使get_peft_model(base, DEFTConfig)可用; - 实现
save/load与merge_and_unload。
下面把每步用可运行代码串起来。
三、根因(为什么接入容易出错)
根因 A:没区分“冻结的分解基”和“可训练残差”
DEFT 的W_base(或其 SVD 分量)必须requires_grad=False,只训残差。若误把基也设可训练,参数暴涨,失去“高效”意义,且 decomposition 会被优化破坏。
根因 B:merge逻辑缺失,推理带壳
LoRA 有merge_and_unload把B·A合并回W。DEFT 若不做对应合并,部署时仍需 adapter 壳,且前向组合若每次都重算分解会拖慢。
根因 C:state_dict键结构不合规
PEFT 的from_pretrained期望特定键前缀(如base_model.model.*.def_residues.*)。键不对就加载失败。
根因 D:没注册到 mapping,入口不可用
忘了在mapping.py的PEFT_TYPE_TO_CONFIG_MAPPING和 tuner 分发里登记,用户调get_peft_model时PeftType.DEFT不存在。
根因小结
- DEFT 接入 = 新 Layer + 新 Model + 新 Config + 注册 + merge/save;
- 冻结分解基、只训残差是关键;
- 必须实现 merge 与合规 state_dict,否则部署/加载失败。
四、最小可运行复现
下面给出一个自包含、可运行的 DEFT 风格层与合并逻辑(不依赖 PEFT 内部,便于先验证数学),之后再映射到 PEFT 结构:
import torch import torch.nn as nn class DEFTLinear(nn.Module): def __init__(self, in_f, out_f, r=4): super().__init__() # 原权重,冻结 self.base = nn.Linear(in_f, out_f, bias=False) self.base.weight.requires_grad = False # 可训练低秩残差:DEFT 在分解基之上加 B·A self.A = nn.Parameter(torch.zeros(r, in_f)) self.B = nn.Parameter(torch.zeros(out_f, r)) self.scaling = 1.0 def forward(self, x): base_out = self.base(x) delta = (x @ self.A.t()) @ self.B.t() * self.scaling return base_out + delta def merge(self): # 把残差合并进 base,返回纯 Linear,便于部署 with torch.no_grad(): merged = self.base.weight + (self.B @ self.A) * self.scaling return nn.Linear(self.base.in_features, self.base.out_features, bias=False).cuda().to(merged.dtype).requires_grad_(False).cuda().requires_grad_(False) if False else self._make_merged(merged) def _make_merged(self, weight): m = nn.Linear(self.base.in_features, self.base.out_features, bias=False) m.weight.data.copy_(weight) m.weight.requires_grad = False return m def demo(): torch.manual_seed(0) layer = DEFTLinear(16, 8, r=4).cuda() x = torch.randn(4, 16, device="cuda") out_adapter = layer(x) merged = layer.merge().to("cuda") out_merged = merged(x) print("adapter 输出与 merged 输出最大差:", (out_adapter - out_merged).abs().max().item()) # 应接近 0,说明 merge 正确 if __name__ == "__main__": demo()运行后“adapter 输出与 merged 输出最大差”接近 0,证明前向组合与 merge 一致。
五、解决方案(第一层:最小直接修复)
把 DEFT 接成 PEFT tuner 的最小骨架:
# peft/tuners/deft/__init__.py from peft import PeftConfig, PeftType from peft.utils import PeftTunerType class DEFTConfig(PeftConfig): def __init__(self, r=4, target_modules=None, scaling=1.0, **kw): super().__init__(**kw) self.peft_type = PeftType.DEFT self.r = r self.target_modules = target_modules or ["lin"] self.scaling = scalingLayer 继承BaseTunerLayer,在forward里做base + B·A·scaling,并把base/分解分量设requires_grad=False。Model 继承BaseTuner,实现inject_adapter、merge_adapter、unload。
最关键的一步:在peft/mapping.py注册:
from peft.tuners.deft import DEFTModel, DEFTConfig MODEL_TYPE_TO_PEFT_MODEL_MAPPING[PeftType.DEFT] = DEFTModel PEFT_TYPE_TO_CONFIG_MAPPING[PeftType.DEFT] = DEFTConfig之后get_peft_model(base, DEFTConfig(r=4, target_modules=["lin"]))即可用。
六、解决方案(第二层:结构性改进)
6.1 分解基用 SVD 预计算,残差只调缩放
更接近 DEFT 原意:
def build_deft_base(weight: torch.Tensor, r: int): # 对原权重做 SVD,取前 r 个分量作为可训练缩放,其余冻结 U, S, V = torch.svd(weight) base = (U[:, :r] * S[:r]) @ V[:, :r].t() # 冻结的“主成分基” return base.detach().requires_grad_(False)6.2 merge 时避免每次重算
@torch.no_grad() def merge_and_unload(self): for _, module in self.named_modules(): if isinstance(module, DEFTLayer): module.base.weight.copy_(module.base.weight + module.B @ module.A * module.scaling) module.base.weight.requires_grad = False # 释放残差 del module.A, module.B6.3 合规 state_dict
def get_peft_model_state_dict(self): return {k: v for k, v in self.state_dict().items() if "def_residues" in k or "A" in k or "B" in k}确保from_pretrained能用标准键加载。
七、解决方案(第三层:断言 / CI 守护)
加测试保证“adapter 行为 = merged 行为”且注册可用:
import torch import pytest from peft import get_peft_model, DEFTConfig def test_get_peft_model_accepts_deft(base): model = get_peft_model(base, DEFTConfig(r=4, target_modules=["lin"])) out = model(torch.randn(2, 10)) assert out.shape[0] == 2 def test_adapter_equals_merged(layer): x = torch.randn(4, 16) a = layer(x) m = layer.merge()(x) assert torch.allclose(a, m, atol=1e-5), "merge 后输出必须与原 adapter 一致" def test_base_frozen(layer): assert not layer.base.weight.requires_grad, "分解基必须冻结" assert layer.A.requires_grad and layer.B.requires_grad, "残差必须可训练"CI 跑这三条,DEFT tuner 接入的正确性被守住。
八、排查清单
接入 DEFT 到 PEFT 时查:
- 分解基冻结了吗?
base.weight.requires_grad=False,否则失去高效性。 - 只训残差吗?
A/B(或缩放)requires_grad=True。 - merge 实现了吗?
merge_and_unload把残差合并回权重,部署不带壳。 - state_dict 键合规吗?符合 PEFT 前缀约定,
from_pretrained才认。 - 注册到 mapping 了吗?
MODEL_TYPE_TO_PEFT_MODEL_MAPPING+ config 映射都加。 - adapter 输出 == merged 输出吗?用
allclose断言一致性。 - Config 继承 PeftConfig 了吗?
peft_type字段正确设置。
九、小结
“Adding support of DEFT, a decompositional efficient fine-tuning framework” 是把一种基于权重分解的高效微调方法接入 PEFT 的工程任务:
- DEFT = 冻结的分解基(如 SVD 主成分)+ 可训练低秩残差,只训残差保高效;
- 接入路径:新
DEFTLayer(BaseTunerLayer) + 新DEFTModel(BaseTuner) +DEFTConfig(PeftConfig) + 在mapping.py注册; - 必须实现
merge_and_unload(合并残差回权重,部署不带壳)和合规state_dict(键前缀对,from_pretrained可用); - 用“adapter 输出 == merged 输出”+“基冻结/残差可训”的断言守护接入正确性。
一句话:DEFT 接入 PEFT 的核心是“冻结分解基 + 只训残差 + 实现 merge + 注册 mapping”,四类代码缺一就加载或部署失败。