ShadowPEFT:把适配器变成一个带状态的小模型

0 阅读

传统参数高效微调怎么做

参数高效微调(PEFT)的核心思路很简单:冻结预训练大模型的绝大部分参数,只训练少量额外参数来适配下游任务。这样既省显存又省存储——你不用保存整个模型副本,只需存下那几兆的适配器。

目前最主流的方法是LoRA。它不改动模型结构,而是在某些线性层(比如注意力里的q_proj、v_proj)旁边加一个低秩更新项。具体来说,原本输出是 Wx,现在变成 Wx + α·(xA)B,其中A和B是两个小矩阵,α是缩放因子。训练时只更新A和B,推理时可以把它们合并回W,或者动态加载。

这类方法虽然高效,但有个隐含假设:任务信息可以被拆解成一堆彼此独立的局部权重扰动。每个层自己改自己的,层与层之间没有显式的任务状态传递。这就像一群人各自调整手里的零件,却没人统一协调整体目标。

ShadowPEFT 的核心想法:让适配器拥有状态

ShadowPEFT 换了个思路——与其把适配看作一堆零散的权重补丁,不如把它当成一个有记忆的小模型。这个“影子模型”从输入开始就维护一个隐藏状态 s,并随着主干网络逐层推进不断更新自己。

具体流程分三步,发生在每个Transformer块:

  1. 影子注入:计算主干当前隐藏状态 h 和影子状态 s 的差异 (h - s),通过一个低秩瓶颈压缩后加到主干输入上;
  2. 主干编码:冻结的主干网络处理这个增强后的表示;
  3. 影子更新:用两个小型MLP,一个生成候选状态,一个生成门控信号,通过门控残差连接更新影子状态 s,传给下一层。

这样一来,信息流变成了双向的:影子指导主干,主干反过来塑造影子。更重要的是,影子状态 s 贯穿整个网络深度,后面层能看到前面所有层积累的任务信息。这解决了传统PEFT缺乏跨层协调的问题。

为什么这个设计有意义

首先,跨层协调能力。传统LoRA每个层的适配器是孤立初始化的,而ShadowPEFT的影子状态天然具有连续性。第12层做决策时,不仅知道当前主干输出,还知道“任务路径”此前积累了什么信息。

其次,适配容量可扩展。LoRA的能力主要靠秩(rank)和覆盖层数控制,而ShadowPEFT多了一个维度:影子模型本身的大小。实验发现,增大影子模型参数量能持续提升性能,而LoRA很快达到平台期。

最关键的是,影子模型是个完整的小模型。它自带预测头,训练时直接接收任务监督信号。这意味着训练完后,你可以选择两种用法:

  • 附加模式:像普通适配器一样和主干一起跑;
  • 分离模式:调用 unload_shadow() 单独运行影子模型。

后者特别适合边缘计算场景——简单请求由轻量影子本地处理,复杂请求再交给云端的大模型+影子组合。

此外,影子模型甚至可以用另一个预训练小模型初始化。比如用Qwen-0.5B作为Qwen-8B的影子,让小模型学会如何引导大模型。这打开了跨模型规模复用适配能力的可能性。

在 PEFT 库中如何使用

ShadowPEFT 已集成进 Hugging Face 的 PEFT 库,用法和其他方法保持一致。先安装开发版:

pip install --upgrade git+https://github.com/huggingface/peft.git

然后像这样配置:

from transformers import AutoModelForCausalLM
from peft import ShadowConfig, get_peft_model

model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-8B")
config = ShadowConfig(
    r=8,
    shadow_num_hidden_layers=1,
    shadow_model="mirror",  # 自动构建缩小版主干
    task_type="CAUSAL_LM"
)
model = get_peft_model(model, config)

![Architecture of ShadowPEFT](https://cdn-uploads.huggingface.co/production/uploads/64c76a70720d494bb2acc47b/T08XieCT5QoBza7w_0kBk.jpeg)

# 正常生成
out = model.generate(input_ids, max_new_tokens=32)

![Differences between LoRAs and ShadowPEFT](https://cdn-uploads.huggingface.co/production/uploads/64c76a70720d494bb2acc47b/ZUNa7NIxr3u6BUY6NJAYQ.jpeg)

# 单独运行影子模型
shadow = model.base_model.unload_shadow()
shadow_out = shadow.generate(input_ids, max_new_tokens=32)

和LoRA的关键区别在于:LoRA是“嵌入权重内部的补丁”,最终要合并;ShadowPEFT是“并行的小模型”,最终可分离。

实测效果对比

数学推理任务(MetaMathQA → GSM8K)

在Llama-3.2-3B上微调,用MetaMathQA训练,在GSM8K测试准确率:

方法 可训练参数 测试准确率 训练时间 峰值显存 检查点大小
LoRA 9.18M 46.9% 15分钟 22.3GB 36.7MB
DoRA 9.29M 46.2% 19分钟 24.5GB 37.2MB
ShadowPEFT 8.66M 48.1% 17分钟 28.2GB 26.0MB

ShadowPEFT 用更少参数达到了更高准确率,检查点也更小。显存稍高是因为影子模型需要维护自己的KV缓存,但可通过缩小影子规模调节。

图像生成(DreamBooth)

在FLUX.2-klein-base-4B扩散模型上,用20张猫图微调:

方法 可训练参数 主体保真度(DINO↑) 知识遗忘(Drift↓) 训练时间 检查点大小
LoRA 38.3M 0.671 0.274 10分钟 153MB
DoRA 39.2M 0.682 0.250 17分钟 157MB
ShadowPEFT 31.2M 0.717 0.244 8分钟 74.5MB

f0d7e79dd519982dbd530622abd31900

ShadowPEFT 全面领先。生成的图像细节更自然,比如猫的毛色过渡平滑,而LoRA/DoRA常出现白色边缘伪影。这得益于影子状态对提示语义的全局理解,避免了局部修补导致的拼接感。

超参数设置建议

  • 注入秩(r):控制瓶颈维度,建议从8或16开始。注意总参数量主要由影子主干决定,r影响较小。
  • shadow_dropout:作用于(h-s)差异向量,默认0.2,可试0.2–0.5,高值有助于防止遗忘。
  • shadow_alpha:注入修正项的缩放因子,默认0.1,初期不宜过大以免训练不稳定。
  • 影子主干大小:通过shadow_num_hidden_layers等参数控制。用shadow_model="mirror"自动构建缩小版主干,或传入预训练小模型路径(如"Qwen/Qwen3-0.6B")初始化,通常能加速收敛。

总的来说,ShadowPEFT 把适配器从“静态补丁”升级为“动态小模型”,通过状态化设计实现了更强的跨层协调和灵活部署能力。对于追求更高微调效率和生成质量的场景,值得一试。