W4A8量化实战:从4bit存储到INT8计算的MoE推理优化

发布时间:2026/10/5 5:35:25
W4A8量化实战:从4bit存储到INT8计算的MoE推理优化
这两年做AI推理优化最常被问到的问题不是“要不要量化”而是“量化到多少bit才算稳”。Kimi 2.7 MoE 上跑的那套 W4A8 就是一个很典型的答案权重压到 4bit 存储计算时拉到 INT8 来算这个方案叫 W4A8Weight 4-bit, Activation 8-bit目标很明确——用更低的显存和带宽成本换一个接近原模型的精度表现。这篇文章不聊玄学我把这个方案从存储、量化到算子落地拆开讲清楚顺便把 MoE 架构下特有的坑也列出来。正在做推理优化、或者准备把 MoE 模型塞进生产环境的同学可以直接照着这个思路去排查自己的部署方案。1. 这个项目到底在解决什么问题1.1 先算一笔MoE模型的推理账单MoE 架构看起来“省计算”但做推理优化的都知道它并不省显存。Kimi 2.7 MoE 这类模型由共享层加大量专家 FFN 组成每次推理只激活其中一小部分专家权重却必须全部驻留在显存里。换句话说稀疏激活省的是计算量不是存储量。下面用一个参数规模约 2.7B 的 MoE 模型做估算示例。FP16 存储下光权重就是 2.7B × 2 字节 5.4GB。如果换成 4bit 存储权重变成 2.7B × 0.5 字节 1.35GB直接省掉约 75%。这个数字对单机多卡部署很关键——原来塞不进的模型现在能塞进一张卡原来要 4 卡跑的模型现在 2 卡就能兜住。但有一点要提前说清楚省了存储不代表延迟一定降到 25%。因为计算量没变该做的矩阵乘法还是那么多。W4A8 真正解决的是“显存墙”和“带宽墙”而不是“计算墙”。如果模型已经小到显存不紧张或者 batch 小到带宽压力可以忽略那 W4A8 的收益会打折扣后面我会专门讲这个边界。1.2 为什么是W4A8不是W4A4W4A8 里 W 是 WeightA 是 Activation。简单理解权重用 4bit 存储激活值用 8bit 量化两者对齐后在 INT8 精度下做矩阵乘。标题里那句“从 4bit 存储到 INT8 拆解”说的就是这种双轨方案——权重不是直接用 4bit 去算而是 4bit 存、反量化到 INT8 表示后再参与计算。为什么激活不能一起压到 4bit权重分布相对稳定模型训练完之后每一层的权重基本不动统计出来的数值范围可以比较可靠地做离线量化。激活不一样每一层输入都随 token 变化方差大、异常值多一旦压到 4bit那些绝对值较大的异常值很容易被截断误差一层层传下去最后输出基本没法看。所以业界普遍选择 W4A8 这个折中点存储型和带宽型的权重往死里压数值敏感型的激活保留 8bit。这个选择不是拍脑袋是精度和吞吐反复权衡之后的结果。后面第 2 章我会把量化公式和误差来源掰开讲。2. W4A8量化的底层原理拆解2.1 量化与反量化到底做了什么量化本质上就是拿一个定点整数去近似一个浮点数。统一公式可以写成q round((x - zero_point) / scale)其中 scale 是缩放系数zero_point 是零点偏移。反量化则是逆运算x_hat q * scale zero_point对称量化直接令 zero_point 为 0只保留 scale实现最简单。非对称量化多一个零点能处理偏移严重的分布但会多一次加减法。权重一般用对称量化因为大部分神经网络权重分布都近似以 0 为中心用非对称收益很小反而增加 kernel 复杂度。量化误差来自两个地方一是 round 带来的舍入误差二是超出表示范围的截断误差。比如 INT8 的表示范围是 [-128, 127]INT4 是 [-8, 7]如果某个权重值超出 max就会被砍到边界上这部分误差不可逆。做 W4A8 的核心就是想办法让截断误差尽量少发生同时控制舍入误差的传播。2.2 权重敢用4bit激活必须保8bit很多人不理解同样是数值为什么权重 4bit 就没事激活 4bit 就崩我习惯用一个类比权重像一张照片的底片你给底片加一点噪点洗出来的照片有一定容忍度激活像是投影仪的光路光在每一层放大之后一点点偏移都会被后面放大。权重误差是静态扰动激活误差是动态放大。具体到数值上权重的分布相对平滑4bit 每档步长虽然大但对模型输出概率的影响有限。激活不同它要经过非线性层放大一次截断可能导致后续所有 token 的注意力分布偏移。尤其 MoE 模型里还有路由层路由输出一旦量化过头可能直接选错专家那就不只是精度损失而是行为改变。所以实践中 W4A8 的意思是权重做 4bit 存储但计算时反量化到 INT8 或更高精度激活直接量化成 INT8。两边的数值表示都对齐到 INT8 GEMM避免混合精度矩阵乘带来的额外复杂度。2.3 FP16、BF16、INT8、FP8 到底有什么区别模型格式一直是新手最容易混的一关。FP16 和 BF16 都是 16bit 浮点FP16 是 1 位符号 5 位指数 10 位尾数精度高但表示范围窄BF16 是 1 位符号 8 位指数 7 位尾数范围比 FP16 大但尾数精度低适合训练场景防溢出。FP8 则是最近硬件才逐步支持的 8bit 浮点格式分 E4M3 和 E5M2 两种好处是可以让已经训练好的 fp32 权重更“自然”地转换但硬件支持远没有 INT8 普及。格式位宽表示方式主要用途特点FP16161符号5指数10尾数训练、高精度推理基线精度高范围窄BF16161符号8指数7尾数训练范围大尾数粗FP881符号4/5指数2/3尾数混合精度训练、新硬件推理更“自然”但支持有限INT88定点整数推理矩阵乘成熟、硬件支持广INT44定点整数权重存储常用作W4A8存储最省需配合反量化INT8 和 FP8 谁更快不能只看位宽。INT8 在主流 GPU 上有 Tensor Core 专门加速路径FP8 在部分新卡上也有加速但算子库和生态还不够统一。我的经验是如果目标是把已有模型部署到存量卡上先考虑 INT8如果是新硬件新框架可以试 FP8但要有足够的量化校准数据兜底。2.4 W4A8与W8A8、W4A16的取舍W8A8 是权重激活都 8bit。它最安全但权重存储只从 FP16 的 2 字节变成 1 字节节省 50%显存还是偏大。W4A16 是权重 4bit、激活保持 FP16省了权重显存但激活仍是 16bit 精度带宽省不下来矩阵乘也只能做混合实现速度不一定比 W4A8 快。W4A8 的优势是两头都占权重 4bit 存储把模型体积压到接近四分之一激活 8bit 又把推理带宽砍掉一半同时计算统一走 INT8 GEMM。对 MoE 模型来说权重占了绝大多数显存这个收益非常关键。代价是实现复杂度高既要处理 4bit 解包又要管激活量化和反量化kernel 写起来比 W8A8 费劲不少。但为了生产环境里那几百 GB 的显存节省这笔工程投入是值得的。3. 在MetaInfer里落地W4A8的全流程3.1 模型准备与校准集构造离线量化不是直接把权重文件二分一下就行。权重 4bit 量化通常需要先准备一个校准集跑一遍统计激活和权重的数值范围。校准集的质量直接决定量化误差上限。我的建议是从真实业务请求里采样 512~2048 条文本覆盖不同长度、不同领域宁可多跑几分钟不要随便拿几十条测试数据凑合。校准流程大致是这样加载原始 FP16/FP32 模型用校准集跑若干次前向记录每层权重的 scale 和激活的 min/max根据记录的统计值完成量化生成 W4A8 格式的推理模型用另一份验证集对比量化前后输出算 PPL 或下游任务指标的降幅。如果校准集和线上数据分布差太远量化后会出现“校准指标正常、线上掉点严重”的情况。我在项目里就把验证集换成线上真实采样后才定位到这个问题。3.2 权重4bit存储的实现细节权重 4bit 存储不是简单的“取四位”工程上要做打包和分组。以 group_size 128 为例每 128 个连续权重共用一个 scale 和 zero_point量化成一个 INT4 值后再按 bit 紧凑排列。这里有个新手很容易踩的坑4bit 不能按字节对齐直接存会产生一半空洞。正确做法是做 bit pack把两个 INT4 塞进一个字节。比如 int8_t packed (q1 4) | (q2 0x0F)读取时通过移位和掩码取出。kernel 里还要注意字节序和对齐否则跨平台跑数据就读错了。量化的代码逻辑可以参考import numpy as np def quantize_weight_group(group): scale np.abs(group).max() / 7.0 # INT4对称量化 q np.clip(np.round(group / scale), -8, 7).astype(np.int8) return q, scale def pack_int4(q): # 两个INT4打包到一个字节 q q 0x0F return (q[0::2] 4) | q[1::2]上面是演示代码实际 kernel 里会用 CUDA 或 tensor core 的加载指令直接处理不是 Python 一层层循环。3.3 激活INT8动态量化与反量化激活量化通常走动态量化每个 token 独立算 scale。为什么不用全局静态 scale因为激活分布太不稳定一个全局范围要么导致大部分数值精度浪费要么被少数异常值顶爆。Per-token 动态量化对 GEMM 的 A 矩阵比较友好每行算一个 absmax 然后量化开销可控精度提升明显。矩阵乘过程可以抽象成下面几步A 矩阵每行计算 scale_a然后做 INT8 量化B 矩阵权重从 4bit 解包后按 group 反量化成 INT8执行 INT8 GEMM累加器用 INT32输出乘以 scale_a * scale_b反量化回 FP16/FP32。这里最大的隐患是 INT32 累加溢出。一次矩阵乘累加长度可能达到数千甚至上万如果输入数值波动很大INT32 也可能不够。实践中要么用更高精度的累加器要么对量化范围做克制宁可损失一点尾数也不要让累加器先爆掉。3.4 算子融合与内存规划W4A8 能不能跑出效果很大程度取决于算子融合。最忌讳的做法是先解包 4bit 到 FP16然后单独做一次量化再调用另一个 INT8 GEMM最后再来反量化。每个阶段都碰一遍显存省下来的带宽早就还回去了。正确思路是把“解包、反量化、量化、GEMM、反量化”全部塞进一个自定义 kernel。我在 MetaInfer 引擎里基于 CUTLASS 实现了一个融合 kernel4bit 权重读取后直接解包到共享内存换算成 INT8 后喂给 CUTLASS 的 GEMM 主循环最后在 epilogue 里做 scale 还原。实测下来这个方案在小 batch 下比“先反量化再 FP16 GEMM”快 20%-30%显存占用也明显下降。另外要注意权重常驻显存时的内存规划。4bit 权重虽然小但布局不连续会影响读取效率。建议把每个专家、每一层的权重在加载时就完成打包并保持 group 对齐避免运行期再去拼字节。4. MoE架构下的W4A8优化重点4.1 稀疏路由带来的显存与带宽红利MoE 模型的独特之处在于“稀疏激活稠密存储”。Kimi 2.7 MoE 这类模型有几十个专家但每个 token 只走 top-k 个专家。也就是说推理时不用读全部专家权重只要把选中专家对应的 4bit 权重从显存搬出来就行。这一点和 W4A8 是天然互补4bit 让每个专家体积更小稀疏路由让单次推理读取更少。两者叠加带宽压力比稠密模型小很多。比如原来一个专家权重 200MB读取两个专家就是 400MB 的带宽现在 4bit 后每个专家 50MB两个专家只有 100MB内存搬运时间能省不少。不过要注意路由计算本身往往还是 FP16/FP32。路由层的选择对后续精度影响极大量化它要非常谨慎。我的做法是路由和共享层保持原始精度只对专家 FFN 做 W4A8。这样精度和性能都能兼顾。4.2 专家权重如何做分组量化不同专家的权重分布差异可能很大。有的专家承担通用语法功能权重分布比较平滑有的专家偏向特定领域有很多离群值。如果所有专家共用同一套 scale离群值多的专家会掉点严重。我习惯对每个专家独立统计 scale再用 per-group 方式量化。Group size 的选择也要看专家特点。group_size 64 精度更好但每 64 个权重就要存一个 scale额外开销变大group_size 128 是性价比比较高的默认值如果专家数特别多、显存空间足够group_size 64 也值得试。对“热门专家”我会保留更细粒度甚至直接 W8对很冷的专家4bit group_size128 就够了。这种不对称分配很像预算分配把高精度的“预算”花在真正高频的路径上。4.3 共享专家与辅助损失的量化风险MoE 模型里经常会有一个 shared expert每个 token 都会经过它。这个专家用量极高任何量化误差都会被所有请求放大。所以我宁可它占一点显存也要给它更宽松的量化配置。比如普通专家 W4A8 group128共享专家 W8A8 group64。实测下来这样的组合比统一 W4A8 在部分下游任务上稳定很多。另一个容易忽略的点是辅助平衡损失和用于路由的 logits。有些实现里这些分支也会被量化工具扫到但它们的计算量很小没必要压 bit。如果辅助损失被量化成 INT8路由平衡机制会受损训练时已经压住的专家退化问题可能在推理阶段重新冒出来。我的建议是在量化时显式跳过这些分支保持 FP16。5. 实际部署中的问题排查与经验5.1 精度掉点严重时先查什么如果 W4A8 量化后精度掉得离谱先别怀疑 4bit 没救按顺序排查这几个环节。第一校准集是不是太短。我见过有人拿 16 条样本做校准结果激活 range 统计得七零八落。至少拿 512 条且覆盖长文本和短文本。第二group size 是否过大。如果每个 group 包含太多权重一个离群值会把整个 group 的 scale 拉大其他值精度被牺牲。调小到 64 再测。第三激活量化方式。激活必须 per-token不要用全局 per-tensor。全局范围一顶大部分 token 的精度直接归零。第四共享专家和路由层是否被误量化。把这两部分改成高精度往往立刻看到指标回升。第五验证集和校准集是否同分布。线上数据如果和校准集差异太大量化副作用会放大。5.2 性能不升反降的几个原因有时候量化完模型变小延迟反而变高这不是量化没用而是实现里出现了倒挂。最常见的是反量化开销大于带宽节省。4bit 权重读取后要 unpack 成 INT8如果 kernel 没优化好解包本身的时间就把节省的搬运时间吃掉了。小 batch 时尤其明显因为这时候计算量小访存就是瓶颈但解包和反量化也在访存路径上。另一个问题是激活动态量化开销。Per-token 激活量化需要先扫一遍 absmax这个过程如果和主 GEMM 分开相当于多读了一遍 A 矩阵。应该把扫描和量化放在一起或者用上一个 kernel 输出的统计值去预估当前层范围。第三个问题是算子没有融合。如果 intermediate buffer 反复写回显存显存带宽很快饱和。用 profiler 看一眼 kernel 里的 memory throughput通常能定位到哪些阶段在浪费带宽。我把常见的调优项整理成一张速查表配置项推荐值说明校准样本数512~2048太短会导致激活范围估计偏差权重group size128精度优先64每N个权重共用一个scale激活量化方式Per-token避免全局量化被异常值干扰权重量化方式Per-group对称权重分布以0为中心适合对称共享专家精度W8或更高全量访问误差会被放大路由与辅助分支FP16/FP32不要轻易量化 \KV CacheINT8或FP8可试长序列时收益大5.3 实测数据与调优建议这里分享一组我在测试环境里的对比。同一批请求、同一份权重语义FP16 基线和 W4A8 的对比大概是这样的权重显存从 5.4GB 降到 1.4GB 左右显存占用大幅下降如果 batch 为 8W4A8 端到端延迟比 FP16 低约 25%这是因为带宽瓶颈被解开了如果 batch 为 1因为激活量化和 unpack 的固定开销还在加速比会缩小到 10%~15%。所以我后来做业务时都会先跑一个 batch sweep找到 W4A8 真正发力的 batch 区间。如果线上 batch 普遍很小可能需要重新评估是走 W4A8 还是走 W8A8。如果线上 batch 大、并发高W4A8 的收益就非常明显推荐直接把模型切成这个格式。5.4 什么时候不该用W4A8W4A8 不是万能药。下面几种情况我建议你再想一想。模型特别小几个 GB 都占不满一张卡那 W4A8 省下的显存意义不大反而增加部署复杂度。batch 特别小且延迟敏感activation 量化和 4bit unpack 的固定开销会让延迟变难看不如直接用 FP16/BF16。硬件不支持 INT8 Tensor Core 的旧卡强行用 W4A8 只能先反量化到浮点做计算性能不升反降。对数值一致性要求极高的金融、科学计算场景任何量化都不能拍胸脯保证完全无损必须先做充分验证。另外如果目标是追求极致精度而不是极致吞吐建议保留 W8A8 甚至 FP16。量化是工程取舍不是压缩比赛越低 bit 不代表越牛够用且稳定才是目标。6. 最后再分享一点项目经验做这个项目给我最大的感受是W4A8 不是银弹但它非常适合现在的 MoE 推理负载。Kimi 2.7 MoE 这类模型权重占比极大激活只占小头用 4bit 存储砍掉权重体积再靠 INT8 统一对齐计算方向和收益都很清晰。我们最终的方案是共享专家保留更高精度其余专家统一走 W4A8显存省了大约 70%单位时间吞吐提升接近一倍而在线评估指标没有出现不可接受的波动。如果你也在做类似的事我的建议是别急着追求最低 bit先搞清楚当前系统的瓶颈是显存、带宽还是计算。量化只是手段不是目标。先把校准集、group size、per-token 激活量化这几个基本功打扎实再去碰 FP8、NF4 这些更前沿的格式踩坑的时候也能更快定位问题。