PEFT 中的 VeRA(Vector-based Random Matrix Adaptation):共享随机投影的低参数微调原理、配置与实战
【免费下载链接】peft🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.项目地址: https://gitcode.com/gh_mirrors/pe/peft
VeRA(Vector-based Random Matrix Adaptation)是 🤗 PEFT 提供的一种基于向量随机矩阵的低参数微调方法:它与 LoRA 同属低秩适配,但通过跨层共享一对冻结的随机低秩矩阵、只训练每层两个缩放向量,用远少于 LoRA 的可训练参数取得相近甚至更优的效果,特别适合参数预算紧张的大模型微调与多任务适配器批量存储场景。本文以仓库文档 vera.md 为主体,结合 config.py、layer.py、model.py 等源码与 test_vera.py 测试用例,完整讲解 VeRA 的工作原理、VeraConfig全参数语义、保存/加载细节、多适配器行为及适用边界,读完即可在 PEFT 中正确配置并训练、部署 VeRA 适配器。
VeRA 工作原理:从 LoRA 的逐层矩阵到共享投影加缩放向量
LoRA 为每个被适配层学习两个小的低秩矩阵(A、B),r是这两个矩阵的内维(秩),它决定适配器的容量。由于每个被适配层都要学习自己的一对低秩矩阵,LoRA 的可训练参数会随r和被适配层数量快速膨胀。
VeRA 保留了"低秩更新"的核心思想,但改变了哪些参数被训练:
- 使用一对冻结的、随机初始化的低秩矩阵
A和B,在所有被适配层之间共享; - 每个被适配层只学习两个缩放向量
vera_lambda_d和vera_lambda_b,用它们缩放共享矩阵,产生该层特有的更新; vera_lambda_d的尺寸为r,因此 VeRA 的可训练参数仍然随r和被适配层数增长,但增长远比 LoRA 缓慢——因为大矩阵A、B是共享且冻结的,不再逐层学习。
论文中的简化参数计数对比如下:
| 方法 | 可训练参数规模 | 说明 |
|---|---|---|
| LoRA | 2 * L_tuned * d_model * r | L_tuned为被适配层数,d_model为模型维度,r为秩 |
| VeRA | L_tuned * (d_model + r) | 大低秩矩阵共享冻结,只训练每层的小向量 |
即 LoRA 的参数量随L_tuned与d_model * r的乘积增长,而 VeRA 只随L_tuned * (d_model + r)线性增长,这正是 VeRA 能用远少于 LoRA 的可训练参数的原因。
从源码看,前向计算路径与上述描述完全一致。在 layer.py 的Linear.forward中,每层先从共享矩阵中切出子矩阵,再按如下公式累加适配结果:
sliced_A = vera_A[:, : self.in_features] # 按输入维度切子矩阵 sliced_B = vera_B[: self.out_features, :] # 按输出维度切子矩阵 result = result + lambda_b * F.linear(lambda_d * F.linear(dropout(x), sliced_A), sliced_B)而 layer.py 的get_delta_weight则在合并(merge)场景下计算等效权重增量:ΔW = λ_b ⊙ (B_sliced @ (λ_d ⊙ A_sliced))(含fan_in_fan_out转置处理),并针对 CPU 上的 fp16/bf16 做了先提升到 fp32 计算再转回的优化。
共享矩阵的尺寸确定与子矩阵切分
不同被适配层的输入输出维度往往不同。由于 VeRA 只有一对共享投影矩阵,PEFT 在初始化时按各维度所需的最大尺寸创建A、B,前向时再切出对应子矩阵。文档给出的例子:同时适配形状为(100, 20)与(80, 50)的两个线性层,会创建形状分别为(rank, 50)和(100, rank)的A、B矩阵;适配形状为(100, 20)的层时,则切出形状为(rank, 20)与(100, rank)的子矩阵。
这一行为在源码中有明确实现:
- model.py 的
VeraModel._find_dim遍历所有目标模块,逐对取max计算全局最大的(out_features, in_features),找不到兼容层时会抛出ValueError提示检查target_modules; - model.py 的
_init_vera_A_vera_B据此创建vera_A: (r, linear_in_dim)与vera_B: (linear_out_dim, r)两个共享 Buffer,并使用基于projection_prng_key的torch.Generator以 Kaiming Uniform 方式确定性初始化; - 前向与
get_delta_weight中均通过vera_A[:, : in_features]、vera_B[: out_features, :]切片取用。
训练参数初始化
在 layer.py 中,每个适配器创建两个可训练参数:vera_lambda_b = ones(out_features)(全 1)、vera_lambda_d = randn(r);随后 layer.py 的reset_vera_parameters会将其重置为vera_lambda_d = d_initial(默认 0.1)、vera_lambda_b = 0。也就是说,训练起始时 VeRA 的增量权重为零,模型输出与基础模型完全一致,从恒等更新出发开始学习。文档同时提示d_initial推荐取较小值(≤0.1,论文 Table 6c)。
VeraConfig 配置参数全解析
VeraConfig定义于 config.py,是VeraConfig(PeftType.VERA,见 peft_types.py)的配置类。各参数语义如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
r | 256 | VeRA 的"秩"(参数维度)。由于 VeRA 可训练参数远少于 LoRA,建议取比 LoRA 更高的值(参考论文 Table 1) |
target_modules | None | 要应用 VeRA 的模块名列表或正则表达式,例如['q', 'v']或'.*decoder.*(SelfAttention|EncDecAttention).*(q\|v)$'。仅支持线性层 |
projection_prng_key | 0 | VeRA 的 PRNG 初始化种子。用于初始化vera_A/vera_B,或在加载未保存投影的 checkpoint 时重建投影 |
save_projection | True | 是否将vera_A/vera_B连同逐层lambda_b/lambda_d一起存入 state dict。设为True会增大 checkpoint,但保证任何平台都能正确重载 |
vera_dropout | 0.0 | VeRA 层的 dropout 概率 |
d_initial | 0.1 | vera_lambda_d向量的初始值,推荐 ≤0.1 |
fan_in_fan_out | False | 被替换层若按(fan_in, fan_out)存储权重(如 GPT-2 的Conv1D)需设为True |
bias | "none" | VeRA 偏置训练方式,可选'none'、'all'或'vera_only'。注意:若为'all'或'vera_only',即使停用适配器,模型输出也不会与基础模型完全一致 |
modules_to_save | None | 除 VeRA 层外需要置为可训练并保存的模块列表(如序列分类/标记分类任务中随机初始化的classifier/score层) |
init_weights | True | 是否用默认方式初始化 VeRA 层权重,除非清楚后果否则不要改动 |
layers_to_transform | None | 仅对指定层索引应用 VeRA 变换,可传列表或单个整数 |
layers_pattern | None | 仅在layers_to_transform非空时使用,用于定位模型的nn.ModuleList(通常名为'layers'或'h') |
在 config.py 的__post_init__中还有两条校验逻辑:
- 指定了
layers_pattern但未指定layers_to_transform时抛出ValueError; - 当
save_projection=False时发出警告:vera_A/vera_B将依据config.projection_prng_key重建,若想保证在所有系统配置上都能正确恢复 checkpoint,建议设回True。
使用示例
文档与 model.py 中给出的最小示例:
from transformers import AutoModelForCausalLM from peft import VeraConfig, get_peft_model base_model = AutoModelForCausalLM.from_pretrained("facebook/opt-125m") config = VeraConfig(r=128) model = get_peft_model(base_model, config)仓库中的实战示例 VeRA.ipynb 展示了序列分类任务的完整配置:
rank = 8 # for best results, increase this number peft_config = VeraConfig( r=rank, target_modules=["query", "value", "intermediate.dense"], task_type="SEQ_CLS", ) model = get_peft_model(model, peft_config)注意该示例面向 RoBERTa 一类模型,target_modules采用了"intermediate.dense"这类带层级前缀的命名方式;在 constants.py 中,PEFT 为常见 transformers 架构预置了默认目标模块映射TRANSFORMERS_MODELS_TO_VERA_TARGET_MODULES_MAPPING(继承自 LoRA 映射,并针对 phi 系列覆盖为["q_proj", "v_proj"]),不传target_modules时会使用该预置映射。
保存与加载:save_projection 与 projection_prng_key 的权衡
VeRA 的 checkpoint 大小主要取决于是否保存共享投影矩阵,这是其部署优势的关键开关:
save_projection=True(默认):vera_A/vera_B随每层lambda_b/lambda_d一起存入 state dict。checkpoint 更大,但任何设备与任何 PyTorch 版本下都能精确恢复;save_projection=False:不保存共享低秩矩阵,checkpoint 显著变小;加载时依据projection_prng_key固定随机种子重新生成。代价是无法保证在所有设备与所有未来 PyTorch 版本上可复现。若你重视可复现性,请保持True(默认值)。
源码层面的实现细节印证了这一点:
- model.py 创建
vera_A/vera_B时使用了BufferDict({}, persistent=config.save_projection)——persistent=False使它们不出现在 state dict 中; - model.py 的
_get_adapter_state_dict会先剔除每层对共享投影的重复引用,再在save_projection=True时以模型级键base_model.vera_A.<adapter>、base_model.vera_B.<adapter>形式写回,避免每个被适配层都重复存储同一份投影; - model.py 的
_remap_adapter_state_dict_for_load在加载时对save_projection与实际 checkpoint 内容做一致性校验并给出明确警告。
对应测试见 test_vera.py:test_save_projection_true_restores_saved_projections验证保存的投影必须被精确恢复(先修改投影再保存,确保不是靠 PRNG 重新生成巧合一致);test_multiple_adapters_save_projection_false_contains_no_vera_A_vera_B则用safetensors直接检查save_projection=False时 checkpoint 中确实不含vera_A/vera_B键。
多适配器支持与共享约束
VeRA 天然支持在一个模型上叠加多个适配器,但共享投影带来两条硬性约束(均由 model.py 的_check_new_adapter_config与 layer.py 的update_layer强制检查):
- 所有适配器的
projection_prng_key必须相同,否则add_adapter抛出ValueError(对应测试 test_vera.py); - 所有适配器的
save_projection必须一致(要么都保存,要么都不保存)。
此外,后添加的适配器会复用已有适配器的共享投影(代码中从现有适配器取一份参数引用),因此后添加适配器的层形状与秩不能超过已有共享矩阵的尺寸;若秩更小导致尺寸不满足,会抛出带有"以相反顺序加载适配器可能解决此问题"提示的ValueError。
测试 test_vera.py 验证了共享语义的底层实现:同一 PRNG key 下,各层各适配器的vera_A/vera_B通过data_ptr()断言共享同一块内存(vera_A与vera_B彼此不共享),而vera_lambda_b/vera_lambda_d在层间与适配器间各自独立(不共享内存)。多适配器场景下,可通过set_adapter("default")/set_adapter("other")切换生效的适配器。支持的数据类型在 test_vera.py 中覆盖了 float32、float16 与 bfloat16。
当前限制
- 仅支持
nn.Linear层(量化线性层与 transformers 的Conv1D亦被支持,见 model.py 的_get_tuner_layer_class);目标为其他模块类型(如卷积、Embedding)时无法使用。 - 各被适配层的输入/输出维度不宜差异过大:由于共享一对投影矩阵并按最大形状创建,若模型中同时存在维度悬殊的层(例如 transformer 的上/下投影层),共享矩阵会被"过度供给"到最大形状,从而削弱 VeRA 的参数效率优势。
何时选择 VeRA
推荐场景:
- 希望在保持与 LoRA 相近效果的前提下,把可训练参数压到最低;
- 需要存储或部署大量任务专属适配器——更小的适配器 checkpoint 直接降低存储成本;
- 在内存或参数预算非常紧张的情况下微调超大规模语言模型(论文在 7B/13B 模型上的指令微调中展示了其应用)。
不推荐场景:
- 需要每个被适配层拥有独立低秩矩阵,以换取学习参数的最大灵活性;
- 需要适配除
nn.Linear以外的模块类型; - 模型各被适配线性层的输入/输出维度差异很大(共享矩阵会因"取最大形状"而过量分配)。
论文摘要指出:VeRA 相比 LoRA 大幅减少了可训练参数数量,同时保持相同性能,其做法正是跨层共享一对低秩矩阵、只学习小的缩放向量,并在 GLUE、E2E 基准、图像分类任务以及 7B/13B 语言模型指令微调上验证了有效性。仓库文档另附方法对比基准(method_comparison目录下的实验结果可作为横向参考,例如 method_comparison 中的运行与评估框架)。
进一步探索
- 配置类完整源码与参数校验:src/peft/tuners/vera/config.py
- 层实现(前向、merge/unmerge、delta weight):src/peft/tuners/vera/layer.py
- 模型级实现(共享矩阵初始化、多适配器校验、state dict 处理):src/peft/tuners/vera/model.py
- 专门针对共享权重挑战的测试套件:tests/test_vera.py
- 序列分类实战 Notebook:examples/sequence_classification/VeRA.ipynb
- 方法对比基准框架:method_comparison/README.md
【免费下载链接】peft🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.项目地址: https://gitcode.com/gh_mirrors/pe/peft
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考