PEFT 中的 RoAd(2D 旋转适配):从原理到微调、量化与混合批次推理的完整实践指南
【免费下载链接】peft🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.项目地址: https://gitcode.com/gh_mirrors/pe/peft
RoAd(2D Rotary Adaptation)是 PEFT 提供的一种参数高效微调方法:它通过学习一组作用于隐藏维度对的 2×2 旋转矩阵(及可选缩放因子)来适配大语言模型,以不足 0.1% 的可训练参数取得与 LoRA 等主流方法相当甚至更优的效果。本文以 docs/source/package_reference/road.md 为骨架,结合 road 调谐器源码 与 road_finetuning 示例,完整讲解 RoAd 的数学原理、RoadConfig 全部配置参数、微调训练流程、bitsandbytes 量化支持,以及同批次多适配器混合推理的实现细节,帮助你在实际项目中直接落地 RoAd。
RoAd 是什么:用稀疏 2D 旋转代替低秩更新
RoAd 的核心思想非常简洁:把隐藏向量切分成许多 2 维向量,然后用各自独立的 2×2 旋转矩阵去旋转每一个 2 维向量;为了获得额外灵活性,每个旋转矩阵还会乘上一个可训练的缩放因子 α。从 layer.py 的 RoadLayer 文档字符串可以看到,对向量R @ x的变换可展开为两个逐元素公式:
y₀ = x₀ * α * cosθ - xₙ * α * sinθyₙ = x₀ * α * sinθ + xₙ * α * cosθ
其中缩放因子 α 与角度 θ 为每个元素对分别学习,且当使用 road_2、road_4 变体时,旋转矩阵中的 4 个分量还可以各不相同(详见下文「变体」一节)。
与 LoRA 的批量低秩更新相比,RoAd 的稀疏旋转可以改写为简单的逐元素(element-wise)运算,因此在前向传播与推理时不引入昂贵的批量矩阵乘法。这一特性带来两个直接收益:
- 极低的参数开销:适配器只需要存储角度 θ 与缩放 α,而不是完整的矩阵 R,可训练参数占比低于 0.1%;
- 更高的服务吞吐:当同一批次中同时处理异构请求(即同时服务多个适配器)时,逐元素运算的开销远小于 LoRA 的批量低秩更新,显著提升混合批次推理的吞吐。
此外,RoAd 还能无缝融入分布式交换干预(distributed interchange intervention)框架:把稀疏的 2D 旋转理解为在隐藏表示学习子空间中的任务特定干预。这些正交子空间可以相互组合,从而在不额外微调的情况下合并多种任务行为(如多语言能力、指令跟随),实现模块化、可解释的 LLM 适配。
需要指出的是,上述「不足 0.1% 参数、更高吞吐、可组合」等表述均来自 road.md 与 examples/road_finetuning/README.md 的官方描述,具体效果以你自己的基准测试为准;仓库的 method_comparison/MetaMathQA 基准目录中提供了 RoAd 在 MetaMathQA 任务上的实测结果 JSON 文件可供参考。
使用约束与适用前提
在动手之前,先明确 RoAd 的边界条件(来自 road.md 与源码):
- 学习率:RoAd 微调通常需要比 LoRA 等方法更高的学习率,建议设为1e-3 左右;
- 目标模块:当前 RoAd仅支持线性层(
torch.nn.Linear),从 model.py 的分发逻辑可以看到,不支持的模块类型会直接抛出ValueError; - 量化支持:可用于 bitsandbytes 量化的模型(4-bit 或 8-bit),对应实现见 bnb.py 中的
Linear8bitLt与Linear4bit; - 混合批次推理:不同 RoAd 适配器在同一批次内混合推理的能力与 LoRA 相同,可参考 lora.md 中「Inference with different LoRA adapters in the same batch」一节,源码实现位于 model.py 的
_enable_peft_forward_hooks与 layer.py 的_mixed_batch_forward。
RoadConfig:核心配置参数详解
RoadConfig 定义在 src/peft/tuners/road/config.py,继承自PeftConfig。其关键参数如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
variant | "road_1" | RoAd 变体,可选road_1/road_2/road_4,详见下文 |
group_size | 64 | 元素分组大小,决定元素如何配成 2D 向量进行旋转 |
init_weights | True | 是否执行 RoAd 权重的默认初始化 |
target_modules | None | 要替换为目标模块名的列表或正则字符串 |
modules_to_save | None | 除 RoAd 层外需要置为可训练并保存的模块列表 |
在__post_init__中(config.py),配置会做三件事:
- 将
peft_type固定为PeftType.ROAD; - 若
target_modules传入列表则转换为set; - 校验合法性:
variant必须是road_1、road_2、road_4之一;group_size必须为正且能被 2 整除,否则抛出ValueError。
variant:三种参数规模档位
variant直接决定每个层级的可训练参数量,对应 config.py 与 layer.py 中的实现:
road_1:所有元素对共享同一组缩放与角度。每个应用 RoAd 的层只存储数量等于输出隐藏维度的参数(实际为out_features // 2,配对被复用),参数最少;road_2:每个元素使用独立的缩放与角度,参数量为road_1的2 倍(out_features个);road_4:每个元素使用两个不同的缩放与角度(旋转矩阵两列分别学习),参数量为road_1的4 倍(out_features * 2个)。
三种变体对应的参数准备逻辑在 layer.py 的_prepare_cols中实现:road_1通过repeat_interleave复用组内group_size // 2个参数;road_2每个元素独立;road_4把参数 reshape 为(-1, 2, group_size),前半段用于第一列(alpha_1 * cosθ),后半段用于第二列(alpha_2 * sinθ)。
group_size:分组配对与推理速度
group_size定义元素如何分组为 2D 向量:在每个组内,元素 0 与元素group_size/2配对,元素 1 与元素group_size/2+1配对,依此类推。这种「前一半与后一半配对」而非相邻配对的方式,是特意设计的——它不改变模型性能(因为元素是无序的),但对推理速度有影响,在 VLLM 等场景下尤为明显。
- 官方建议:group_size 至少取 32,推荐 64(默认值)以获得最佳速度;
- 硬性约束:模型的隐藏维度(使用张量并行时为每个分区的隐藏维度)必须能被
group_size整除。因此对于隐藏维度很小的模型,你可能需要调小该参数。若out_features % group_size != 0,layer.py 会抛出ValueError。
target_modules 与 modules_to_save
target_modules:传字符串时执行正则匹配;传列表时做精确匹配或以列表元素为后缀的匹配;传"all-linear"通配符时选择所有 linear/Conv1D 模块(若模型是PreTrainedModel,输出层会被排除);若不指定,则按模型架构自动选择(RoAd 的目标模块映射TRANSFORMERS_MODELS_TO_ROAD_TARGET_MODULES_MAPPING由 LoRA 的映射复制而来,见 constants.py)。对于未知架构,会抛出错误,此时需手动指定target_modules。modules_to_save:例如在 Sequence Classification 或 Token Classification 任务中,随机初始化的classifier/score最终层需要被设为可训练并保存,就通过该参数指定。
权重初始化
road.md 未展开初始化细节,但从 layer.py 的reset_parameters可以看到:
init_weights=True(默认):road_theta用nn.init.zeros_置零、road_alpha用nn.init.ones_置为 1,即初始为恒等旋转,保证训练开始时与基础模型行为一致;init_weights=False:road_theta用均值 0、标准差 0.5 的正态分布初始化,road_alpha用均值 1、标准差 0.5 的正态分布初始化。
config 的帮助文本特别提醒:除非你完全清楚自己在做什么,否则不要修改init_weights默认值。
快速开始:用 RoadConfig 微调因果语言模型
最直接的入口是 examples/road_finetuning/README.md 提供的快速开始代码:
from peft import RoadConfig, get_peft_model from transformers import AutoModelForCausalLM, AutoTokenizer, 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") road_config = RoadConfig(variant="road_1") peft_model = get_peft_model(model, road_config) trainer = Trainer( model=peft_model, train_dataset=dataset, dataset_text_field="text", max_length=2048, tokenizer=tokenizer, ) trainer.train() peft_model.save_pretrained("road-llama-3-8b")注意:示例 README 中RoadConfig(variant="1")属于示例中的写法,而配置校验(config.py)要求变体必须严格等于road_1/road_2/road_4,因此请使用variant="road_1"这类完整取值。
微调时请务必把学习率调高到1e-3 附近(相对 LoRA 而言),这是 road.md 明确给出的建议。
完整训练脚本与命令行参数
仓库在 examples/road_finetuning/road_finetuning.py 提供了开箱即用的完整训练脚本,支持 Hugging Face Trainer、4-bit 量化与推送 Hub。核心配置段如下(road_finetuning.py):
from peft import RoadConfig, get_peft_model, prepare_model_for_kbit_training # RoAd config for the PEFT model road_config = RoadConfig( variant=variant, # road_1 / road_2 / road_4 target_modules=( road_target_modules.split(",") if road_target_modules else ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"] ), ) # get the peft model with RoAd config model = get_peft_model(model, road_config)脚本内部支持量化路径(road_finetuning.py):通过BitsAndBytesConfig(load_in_4bit=True, ...)加载模型后调用prepare_model_for_kbit_training(model, use_gradient_checkpointing=True),与 RoAd 无缝配合。
运行方式(examples/road_finetuning/README.md):
# 普通微调 python examples/road_finetuning/road_finetuning.py \ --base_model meta-llama/Meta-Llama-3-8B \ --data_path timdettmers/openassistant-guanaco # 4-bit 量化微调 python examples/road_finetuning/road_finetuning.py \ --base_model meta-llama/Meta-Llama-3-8B \ --quantize完整参数示例(含学习率、变体与目标模块):
python road_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 1e-3 \ --cutoff_len 512 \ --val_set_size 500 \ --quantize \ --eval_step 10 \ --save_step 100 \ --device "cuda:0" \ --variant road_1 \ --road_target_modules "q_proj,k_proj,v_proj,o_proj" \ --hub_model_id "YOUR_HF_REPO" \ --push_to_hub训练完成后可用save_pretrained保存适配器;加载时与普通 PEFT 模型一致,通过AutoModel.from_pretrained即可直接使用。
推理与合并:逐元素运算背后的数学
RoAd 前向传播的逐元素实现位于 layer.py 的_apply_road:
x_grouped = x.reshape(-1, 2, group_size // 2) x1 = x_grouped[:, 0, :] x2 = x_grouped[:, 1, :] rotate_half_x = torch.stack((-x2, x1), dim=1).reshape(x.shape) result = x * first_col + rotate_half_x * second_col即先按组把向量切成前后两半并配对,构造「旋转一半」((-x2, x1),与 RoPE 的 rotate_half 思路一致),再与预计算的first_col、second_col(α*cosθ与α*sinθ)逐元素相乘累加——整个旋转过程不出现稠密矩阵乘法,这正是 RoAd 高效推理的基础。
如果需要把适配器合并进基础权重,layer.py 给出了合并公式:
R @ (W @ x + b) = (R @ W) @ x + R @ b
左侧是未合并状态下的使用方式(用高效的逐元素实现替代矩阵乘法),右侧是合并后(R @ W)成为新权重、R @ b成为新偏置。具体合并实现见Linear.merge(layer.py),它通过_get_delta_weight由角度与缩放重建分块对角旋转矩阵 R 并与权重做torch.matmul;safe_merge=True时会在副本上检查 NaN 再写入。合并后的unmerge则利用torch.linalg.inv求逆还原(由于旋转矩阵未必正交,因此用逆而非转置,layer.py)。
对 bitsandbytes 量化模型,合并逻辑在 bnb.py 中:8-bit 合并(Linear8bitLt.merge)与 4-bit 合并(Linear4bit.merge)都会先将量化权重反量化、与 R 相乘后重新封装为bnb.nn.Int8Params/bnb.nn.Params4bit,且官方在源码中提示:对量化层合并/反合并可能因舍入误差导致生成结果与未合并时略有差异。
同批次混合多适配器推理
RoAd 与 LoRA 一样支持在同一个批次中为不同样本使用不同适配器。调用方式与 lora.md 中的「Inference with different LoRA adapters in the same batch」一致:在模型 forward 时传入adapter_names列表(长度与输入批次一致),其中可用"__base__"表示不使用任何适配器。
其底层实现路径是:
- model.py 的
_enable_peft_forward_hooks:校验adapter_names中不存在的适配器名(防止拼写错误)并注入 forward 预钩子;训练模式下传adapter_names会直接抛错; - layer.py 的
_mixed_batch_forward:先跑一次基础层前向,再按adapter_names把批次切分为子批次,对每个子批次分别应用对应适配器的_apply_road,最后写回原位置。由于 RoAd 是逐元素运算,这种「按样本分派」的开销远小于逐样本执行批量矩阵乘法,这正是文档所述高吞吐的来源; - 若存在已合并的适配器,同时传入
adapter_names会抛出错误(layer.py),需先调用unmerge_adapter。
该能力在测试侧同样有覆盖:tests/testing_common.py的_test_mixed_adapter_batches明确将RoadConfig与LoraConfig一并列为支持混合适配器批次的配置类(tests/testing_common.py)。
配置与测试验证
- 配置合法性:tests/test_config.py 将
RoadConfig纳入全量配置测试矩阵,验证其默认值、__post_init__校验(非法 variant、非偶数 group_size 抛错)以及保存/加载往返一致性; - 与框架集成:
RoadConfig在 auto.py 与 peft_types.py 中注册为PeftType.ROAD,因此AutoPeftModel、get_peft_model等统一入口均可用; - 目标模块映射:复用 LoRA 的映射表(constants.py),意味着主流 Transformers 架构开箱即用。
小结
RoAd 是 PEFT 家族中「以最简几何操作换取极致参数效率」的代表方法:2×2 旋转 + 缩放即可完成适配,逐元素前向天然适配混合批次高吞吐服务,稀疏旋转还可作为可组合、可解释的子空间干预。落地时记住三个要点:学习率用 1e-3 量级、只作用于线性层、group_size需整除隐藏维度。需要深入时,可继续阅读 road 调谐器源码(config / layer / model / bnb 四个文件)与 road_finetuning 示例,并结合 method_comparison/MetaMathQA 的实测数据自行评估效果。
【免费下载链接】peft🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.项目地址: https://gitcode.com/gh_mirrors/pe/peft
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考