一台三年前的中端手机,想离线跑一个能对话的助手。内存预算只有几百兆,模型动辄几个 G。把权重原样搬进来不现实,工程上只剩一条路:模型压缩。
但这个词容易误导。它更像是主动放弃一部分数值精度和参数,去换内存、功耗和延迟;而不是把文件打包得更小。放弃哪一部分,决定了压完之后这个模型还能干什么。
先看权重里到底有什么
训练收敛后的矩阵,数值分布往往很集中:大量权重贴着零,少数几个撑着主要贡献。这种冗余来自过参数化——训练时需要足够多的自由度去找解,找到之后,很多自由度就闲着了。压缩做的事,是识别哪些可以舍、哪些舍了会出事。
四种手法,各自在什么地方让步
- 量化:把浮点换成低比特整数,省内存也省带宽,通用硬件上还能直接提速。代价是误差会累积,长尾 token 和罕见字符最先出问题。
- 剪枝:按重要性删掉一部分连接或整个通道。非结构化稀疏看着漂亮,落到实际硬件上未必更快;结构化剪枝好落地,但删得粗。
- 蒸馏:让小模型去拟合大模型的输出分布。它学到的是行为而非知识本身,碰上训练分布之外的问题容易露怯。
- 低秩分解:把一个宽矩阵拆成两个瘦的相乘。数学上干净,但对结构有假设,不是所有层都吃这一套。
实际项目里很少只用一种。常见做法是先蒸馏出骨架,再量化压到目标位宽,中间穿插剪枝。每一步都在消耗一点余量。
代价不是均匀分布的
这是我觉得最容易被忽略的一点。压完之后,模型在分类、改写、摘要这类任务上常常看不出明显变化;但多步推理、精确格式遵循、冷门事实的召回,掉得很快。换句话说,平均指标平稳,不代表你要的那个能力还在。
所以谈模型压缩,先确定场景里哪一类错误不可接受,比选哪种算法更重要。一个只做意图识别的客服入口,和一个要生成合同条款的助手,能承受的损失不在一个量级。
它更像调参,不像一次性交付
把压缩当成流水线上的一步,做完打勾,通常会返工。更实际的做法是把它当成一轮又一轮的试探:压一档,跑一遍真实请求,看哪里先坏,再决定往回退多少。预算和目标能力之间那条线,是试出来的,不是算出来的。
也正因为如此,我倾向于在项目早期就把它摆上桌,而不是等模型定型再补。结构怎么设计、层怎么排,会影响后面能压到什么程度。等到要上端侧了才想起来,选择余地会小很多。