深入解析 PEFT 中的 RandLora:全秩参数高效微调的原理与实战
【免费下载链接】peft🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.项目地址: https://gitcode.com/gh_mirrors/pe/peft
RandLora(RandLoRA)是 🤗 PEFT 内置的一种参数高效微调方法,它用多个固定随机低秩基的线性组合来构造全秩权重更新,在保留 LoRA 参数量与内存效率的同时突破低秩约束对性能的天花板。本文以examples/randlora_finetuning/下的官方指南与训练脚本为核心,结合src/peft/tuners/randlora/的源码实现,完整讲解 RandLora 的设计动机、配置项、训练脚本用法、与 LoRA/VeRA 的对比以及底层实现细节,读完即可用一行命令复现 RandLora 微调并完成与 LoRA 的对比实验。
一、RandLora 是什么:一次对低秩范式的挑战
RandLora 是发表于 ICLR 2025 的参数高效微调(PEFT)方法,与 LoRA、VeRA 同属"随机基 + 可训练缩放"家族,但核心差异在于:它执行的是全秩(full-rank)权重更新。
原文档 examples/randlora_finetuning/README.md 对这一动机有清晰交代:LoRA 及其变体通过低秩分解大幅压缩可训练参数量,但低秩更新的表示能力本身存在上限——当任务复杂、需要表达更丰富的权重变化时,低秩约束会限制性能增益,即使提高 LoRA 的秩(rank)也是如此。RandLora 正是为了回答论文中提出的关键问题:LoRA 与全量微调之间的性能差距,究竟来自"可训练参数变少",还是来自"更新矩阵的秩不足"?
RandLora 的答案是通过线性缩放随机基(linearly scaling random bases)构造全秩更新:
- 随机基是多个低秩矩阵的集合,这些低秩矩阵的秩之和 ≥ 被适配权重矩阵的满秩;
- 可训练参数只有两个对角缩放矩阵(向量),分别记为
randlora_lambda(λ)与randlora_gamma(γ),它们与随机基相乘的方式与 VeRA 的更新类似; - 为了维持低内存占用,RandLora 使用自定义的反向传播函数(
UniqueBaseGrad),避免为反向传播在内存中保存不必要的基。
在 PEFT 仓库中,该方法的注册、配置、层实现与量化支持分别位于 src/peft/tuners/randlora/config.py、src/peft/tuners/randlora/layer.py、src/peft/tuners/randlora/model.py 与 src/peft/tuners/randlora/bnb.py,并在 src/peft/tuners/randlora/init.py 中通过register_peft_method(name="randlora", ...)注册为RANDLORA类型。完整 API 文档见 docs/source/package_reference/randlora.md。
二、核心原理:多个低秩随机基如何拼出全秩更新
2.1 基的数量与秩的反比关系
假设被适配的线性层最小维度为min_dim,配置的基秩为r,则需要的随机基数量num_bases在 src/peft/tuners/randlora/layer.py 中计算:
num_bases = min(self.in_features, self.out_features) / r self.num_bases = int(num_bases) if num_bases.is_integer() else int(num_bases) + 1 # Full rank即num_bases × r ≥ min_dim,保证所有基的秩之和不小于被适配矩阵的满秩。这一设计带来一个与 LoRA完全相反的特性(原文档与 docs/source/package_reference/randlora.md 均强调):
RandLora 的可训练参数量与秩
r成反比:num_bases × r恒定,降低r会增加基的数量,进而增加基特有的可训练对角矩阵数量;提高r则减少可训练参数。
因此使用RandLoraConfig时,r不再是"越大越好"的旋钮,而是需要按目标参数量反向调节。
2.2 可训练参数:两个对角缩放矩阵
每个适配器只包含两个可训练参数(layer.py):
self.randlora_lambda[adapter_name] = nn.Parameter(torch.randn(r, self.num_bases), requires_grad=True) self.randlora_gamma[adapter_name] = nn.Parameter( torch.ones(self.num_bases, min(self.out_features, self.in_features)) / max(self.out_features, self.in_features), requires_grad=True, )初始化时randlora_lambda置零、randlora_gamma设为常数(见 layer.py 的reset_randlora_parameters),保证训练初期适配器输出为零、模型行为与基座一致,这也是标准 LoRA 式初始化惯例。
缩放系数scaling = randlora_alpha / r(layer.py),与 LoRA 的alpha/r形式一致。
2.3 共享随机基与切片
与 VeRA 类似,RandLora 的randlora_A/randlora_B随机基是全局共享的:按所有被适配层中每个维度的最大尺寸初始化,前向时对每个层切片出所需子矩阵(src/peft/tuners/randlora/model.py 初始化,layer.py 切片)。文档中的示例:适配形状为 (100, 20) 与 (80, 50) 的两个线性层,会生成形状 (rank, 50) 与 (100, rank) 的共享 A/B 矩阵,再为 (100, 20) 层切出 (rank, 20) 与 (100, rank) 的子矩阵。
_find_dim(model.py)遍历被适配模块求出最大 in/out 维度,若未找到兼容层会抛出 "No layers types compatible with RandLora were found" 错误。
2.4 内存优化:自定义反传函数
为了不在反向传播中保存冗余中间张量,RandLora 在 layer.py 定义了UniqueBaseGrad自定义 autograd 函数:
class UniqueBaseGrad(torch.autograd.Function): @staticmethod def forward(ctx, randlora_A, randlora_lambda, randlora_gamma): out = randlora_lambda[:, :, None] * randlora_A * randlora_gamma[None,] ctx.save_for_backward(randlora_A, randlora_lambda, randlora_gamma) return out @staticmethod def backward(ctx, grad_output): randlora_A, randlora_lambda, randlora_gamma = ctx.saved_tensors ... grad_randlora_lambda = torch.einsum("kbj,kvj,bj->kb", grad_output, randlora_A, randlora_gamma) grad_randlora_gamma = torch.einsum("kbj,kvj,kb->bj", grad_output, randlora_A, randlora_lambda) return None, grad_randlora_lambda, grad_randlora_gamma其巧妙之处在于:同一组随机基被所有被适配层共享,因此对randlora_lambda/randlora_gamma的梯度可跨层(batch 维 k)聚合,无需为每层单独保留一份基的副本,这正是原文档所说的"随机基共享策略带来内存优势"。
三、配置详解:RandLoraConfig 全参数
RandLoraConfig(src/peft/tuners/randlora/config.py)继承自PeftConfig,peft_type自动设为RANDLORA。核心参数如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
r | 32 | 随机基的秩。与可训练参数量成反比:调低r增加参数,调高r减少参数 |
target_modules | None | 要应用 RandLora 的模块名列表或正则,仅支持线性层;例如["q_proj", "v_proj"]或".*decoder.*(SelfAttention|EncDecAttention).*(q\|v)$" |
randlora_alpha | 640 | 缩放系数,通常取基秩的 20 倍;过大会导致数值不稳定,此时应降低学习率或该系数 |
randlora_dropout | 0.0 | 适配器层 dropout 概率 |
sparse | False | 使用三元稀疏随机基(仅含 -1、0、1),-1/1 的赋值概率各 1/6、0 为 2/3;有助减少过拟合(见下方原理) |
very_sparse | False | 高度稀疏随机基,-1/1 的赋值概率为1/√D(D 为最小维度);可进一步减少过拟合,但通常会降低性能,需谨慎使用 |
save_projection | True | 是否把全局randlora_A/randlora_B存入 state dict。设为False可显著缩小 checkpoint,但恢复时依赖projection_prng_key的固定随机种子,无法在所有设备与 PyTorch 版本上保证可复现 |
projection_prng_key | 0 | 用于确定性初始化 A/B 基的 PRNG 种子;新增适配器时所有适配器必须使用相同 key |
fan_in_fan_out | False | 目标层是否以 (fan_in, fan_out) 存储权重,GPT-2 的Conv1D需设为True |
bias | "none" | 可选"none"、"all"、"randlora_only";注意设"all"/"randlora_only"后即使禁用适配器,输出也不再等价于基座模型 |
modules_to_save | None | 除 RandLora 层外需置为可训练并保存的模块(如分类头) |
init_weights | True | 是否用默认初始化初始化 RandLora 层权重,非明确原因不要改动 |
layers_to_transform/layers_pattern | None | 指定仅对部分层索引应用变换,layers_pattern仅在该索引列表非常见层模式时使用 |
两个关键配置的底层行为值得展开:
- 稀疏基的生成(model.py):
_init_randlora_A_randlora_B_sparse先生成均匀随机张量,按阈值1/(2*sparsity)置为 -1/0/1 三元基,再做标准差归一化。sparse模式下sparsity=3;very_sparse模式下sparsity=√min_dim(model.py 的_pre_injection_hook据此分发)。原文档指出:目前实现属于 proof-of-concept,稀疏性并未用于加速或省内存,但其"矩阵元素多为 0"的结构天然起到正则化作用、可缓解过拟合。 - checkpoint 体积与可复现性:
save_projection=False时,A/B 基从 state dict 中排除(通过BufferDict(persistent=config.save_projection),见 model.py),加载时依据projection_prng_key重建。__post_init__会在该配置下给出警告(config.py)。仓库测试 tests/test_randlora.py 中的test_multiple_adapters_save_projection_false_contains_no_randlora_A_randlora_B验证了该模式下 state dict 确实不含randlora_A/randlora_B。
四、快速开始:三行切换 LoRA → RandLora
原文档给出的最小示例(已修正其中get_peft_model(model, lora_config)的笔误为randlora_config):
import torch from peft import RandLoraConfig, get_peft_model from transformers import AutoTokenizer, AutoModelForCausalLM, Trainer from datasets import load_dataset model = AutoModelForCausalLM.from_pretrained("huggyllama/llama-7b", device_map="auto") tokenizer = AutoTokenizer.from_pretrained("huggyllama/llama-7b") dataset = load_dataset("timdettmers/openassistant-guanaco", split="train") randlora_config = RandLoraConfig() peft_model = get_peft_model(model, randlora_config) trainer = Trainer( model=peft_model, train_dataset=dataset, dataset_text_field="text", max_length=2048, processing_class=tokenizer, ) trainer.train() peft_model.save_pretrained("randlora-llama-7b")要点与原文档一致:无需改变任何标准 PEFT 训练流程,只需把LoraConfig换成RandLoraConfig。唯一的反直觉点在于可训练参数量与r成反比——降低r增加参数,提高r减少参数,规划参数量预算时务必注意。
五、实战脚本:randlora_finetuning.py 全参数指南
仓库在 examples/randlora_finetuning/randlora_finetuning.py 提供了完整可运行的微调脚本(基于examples/dora_finetuning/dora_finetuning.py改写)。直接运行:
python examples/randlora_finetuning/randlora_finetuning.py --base_model meta-llama/Meta-Llama-3-8B --data_path timdettmers/openassistant-guanaco默认即加载 RandLora 配置的 PEFT 模型。脚本支持的参数与默认值如下(对应argparse定义,见 randlora_finetuning.py):
| 参数 | 默认值 | 说明 |
|---|---|---|
--base_model | huggyllama/llama-7b | 基座模型路径或名称 |
--data_path | timdettmers/openassistant-guanaco | 数据集路径或名称 |
--output_dir | path/to/output | 微调输出目录 |
--batch_size | 1 | 批大小 |
--num_epochs | 1 | 训练轮数 |
--learning_rate | 3e-4 | 学习率 |
--cutoff_len | 512 | 分词截断长度 |
--val_set_size | 500 | 验证集大小 |
--use_lora | False(flag) | 改用 LoRA 而非 RandLora |
--quantize | False(flag) | 使用 4-bit 量化 |
--eval_step | 10 | 评估间隔 |
--save_step | 100 | 保存间隔 |
--device | auto | 训练设备,auto时自动探测加速器 |
--rank | 32 | RandLora 基秩 |
--randlora_alpha | 640 | RandLora alpha |
--randlora_dropout | 0.05 | dropout |
--randlora_target_modules | None(默认 k_proj,v_proj) | 逗号分隔的目标模块列表 |
--sparse | False(flag) | 使用稀疏随机基 |
--very_sparse | False(flag) | 使用高度稀疏随机基 |
--hub_model_id/--push_to_hub | — | 推送模型到 Hub |
5.1 与 LoRA 快速对比
原文档强调:想快速和 LoRA 对比,只需加--use_lora,并把--randlora_alpha降到 2 倍秩:
python examples/randlora_finetuning/randlora_finetuning.py --base_model meta-llama/Meta-Llama-3-8B --data_path timdettmers/openassistant-guanaco --use_lora --rank 32 --randlora_alpha 64脚本内部(randlora_finetuning.py)在--use_lora时构造LoraConfig(r=rank, lora_alpha=randlora_alpha, target_modules=..., lora_dropout=randlora_dropout, bias="none"),否则构造RandLoraConfig(...)——两个分支共用同一组命令行参数,保证对比时除方法本身外其余设置完全一致。
5.2 稀疏基与量化
- 稀疏训练:
--sparse(稀疏)或--very_sparse(高度稀疏),脚本会原样传入RandLoraConfig。原文档提示稀疏矩阵有助减少过拟合。 - 4-bit 量化(QRandLora):加
--quantize,脚本内部使用BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4")加载基座,并通过prepare_model_for_kbit_training做 k-bit 训练准备(randlora_finetuning.py)。PEFT 为量化场景提供了Linear8bitLt与Linear4bit实现(src/peft/tuners/randlora/bnb.py),二者通过 model.py 的_create_new_module按目标层类型自动分发。
5.3 自定义目标模块
默认只适配 Llama 的k_proj、v_proj(README 中也说明默认是 key/value 层)。向更多层添加适配器会线性增加内存与参数开销,可自定义:
python examples/randlora_finetuning/randlora_finetuning.py --randlora_target_modules "q_proj,k_proj,v_proj"脚本按逗号拆分传入target_modules。由于 RandLora 的目标模块映射直接复用 LoRA 的映射表(TRANSFORMERS_MODELS_TO_RANDLORA_TARGET_MODULES_MAPPING = TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING.copy(),见 src/peft/utils/constants.py),各主流架构的默认目标模块与 LoRA 一致,且支持target_modules=None时自动应用默认映射。
5.4 完整示例命令
python examples/randlora_finetuning/randlora_finetuning.py \ --base_model "PATH_TO_MODEL" \ --data_path "PATH_TO_DATASET" \ --output_dir "PATH_TO_OUTPUT_DIR" \ --batch_size 1 \ --num_epochs 3 \ --learning_rate 3e-4 \ --cutoff_len 512 \ --val_set_size 500 \ --quantize \ --eval_step 10 \ --save_step 100 \ --device "auto" \ --rank 32 \ --randlora_alpha 640 \ --randlora_dropout 0.05 \ --randlora_target_modules "k_proj,v_proj" \ --hub_model_id "YOUR_HF_REPO" \ --push_to_hub该脚本的训练配置也值得留意:warmup_steps取总步数的 10%,weight_decay=0.01,并且通过gradient_accumulation_steps = 16 // batch_size保证累积后批大小不小于 16(脚本注释明确建议维持这一最小批量以获得良好性能,randlora_finetuning.py)。
六、RandLora vs. LoRA:为什么值得换
原文档对二者的定位非常明确:
- 挑战低秩范式:RandLora 学习全秩更新。论文实验表明 LoRA 的低秩约束会在可训练参数增加(秩提高)时限制性能增益,而 RandLora 用固定随机基的线性组合突破这一瓶颈。
- 适用场景:优先推荐用于 LoRA 欠拟合的困难任务;同时"如果提高 LoRA 的秩能改善你的任务表现,RandLora 大概率会表现更好"。
- 预期收益:在可训练参数量相当时,RandLora 预计优于 LoRA,尤其是等效 LoRA 秩大于 4 的较大参数量区间。
代价是两点限制:
- 对缩放系数敏感:性能依赖较大的
randlora_alpha(通常取基秩的 20 倍,即默认640/32)。过大的缩放系数可能使训练不稳定,此时应降低学习率或调小randlora_alpha。 - 极低秩下训练变慢:降低
r会增加基数量,RandLora 在非常低的秩(典型如r < 4)下训练时间相比 LoRA 会明显增加。不过这不影响推理——适配器可以合并回预训练权重矩阵(merge/unmerge实现见 layer.py,量化层对应实现见 bnb.py)。
七、RandLora vs. VeRA:同门不同路
RandLora 与 VeRA 都用随机基组合来回应 LoRA 的某些局限,但解决的问题不同:
- VeRA的目标是超越 rank-1 LoRA 进一步压缩可训练参数;
- RandLora的目标是消除随可训练参数增长而出现的低秩性能瓶颈。
由此得到两个预期结论(原文档原话):
- 在需要更多可训练参数的困难任务上,RandLora 预计优于 VeRA;
- 得益于随机基共享策略,RandLora 相比 VeRA 具有更低的内存占用。
从实现看,这一内存优势确有依据:A/B 基在RandLoraModel层面全局共享一份(self.randlora_A/self.randlora_B,模型级 BufferDict),各层只保存自己的randlora_lambda/randlora_gamma。仓库测试test_randlora_A_randlora_B_share_memory断言了不同层之间randlora_A/randlora_B的data_ptr()相同(即共享同一份内存),而test_randlora_lambda_dont_share_memory验证了每个适配器/每层的 λ、γ 各自独立(tests/test_randlora.py)。
八、底层实现:共享基、多适配器与保存加载
8.1 前向计算路径
被适配层的前向(layer.py)可概括为:
result = self.base_layer(x, *args, **kwargs) for active_adapter in self.active_adapters: dropout = self.randlora_dropout[active_adapter] update_B, update_A = self.get_scaled_bases(active_adapter, device=x.device) scaling = self.scaling[active_adapter] result = result + F.linear(F.linear(dropout(x), update_B), update_A) * scalingget_scaled_bases(layer.py)完成三件事:从共享 A/B 基切片出当前层所需子矩阵、通过UniqueBaseGrad把 λ/γ 应用到最小的基(randlora_A)上、展平矩阵维度以减少内存;随后根据min_dim与 in/out 维度的关系决定update_A/update_B的相乘顺序,以最小化可训练参数。get_delta_weight(用于合并)则计算(update_B.T @ update_A.T).T * scaling。
8.2 多适配器约束
由于 A/B 基全局唯一,新增第二个适配器时(layer.py)会复用已有适配器的基,并做形状兼容校验:若新适配器需要更大的基(更高的r或更大的层维度),会抛出明确的 ValueError,提示"加载适配器时按逆序加载可能解决"。
同时 model.py 的_check_new_adapter_config强制要求:所有适配器的projection_prng_key必须一致、save_projection取值必须统一,否则直接报错。这些约束正是为了维护"一份共享基、多个可训练对角矩阵"的数据结构的正确性。
8.3 初始化细节
- 普通模式(model.py):A/B 基用带显式
torch.Generator(device="cpu").manual_seed(config.projection_prng_key)的 Kaiming Uniform 初始化(_kaiming_init,a=√5),保证给定 key 时结果完全确定;随后除以自身标准差做归一化(源码注释说明这是经验上最优的归一化方式)。 - 稀疏模式:三元稀疏基 + 标准差归一化,如 5.2 节所述。
8.4 仓库基准与测试验证
仓库method_comparison目录中提供了 RandLora 在 MetaMathQA(Llama-3.2-3B)与图像生成(FLUX.2 klein)上的真实基准记录:
- 配置快照 method_comparison/MetaMathQA/experiments/randlora/llama-3.2-3B-default/adapter_config.json:
r=32、randlora_alpha=640、randlora_dropout=0.0、save_projection=true、target_modules=["q_proj","v_proj"],与本文所述默认配置一致。 - 运行记录 method_comparison/MetaMathQA/results/randlora--llama-3.2-3B-default.json:在 L40S 上 5000 步训练成功,
num_trainable_params=9,289,728(约 929 万),num_total_params=3,222,039,552(约 32.2 亿),即可训练参数占比约 0.29%——可作为理解"参数高效"的直观参考。注意这些是仓库内单次实验记录,不宜推广为普适结论。
单元测试方面,tests/test_randlora.py 覆盖了共享内存语义、多适配器 PRNG 一致性校验、save_projection=False的保存/加载与 state dict 内容、不同形状层的共享基切片(test_randlora_different_shapes断言randlora_A.shape == (r, 1, in_features)、randlora_B.shape == (out_features, 1, r))以及多种 dtype 下的训练/推理,可作为理解实现语义的补充材料。
九、限制与注意事项小结
综合原文档与源码,使用 RandLora 前请确认以下约束:
- 仅支持
nn.Linear层(含Conv1D,后者需fan_in_fan_out=True),不支持卷积等其它层类型; - 可训练参数随
r反比变化,与 LoRA 直觉相反,规划预算时勿照搬 LoRA 的调参习惯; randlora_alpha默认较大(640),高学习率下易数值不稳,训练发散时优先降学习率或降 alpha;- 极低秩(
r < 4)训练速度明显慢于 LoRA,但合并后推理无额外开销; - 多适配器共享同一份随机基,
projection_prng_key与save_projection必须在所有适配器间保持一致; - 若追求 checkpoint 最小化可设
save_projection=False,但可复现性依赖 PRNG 种子,跨设备/跨 PyTorch 版本无法保证,默认True更稳妥。
十、引用
使用 RandLora 开展研究时,可引用原论文:
@inproceedings{2025_ICLR_RandLoRA, title="{RandLoRA: Full rank parameter-efficient fine-tuning of large models}", author="Albert, Paul and Zhang, Frederic Z. and Saratchandran, Hemanth and Rodriguez-Opazo, Cristian and van den Hengel, Anton and Abbasnejad, Ehsan", booktitle="{International Conference on Learning Representations (ICLR)}", year="2025" }总结:RandLora 用"多个固定低秩随机基之和达到满秩 + 仅训练两个对角缩放矩阵"的设计,在维持 LoRA 级参数效率的同时提供全秩更新的表达能力。无论你是在复现论文、为困难任务寻找比 LoRA 更强的 PEFT 基线,还是想快速对比稀疏基与量化组合的效果,都可以从 examples/randlora_finetuning/randlora_finetuning.py 出发,一行命令开始实验。
【免费下载链接】peft🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.项目地址: https://gitcode.com/gh_mirrors/pe/peft
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考