当前位置:首页 > 文章列表 > 科技周边 > 人工智能 > Transformers PEFT adapter 怎样只保存可训练权重

Transformers PEFT adapter 怎样只保存可训练权重

来源:17golang原创 2026-11-01 02:57:40 0浏览 收藏

要把 Transformers 模型的 PEFT 微调结果保存成小文件,使用训练完成的 PeftModel 对象的 save_pretrained(),并按需要指定 selected_adapters。它按适配器配置保存权重和配置,通常不包含冻结的基础模型。不要直接保存整个 model.state_dict(),也不要把“只保存可训练权重”理解成对所有 requires_grad=True 参数做一次通用过滤。

我更愿意把这个任务叫作“交付一份可加载的适配器”:文件小只是一个指标,另一个指标是接收方知道要配哪份基础模型。下面给出单进程、未合并 LoRA 的保存写法;示例代码供读者在自己的训练工程中使用,本文未执行训练或体积测试。

官方地址:https://huggingface.co/docs/peft/

先算清楚要省的是哪一份权重

先建立一个可计算的基线。对输入、输出维度均为 768 的线性层,秩为 8 的普通 LoRA 增量矩阵包含 8 × (768 + 768) = 12,288 个参数。如果假设 12 层、每层只改 query 和 value 两个投影,总数是 12 × 2 × 12,288 = 294,912 个参数。按每个参数 4 字节计算,裸张量约 1.125 MiB。

另一个纯算术基线是假设完整模型有 1.1 亿个 FP32 参数,那么裸参数是 440,000,000 字节。二者的比值约 0.268%。这不是某个训练任务的实测压缩率:真实 checkpoint 还有张量头、配置、可能额外保存的模块,dtype 也可能不同。LoRA 的基本结构可参见 官方 LoRA 说明。

我的判断是,先统计适配器参数量,再记录真实文件字节数,才能解释文件为什么变小。小文件并不直接代表推理更快、训练更省多少显存,更不代表微调效果更好。

把可训练范围交给 LoRA 配置表达

下面选用 BERT 风格的 query、value 模块名,构造默认名称的适配器。base 是读者已经加载的兼容基础模型;模型结构不同,target_modules 也应使用对应的真实模块名。不要把这一组名字套到所有架构。

from peft import LoraConfig, TaskType, get_peft_model

def attach_lora(base):
    # 只对 BERT 风格注意力投影添加 LoRA,不额外训练偏置或完整模块。
    config = LoraConfig(
        task_type=TaskType.FEATURE_EXTRACTION,
        target_modules=["query", "value"],
        r=8,
        lora_alpha=16,
        lora_dropout=0.05,
        bias="none",
        modules_to_save=None,
    )
    return get_peft_model(base, config)

# 将返回的 PeftModel 交给你自己的训练代码;此处没有执行训练。

这里选择 bias="none"、不增加 modules_to_save,是为了把讨论范围限制为普通 LoRA。若任务需要训练分类头,就应把分类头纳入相应保存配置;不能为了缩小文件而漏掉它。额外完整模块的语义见 Transformers 的 PEFT 集成说明。本文后续使用 PEFT 封装对象的接口,不混用 Transformers add_adapter() 直接集成方式的参数。

LoRA 配置、冻结基础模型和适配器导出之间的静态关系
图1:保存范围结构图。模型组成区包括冻结基础模型与 LoRA 增量;保存约定区包括适配器配置及其权重、配置文件。连线表示组成与保存关联,不表示执行顺序,也不是训练截图。

训练结束后从 PeftModel 导出适配器

将训练完成、未合并且已解除分布式包装的 PeftModel 传入下面函数。使用一个全新目录,能避免以前留下的完整模型文件混入本次交付。示例只处理默认名称 default;非默认名称可能对应输出子目录,应把实际包含 adapter_config.json 的目录交给加载接口。

from pathlib import Path
from peft import PeftModel

def export_adapter(trained_model: PeftModel, output_dir: str):
    if not isinstance(trained_model, PeftModel):
        raise TypeError("需要训练后的 PeftModel 对象")
    folder = Path(output_dir)
    # 全新目录避免混入旧的完整模型文件;存在时由调用方另选输出目录。
    folder.mkdir(parents=True, exist_ok=False)
    trained_model.save_pretrained(
        str(folder),
        selected_adapters=["default"],
        safe_serialization=True,
        save_embedding_layers="auto",
    )
    return folder

# 在你自己的训练完成后调用:
# adapter_dir = export_adapter(trained_model, "exports/task_adapter_v1")

PEFT 标准导出包含 adapter_model.safetensors 和 adapter_config.json,并可能生成模型卡 README.md。权重描述适配器,配置描述它的类型和加载所需信息;基础模型仍需另外可用。文件结构详见 PEFT checkpoint 格式。

selected_adapters 控制保存哪些适配器,未指定时会保存全部。save_embedding_layers="auto" 保留库的自动处理逻辑;如果明确改变过词表或训练过嵌入层,应结合自己的配置决定是否显式保存,不能只因文件变大就强制设为 False。这些选项属于 PeftModel 保存接口。

另一个容易误会的地方是:适配器即便当前被冻结用于推理,其保存规则仍围绕适配器状态,而非简单依据此刻的 requires_grad。反过来,手工解冻一个不在 PEFT 保存约定内的基础层,也不能假定这个层会自动进入适配器包。

用同一口径统计导出体积

如果要报告节省比例,我会把“参数字节数”和“目录字节数”分开。前者是理论存储量,后者包含配置等附带文件。下面代码只给读者记录指标,不代表本文已有测试输出,也不是发布前执行的校验。

from pathlib import Path
from peft import get_peft_model_state_dict

def collect_export_metrics(model, adapter_dir: str):
    # 按 PEFT 规则提取默认适配器,不手工拼接或改写权重键名。
    state = get_peft_model_state_dict(
        model, adapter_name="default", save_embedding_layers="auto"
    )
    tensor_bytes = sum(t.numel() * t.element_size() for t in state.values())
    parameter_count = sum(t.numel() for t in state.values())
    # 递归计入配置、模型卡等文件;不把共享的基础模型缓存算入目录。
    disk_bytes = sum(
        f.stat().st_size for f in Path(adapter_dir).rglob("*") if f.is_file()
    )
    return {
        "adapter_parameters": parameter_count,
        "adapter_tensor_bytes": tensor_bytes,
        "adapter_directory_bytes": disk_bytes,
    }

# 在与导出一致的模型状态下收集;没有虚构固定的输出数字。

比较方案时,基础模型版本、LoRA 秩、目标层、dtype、额外模块和词表处理都应相同。若比较保存耗时,还应固定存储介质、缓存条件和计时范围,多次记录;本文不给未经测试的耗时结论。对分布式或量化模型,这段简单的张量统计也不能替代框架自己的权重收集与存储口径。

比较项本文能给出的结论需要读者记录的实际值
普通 LoRA 张量量示例假设下 294,912 参数、FP32 约 1.125 MiB实际目标层与 dtype 下的字节数
交付目录大小适配器不必重复包含冻结基础模型新目录文件总字节数
保存耗时与推理速度不能从参数比值直接推出各自独立的测量结果

加载时补齐基础模型与分词器

小包的代价是依赖基础模型。我会把基础模型的准确版本、所用库版本和分词器约定一起交接。下面假设 base_snapshot 是读者已经准备好的、与训练时一致的本地 BERT 模型快照目录;tokenizer_assets 是对应分词器目录,未扩词时可来自同一快照,改变分词器后则应另外保存其资产。

from peft import PeftModel
from transformers import AutoModel, AutoTokenizer

def load_for_inference(base_snapshot, adapter_dir, tokenizer_assets):
    # 本地基础模型必须与训练时一致,不能随意换成另一个同名版本。
    base = AutoModel.from_pretrained(base_snapshot, local_files_only=True)
    model = PeftModel.from_pretrained(
        base, adapter_dir, is_trainable=False
    )
    tokenizer = AutoTokenizer.from_pretrained(
        tokenizer_assets, local_files_only=True
    )
    model.eval()  # 明确切换推理行为;计算时仍可使用 torch.inference_mode()。
    return model, tokenizer

# 继续训练时改用 is_trainable=True,并自行恢复训练所需状态。

适配器加载成功不等于恢复了优化器、调度器、随机状态或数据进度。上面函数服务于推理组装,不能冒充一次完整的训练续跑。分词器也不属于普通 adapter 权重,目录小不应以遗漏它的改动为代价。

基础模型快照、适配器文件、分词器和加载对象的依赖结构
图2:加载依赖结构图。共享资产区的基础模型快照、分词器资产与适配器交付区的权重和配置共同关联模型组装区。图示强调依赖要齐全,不代表执行过程或实际测试结果。

哪些情况不能追求最小文件

为什么适配器还是很大?先看是否有 modules_to_save、偏置训练、嵌入层或多个适配器。这些可能是任务所需的正确状态。不能删掉后再宣称保存优化成功。

只筛选 requires_grad 的参数可以吗?可以得到一个自定义张量字典,但它不是天然符合 PEFT 加载格式的交付物。配置、键名映射及额外模块都有语义,常规导出直接交给 PEFT。

能不能合并以后再存?merge_and_unload() 是另一种部署目标:适配器合入基础模型后保存通常面向完整模型。要保留独立适配器时,应在合并前导出,不把两种包的大小放在同一栏比较。

Adapter 包能不能代替训练断点?单独的 save_pretrained() 不保存全部训练运行状态。需要精确续训时,保留训练框架的 checkpoint;只做推理交付时,独立适配器更适合复用同一基础模型。

对我来说,这个方案适合“一个基础模型配多份任务增量”的交付方式。最终要追求的是文件范围明确、基础模型可定位、加载依赖齐全,而不是单看一个漂亮的体积百分比。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
QLoRA 的 nf4 与 double quant 参数配置方法QLoRA 的 nf4 与 double quant 参数配置方法
上一篇
QLoRA 的 nf4 与 double quant 参数配置方法
Transformers quantization picker 怎样按硬件选择量化方式
下一篇
Transformers quantization picker 怎样按硬件选择量化方式
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    819次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    811次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    903次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    873次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    695次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码