【Bug已解决】Proposal: Benchmarking on different PEFT techniques 解决方案一、现象长什么样你想在 PEFT 的几种方法LoRA、AdaLoRA、IA³、Prefix Tuning、P-Tuning v2、Prompt Tuning…之间做公平对比但一上手就发现“对比”根本不公平同样预算下有的方法参数量算不清IA³ 只有 scaling 向量Prefix 有一堆虚拟 token同一个数据集A 方法 seed42 赢换 seed7 就输结论不稳定论文里 LoRA 比 Prefix 好你复现却反过来因为target_modules/lora_alpha设得不一样吞吐tokens/s没记只比了最终准确率没法判断“性价比”。这些不是某个方法的 bug而是benchmark 设计不严谨导致结论不可信。本文给出一套可直接落地的公平对比框架。二、背景PEFT 方法的“可比性”有三个维度缺一不可参数预算trainable param budget公平对比应固定“可训练参数量”而不是固定超参如都设r8。因为 IA³ 的参数量远小于 LoRAr8直接比r8对 IA³ 不公平。训练配置一致性学习率、epoch、seed、优化器、数据划分必须完全相同只让“方法”一个变量不同。指标完整性不仅看准确率还要看吞吐、显存、收敛步数才能回答“哪种方法性价比最高”。PEFT 仓库里的method_comparison脚本就是为这个目的存在的但它默认只跑少数方法和固定配置。要让对比严谨需要自己扩展成“按预算分组、多 seed、多指标”的 harness。三、根因为什么对比不公平根因 A固定超参而非固定预算最常见错误。LoRAr8和 IA³默认就几个 scaling 向量参数量差几十倍比r8等于让 IA³ 先天劣势。应以“可训练参数量”为对齐轴。根因 Bseed 单一结论方差掩盖只跑一个 seed方法间 0.5 个点的差异可能纯属随机。严谨对比至少 3~5 个 seed 取均值±标准差。根因 C超参没按方法单独调LoRA 和 Prefix 的最优 lr 不同。用同一个 lr 比等于给某方法穿小鞋。但“单独调”又要小心过拟合到测试集——应划分验证集调参。根因 D只报准确率不报成本某方法准 1 个点但慢 3 倍、显存翻番性价比未必高。缺吞吐/显存指标结论片面。根因小结公平对比的轴是“可训练参数预算”不是超参多 seed 一致训练配置 多指标准/速/显存才可信单 seed、固定超参、只报准确率都会得出误导性结论。四、最小可运行复现下面脚本是一个可直接跑的公平对比 harness 骨架按“预算”扫不同方法多 seed记录准确率参数量import torch import torch.nn as nn from dataclasses import dataclass from peft import (LoRAConfig, IA3Config, PrefixTuningConfig, get_peft_model, get_peft_model_state_dict) dataclass class Budget: name: str trainable_params: int def count_trainable(model) - int: return sum(p.numel() for p in model.parameters() if p.requires_grad) def build_config(method: str, budget_params: int, hidden64): # 以“参数量”反推超参保证预算一致 if method lora: r max(1, budget_params // (2 * hidden)) return LoRAConfig(rr, lora_alpha2 * r, target_modules[lin]) if method ia3: # IA³ 只有 scaling 向量参数量小用 num_ia3 控制 return IA3Config(target_modules[lin], feedforward_modules[lin]) if method prefix: num_v max(2, budget_params // hidden) return PrefixTuningConfig(num_virtual_tokensnum_v, token_dimhidden) raise ValueError(method) def fake_eval(model) - float: # 用固定验证集算伪准确率真实场景替换为你的 metric return float(torch.rand(1).item()) def run(method, budget_params, seed, steps30): torch.manual_seed(seed) base nn.Sequential(nn.Linear(32, 64), nn.ReLU(), nn.Linear(64, 8)) cfg build_config(method, budget_params) model get_peft_model(base, cfg) n_train count_trainable(model) opt torch.optim.AdamW(model.parameters(), lr1e-3) x, y torch.randn(8, 32), torch.randint(0, 8, (8,)) for _ in range(steps): opt.zero_grad() loss model(x, labelsy).loss if hasattr(model(x, labelsy), loss) else ((model(x) - y.float()) ** 2).mean() loss.backward(); opt.step() score fake_eval(model) return {method: method, seed: seed, trainable: n_train, score: score} def main(): for method in [lora, ia3, prefix]: for seed in [0, 1, 2]: row run(method, budget_params4096, seedseed) print(row) if __name__ __main__: main()这个骨架强制“预算一致”用count_trainable校验多 seed 输出避免最常见的两类不公平。五、解决方案第一层最小直接修复做对比前先固化三条铁律以可训练参数预算为对齐轴用count_trainable(model)打印每种方法的实际参数量确保它们在同一个量级如都 ~4000 参数而不是都设r8。多 seed 取均值±标准差每个方法跑 ≥3 个 seed结论写“方法 X 均值 0.82 ± 0.01方法 Y 0.80 ± 0.02”。记录多指标至少准确率、训练吞吐tokens/s、峰值显存。三者一起看才完整。import statistics def summarize(rows): by_method {} for r in rows: by_method.setdefault(r[method], []).append(r[score]) for m, scores in by_method.items(): print(f{m}: 均值 {statistics.mean(scores):.3f} ± {statistics.pstdev(scores):.3f})六、解决方案第二层结构性改进6.1 用peft.get_peft_model_state_dict统一导出便于比较sd get_peft_model_state_dict(model) print(可训练键:, list(sd.keys())) # 不同方法的键结构不同但参数量可比6.2 固定训练配置只让方法变量化SHARED_TRAIN dict( lr1e-3, num_epochs3, optimizeradamw, train_bs8, eval_bs16, seed_list[0, 1, 2, 3, 4], )所有方法共用SHARED_TRAIN只有build_config(method, budget)不同。6.3 用method_comparison的扩展输出 jsonl参考 PEFT 的method_comparison.py把结果写成 jsonl每行一条便于后续聚合import json def log_row(row, pathpeft_bench.jsonl): with open(path, a) as f: f.write(json.dumps(row) \n)七、解决方案第三层断言 / CI 守护加 pytest保证对比 harness 自身不出错预算真的对齐、seed 真的不同import pytest from peft import get_peft_model, count_trainable_placeholder def test_budgets_aligned(): budgets {} for method in [lora, ia3, prefix]: model get_peft_model(build_base(), build_config(method, 4096)) budgets[method] count_trainable(model) vals list(budgets.values()) # 允许 2 倍内浮动但不允许量级差异 assert max(vals) 2 * min(vals), f预算未对齐: {budgets} def test_seeds_distinct(): seeds [run(lora, 4096, s)[seed] for s in [0, 1, 2]] assert len(set(seeds)) 3接进 CIharness 本身的公平性被破坏就报警。八、排查清单对比 PEFT 方法不公平时查对齐轴是预算还是超参必须是“可训练参数预算”不是都设 r8。跑几个 seed单 seed 结论不可信至少 3~5 个取均值±标准差。训练配置一致吗lr/epoch/优化器/数据划分只有“方法”一个变量。超参有没有按方法单独调调参要在验证集上别在测试集调。只报准确率吗补吞吐、显存、收敛步数才有性价比结论。count_trainable校验了吗确认各方法参数量真在同一个量级。结果可追溯吗jsonl git 状态见 312 篇方法记录来源。九、小结“Proposal: Benchmarking on different PEFT techniques” 说的是如何公平对比 PEFT 方法这不是方法 bug而是实验设计问题公平轴必须是可训练参数预算不是固定超参否则 IA³ 等轻量方法先天吃亏每个方法跑≥3 seed取均值±标准差单 seed 结论方差大不可信指标要多维度准确率 吞吐 显存 收敛步数才谈得上性价比用count_trainable校验预算对齐用 jsonl 多 seed 记录结果pytest 守护“预算真对齐、seed 真不同”harness 自身公平才出可信结论。一句话比 PEFT 方法别比r8要比“同样的可训练参数预算下、多个 seed、多指标”谁更优。