推理成本过高时,摆在面前的通常有三条路:蒸馏、剪枝、低秩分解。它们的名字常被并列提及,但改变的东西完全不在同一个层面。区分清楚”动的是什么”,是选择路线的第一步。
三条路线分别动的是什么
蒸馏改的是学习信号。教师的输出分布成为学生的监督目标,学生用什么结构跟教师可以毫无关系。蒸馏本身不产生推理加速,加速来自你给它配了更小的学生。
剪枝改的是结构。哪些权重、通道、注意力头还在,剩下的靠微调重新收敛。删掉的东西在推理图上就真的不存在了。
低秩分解改的是参数表示。把一个 $W in mathbb{R}^{m times n}$ 拆成两个更小的矩阵相乘,拓扑结构不变,算子还是那个算子,只是参数被重新参数化了。
一个反直觉的结论:剪枝听起来最”物理”,对推理速度的影响却最不确定;低秩分解听起来最数学,加速比反倒能精确算出来。
知识蒸馏的温度与软标签
带温度的 softmax:
$$q_i = frac{exp(z_i / T)}{sum_j exp(z_j / T)}$$
$T = 1$ 时就是普通 softmax,$T to infty$ 时分布趋于均匀,$T to 0^+$ 时趋于 one-hot。
软标签为什么比硬标签信息量大。四分类样本,硬标签是 $(0, 0, 1, 0)$,只表达”答案是第三类”;教师的软标签可能是 $(0.01, 0.12, 0.70, 0.17)$:第四类跟它比较像,第二类沾点边,第一类完全不像。这个类间相似结构是教师从数据里学到的、标注里没有的东西,也就是常说的暗知识。学生拟合它,等价于学一个更平滑的决策边界。
梯度视角更直接:用软化后的教师分布做交叉熵,损失对学生 logit 的梯度含 $1/T^2$ 因子,温度越高梯度越小。为了不让 KD 项的量级随 $T$ 漂移,标准做法是显式乘回 $T^2$:
def kd_loss(student_logits, teacher_logits, labels, T=4.0, alpha=0.5):
# 常规监督项:对硬标签
ce = F.cross_entropy(student_logits, labels)
# 蒸馏项:对教师的软化分布
kd = F.kl_div(
F.log_softmax(student_logits / T, dim=-1),
F.softmax(teacher_logits / T, dim=-1), # 教师 logits 已 detach
reduction="batchmean",
) * (T * T)
return (1.0 - alpha) * ce + alpha * kd
几个容易踩的点:
- 教师必须
no_grad()且处于eval()模式,否则 dropout 会给软标签注入噪声。 - 学生和教师的词表必须一致,否则 logits 维度对不上,或者更糟——维度对上了但语义错位。
- $T$ 经验上取 2 到 8。容量差距越大,$T$ 越高越容易把教师的噪声一起放大。类别数多时通常取大一些。
- $alpha$ 是硬标签与软标签的权重。学生能力弱时提高 $alpha$,让它多跟教师;标注质量差时也提高 $alpha$,减少噪声标签的干扰。
- 只对齐输出 logits 对深层网络往往不够,中间层蒸馏是常见补充:对 hidden state 投影后算 MSE,或对 attention map 做损失。
剪枝:稀疏化不等于加速
这是压缩领域最容易被误解的地方。
非结构化剪枝把单个权重置零,得到稀疏权重矩阵。参数量确实降了,文件确实小了,但在通用 GPU 上稠密 GEMM 走的是高度优化的 kernel 与 tensor core。一个随机稀疏的矩阵,除非稀疏模式是硬件原生支持的(例如 2:4 结构化稀疏),要么退化成稠密计算——等于没加速,要么走稀疏 kernel——只有稀疏度相当高时才划算。
结构化剪枝整通道、整注意力头、整层地删,剩下的是一个更小的稠密矩阵。任何推理框架都能直接吃下这个收益,不需要特殊算子。代价是同等压缩率下精度损失更大,因为自由度是按块消失的。
| 维度 | 非结构化剪枝 | 结构化剪枝 |
|---|---|---|
| 剪裁粒度 | 单个权重 | 通道 / 注意力头 / 层 |
| 常见可用稀疏度 | 较高(量级上八成以上) | 较低(三到五成已明显掉点) |
| 推理加速 | 依赖稀疏算子或硬件原生模式 | 通用,直接生效 |
| 存储与显存 | 显著下降 | 显著下降 |
| 精度保持 | 同等比例下更好 | 同等比例下更差 |
剪枝判据不止”权重绝对值最小”一种:按通道的 L1 范数、带缩放因子的稀疏训练、基于激活统计的重要性打分都是常见做法。迭代式剪枝(剪一点、微调、再剪)精度明显优于一次性剪到底,代价是时间成本成倍增加。
def prune_linear_by_l1(module: nn.Linear, keep_ratio: float):
"""按输出通道的 L1 范数做结构化剪枝,返回保留的通道索引"""
w = module.weight.data # [out_features, in_features]
score = w.abs().sum(dim=1)
k = max(1, int(w.size(0) * keep_ratio))
keep_idx = torch.topk(score, k).indices.sort().values
module.weight = nn.Parameter(w[keep_idx].clone())
if module.bias is not None:
module.bias = nn.Parameter(module.bias.data[keep_idx].clone())
return keep_idx
真正的坑不在剪枝本身,而在维度联动。剪掉一个 Linear 的输出通道后,下一层的输入维度、LayerNorm 的归一化维度、残差相加的形状、多头注意力里 head 与 head_dim 的对应关系,全都要同步调整。漏掉任何一处,要么 shape 报错,要么不报错但语义错位——后者更危险,模型还能跑,只是悄悄变差。剪完必须微调,别指望一次剪完就能用。
低秩分解:改的是表示
对权重做奇异值分解 $W = U Sigma V^T$,取前 $r$ 个奇异值:
$$W approx U_r Sigma_r V_r^T$$
Eckart-Young 定理保证这是在 Frobenius 范数下秩不超过 $r$ 的最优逼近,误差由被丢弃的奇异值平方和控制。这也给了选 $r$ 的依据:画奇异值能量曲线,取累计能量达到某阈值(如 90% 或 95%)的位置。
计算量从 $m cdot n$ 降到 $r cdot (m + n)$。举个例子,假设 $m = n = 4096$、$r = 512$,参数量从约 16.7M 降到约 4.2M,量级上压到四分之一。
但有两个实际的折扣:
- 一次矩阵乘法变成两次,中间激活要写一次再读一次。在访存受限的场景(小 batch、边缘设备),实际加速比通常达不到 FLOPs 的比例。
- 直接截断 SVD 的精度损失在低比特下会被放大,所以分解后一般要接一段短程重训来恢复。
LoRA 是低秩在微调侧的形态,不是压缩
LoRA 把更新量参数化为 $Delta W = BA$,其中 $B in mathbb{R}^{m times r}$、$A in mathbb{R}^{r times n}$,训练时冻结原始 $W$,只优化 $A$ 和 $B$。它跟推理侧的低秩分解用了同一个数学工具,目标和结果完全不同。
| 低秩分解(推理侧) | LoRA(微调侧) | |
|---|---|---|
| 目的 | 降低推理 FLOPs 与权重显存 | 降低训练参数量与优化器状态 |
| 推理计算量 | 减少 | 合并后不变,不合并则略增 |
| 起点 | 已训练权重的 SVD 截断 | 预训练权重上挂旁路,随机初始化 |
| $r$ 由什么决定 | 能容忍的精度损失 | 任务与预训练分布的距离 |
$r$ 的取值逻辑因此也不同。分解里的 $r$ 是精度预算,由下游指标允许跌多少决定;LoRA 里的 $r$ 是适配容量,常见 8、16、32 就够了,任务差异大时才往上加,加过头就是过拟合。
一个常见误解是”用了 LoRA 模型就变小了”。变小的只是检查点文件(只需要存 $A$、$B$),推理时把 $BA$ 合并回 $W$,计算量一分不减。如果确实需要推理加速,还是得走剪枝、分解或量化。
组合顺序与各自代价
几条路线常叠加使用,顺序不是随意的:
- 蒸馏换骨架:先把大模型的能力迁移到一个结构上就更小的学生。这一步决定了后续所有操作的起点。
- 结构化剪枝砍冗余:在学生的宽度和深度上做减法。
- 低秩分解压表示:对剩余的大矩阵再做低秩逼近。
- 量化压数值精度:权重和激活从 FP16 降到 INT8 或更低。
- 短程重训恢复:每个步骤之后各做一次,收尾时整体微调一轮。
量化放到收尾是因为量化参数(缩放因子、零点)依赖激活统计分布,而前面每步都会改变分布,提前量化会让标定结果失效。
代价上:蒸馏最贵,教师要推理一遍全量训练数据,显存和算力都是双份;剪枝显存开销最小,但迭代式剪枝的训练轮次会拉长;低秩分解单步便宜,重训恢复是主要成本;量化最便宜,对精度最不友好。
压缩之后必须重新评测
压缩不是等价变换,模型的错误模式会变。只看总指标会漏掉很多问题。
误差不是均匀分布的。整体准确率掉一个点,背后可能是某个子群体掉了十个点。长尾类别、超长或超短输入、低频语言、格式特殊的请求都是重灾区。评测要分桶,每个桶有自己的基线和阈值。
标定集与线上分布要一致。剪枝的重要性统计、量化的激活范围标定都依赖校准集。校准集偏简单或偏干净,估计出的范围就偏窄,线上遇到分布外输入时误差会被放大。
生成任务的退化是复合的。分类任务错一题就是错一题;生成任务里前几个 token 的偏差会沿自回归链路放大,perplexity 接近的两个模型输出质量可能差很远。所以必须配任务级评测,不能只看 loss。
下游系统会连带受影响。检索系统里 embedding 模型被压缩后召回集合变化,与之耦合的重排模型面对的是它没见过的候选分布。这类跨模块效应只有在端到端评测里才能暴露。
把每一步的代价摆到明面上,按业务能接受的方向去分配,比追求某个漂亮的数字更实际。
评论(0)
还没有评论,来抢沙发。
请 登录 后参与评论。