☰
OBC解析:用Hessian二阶信息统一剪枝与量化的大模型压缩
2026/10/17 13:56:02 网站建设 项目流程

Optimal Brain Compression(OBC)这篇论文,我前前后后读了好几遍。每次读都有新收获,不是因为它的数学有多难,而是因为它把“剪枝”和“量化”这两个原本各玩各的领域,硬生生拽到了同一个优化框架里。后来大火的各种低比特量化工具、稀疏化方案,不少都能从这篇论文里找到思想的源头。这篇论文读懂了,后面去看GPTQ、SparseGPT之类的框架,会顺畅很多。

先说这篇论文解决什么问题。大模型训练好之后,直接部署往往又大又慢,通常要做压缩。压缩手段无非两大流派:剪枝,把不重要的权重直接干掉;量化,把高精度权重塞进低比特的格子里。以前的做法很“直觉”:剪枝就挑绝对值最小的权重剪,量化就按数值范围均匀切分。问题在于,这些方法很少考虑权重对损失函数的真实影响,压缩完经常掉点严重。OBC的核心主张是:压缩本质上是一个优化问题,目标是在修改权重的同时,让模型输出的损失变化最小。这个思路,业界管它叫“最优脑损伤”,论文把它从理论变成了真正能用的框架。

这篇论文适合谁读?做端侧部署、模型压缩、量化工具开发的人,应该把它当案头必备。做算法研究的人,也可以从中看到“如何把一个好想法变成可落地的方法”。如果你只是对量化感兴趣,想搞懂那些量化工具背后的原理,这篇论文也是绕不开的起点。

1. “最优脑损伤”到底在优化什么

1.1 传统压缩方法的直观与局限

先聊一个很朴素的问题:剪枝的时候,该剪哪些权重?很多人第一反应是“绝对值小的权重不重要,剪了影响不大”。这个想法不能说错,但很不完整。一个权重对模型输出的影响,不仅要看自己多大,还要看它和其他权重怎么配合。就好比拆一堵墙,你不能只看某块砖的大小,得看整面墙的受力结构。可能一块体积很小的砖,恰好卡在关键受力点上,拆了整面墙就不稳了。权重也一样,有的数值不大,但模型对它特别敏感,动一点都不行。

量化的问题更隐蔽。均匀量化(RTN)把权重按最小值到最大值切成等间隔的格子,然后每个权重就近落进一个格子。这个方案好处是快,坏处是它压根不管误差分布在哪里。结果经常是:压缩完的模型,整体误差看着不大,但某些关键层被彻底搞坏了,模型输出直接崩掉。

OBC想做的事情非常明确:与其让误差均匀地洒在所有权重上,不如有选择地让少数权重多承担一些改变,换来的代价是整体的损失变化最小。这个思路本质上是“结构性取舍”,而不是“平均主义”。

1.2 从OBD到OBS:二阶信息的价值

这个思路在学术上有个清晰的演进脉络。最早的OBD(Optimal Brain Damage)提出用损失函数对权重的二阶导数(Hessian矩阵的对角近似)来衡量权重重要性。后来OBS(Optimal Brain Surgeon)更进一步,用完整的Hessian矩阵来精确计算“如果剪掉某个权重,其他权重该补偿多少才能让损失恢复回去”。

这里面的数学直觉很简单。权重扰动对损失的影响可以展开成泰勒级数。一阶项在训练收敛后基本为零(因为梯度接近零),所以起决定作用的是二阶项——Hessian矩阵。也就是说,权重重要性不能只看w本身,还要看Hessian矩阵的结构。OBS的经典结论是:如果要剪掉第i个权重,最优的做法不只是把这个权重清零,还要同时调整其他所有权重,调整量由一个闭式解给出。这个闭式解里,分母出现的是Hessian逆矩阵的对角元素。这就解释了为什么有的权重绝对值大但灵敏度低,有的权重绝对值小但灵敏度极高。

OBS理论很漂亮,但在深度学习模型上不好落地。原因也很直白:模型的参数量动不动上亿,完整Hessian矩阵是平方级的存储开销,直接求逆更是天方夜谭。所以OBS更多停留在理论层面,很少被大规模应用。

1.3 OBC的两个关键改进

OBC之所以重要,是因为它把OBS的理论框架做了两个非常务实的改进,让它能在今天的模型规模上跑起来。

第一个改进是把“一次性决定所有被剪权重”改为“逐行(逐列)处理”。OBS试图全局求解一个优化问题:同时决定剪哪些权重、其他权重怎么补偿。这个全局问题在高维空间极其难解。OBC的做法是退一步:每次只看权重矩阵的一列(或者一行,取决于矩阵布局),在这一列内部做局部优化,决定这一列里哪些权重被修改、修改成什么值。因为权重矩阵的列数是有限的,每一列内部的处理可以快速完成,整体分摊下来计算量就可控了。

第二个改进是处理“已经被修改过的权重”时,不是把它们的误差当作噪音忽略掉,而是策略性地把误差转移到那些对损失不那么敏感的权重上。这个思想在量化场景里尤其关键。量化是一步一步做的,先量化的权重会有误差,后量化的权重在补偿时不仅要抵消自己要量化带来的误差,还可以顺手帮前面已经量化的权重的误差“擦屁股”。这就是OBC框架里“误差补偿”的核心含义。

这两个改进合在一起,让“最优脑损伤”从理论变成了可实现的算法。

2. 核心推导拆解:闭式解、Hessian逆更新与逐层重建

2.1 固定扰动集合时的最优补偿

聊完思想,进数学。前面说了,OBC处理的是逐列问题。假设某层权重矩阵是[W_{row \times col}],量化或剪枝时,我们逐列处理。当前处理到第j列,假设我们要修改这一列的一部分权重,被修改的集合记为[P],未修改的集合记为[U]。

我们关心的是:给这列权重一个扰动[\delta],模型损失的变化近似是多少?在训练收敛点附近,一阶项为零,所以损失变化可以近似为:

[\Delta L \approx \frac{1}{2} \delta^T H \delta]

其中H是这一列对应的Hessian矩阵,通常是逐层计算出来的。我们要找的,是在约束[\delta_P]固定(比如量化误差已经确定了)的情况下,最优的[\delta_U]要让整个[\Delta L]最小。

这是一个标准的有约束二次优化问题。用拉格朗日乘子法,可以得到闭式解:

[\delta_U = - H_{UU}^{-1} H_{UP} \delta_P]

这个公式信息量很大。它告诉我们:当某些权重被修改后,其他权重的“最优补偿量”不是随便给的,而是通过块Hessian逆矩阵[H_{UU}^{-1}]和交叉项[H_{UP}]计算出来的。直观理解就是:被修改的权重会把“压力”传递给其他权重,传递的程度取决于它们之间的二阶交互关系。如果两个权重高度相关,补偿量就大;如果相关性弱,补偿量就小。

剪枝场景是这个公式的特例。如果剪掉权重i,也就是[\delta_i = -w_i],那么最优的补偿向量可以写成:

[\delta_U = - \frac{w_i}{(H^{-1}){ii}} (H^{-1}){:,i}]

这里出现了(OBS里经典的那个结论:敏感度量化公式[-\frac{w_i^2}{(H^{-1})_{ii}}],权重w_i对损失的影响力,本质上是看它除以对应Hessian逆对角元之后的值。如果Hessian逆对角元特别小,说明这个权重方向上的“曲率”很大,剪掉它的代价极其高昂。

2.2 列与列之间的“解耦”:Woodbury恒等式的妙用

上面的公式把“一列里的最优补偿”讲清楚了。但还有一个问题悬而未决:处理完一列之后,下一列的Hessian怎么算?如果每一列都重新构建Hessian并求逆,计算量还是受不了。

这里OBC用了一个非常巧妙的手段:列与列之间的Hessian关系,可以通过Woodbury矩阵恒等式递推更新。什么意思呢?处理完第j列之后,这列的权重已经被修改了,对后续列而言,它相当于被“移除”了。此时,全局Hessian的逆矩阵可以不用重新求,而是基于之前的逆加上一个低秩修正项来更新。

公式大致长这样:

[H^{-1} \leftarrow H^{-1} - \frac{H_{:,j}^{-1} H_{j,:}^{-1}}{(H^{-1})_{jj}}]

这个更新每次代价是O(n^2)而不是O(n^3)的重新求逆。对于矩阵列数几千甚至上万的情况,这个缩减是决定性的。没有它,OBC的理论再漂亮也跑不动。

实操中这个递推还有一个好处:它天然地把“处理完的列”和“没处理的列”隔离开来。处理完的列贡献过的误差,不会在后续列的优化里被重复考虑。这就保证了整个算法的收敛性和稳定性。

我在复现这个递推的时候踩过一个坑:直接用官方实现里的这个更新公式,结果数值误差越积越大,跑了大半层之后突然崩溃。后来定位到原因:Hessian逆矩阵在低精度(float16)下做递推更新,误差会指数累积。解决办法很土也很有效:中间计算用float64,只在最后写回权重时才用float16。这个细节论文里没细说,但实际实现时必须注意。

2.3 量化场景:扰动不再是“清零”,而是“落格”

前面推导基本是在讲剪枝(扰动是把权重清零),但OBC真正的“爆款应用”是量化。量化场景下,扰动不是把权重清零,而是[d = Q(w) - w],也就是权重量化前后的差值。

这个改动看似简单,实则影响深远。因为量化时,我们可以决定一个组里哪些权重被量化(落进格子)、哪些权重暂时不动(但后面会被补偿)。与剪枝不同,量化不需要把任何权重彻底去掉,它只是让一部分权重改变数值。

OBC框架对量化的处理,完美契合了逐列处理的设定。处理某一列时,假设我们决定这一列所有的权重都参与量化(实际算法里可以细分成组来分批处理),那么量化误差就相当于给这一列加上了一个已知的扰动向量。下一个问题是:如何在这个扰动的基础上,对未处理的权重做最优补偿?答案还是上面那个公式——把量化的扰动[d]代进[\delta_P],然后求[U]部分的最优响应。

计算整个算法流程,实际做下来是这样的:

# 伪代码:OBC风格的后训练量化逐层处理 for layer in model.layers: # 收集该层输入特征,构造Hessian H = compute_hessian(layer_inputs) # shape: [out_features, in_features] # 加阻尼项,防止奇异 H += damping * I H_inv = torch.inverse(H) for col in range(weight.shape[1]): w = weight[:, col].clone() # 计算量化误差:每个权重落格后的差值 d = quantize(w) - w # 用闭式解计算对未修改部分的补偿 # 实际实现里是逐列/逐块更新,为了展示只取单列形式 compensation = - (d / torch.diag(H_inv)) # 按Hessian逆结构更新所有权重 weight[:, col] += compensation # 用Woodbury恒等式更新H_inv,移除已处理列的影响 H_inv = update_hessian_inv(H_inv, col)

这个流程就是后来各种OBC系量化方法(GPTQ一路的)的基本骨架。区别主要在于一些工程细节:比如用LDL分解代替直接求逆、按块处理多列、用校准集观测值来近似Hessian等等。

必须承认,这里省略了大量工程细节和严格推导,真正要复现这个算法,光靠上述伪代码是不够的。但理解了这段骨架,再去读论文里的算法框图和公式,理解难度会直线下降。

3. 实操落地:从论文公式到可用代码

3.1 校准数据的选取:Hessian从哪里来

前面说了Hessian是逐层计算的,但实际模型里,我们不可能拿到全局损失函数对每一层权重的精确Hessian。业界通用的做法是采样校准集。具体操作:拿一小部分真实数据(几十到几百个样本),喂进模型,跑前向,在每一层记录输入特征(就是前一层的输出)。然后这一层的Hessian用这些输入特征的协方差来近似:

[H = X^T X]

其中X是该层输入特征矩阵。这个近似建立在“损失对权重的二阶效应主要通过输入特征的二阶矩来传导”这个假设上,实际效果相当好。

校准集怎么选,直接决定压缩后模型的性能。这一点我吃过亏。一开始图省事,直接用训练集里随机选的样本做校准,结果量化后模型在真实任务上掉点很严重。后来换成了和目标部署场景分布更接近的数据,同样层数、同样比特,效果立刻改善。原因也不难理解:如果校准集的分布和真实场景错配,Hessian的结构就是错的,算出来的“最优补偿”自然也是错的。这条经验对量化、剪枝全都适用。

校准集大小的选择也要讲究。太少了(比如几十个),Hessian估计的噪声很大,逆矩阵对噪声极其敏感,压缩结果很不稳定。太多了(比如几千个),计算开销大且边际收益递减。我在实践中做下来,128到256个样本是一个比较稳的区间,既能得到可靠的Hessian估计,又不会让校准过程等太久。

3.2 阻尼项:避免Hessian逆的“爆炸”

直接从校准集算出来的Hessian矩阵,不可避免地会有接近零的特征值。尤其当某些方向在数据里几乎不激活时,Hessian在那个方向上的值会非常小。求逆之后,这些接近零的特征值会变成巨大的数值,导致补偿量完全失控。这也是这类二阶方法最经典的坑。

解决办法就是给Hessian加一个阻尼项:

[H_{regularized} = H + \lambda I]

λ选多大,需要根据模型和数据的尺度来调。太大,会压掉真实的曲率信息,让方法退化成近似一阶方法;太小,逆矩阵依然不稳定。我的习惯做法是先跑一个小模型,观察不同λ下压缩后指标的变化,找一个平缓区间里的值。一般来说,Hessian矩阵平均对角元的千分之一到万分之一是一个比较合理的起点。

这个细节论文里提过,但没强调它的敏感性。实际操作中我发现,λ对最终压缩效果的影响,在某些模型上甚至比量化比特数还大。所以如果你复现时发现压缩结果飘忽不定,第一个要检查的就是阻尼项。

3.3 逐层重建的工程节奏

整个压缩过程是逐层进行的:处理完一层,才进入下一层。这里有一个工程上的选择:到底是一次性把所有层的Hessian都算出来,然后逐层压缩,还是算一层、压一层、再算下一层?前者更节省时间,因为前向传播只需要跑一次;后者更灵活,因为每一层的误差可以被后续层感知到,理论上可以做一些反馈修正。主流的实现(包括GPTQ的代码)用的是前者,先缓存所有层的输入特征和Hessian,再逐层压缩。这么做速度快,而且足够实用。

在某一层的内部,处理顺序是从第一列到最后一列(或者按块处理)。这个顺序对最终效果有影响吗?我试过按列敏感度降序处理,也试过随机顺序,和默认顺序相比,指标差别不大。真正影响效果的是Woodbury更新是否正确应用,以及阻尼项是否稳定。所以如果你发现结果很差,不要急着换处理顺序,先把Hessian更新这块的数值稳定性检查一遍。

4. 常见问题与排查经验

4.1 压缩后模型极度崩坏,连随机输出都不如

这个现象最常见的原因是Hessian估计有严重噪声或者逆矩阵爆炸。排查顺序:

第一,看阻尼项是否太小。可以试着把λ调大一个数量级,跑一遍,看指标是否恢复。如果指标明显变好,说明问题就是阻尼不够。

第二,检查校准集是否和真实数据分布偏离太大。一个很有效的验证方法:用校准集本身做一遍保留测试,如果压缩后的模型在校准集上的表现都很差,问题大概率出在校准数据的质量或者数量上。

第三,检查代码里的数值精度。逐列更新的递推公式,如果全程用float16,数值误差会在几百列之后累积到灾难级别。务必在中间计算中用float64。

4.2 某些特定层怎么压都崩,其他层压完没事

这种情况通常意味着这些层里有极其敏感的权重方向,修改一点点就影响巨大。可以单独给这些层分配更高比特(比如其他层用4bit,它们用6bit甚至8bit),OBC框架天然支持混合精度,因为每一层的Hessian和压缩都是独立的。实际部署中,这个“局部加码”策略往往能用一个很小的平均比特数换取很大幅度的性能恢复。

4.3 剪枝稀疏度一高就断崖式掉点

如果稀疏度超过90%之后效果崩坏,这几乎是必然的,不是bug。此时可以试试稀疏化程度从小到大逐步递增,而不是一步到位。比如从50%开始,以10%为步长,每一步都在前一步的基础上继续稀疏化。这样每一步只需修改少量权重,误差控制起来会容易很多。

下面整理一份我实际踩过的坑和对应的检查项,做成表格方便排查。

异常现象高概率原因排查与对策
压缩后损失剧增阻尼项过小,Hessian逆爆炸增大λ,观察指标恢复;确认中间用float64
每个单独层分析没问题,合起来就崩校准集与真实分布错配更换校准集,检查样本量和分布对齐程度
与官方报告指标差距大校准集样本数过少从128开始逐步增加到512,观察指标收敛
高稀疏度下大幅掉点稀疏度本身过高采用渐进式稀疏化,或混合精度策略
首次跑通耗时过长逐列更新拖慢整体速度按块处理多列(如128列一块),用LDL分解代替直接求逆
部分层崩掉,其他层正常该层Hessian结构特殊,极度敏感对该层单独提高量化比特,或加大阻尼系数

4.4 速度优化:按块处理多列

逐列更新虽然思想清晰,但效率不高。实际实现里,几乎所有有效率要求的压缩工具都会按块处理:一次性对128列或更多列做量化,然后做一次块级的最优补偿。数学上,这是把单列的公式推广到块场景,本质相同,但矩阵运算可以轻易并行,速度能提升一到两个数量级。如果自己实现,建议直接上块处理,别先写逐列版本再改。

5. 扩展应用:量化、剪枝之外的OBC

OBC的框架魅力在于,它的核心是“给定一组权重扰动,找最优补偿”,这个抽象几乎适用于一切结构化的权重修改场景。除了量化和剪枝,还有几个方向值得留意。

一是混合精度搜索。传统做法是靠启发式规则给不同层分配不同比特,但OBC框架里每一层都有自己独立的Hessian,可以准确计算“这层用低比特会损失多少”。于是可以把压缩问题变成一个资源分配问题:用最小的总比特预算,达到指定的精度目标。这个思路在一些量化工具里已经落地,效果远超启发式规则。

二是低秩分解与稀疏化的结合。低秩分解(比如SVD压缩)本质上也是修改权重矩阵,而且它引入的误差分布非常有结构性。如果把OBC的补偿思想引入低秩分解,理论上可以弥补只做SVD时忽略的“权重间交互影响”。我见过一些工作在这个方向上做过尝试,虽然成熟度不如量化,但思路是很自然的延伸。

三是和其他压缩手段叠加。比如先剪枝、再量化、最后做知识蒸馏微调。这里要注意的是,OBC类方法提供了很好的压缩初始点,但它替代不了微调。实际操作时,压缩完通常还是要做一步轻量级微调来恢复最终性能。OBC的质量决定了微调收敛的速度和上限。

读这篇论文时,我最大的体会是:它把一个领域里很多人靠经验和试错摸索出来的“套路”,用数学语言统一起来,并且给出了一个非常优雅的推导框架。这种从零散经验到统一理论的跃迁,是研究最迷人的地方之一。如果你也想做部署落地,我的建议很直接:这篇论文的算法流程吃透,再结合现成的开源量化工具跑几个模型,亲手观察一下Hessian、阻尼、校准集这些因素对结果的影响,很快就能建立起“压缩这事到底在干嘛”的完整图景。

最后分享一个小习惯:我每次做量化实验,都会先把各个层的Hessian对角元分布画出来看看。这一步几乎不花时间,但对判断瓶颈在哪、哪些层需要特殊照顾,帮助极大。这个习惯就是受OBC论文启发的——毕竟,理解了权重在损失曲面上的“地形”,才能知道压缩的哪一步最危险。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询