拼多多大模型算法秋招二面:LoRA 微调与注意力原理
- 轮次
- 二面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- LoRA 微调的低秩假设是什么?rank 设置过小或过大会有什么问题?
- LoRA 的 alpha 为什么通常要和 rank 一起考虑?
- QLoRA 为什么可以显著降低显存,但训练时仍然可能出现不稳定?
- Self-Attention 中为什么要除以 √d_k?如果不除会发生什么?
- 标准 Self-Attention 的复杂度瓶颈在哪里?长上下文模型一般如何优化?
- RoPE 在扩展上下文长度时为什么会失效?YaRN、NTK Scaling 解决的是什么问题?
- Transformer 很深时,Pre-LN 和 Post-LN 在训练稳定性上有什么差异?
- SwiGLU 相比 ReLU 或 GELU,为什么常被用于大模型的 FFN 层?
- 大模型训练中,激活显存、参数显存、梯度显存和优化器状态分别如何估算?
- 梯度累积和增大 batch size 是否完全等价?
- 混合精度训练中,为什么会出现 loss 是正常的但梯度已经溢出?
- 怎样判断模型训练出现的是梯度消失、梯度爆炸,还是数据与标签本身存在问题?
- 请做一下自我介绍
- 简历与实习经历拷打
《参考解析》
LoRA 的 rank 与 alpha:真正起作用的是 α/r
低秩假设是:下游适配所需的权重更新 ΔW 虽然形状是 d×k,但有效更新方向落在一个很低的秩子空间里,所以可以写成 ΔW = BA(B 是 d×r,A 是 r×k,r ≪ min(d,k))。初始化上 A 用高斯或 Kaiming、B 置零,保证训练起点 ΔW = 0,模型从预训练状态平滑出发。前向实际算的是 h = W₀x + (α/r)·BAx,冻结 W₀,只更新 A、B。
rank 太小(比如 r=4)在指令跟随、格式约束这类任务上容量不够,学不动领域表达;rank 太大(r 逼近隐藏维、或直接开到 256)参数、显存、过拟合风险都上去,效果未必比全参微调好,而收益是递减的。7B13B 规模上 r 取 864 是比较常见的区间。
关键在于 α/r 这个缩放比,而不是各自的值——它决定低秩分支对原始输出的影响强度。改了 rank 不同步改 α,更新幅度会跟着漂移:α/r 过大,低秩分支一步迈得太远,输出偏离预训练分布,loss 震荡;过小则分支长期学不动。α = 2r 只是 QLoRA 论文里的经验起点,不是规则,换模型规模、换 target_modules、换学习率都要重扫。另外 LoRA 的学习率通常要比全参微调高一个量级(1e-4 ~ 2e-4),因为只有一小部分参数在更新。
rank 也不必全局统一。Q/K/V/O 投影和 MLP 的 gate/up/down 对任务的敏感度不同,可以按梯度范数或模块敏感度分配预算,AdaLoRA 就是按重要性打分动态调整各层 rank 的做法。上 production 前记得在 fp32 下 merge 再转 bf16,直接低精度 merge 会有精度损失;多个 adapter 同时挂载还要注意互相干扰。
QLoRA:省下的是权重显存,不是全部显存
QLoRA 把冻结的基础模型权重量化到 4bit 的 NF4 格式(按正态分位做非均匀量化,对近似正态分布的权重信息损失最小),再做双重量化——把每 64 个权重共享的量化常数本身再量化到 8bit,平均每个参数再省约 0.37 bit。LoRA 分支的参数、梯度、优化器状态仍保持 bf16/fp32,所以 7B 模型的权重从约 14GB 降到 4GB 上下,单张 24GB 卡就能微调。
不稳定的来源有几处。量化误差本身:权重里少数几个通道的异常值(outlier)量化误差很大,靠 LoRA 补不回来,实践上 embedding、lm_head 这类层通常跳过量化(bitsandbytes 有 llm_int8_skip_modules 之类的开关)。计算 dtype:反量化后参与计算的精度如果是 fp16,长序列下容易溢出,换成 bf16(指数范围与 fp32 一致)会明显稳。还有学习率偏大、梯度检查点带来的重算误差累积。表现出来是梯度 NaN、loss 尖刺、输出重复退化、PPL 异常震荡——所以不能只看训练 loss 下降就认为量化微调成功,要盯梯度范数、验证集 PPL 和领域样本的实际生成质量。
最容易被忽略的一点:QLoRA 省的是参数和优化器状态那部分,长序列的激活显存一分没省。序列开到 8k 以上 OOM 时,先看 profiler 分清是权重、优化器状态还是激活爆的,再决定上梯度检查点还是 FlashAttention,而不是继续降量化位数。
注意力为什么要除以 √d_k,以及长上下文的几种优化并不等价
设 q、k 各维独立、均值 0 方差 1,点积 q·k = Σ qᵢkᵢ 的方差就是 d_k,标准差是 √d_k。d_k = 128 时 logits 的标准差已经有 11 左右,最大 logit 比次大约 4 个标准差就是 45 的量级,softmax 概率比 e⁴⁵ ≈ 3×10¹⁹,分布直接压成 one-hot,绝大多数位置梯度接近 0,训练变慢甚至发散。除以 √d_k 把 logits 的方差拉回 1 附近,本质是控制数值分布和梯度尺度,不是提升表达能力。现代模型还有更强的替代:对 q、k 做 QK-Norm(RMSNorm/LayerNorm)比单纯缩放更稳,Gemma 2 那类做法则是对 logits 做 soft-cap(tanh 截断)。
长上下文的优化路线要分清各自解决什么。FlashAttention 是 IO 感知的分块计算加在线 softmax,把注意力矩阵的显存从 O(n²) 降到 O(n),计算量仍是 O(n²),结果与标准注意力数值等价,所以它解决显存和带宽,不解决”能不能理解长文”。滑窗注意力(Mistral 用 4096 的窗口)把复杂度压到接近线性,代价是丢掉远距离直接交互,靠多层堆叠扩大感受野。稀疏注意力(Longformer/BigBird)保留少量全局 token 加局部窗口。线性注意力、状态空间模型重写了计算,会损失表达能力且训练更挑设置。Ring Attention 是把序列切到多设备、环形传递 KV 块,解决的是单卡放不下。选型要看任务是否真的需要全局精确注意力:代码仓库问答、多跳推理、Agent 长轨迹对远距离依赖敏感,不能为了省显存盲目缩窗口。
RoPE 外推为什么会坏,NTK 与 YaRN 在做什么
RoPE 对 q、k 的每一维按位置施加旋转,旋转角是 θᵢ·m,其中 θᵢ = base^(-2i/d)、base 默认 10000。位置 m 超出预训练长度后,低频维度(θ 小、波长大)的旋转角进入模型从未见过的区间,注意力相对位置的模式随之失真;高频维度波长本来就短,也缺少长距离的训练信号。
位置插值(PI)把 m 线性压回训练范围,代价是相邻 token 的相对距离被压缩,短距离分辨率下降,需要少量微调(千步量级)来适应。NTK-aware 走的是另一条路:不动位置索引,直接把 base 抬高(10000 → 5e5~1e6 量级),让不同频率维度的缩放不一致——高频几乎不变以保住局部分辨率,低频被显著压缩以扩展可表示范围。YaRN 在此之上加了两件事:一是对注意力分数做温度补偿(乘上与扩展比相关的缩放),二是把维度分段处理(高频不插值、中频按 NTK、低频线性插值),官方实现里把上下文扩到 128k 只需要约 400 步微调,比插值路线省一个量级的数据。动态 NTK 则是按当前序列长度实时改 base,短输入保持原状。
必须记住:这些方法只保证位置编码不崩,不等于模型获得了长文理解能力。评测要用 RULER、LongBench 这类需要跨段定位和多跳证据的基准,加 needle-in-a-haystack 做最基础的检索验证;“lost in the middle” 现象说明模型往往能用到开头和结尾却忽略中部,光测”能不能吃下更长输入”是没有意义的。
Pre-LN、Post-LN 与 RMSNorm
Post-LN 是 x = LN(x + Sublayer(x)),LayerNorm 坐在残差主通路上,反向时梯度要穿过每一层的 LN,尺度容易被放大或压缩,深层模型训练初期很不稳,对初始化和 warmup 极其敏感,但调好了最终 loss 往往不差。Pre-LN 是 x = x + Sublayer(LN(x)),残差成了一条恒等通路,梯度可以近乎无损地直达底层,深层也能稳定训练,代价是输出方差随深度累积、需要在最后补一个 final LN,且同规模下的表达效率略逊于调好的 Post-LN。折中方案是 DeepNorm:回到 Post-LN 结构但给残差加一个随深度缩放的系数(α = (2N)^(1/4)),配合特定初始化,可以把上千层的 Transformer 训起来。现在的大模型基本是 Pre-LN 家族。
RMSNorm 只做均方根归一化:x / sqrt(mean(x²) + ε) · g,不减均值、不加偏置。省掉一次均值统计和一次减法,RMSNorm 论文里报告的加速在 7%~64% 之间(模型和硬件不同),效果与 LayerNorm 基本持平甚至更好。它不保证输出零均值,但语言模型里这并不重要。放 LoRA 的时候注意 target_modules 选的是 q/k/v/o 和 gate/up/down 这些线性层,不要往归一化层上加。
显存账本、梯度累积与混合精度溢出
以 AdamW + 混合精度、参数量 P 计:bf16/fp16 参数副本 2P、fp32 主权重 4P、Adam 的一阶二阶动量 8P、梯度按 fp32 累加 4P(fp16 存则 2P),合计约 1618P 字节。7B 模型就是 110125GB,单张 80G 卡放不下,必须分片:ZeRO-1 切优化器状态、ZeRO-2 再切梯度、ZeRO-3/FSDP 连参数一起切,代价是通信量逐级上升。所以 OOM 时第一反应不该是”上 ZeRO-3 或量化”,而是先确认爆的是哪一块。
激活是另一本账,规模与 batch size × 序列长度 × 隐藏维 × 层数成正比,标准注意力还要存 n×n 的注意力矩阵(序列翻倍,这部分按平方涨)。FlashAttention 把这块降到 O(n),梯度检查点则用约 30% 的额外前向计算换取激活内存的大幅下降。排查顺序建议是:torch.cuda.memory_summary() 或 profiler 看清构成,再依次考虑减 micro batch、开梯度检查点、换 FlashAttention、序列并行、最后才是分片方案,另外把 expandable_segments 打开可以缓解碎片导致的假 OOM。
梯度累积和增大 batch size 只在理想条件下等价:没有 BatchNorm、没有随机算子、loss 按累积步数归一化、优化器每 N 个 micro step 才更新一次。实践中的差异来源包括 dropout 与数据增强的随机性、动态 loss scaling、梯度裁剪的时机(必须在累积完之后裁才等价,否则每步裁的尺度都不一样)、优化器 step 计数和学习率 schedule。最容易踩的坑是忘记 loss = loss / accum_steps——不除,梯度就是 N 倍,等价于把学习率悄悄放大了 N 倍。DDP 下还要用 model.no_sync() 只在最后一个 micro step 才做 All-Reduce,否则通信白烧 N 倍。真正的全局 batch 是 micro_bs × 数据并行度 × 累积步数。
混合精度下”loss 正常但梯度溢出”同样常见:loss 是成千上万个 token 损失的聚合均值,量级自然正常;而反向传播是链式相乘,在深层网络、长序列的注意力反传、LayerNorm 和 embedding 的梯度路径上,局部梯度很容易超过 fp16 的上限 65504,一步 inf 就把参数污染成 NaN。
动态 loss scaling 的机制是:反向前把 loss 乘一个缩放因子 S(GradScaler 默认 init_scale 65536)以提高小梯度的表示精度,更新前先 unscale_ 还原,检查有没有 inf/nan;溢出就跳过这次 step() 并把 S 减半,连续若干个 step(默认 growth_interval=2000)正常则把 S 翻倍。调用顺序不能乱:
scaler.scale(loss).backward() → scaler.unscale_(optimizer) → clip_grad_norm_ → scaler.step(optimizer) → scaler.update()
裁剪必须放在 unscale_ 之后,否则裁的是放大后的梯度,裁剪阈值形同虚设。bf16 的指数位和 fp32 一样是 8 位,几乎不会上溢,基本不需要 loss scaling,代价是尾数只有 8 位、精度低于 fp16——所以大模型训练普遍首选 bf16,只有在硬件不支持或对精度敏感时才用 fp16 + scaler。