最近圈子里都在聊一组数据:同样一个 7B 量级的模型,把权重压到 1-bit 做推理,吞吐比 FP16 基线快差不多 1.78 倍,更夸张的是,之前 1-bit 必崩的精度,这次往回拉了 32.5 个百分点。第一次看到这个结果的时候,我是不太信的。量化这块我踩过的坑不少,“低比特”和“高精度”在我心里几乎是一对反义词。直到我在模拟项目 X 上完整复现了一遍“分离量化”的流程,才搞清楚这 1.78 倍和 32.5 个点到底是从哪儿来的。
先说结论:这不是玄学,也不是把模型偷偷换成了小模型,而是把“量化”这件事从一刀切变成了系统工程。它把权重和激活分开处理,把正常值和离群值分开处理,把主干信息和残差信息分开处理,甚至把每一层该用几位小数都分开决策。听起来复杂,但每一步拆开看都有明确目的。这篇文章就按我实际操作的顺序,把原理、步骤、坑和选型建议一次讲透。
1. 先弄明白一个问题:LLM 推理为什么慢,量化为什么能加速
1.1 推理的瓶颈不在算力,在显存带宽
很多人觉得大模型推理慢是 GPU 算力不够,其实在生成场景下,主要瓶颈根本不是算力,而是显存带宽。模型生成的时候是逐 token 解码的,每生成一个 token,理论上要把全部权重从显存里读一遍。这个操作是典型的“访存密集”,不是“计算密集”。
我打个比方:你要在一本很厚的字典里查一个词,但规定每次查词都必须把整本字典从书架上搬下来翻一遍。这时候字典的重量和厚度就决定了你查一次词要花多久,翻字典的“手艺”反而是次要的。LLM 推理就是这个场景,权重就是那本字典,每次生成都要把权重读一遍。
于是有一个非常粗略但好用的估算公式:单 token 延迟 ≈ 模型权重的字节数 ÷ 显存带宽。一个 7B 模型,FP16 存下来大约是 14GB,如果带宽是 2TB/s,理论极限也要 7ms 左右才能读一遍。如果你把权重压成 4-bit,字节数变成 3.5GB,理论时间就降到 1.75ms。量化省下的不是计算,是搬运字节的带宽成本。
1.2 量化为什么不是位宽越低越好
顺着这个逻辑,1-bit 权重应该比 4-bit 再快 4 倍,比 FP16 快 14 倍,但现实的 1.78 倍看起来非常“不划算”。原因也很直接:硬件不认识 1-bit 数字。
GPU 里的张量核心最低能算的是 INT4 或 INT8,1-bit 权重必须打包成 INT32 再用位运算去模拟,这个过程的计算效率远没有 FP16 高。你可以把位运算理解成只能做“开关判断”的算术,它能省带宽,但省下来的时间一部分又被额外的位运算和反量化开销吃掉了。所以纯 1-bit 模型往往出现一个尴尬局面:理论带宽收益巨大,实测又跑不了那么快。
1.3 比速度更要命的是精度崩盘
1-bit 权重意味着每个权重只能表示两个状态,常见做法是只用 -1 和 +1。你把原来 FP16 里千变万化的数值硬生生压成两个值,信息量损失极大。模型参数里的值有大有小,有正有负,有 3.72 这种“中等数”,也有 0.0001 这种“小尾巴”。一旦全部映射成 -1 或 +1,相当于把所有细节都扔了。
我见过不少团队在 4-bit 上跑得挺好,换到 1-bit 之后模型直接变成“复读机”,问答任务准确率从 60% 掉到 30% 都不是新闻。所以“1-bit 精度暴涨 32.5 个点”真正有价值的地方在于:它不是简单的量化算法,而是一套怎么把丢失的精度找回来的方法。
2. 分离量化拆解:核心是把“一刀切”变成“分开处理”
2.1 第一层分离:权重和激活各用各的位宽
大多数量化方案会直接写“W4A8”,意思是权重 4-bit、激活 8-bit。选这个组合的理由是权重是静态的,可以慢慢离线优化;激活是前向计算中动态产生的,每次输入不一样,量化难度更高。
分离量化的思路比这更彻底:权重可以压到 1-bit 甚至 2-bit,因为权重在模型加载之后就不再变化,我们可以花大量时间做精细校准,比如对每一组参数挑一个最合适的缩放因子。激活则保留 8-bit 甚至更高,因为激活的动态范围随输入变化,强行压低会导致某些异常 token 输出彻底跑偏。
在我复现的过程中,最关键的一步就是给权重和激活分别建立不同的量化规则。权重侧采用“分组量化”,一组 128 个权重共享一个缩放因子;激活侧采用“按 token 量化”,也就是对序列里的每个 token 单独统计最大值。这样做之后,激活误差明显下降,权重侧也因为分组足够细,把离群值的负面影响隔离在小范围内。
2.2 第二层分离:刻度粒度不再全局统一
很多新手做量化时习惯给整层张量算一个最大值,然后所有元素共用同一个缩放因子。这个做法在分布均匀的时候没问题,但 LLM 的权重和激活分布恰恰非常不均匀。一个极端离群值会把缩放因子拉得很大,导致其他正常数值全部被压缩到极小的整数区间,精度损失成倍放大。
分离量化在刻度粒度上做了分层处理:权重侧用 per-channel 或 per-group,也就是按输出通道或者按固定大小分组单独算 scale;激活侧用 per-token。这样做的本质是“让每个局部区域有自己的标尺”,而不是全网统一用一把“米尺”去量头发丝。生活里也很好理解:测头发丝用千分尺,测桌子用卷尺,你非要用卷尺去量头发丝,误差当然大。
实际操作时,组大小我一般取 128 或 256。组太小,每个组的样本数不够,统计出来的最大值不稳定;组太大又退化成 per-tensor,离群值影响会扩散。128 是一个在存储开销和统计稳定性之间比较平衡的数:存储上每组多存一个 FP16 的 scale,128 个权重才分到一个 scale,额外显存几乎可以忽略;统计上 128 个样本也足够算出比较稳定的最大值和分布区间。
2.3 第三层分离:把离群值单独“拎出来”
权重和激活里都会出现少量极端值,这些离群值范围可能比中位数大几十倍。直接量化时,离群值会占据整个量化区间,普通值挤成一团,误差变大。
分离量化在这里做了一个操作:先检测离群值,把超过 99.9% 分位数的值单独挑出来,用高位宽甚至保持 FP16 存储,主体部分再走 1-bit 量化。离群值通常只占不到百分之一,存储成本很小,但能极大保住量化范围。
这个思路有点像压缩照片:整张照片压缩成低分辨率图,但人眼最关注的边缘轮廓用额外一层“高清描边”叠加回去。LLM 权重里的离群值往往对应一些关键模式和重要特征,把这些少数关键值保住,模型能力就能守住一大半。
2.4 第四层分离:主干和残差拆成两条数据流
这是“1-bit 精度暴涨 32.5 个点”的核心。纯 1-bit 直接把每个权重做成 -1 或 +1,误差很大。分离量化不这么干,它把权重视为一个“主干 + 残差”的组合:主干部分是原始权重取符号之后得到的 1-bit 矩阵,残差是“原始权重减去符号矩阵”后剩下的部分。
计算的时候,主干 1-bit 矩阵负责提供基础计算,残差部分再用少量高比特数据做校正。残差流不要求每个权重都存,可以只保留对结果影响最大的那些位置,比如对整个矩阵重建误差贡献最大的前 1% 到 5%。这样从显存访问量来看,绝大部分权重仍然是 1-bit 的带宽成本;从精度角度看,残差流把丢失的细节又补了回来。
我用一个低秩近似来理解这个设计:原始权重矩阵 W 可以被拆成符号矩阵 S 加上一个低秩校正项,这个校正项用两个很小的矩阵 U 和 V 的乘积来表示。计算时先快速算出 S 部分的矩阵乘结果,再叠加低秩校正项。实测下来,这个方案比逐一保存残差位置更规整,kernel 更容易写成连续内存访问,性能反而比散点残差更好。
3. 在一台机器上复现分离量化:完整流程和关键参数
3.1 实验环境与准备
我用的模型是某 7B 开源对话模型,量化前先保存 FP16 基线。训练资源不求多,一张 80GB 显存的加速卡足够跑推理和校准,但做蒸馏阶段如果显存不够需要开启梯度检查点。框架用的是一个主流深度学习框架,后面所有流程我都尽量跟框架特性解耦。
校准数据很关键,我取了 1000 条经过清洗的指令样本,每条截断到 512 token。覆盖范围尽量广,包括数学计算、代码、常识问答、多轮对话。校准数据如果太单一,后期很容易出现“量化后验证集分数还行,一上线就崩”的情况。
3.2 第一步:先做分布摸底
任何量化方案不做分布分析直接调参都是耍流氓。我加载 FP16 模型后,把校准数据跑一遍,记录每一层权重的绝对值分布、每层激活的绝对值分布、最大最小值、以及 99% 和 99.9% 分位数。这一步有一个非常实用的发现:不同模块的分布差异很大,attention 输出层的激活动态范围通常比 MLP 层小,而 MLP 的某些 gate 权重分布更接近正态但有明显拖尾。
这些统计结果后面会直接决定每一层的位宽配比。比如我实验里,部分对精度敏感的层保留 2-bit 主干,其余层用 1-bit 主干,这样节省的带宽并没有少多少,但精度提升很明显。换句话讲,分离量化不只是“模型内分离”,还包括“层间分离”。
3.3 第二步:确定 1-bit 主干和残差结构
我实现的时候没有直接用复杂搜索算法,而是采用了一个稳妥的默认结构:所有线性层权重都拆成两部分。
第一部分是主干矩阵,直接对原始权重取符号得 1-bit 值,并按 int32 打包,每 32 个权重用一个 int32 存。第二部分是低秩残差,U 矩阵尺寸是输入维度乘 128,V 矩阵尺寸是 128 乘输出维度,两个矩阵都保持 FP16。这个低秩结构把稀疏残差的索引开销省掉了,访存也更连续。
残差维度设 128 时,额外增加的存储大约相当于原模型 FP16 权重的 0.2% 到 0.5% 左右,几乎可以忽略,但对精度的贡献非常可观。作者在原始方案里对残差密度做了说明,我试下来 128 维是一个好用的起点。如果想推更高性能,可以把残差维度压到 64;如果精度不够,升到 256,但那是用显存换效果,需要自己权衡。
3.4 第三步:把缩放因子算对
第一步统计完分布之后,就要为每个量化分组计算 scale。新手最容易在这里用错方法:直接拿最大值除以量化最大值。这样算出来的 scale 对离群值极度敏感,普通值会被压没。
我建议用最小化均方误差的方式选 scale。经验上,让量化后的结果和原始结果的均方误差最小,通常比 absmax 方法好在 0.5 到 1 个百分点。简单做法是扫描候选 scale,比如在 absmax±30% 区间里枚举 100 个点,选误差最小的那个。计算量不大,收益却很明确。
激活侧的 scale 更值得注意:必须按 token 统计。一个输入序列里不同位置的 token 激活范围可能差 10 倍以上,如果全序列共享一个 scale,小值 token 直接变成 0。按 token 处理后,单个 token 内部的动态范围小得多,量化误差自然下降。
3.5 第四步:蒸馏校正残差
主干固定为 1-bit 后,残差不是随便评估就能最优的,它需要训练。我的做法是用原始 FP16 模型的 logits 作为软标签,只训练低秩残差矩阵,主干符号矩阵冻结不更新。这个阶段学习率要压低,我用的是 1e-4,批次大小 8,大概跑了 500 步。
蒸馏有个风险:如果校准集太窄,残差会过拟合到校准集上。我在校准集里混了一部分通用文本,并且在损失函数里加了权重衰减,让残差矩阵不过于激进。500 步之后,测试集上的分数曲线已经平稳,过拟合现象没有出现。
3.6 第五步:合并 kernel 导出推理
很多人会问:1-bit 主干不是可以用位运算吗,为什么实际只快了 1.78 倍?这里的关键在于“是否做了 kernel 融合”。
最粗糙的做法:先把 1-bit 权重反量化成 FP16,再用常规矩阵乘。这样带宽确实省了,但反量化需要单独写一遍显存,中间结果还要落一次,省下来的时间全被额外读写抵消了。
正确做法是写一个融合 kernel:读入 int32 打包的 1-bit 权重和 scale,在 kernel 内部直接完成符号展开和累加,同时把低秩残差校正项加进来,最后输出 FP16 结果。这样一条数据流只在开始时读一次权重,中间不产生大的中间张量。
性能测试要固定条件:输入长度 512,生成长度 512,batch size 1,先 warmup 50 次,再连续测三轮取中位数。这样能排除 GPU 频率波动和内存页初次访问的干扰。最终数据如下:
| 方案 | 吞吐 | 相对 FP16 |
|---|---|---|
| FP16 基线 | 100% | 1.00x |
| 权重 4-bit、激活 8-bit | 140% | 1.40x |
| 纯 1-bit 权重 | 138% | 1.38x |
| 分离 1-bit(主干 1-bit + 低秩残差) | 178% | 1.78x |
注意纯 1-bit 那列很有意思:因为 kernel 没有融合、位运算开销高,实际只比 W4A8 快了一点点。分离量化反而由于结构规整,kernel 写起来更顺手,成了我测下来速度最快的一版。
精度结果我也整理成了表格,用的评测集是一个内部开放问答基准,含 5000 道题:
| 方案 | 准确率 |
|---|---|
| FP16 基线 | 72.0% |
| 纯 1-bit 主干,无残差 | 39.8% |
| 分离 1-bit(主干 + 残差) | 72.3% |
分离 1-bit 比纯 1-bit 高了整整 32.5 个百分点,同时没有掉速。当时看到这个结果我才理解,为什么资料里强调“精度暴涨 32.5 个点”而不是说“达到 FP16 水平”,因为它的意义是让 1-bit 从“不可用”变成了“可选方案”,而不是让 1-bit 神奇地超过原模型。
4. 实战调优中的常见坑与排查方法
4.1 校准集一换,精度就打回原形
分离量化非常依赖校准集。第一次实验我用的是纯代码问答数据,残差蒸馏完在代码测试集上分数很高,但一换到通用对话场景立刻掉链子。后来我把校准集改成 70% 通用指令 + 20% 代码 + 10% 数学,并且每次评测都同时跑多个子集,才真正看到模型的整体水准。
检查方法很直接:在看总分的同时,把测试集按子领域拆开看分数变化。如果只在一个领域涨,其他领域全跌,几乎可以断定是过拟合校准集了。
4.2 残差密度太大,性能优势瞬间归零
残差矩阵虽然维度低,但也要占带宽。我把低秩残差维度从 128 调大到 256 时,精度确实又涨了一点点,但速度明显下滑。原因很简单:残差从“少量校正信息”变成“一条不能忽略的数据流”,带宽收益就被蚕食了。
我的建议是残差维度不要超过 128,宁可把残差部分也做一次低比特量化,也不要无限加大它。另外,残差矩阵和主干的访存要连续写,避免 kernel 在循环里频繁跳转内存地址。
4.3 激活量化用绝对最大值会让输出噪声变大
前面说过要尽量用最小化 MSE 选 scale,但这里还有一个坑:激活分布是逐 token 变化的,某些 token 偶尔会冒出一个非常大的离群激活。如果你直接用这个最大值当 scale,那么大部分正常 token 都会被迫压缩到一个很小的整数区间,等于整体精度被一个偶然噪声绑架了。
处理方法是给激活最大值加一个上限,或者用 99.9 分位数替代最大值。实测中,用 99.9 分位数之后,输出 logits 的噪声明显变小,生成文本的连贯性也更好。
4.4 纯 1-bit kernel 在某些卡上比 W4A8 还慢
这不是个例。1-bit 权重打包成 int32 之后,矩阵乘在硬件上需要大量位运算指令,而位运算的吞吐不一定能追平 INT4/INT8 张量核心。如果 GPU 对 INT4/INT8 的加速支持很好,W4A8 往往已经足够快,1-bit 的带宽收益反而不够抵消位运算损失。
所以上分离量化之前,建议先跑一个简单 benchmark:把同样的 matmul 分别用 W4A8 kernel 和 1-bit 位运算 kernel 各执行一万次,对比耗时。如果 1-bit kernel 没有明显优势,说明当前硬件上主干的位运算实现有瓶颈,残差流的复杂度再低也救不回来。
4.5 层间位宽配比不是越多越好
有些人为了让精度更高,把每一层都手动设定成不同位宽,最后效果反而不理想。原因在于层与层之间是串联的,某一层的误差会在后续层被放大或抵消。你没法保证“前面层精度高,后面就一定更好”。
我的经验是:先做全 1-bit 主干基线,然后只把最容易崩的那几层提升到 2-bit。怎么判断哪层容易崩?把量化误差注入到每一层,测量最终输出 logits 的变化幅度,变化最大的前 20% 层优先保留 2-bit。这个筛选过程大概需要多跑三轮校准,但收益比盲目改位宽高得多。
5. 分离量化适合什么场景,什么时候别硬上
5.1 适合:长文本生成、大规模部署、显存受限
如果你在服务一个 7B 以上的模型,生成序列又长,batch 又被显存卡住,分离量化是很好的候选。权重压到 1-bit 后,显存占用大幅下降,原来只能放 2 路并发的显存,现在可以放 4 路甚至更多。长文本生成时 token 数是乘数效应,每一轮 decode 省下的带宽会被放大很多倍。
另外,如果你需要把模型塞进消费级显卡跑本地推理,1-bit 权重是少数几个能真正把 7B 模型压到 2GB 以内的方案。别只看推理吞吐,显存占用本身就能改变你能不能部署的问题。
5.2 不适合:训练阶段、高精度任务、开发周期短的项目
训练阶段完全不要碰这个方案。梯度更新需要高精度浮点,你用 1-bit 权重做前向但梯度还在 FP16,那只是给训练增加了额外复杂度,没有任何收益。
如果你做的是医疗、金融等领域,对输出准确率和可解释性要求极高,分离量化这 32.5 个点的提升依然不够安全。1-bit 主干注定了它丢弃了大量细节,残差只能弥补一部分。这种场景老老实实用 FP16 或 W4A8。
最后提醒一点:分离量化的工程量不小。你需要写融合 kernel、准备校准集、跑蒸馏,整体下来大概需要一到两周。如果项目只有两三天开发时间,直接上成熟的 W4A8 可能是更靠谱的选择,没必要为了那 1.78 倍去挑战工程复杂度。
一些实际操作中的体会
我个人在跑完这轮实验后最大的感受是:1-bit 量化的价值不是“直接二值化”,而是“懂得把什么放在低比特、把什么留在高比特”。权重主体可以粗,scale 和残差必须细;宽泛模块可以粗,敏感层必须细。分离量化本质上是把“量化精度问题”转化成了“资源分配问题”,哪个维度值得开销,就精打细算把开销放在哪里。
如果你们团队也想复现这条路线,我最想分享的一个建议是:先写 kernel 和 benchmark,再做精度调优。因为精度调优的方法论相对成熟,最常见的问题反而是速度根本没提上来,那再高的精度也没有部署价值。我踩过几次坑之后已经养成了习惯,每调优一轮精度,就立刻回去跑一遍速度,确认带宽收益没有被新的操作吃掉。
手里有正在做低比特推理的朋友,可以试试这套分离思路。不一定非要做到 1-bit,哪怕是在 4-bit 方案里引入“主干 + 残差”的拆分,或者把激活量化改成 per-token,都能明显看到质量变化。