PEFT 中的 X-LoRA:基于 LoRA 专家混合的动态门控微调完整指南
【免费下载链接】peft🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.项目地址: https://gitcode.com/gh_mirrors/pe/peft
X-LoRA(Mixture of LoRA Experts)是一种基于低秩适配(LoRA)的专家混合(MoE)式参数高效微调方法:它通过一个高粒度(token、layer、sequence 级别)的 scalings 矩阵,对一组冻结的 LoRA 适配器进行稀疏或稠密混合。本指南以 PEFT 仓库的 xlora.md 文档为核心,结合 xlora 源码目录 与 test_xlora.py,系统讲解 X-LoRA 的原理、双层前向机制、XLoraConfig 全部参数、构建与推理流程,帮助你直接用get_peft_model为任意 transformers 模型挂载一组 LoRA 专家并动态调配它们的贡献。
X-LoRA 是什么:冻结 LoRA 专家 + 动态门控
X-LoRA(论文:Mixture of LoRA Experts,Buehler & Buehler, APL Machine Learning, 2024)是 PEFT 中的一种方法,它基于一个高粒度(token、layer、sequence)的 scalings 矩阵,实现对 LoRA 专家的稀疏或稠密混合。由于专家是冻结的 LoRA 适配器、底座模型也是冻结的,需要微调的参数量被大幅压缩——实际只有 X-LoRA 门控分类器本身是可训练的。
X-LoRA 的一个独特优势是通用性:只要某个transformers底座模型上挂着 LoRA 适配器,X-LoRA 就可以直接套用。尽管采用了专家混合策略,完全不需要修改模型代码——PEFT 通过替换 LoRA 层的forward方法(见下文源码分析)在运行时注入 scalings,模型架构代码保持原样。
从源码结构看,X-LoRA 在 PEFT 中作为一等公民注册:src/peft/tuners/xlora/__init__.py通过register_peft_method(name="xlora", config_cls=XLoraConfig, model_cls=XLoraModel)完成注册,PeftType.XLORA定义于 peft_types.py,并从包顶层导出XLoraConfig、XLoraModel(见 src/peft/init.py)。
论文摘要的核心表述是:从一组预训练的 LoRA 适配器出发,门控策略利用 hidden states 动态混合各适配层,使 X-LoRA 模型能够"从未用过"地组合深层逐层(deep layer-wise)能力来解决任务;这种设计受生物学中"通用性与多样性"原则启发,神经网络构建块在不同层级结构中被复用,因此 X-LoRA 可以轻松应用于任何现有 LLM 而无需修改底层结构。原论文的应用聚焦于生物材料分析、蛋白质力学与设计、分子设计等科学场景,但方法本身与领域无关。
核心机制:双层前向传播(dual forward pass)
X-LoRA 每个推理步骤都需要将底座模型运行两次,这是理解其原理与算力成本的关键:
- 第一次前向(scaling pass):在不挂载任何 LoRA 适配器的情况下运行底座模型,得到各 token 的 hidden states;同时注入一组"占位 scalings"(默认全为
scaling_pass_value),使后续替换过的 LoRA 层能正常执行。 - 计算 scalings:将第一步得到的 hidden states 喂给 X-LoRA 分类器,分类器输出形状为
(batch_size, seq_len, n_layers, n_classes)的 scalings 矩阵,其中n_layers是LoRA 适配层的数量(不是模型层数),n_classes是专家(LoRA 适配器)数量。 - 第二次前向(real forward pass):把计算出的 scalings 注入到各 LoRA 层,重新运行模型。第二次运行的输出即为该步的最终结果。
由于这套双层前向机制,X-LoRA 能让模型"反思自己的知识"——每次生成都依据当前上下文动态重配架构,不同 prompt 下每个 token 激活的适配器组合都会变化。
源码中,这一流程由 model.py 的_enable_peft_forward_hooks上下文管理器实现:它注册一个模型的 pre-forward hook,hook 内部先调用internal_xlora_classifier.make_dummy_scalings生成占位 scalings,通过register_forward_pre_hook挂到每个LoraLayer上,然后在torch.no_grad()与disable_adapter_layers()环境下执行 base model 前向,拿到output_hidden_states;接着调用self.internal_xlora_classifier(...)计算真实 scalings 并保存到internal_xlora_scalings,最后再挂载一次真实 scalings 的 hook 并执行第二次前向。测试test_forward_hooks_are_cleaned_up专门验证了 generate 多次调用 forward 时 hook 不会累积泄漏(详见 test_xlora.py)。
源码架构:分类器、替换层与组装流程
src/peft/tuners/xlora/目录包含 5 个文件,职责划分清晰:
| 文件 | 职责 |
|---|---|
| config.py | 定义XLoraConfig配置类 |
| model.py | 定义XLoraModel,负责组装、加载专家、双层前向 |
| classifier.py | 定义XLoraClassifier与TemperatureScaledSoftmax门控网络 |
| layer.py | 定义XLoraLayer及线性/嵌入/卷积三种替换层 |
| init.py | 注册 PEFT 方法并导出公开 API |
门控网络 XLoraClassifier
XLoraClassifier(classifier.py)是一个小型 MLP,输入为最后一层 hidden states,输出每个 token 对每个 (层, 专家) 的 scalings:
xlora_depth=1(默认)时,只有一个nn.Linear(config.hidden_size, n_classes)(若layerwise_scalings=True则为hidden_size → n_classes * n_layers),是一个线性门控;xlora_depth > 1时,结构为Linear(hidden_size, xlora_size) → ReLU → Dropout → 若干Linear(xlora_size, xlora_size)隐藏层 → 最终 Linear;- 若
layerwise_scalings=False(默认),分类器只输出非逐层的 logits,源码通过logits.unsqueeze(2)+expand(-1, -1, n_layers, -1)将其广播复制到每一层; enable_softmax=True时使用TemperatureScaledSoftmax(logits / temperature后 softmax)归一化 scalings。
替换层 XLoraLayer 族
convert_layers_to_xlora(model.py)遍历 base model 的所有模块,按精确类型匹配lora.Linear、lora.Embedding、lora.Conv2d,分别包装为XLoraLinearLayer、XLoraEmbeddingLayer、XLoraConv2dLayer,并直接替换module.forward为包装层的前向方法。包装层的核心逻辑(layer.py):
- 若适配器被禁用(
adapters_disabled),直接返回 base layer 结果; - 用
get_maybe_topk_scalings取出本层 scalings(scalings[:, :, layer_number, :]),并按需执行 top-k 掩码与 top-k softmax; - 对每个激活的适配器,将输入
x乘以对应专家维度上的 scalings(apply_scalings_to_x),再走标准的 LoRA 路径lora_B(lora_A(dropout(x_mod))) * scaling * scaling_weight累加到 base 输出上。
其中global_scaling_weight作为乘子统一放大/缩小每个 LoRA 专家的输出;XLoraEmbeddingLayer还会额外处理带缩放因子的嵌入层(如 Gemma3 的embed_scale,有测试test_xlora_embed_scale_is_applied验证)。
组装流程 XLoraModel
XLoraModel.__init__(model.py)依次完成:
- 用
DUMMY_TARGET_MODULES创建一个空的LoraModel骨架; - 检查
use_cache必须为False,否则抛ValueError; - 遍历
config.adapters字典,调用_load_adapter_into_lora_model逐个自动加载LoRA 专家(等价于PeftModel.from_pretrained的行为,支持 hub 模型 id 或本地 checkpoint 路径); set_adapter激活全部专家,随后_maybe_freeze_all_adapters:当use_trainable_adapters=False时,把名字含.lora_的参数requires_grad=False,保证只有分类器可训练;- 执行
convert_layers_to_xlora替换 LoRA 层,并构建XLoraClassifier(n_classes = len(adapters))。
XLoraModel还实现了_mark_only_adapters_as_trainable(空操作,因为 X-LoRA 要求专家冻结)、enable_adapter_layers/disable_adapter_layers(切换disabled标志),以及_get_adapter_state_dict(只保存internal_xlora_classifier相关键,即只序列化分类器权重)。
XLoraConfig 参数详解
XLoraConfig继承自PeftConfig(config.py),全部参数如下表:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
hidden_size | int | None(缺失时警告并置为 4096) | 底座模型的 hidden size,必须与实际模型匹配 |
adapters | dict[str, str] | None(缺失时置为空 dict) | 适配器名字到 LoRA 模型 id(hub id 或本地目录)的映射,构造时会自动加载这些 LoRA 作为专家;from_pretrained加载时需通过关键字参数传入新的 adapters dict |
enable_softmax | bool | True | 是否对 X-LoRA 分类器输出施加 softmax |
enable_softmax_topk | bool | False | 是否对 top-k 选中的 LoRA 专家施加 softmax;与enable_softmax互斥,且必须同时设置top_k_lora |
softmax_temperature | float | 1.0 | softmax 温度,越小预测越尖锐 |
layerwise_scalings | bool | False | 为 True 时为每个 LoRA 层(每个专家)分别生成 scalings;为 False 时 scalings 广播复制到每一层 |
top_k_lora | int | None | 稀疏选择 top-k 个 LoRA 专家,代替默认的稠密方法;None表示稠密 |
xlora_depth | int | 1 | X-LoRA 分类器的深度 |
xlora_size | int | 2048 | X-LoRA 分类器的隐藏层大小,xlora_depth=1时无关 |
xlora_dropout_p | float | 0.2 | X-LoRA 分类器的 dropout 概率,xlora_depth=1时无关 |
use_trainable_adapters | bool | False | 是否让 LoRA 专家可训练(默认冻结专家,只训分类器) |
scaling_pass_value | float | 0.0 | 第一次前向(scaling pass)中占位 scalings 的填充值 |
global_scaling_weight | float | 1.0 | 乘以每个 LoRA 专家输出的全局缩放权重 |
__post_init__中还会做几项校验与告警:hidden_size/adapters缺失时给出 warning 并填充默认值;enable_softmax_topk=True但未设置top_k_lora时告警;enable_softmax_topk与enable_softmax同时开启时告警"会导致更差的表现";top_k_lora < 1时告警。
另外,加载配置时adapters字段中的路径会被忽略,只使用字典的 key(配置重载后优先使用已保存的适配器),这点在XLoraConfig的 docstring 中已明确说明。
实战:用 get_peft_model 构建 X-LoRA 模型
以下是 model.py 文档字符串中的官方示例,展示了最典型的用法——基于 Mistral-7B-Instruct 与 int8 量化底座构建 X-LoRA:
import torch from transformers import AutoModelForCausalLM, AutoConfig, BitsAndBytesConfig from peft import XLoraConfig, get_peft_model, prepare_model_for_kbit_training model_config = AutoConfig.from_pretrained("mistralai/Mistral-7B-Instruct-v0.1") config = XLoraConfig( task_type="CAUSAL_LM", hidden_size=model_config.hidden_size, xlora_depth=4, adapters={ "adapter_1": "./path/to/the/checkpoint/", "adapter_2": "./path/to/the/checkpoint/", "adapter_n": "./path/to/the/checkpoint/", }, ) int8_config = BitsAndBytesConfig(load_in_8bit=True) model = AutoModelForCausalLM.from_pretrained( "mistralai/Mistral-7B-Instruct-v0.1", trust_remote_code=True, attn_implementation="flash_attention_2", device_map="cuda:0", torch_dtype=torch.bfloat16, quantization_config=int8_config, ) model = prepare_model_for_kbit_training(model) xlora_model = get_peft_model(model, config)要点解读:
- 专家来源:
adapters的值可以是 Hub 上的 LoRA 模型 id,也可以是本地保存的 LoRA checkpoint 目录;每个专家的LoraConfig(如target_modules)可以互不相同,_load_adapter_into_lora_model会为每个专家独立读取并注入配置; - use_cache 必须关闭:构造器会强制校验
model.config.use_cache为False,因此记得设置model.config.use_cache = False(测试 fixtures 中正是这样做的); - 专家默认冻结:
use_trainable_adapters=False时只有internal_xlora_classifier.*参数可训练,训练时用常规 Trainer 即可,所有 LoRA 专家不更新。
参考测试 test_xlora.py 的 fixture,最小可用配置也可以简化为:
peft_config = XLoraConfig( task_type=TaskType.CAUSAL_LM, peft_type=PeftType.XLORA, hidden_size=base_model.config.hidden_size, xlora_depth=8, adapters=adapters, # {"0": "path/to/lora-0", "1": "path/to/lora-1", ...} ) model = get_peft_model(base_model, peft_config).to(device)推理与生成:generate 与运行时注意事项
XLoraModel.generate(model.py)在调用self.lora_model.generate前强制kwargs["use_cache"] = False(因为门控需要每步重新计算 hidden states,缓存与双层前向不兼容),生成结束后会再次_maybe_freeze_all_adapters,确保专家保持冻结。
其它运行时行为(均有测试覆盖,见 test_xlora.py):
disable_adapter上下文:with model.disable_adapter():内禁用 X-LoRA 后,模型输出应与纯底座模型完全一致(test_disable_adapter_matches_base_model用torch.allclose(atol=1e-5)验证,包括嵌入层变体);- 训练模式保持:
generate不会把模型悄悄切到 eval,调用前后model.training不变(test_generate_preserves_training_mode); - 生成后仍可训练:
generate之后分类器参数仍保持requires_grad=True、专家仍冻结(test_classifier_stays_trainable_after_generate); - DoRA 限制:X-LoRA 目前不支持 LoRA+DoRA 层,遇到
use_dora的专家会直接抛ValueError(见 layer.py 的 TODO 注释)。
运行时调参与 scalings 调试 API
XLoraModel暴露了一组方法,可在构造后动态调整门控行为或观察 scalings:
| 方法 | 作用 |
|---|---|
set_topk_lora(value) | 动态切换为 top-k 稀疏选专家;传None恢复稠密 |
set_global_scaling_weight(weight) | 设置全局专家输出缩放权重(默认 1.0) |
set_scaling_pass_value(value) | 设置 scaling pass 的占位值;传None时自动用1/n_classes |
get_global_scaling_weight() | 读取当前全局缩放权重 |
get_latest_scalings() | 返回最近一次预测的 scalings 张量,形状(batch_size, seq_len, n_layers, n_classes),未预测时返回None |
enable_scalings_logging()/disable_scalings_logging() | 开启/关闭 scalings 日志(关闭不清空已有日志) |
clear_scalings_log() | 清空日志 |
get_scalings_log() | 返回 scalings 日志列表的浅拷贝 |
get_bucketed_scalings_log() | 按seq_len分桶返回日志(值为"位置列表 + 张量列表") |
示例用法(对应测试test_scalings_logging_methods):
model.enable_scalings_logging() outputs = model.generate(input_ids=inputs.to(device), max_new_tokens=32) latest = model.get_latest_scalings() # (batch, seq, n_layers, n_classes) log = model.get_scalings_log() # 每个生成步的 scalings 列表 bucketed = model.get_bucketed_scalings_log() # 按 seq_len 分桶 model.disable_scalings_logging() model.clear_scalings_log()set_topk_lora与set_global_scaling_weight的修改会同步写回分类器的config(即internal_xlora_classifier.config),下次前向立即生效;get_maybe_topk_scalings中 top-k 通过对该层 scalings 做torch.topk后掩码实现,若同时开启enable_softmax_topk还会对非零部分做 per-token 归一化(测试test_per_token_normalization_with_softmax_topk验证每 token 权重和恒为 1)。需要注意enable_softmax_topk与enable_softmax不要同时开启,否则会得到更差的门控结果。
保存、加载与仓库内的进一步参考
- 保存与加载:
XLoraModel的状态字典只包含internal_xlora_classifier相关键(见_get_adapter_state_dict),因此save_pretrained只保存门控分类器;重新加载时通过PeftModel.from_pretrained(model=base_model, model_id=tmp_path)即可,且加载时adapters字段的路径会被忽略、以已保存适配器为准。测试test_save_load_functional验证了保存-加载前后生成 logits 完全一致(torch.equal),safe_serialization=False的 PyTorch 权重路径也覆盖在test_save_load_functional_pt中; - 测试:tests/test_xlora.py 提供了完整的参考实现——用 4 个不同
target_modules组合的 LoRA checkpoint 构造 X-LoRA 模型,覆盖功能、逐层 scalings(layerwise_scalings=True)、嵌入层专家、top-k、softmax-topk、scaling pass 值、禁用适配器、日志与分桶等全部行为; - 推理示例:examples/xlora/README.md 介绍了
xlora_inference_mistralrs.py,演示如何把保存的 X-LoRA/LoRA checkpoint 交给 mistral.rs 推理引擎(该引擎原生支持 X-LoRA,并采用 dual-KV cache、连续批处理、Paged Attention 等优化提升吞吐)。
引用
若在你的工作或研究中使用了 X-LoRA,请按仓库文档给出的 BibTeX 引用:
@article{10.1063/5.0203126, author = {Buehler, Eric L. and Buehler, Markus J.}, title = "{X-LoRA: Mixture of low-rank adapter experts, a flexible framework for large language models with applications in protein mechanics and molecular design}", journal = {APL Machine Learning}, volume = {2}, number = {2}, pages = {026119}, year = {2024}, month = {05}, doi = {10.1063/5.0203126}, }小结
X-LoRA 在 PEFT 中的实现遵循"冻结专家 + 可训门控 + 双层前向"的简洁范式:XLoraConfig用一段字典声明专家集合与门控结构,get_peft_model一行完成自动加载、层替换与分类器构建。理解其 scalings 矩阵的(batch, seq, n_layers, n_classes)语义、layerwise_scalings与top_k_lora等关键开关,以及use_cache=False、DoRA 不支持等运行约束,即可在任意 transformers 底座上组合现有 LoRA 资产,获得随输入动态重配架构的专家混合模型。
【免费下载链接】peft🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.项目地址: https://gitcode.com/gh_mirrors/pe/peft
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考