27届大模型岗面试准备(八):参数高效微调 PEFT——LoRA/QLoRA/Adapter 原理、 27届大模型岗面试准备八参数高效微调 PEFT——LoRA/QLoRA/Adapter 原理、显存账与手写注入上一篇讲了 RLHF 和 DPO无论走哪条对齐路线都绕不开一个现实问题全参数微调一个 7B 模型需要多少显存答案是至少 100GB 以上——这对绝大多数团队是不可接受的。参数高效微调PEFTParameter-Efficient Fine-Tuning就是为了解决这个问题而其中的 LoRA 几乎是27届面试的必考题原理推导、超参选择、显存计算、和全参微调的效果差距四连问是标准套路。这一篇把 PEFT 的主流方法讲透重点是 LoRA 和 QLoRA最后手写一个不依赖 peft 库的 LoRA 注入实现——面试手撕环节真的会考。一、先算清楚全参微调到底贵在哪面试官问为什么需要 PEFT不要只说省显存要能把显存账算出来。以 7B 模型、BF16 训练、Adam 优化器为例模型权重7B × 2 字节 14 GB梯度7B × 2 字节 14 GBAdam 优化器状态一阶动量 二阶动量通常以 FP32 存储7B × 4 × 2 56 GBFP32 主权重副本混合精度训练需要7B × 4 28 GB激活值与 batch size、序列长度、是否开梯度检查点有关数 GB 到数十 GB合计约112 GB 激活值单卡 A100 80GB 都放不下必须多卡 ZeRO。而推理只需要 14GB——训练贵就贵在梯度和优化器状态是权重的 5 倍。PEFT 的核心思路冻结原模型权重只训练极少量新增参数。梯度和优化器状态只为新增参数分配显存立刻塌缩下来。二、PEFT 方法谱系PEFT 不止 LoRA 一家面试要能说出谱系和各自的代价方法核心思路可训练参数量推理额外延迟主要缺点Adapter (2019)在 FFN 后插入瓶颈层降维→非线性→升维~1%有串行结构增加推理延迟Prefix Tuning在每层 KV 前拼接可训练前缀向量~0.1%有占用上下文压缩有效序列长度、训练不稳定Prompt Tuning只在输入层加软提示向量~0.01%轻微小模型上效果差LoRA (2021)用低秩矩阵 BA 学习权重增量 ΔW0.1%~1%无可合并秩的选择需要调参QLoRA (2023)4-bit 量化底座 LoRA同 LoRA无合并后训练略慢反量化开销DoRA (2024)把 ΔW 分解为幅度方向分别学习略高于 LoRA无实现复杂度稍高LoRA 胜出的关键就在推理零额外延迟这一格训练完把 BA 合并回原权重W W BA部署时和原模型结构完全一致。Adapter 和 Prefix 都做不到这一点。三、LoRA 原理精讲核心假设微调时权重的变化量 ΔW 是低秩的。直觉解释预训练模型已经学到了通用能力下游任务适配只需要在少数几个方向上调整权重不需要动整个满秩空间。这个假设有实证支撑——原论文测得 GPT-3 微调的 ΔW 有效秩intrinsic rank非常低r1~4 就能逼近全参效果。数学形式对一个线性层 W ∈ R^(d×k)LoRA 不直接更新 W而是学习h Wx (α/r)·BAx其中 A ∈ R^(r×k)B ∈ R^(d×r)r ≪ min(d,k)。可训练参数从 d×k 降到 r×(dk)。以 4096×4096 的注意力投影、r8 为例参数量从 1678 万降到 6.5 万压缩 256 倍。三个必考细节初始化A 用高斯初始化B 初始化为全零。这样训练开始时 BA0模型输出和原模型完全一致保证训练起点稳定。反过来A 零、B 高斯也能保证起点为零但会导致 A 的梯度初始为零、更新不对称实践用前者。缩放因子 α/rα 是常数常取 16 或 32除以 r 是为了在改变 r 时保持增量的量级稳定减少重调学习率的需要。挂载位置原论文只挂 Q、V 投影后续实践QLoRA 论文表明挂所有线性层Q/K/V/O FFN 的 gate/up/down效果更好是当前默认做法。秩 r 怎么选常见 8~64。任务和预训练分布差距越大如中文医疗、代码r 应越大简单风格化任务 r8 足够。r 增大收益边际递减且过大会过拟合小数据集。四、QLoRA单卡 24GB 微调 33B 的魔法QLoRA 的贡献是把冻结的底座压到 4-bit让显存大头模型权重也塌下来。三个技术点面试常被追问1. NF4 量化4-bit NormalFloat。普通 INT4 是均匀量化但神经网络权重近似正态分布——大部分值集中在 0 附近。NF4 按标准正态分布的分位数设置 16 个量化格点让每个格点覆盖等概率质量对正态分布权重是信息论最优的。这是数据分布感知量化的典型案例。2. 双重量化Double Quantization)。分块量化需要为每块存一个 FP32 的缩放常数block64 时平摊 0.5 bit/参数QLoRA 把这些常数再量化到 8-bit每参数再省 0.37 bit。7B 模型省约 0.3GB不大但白捡。3. 分页优化器Paged Optimizer。用 CUDA 统一内存把优化器状态在显存吃紧时换页到内存防止长序列导致的 OOM 尖峰。关键理解QLoRA 中 4-bit 的只有冻结底座LoRA 增量和计算仍在 BF16 进行——前向时把 4-bit 权重反量化为 BF16 再算。所以 QLoRA 训练比 LoRA 慢约 30%是拿时间换显存。显存对比7B 模型微调方案底座权重梯度优化器大致总需求全参微调 BF1614 GB~98 GB112 GBLoRA (r16)14 GB1 GB~20 GBQLoRA (r16, NF4)~3.8 GB1 GB~9 GBQLoRA 论文的结论也要记住4-bit 底座 LoRA 在指令微调基准上可以追平 16-bit 全参微调——这是它敢叫高效而不叫降级的底气。五、手写 LoRA 注入不依赖 peft 库面试手撕环节的经典题给你一个nn.Linear把它替换成带 LoRA 的版本。完整可运行代码import torch import torch.nn as nn import math class LoRALinear(nn.Module): 把一个冻结的 nn.Linear 包装成 W x (alpha/r) * B A x def __init__(self, base: nn.Linear, r8, alpha16, dropout0.0): super().__init__() self.base base for p in self.base.parameters(): # 冻结原权重 p.requires_grad_(False) self.r, self.scaling r, alpha / r self.lora_A nn.Parameter(torch.empty(r, base.in_features)) self.lora_B nn.Parameter(torch.zeros(base.out_features, r)) nn.init.kaiming_uniform_(self.lora_A, amath.sqrt(5)) # A 高斯/均匀B 全零 self.dropout nn.Dropout(dropout) def forward(self, x): delta self.dropout(x) self.lora_A.T self.lora_B.T return self.base(x) self.scaling * delta torch.no_grad() def merge(self): 训练结束后合并W - W (alpha/r) * B A推理零开销 self.base.weight self.scaling * (self.lora_B self.lora_A) def inject_lora(model: nn.Module, targets(q_proj, v_proj), r8, alpha16): 递归遍历模型把名字命中 targets 的 Linear 换成 LoRALinear for name, child in model.named_children(): if isinstance(child, nn.Linear) and any(t in name for t in targets): setattr(model, name, LoRALinear(child, rr, alphaalpha)) else: inject_lora(child, targets, r, alpha) return model # ---- 演示一个迷你注意力块 ---- class TinyAttn(nn.Module): def __init__(self, d64): super().__init__() self.q_proj nn.Linear(d, d) self.k_proj nn.Linear(d, d) self.v_proj nn.Linear(d, d) model TinyAttn() inject_lora(model, targets(q_proj, v_proj), r4, alpha16) total sum(p.numel() for p in model.parameters()) trainable sum(p.numel() for p in model.parameters() if p.requires_grad) print(f总参数 {total} | 可训练 {trainable} | 占比 {trainable/total:.2%}) x torch.randn(2, 10, 64) y_before model.q_proj(x) # B 初始为零 输出等于原模型 base_out model.q_proj.base(x) print(注入后初始输出与原模型一致:, torch.allclose(y_before, base_out)) # 模拟一步训练后合并 model.q_proj.lora_B.data.normal_(0, 0.02) merged_ref model.q_proj(x) # 合并前走 LoRA 分支 model.q_proj.merge() merged_out model.q_proj.base(x) # 合并后只走原 Linear print(合并后输出一致:, torch.allclose(merged_ref, merged_out, atol1e-5))运行结果三行输出分别验证了 LoRA 的三个关键性质可训练参数占比极小本例约 2%、零初始化保证起点等价、合并后推理与训练时数值一致。把这三个验证点讲给面试官比背十遍论文都有说服力。实际项目中用 HuggingFace peft 库两行搞定LoraConfigget_peft_model但原理实现必须会——peft 库里 merge_and_unload 做了什么这种问题就是在筛掉只会调库的人。六、工程实践要点与高频追问多 LoRA 服务一个底座 N 个业务各自的 LoRA 权重每个几十 MB推理时按请求动态切换——vLLM 的 multi-LoRA 功能支持同 batch 混合不同 adapter。这是一个基座服务全公司的标准架构面试聊到部署时主动提这个是加分项。LoRA 的局限被问LoRA 是万能的吗时用注入新知识的能力弱于全参微调——低秩假设对风格/格式适配成立对大量新领域知识不成立后者更适合继续预训练或 RAG大学习率下训练不稳定常用比全参微调大 10 倍左右的 lr 但要配 warmupr 和挂载位置是新增超参需要实验。高频面试题清单LoRA 为什么 B 矩阵初始化为零保证起点等价于原模型α/r 缩放的作用换 r 时保持增量量级稳定QLoRA 的 NF4 比 INT4 好在哪按正态分位数量化等概率格点信息论最优LoRA 挂在哪些层效果最好全线性层 只挂 QVLoRA 训练完怎么部署merge 回底座或 multi-LoRA 动态加载什么场景不该用 LoRA大规模知识注入、与预训练分布差异极大的任务至此训练侧的主线预训练 → SFT → RLHF/DPO → PEFT全部讲完。下一篇进入推理侧量化实战 INT8/INT4/GPTQ/AWQ。