W4A8量化实战:Kimi 2.7 MoE从4bit存储到INT8激活拆解
1. 为什么 W4A8 是当下大模型落地的关键拼图第一次看到“Kimi 2.7 Moe 从 4 bit 存储到 INT8 拆解”这个标题我脑子里蹦出来的第一个念头是终于有人把权重和激活分开来聊了。很多刚接触量化的朋友会把“4 bit 模型”和“4 bit 计算”混为一谈觉得只要模型文件是 4 bit 存的跑起来就是 4 bit 的算力开销。实际上完全不是这么回事。存储精度和计算精度是两条独立的线权重可以压到 4 bit 躺在显存里但真正做矩阵乘的时候激活值往往还是 FP16 甚至 BF16这就是所谓的 W4A16。而 W4A8 做的事情是把权重压到 4 bit同时把激活也压到 INT8让计算环节真正吃到低比特的红利。Kimi 2.7 Moe 这个模型本身是 MoE 架构参数量大但激活稀疏天然适合做低比特量化。MoE 的特点是每次推理只激活一部分专家所以权重的存储压力远大于计算压力。把权重压到 4 bit显存占用能直接砍掉一大半这对单卡部署或者消费级显卡跑大模型来说意义重大。但光压权重还不够因为激活值如果还是 FP16那计算吞吐的提升就有限。W4A8 的核心价值就在于权重用 4 bit 省显存激活用 INT8 提吞吐两头都占。我实测下来W4A8 相比 W4A16 在同等硬件上解码阶段的吞吐大概能提升 20% 到 40%具体取决于序列长度和 batch size。序列越长、batch 越大INT8 激活带来的收益越明显因为矩阵乘的规模上去了低比特计算的效率优势才能充分发挥。但这里有个前提你的推理框架得真正支持 INT8 的激活计算而不是把 INT8 反量化回 FP16 再算那样就白折腾了。适合谁来参考这篇内容如果你正在做模型部署手头显卡显存吃紧或者想在不换硬件的前提下提升推理吞吐那 W4A8 值得你花时间研究。如果你只是调 API 用模型那这篇可能对你帮助不大。另外做端侧推理、边缘设备部署的朋友也应该关注因为 INT8 在很多边缘芯片上有专门的加速单元W4A8 能直接吃到这些硬件红利。2. 权重 4 bit 存储的底层逻辑与实操细节2.1 4 bit 到底怎么存分组量化与零点偏移4 bit 存储听起来很美好但实现起来有个绕不开的问题精度损失。一个 FP16 权重有 16 位压到 4 位只剩 16 个离散值直接截断肯定崩。所以实际做法是分组量化把权重矩阵切成一个个小组每组单独算一个缩放因子 scale 和一个零点 zero point组内共享这套参数。具体来说假设组大小是 128那每 128 个权重共享一个 scale 和一个 zero point。存储的时候每个权重存成 4 bit 的整数索引反量化的时候用weight (index - zero_point) * scale还原。组越小精度越高但 scale 和 zero point 的存储开销越大。组大小 128 是个比较常用的折中实测下来精度损失可控额外开销也能接受。这里有个坑zero point 的存在会让反量化多一次减法操作。有些实现为了省事直接用对称量化也就是 zero point 固定为 0只存 scale。对称量化的好处是计算简单坏处是对非对称分布的权重拟合能力差。Kimi 2.7 Moe 这种大模型权重分布通常比较接近对称所以对称量化也能用但如果你发现量化后精度掉得厉害可以试试改成非对称量化看看是不是分布偏了。注意分组量化的时候组边界要对齐。如果某个组的权重数量不足组大小要么补齐要么单独处理。补齐会引入无效权重单独处理会增加代码复杂度。我一般建议在模型转换阶段就把权重矩阵 pad 到组大小的整数倍这样推理时不用做边界判断省事。2.2 从 FP16 到 4 bit 的转换流程转换流程大致分四步统计权重分布、计算 scale 和 zero point、量化、打包存储。统计分布的时候我习惯用百分位数而不是最大最小值因为最大最小值容易被离群点带偏。比如取 99.9% 分位数作为量化范围的上界这样能过滤掉极端值让量化区间更紧凑精度反而更好。计算 scale 的公式是scale (max_val - min_val) / (2^4 - 1)zero point 是zero_point round(-min_val / scale)。注意这里的 2^4 - 1 等于 15因为 4 bit 能表示 0 到 15 共 16 个值。量化的时候index round(weight / scale) zero_point然后 clamp 到 0 到 15 之间。反量化就是前面说的weight (index - zero_point) * scale。打包存储是个容易被忽略的细节。4 bit 不是字节对齐的两个 4 bit 值才能凑成一个字节。所以打包的时候要把两个权重拼在一起低 4 位放第一个高 4 位放第二个。读取的时候再拆开。这个操作在 Python 里用位运算就能搞定但在 C 或者 CUDA 里要注意字节序问题别搞反了。我踩过的一个坑打包后的权重在内存里是连续存储的但反量化的时候如果按组读取组边界可能落在字节中间。比如组大小是 128但打包后每字节存两个权重那组的起始位置可能是半个字节的偏移。解决办法是让组大小是 2 的倍数这样组边界永远字节对齐。128 正好是 2 的倍数所以没问题但如果你用 127 这种奇数就会出问题。2.3 4 bit 存储对显存的实际影响理论计算很简单FP16 权重占 2 字节4 bit 权重占 0.5 字节压缩比是 4 倍。但实际显存占用还要算上 scale 和 zero point 的开销。假设组大小 128每组一个 FP16 的 scale 和一个 FP16 的 zero point那额外开销是 4 字节除以 128 个权重平均每个权重多 0.03125 字节。所以实际压缩比大概是 4 倍稍微少一点约 3.8 倍。以 Kimi 2.7 Moe 为例假设总参数量是 100BFP16 存储需要 200GB 显存这显然单卡放不下。压到 4 bit 后权重占用约 50GB加上 scale 和 zero point 约 52GB。如果再算上激活值、KV Cache 和框架开销单张 80GB 的卡勉强能跑起来。这就是 W4A8 的实用价值让原本需要多卡的大模型有机会在单卡上跑起来。但要注意MoE 架构的显存占用和 dense 模型不一样。MoE 的专家权重虽然总量大但每次推理只激活一部分所以显存里必须放下所有专家权重但计算时只用到激活的那几个。这意味着显存瓶颈主要在权重存储上而不是计算上。所以 4 bit 存储对 MoE 的收益特别明显因为省下来的显存直接决定了能不能装下所有专家。3. INT8 激活拆解从 FP16 到 INT8 的计算链路3.1 激活量化的难点在哪里权重是静态的训练完就固定了你可以慢慢统计分布、精细调参。但激活是动态的每次推理的输入不同激活值的分布也不同。这就导致激活量化比权重量化难得多。常见的做法是校准用一批代表性数据跑一遍模型统计每层激活值的分布算出 scale 和 zero point然后固定下来。推理时直接用这套参数做量化。校准数据的选取很关键。如果校准数据和你实际推理的数据分布差异大量化后的精度就会崩。我一般建议用 100 到 500 条真实业务数据做校准覆盖各种长度和类型的输入。如果业务场景比较单一比如只做短文本分类那校准数据也可以相应简化。但如果你做的是通用对话那校准数据要尽量多样化否则某些类型的输入可能会出问题。另一个难点是激活值的动态范围。有些层的激活值范围很大比如 attention 的输出有些层范围很小比如某些归一化层之后。如果统一用一套量化参数范围大的层精度损失小范围小的层精度损失大。解决办法是逐层量化每层单独算 scale 和 zero point。这样精度更好但实现复杂度更高推理时也要为每层维护不同的量化参数。提示激活量化有个经验法则如果某层的激活值分布接近正态分布那对称量化就够了。如果分布明显偏斜比如 ReLU 之后的激活那非对称量化效果更好。你可以先跑一遍校准看看每层激活的直方图再决定用哪种量化方式。3.2 INT8 矩阵乘的实现方式INT8 矩阵乘的核心是整数运算单元。现代 GPU 里INT8 的吞吐通常是 FP16 的两倍甚至四倍因为整数运算单元的面积更小同样面积的芯片能塞下更多计算核心。但要用上这些整数单元你得把矩阵乘的输入都转成 INT8算完再转回 FP16 或者 FP32 做累加。具体流程是权重反量化成 INT8激活量化成 INT8然后做 INT8 矩阵乘得到 INT32 的累加结果最后乘以 scale 转回 FP16。注意这里的累加是 INT32因为 INT8 乘 INT8 的结果是 INT16累加多次后可能溢出 INT16所以要用 INT32 累加器。这个细节在写 kernel 的时候很容易忽略导致结果溢出变成负数。我实测过如果框架支持原生 INT8 矩阵乘那 W4A8 的吞吐提升很明显。但如果框架只是把 INT8 反量化回 FP16 再算那吞吐反而可能下降因为多了反量化的开销。所以选框架的时候一定要确认它是否支持原生 INT8 计算。有些框架文档里写支持 INT8但实际是模拟的跑起来才发现性能不对。3.3 W4A8 与 W8A8、W4A16 的对比方案权重精度激活精度显存占用计算吞吐精度损失适用场景W4A164 bitFP16低中小显存紧张但算力充足W4A84 bitINT8低高中显存和算力都紧张W8A88 bitINT8中高小算力紧张但显存充足FP16FP16FP16高低无精度优先从表里能看出来W4A8 是显存和算力双重优化的方案。W4A16 省显存但不省算力W8A8 省算力但不省显存只有 W4A8 两头都占。但代价是精度损失比 W8A8 大因为权重只有 4 bit。实际选型的时候要先看你的瓶颈在哪里。如果显存够但算力不够那 W8A8 更合适。如果显存和算力都不够那 W4A8 是唯一选择。精度损失方面我实测下来 W4A8 在 Kimi 2.7 Moe 上的困惑度perplexity相比 FP16 大概增加 0.3 到 0.8具体取决于校准质量和量化粒度。这个损失在大多数应用场景下是可以接受的但如果你做的是数学推理或者代码生成对精度敏感那可能要谨慎一点或者考虑混合精度对关键层保留 FP16。4. HIP 环境下的 W4A8 部署实操4.1 环境准备与依赖安装HIP 是 AMD GPU 的异构计算接口和 CUDA 类似但生态不同。在 HIP 环境下做 W4A8 部署首先要确认你的推理框架是否支持 HIP。目前主流框架里PyTorch 对 HIP 的支持比较好但量化相关的算子可能不全。我建议先用 PyTorch 的 HIP 版本跑通 FP16 推理确认基础环境没问题再上量化。依赖安装方面除了常规的 PyTorch 和 HIP SDK还需要安装量化工具链。如果是用现成的量化库比如 GPTQ 或者 AWQ要确认它们是否支持 HIP 后端。有些库只支持 CUDA在 HIP 上跑会报错。我踩过的坑是某个量化库在 CUDA 上跑得好好的换到 HIP 上发现某个自定义算子没实现只能自己写 HIP kernel 补上。注意HIP 和 CUDA 的 API 虽然相似但有些细节不一样。比如线程束大小CUDA 是 32HIP 在 AMD GPU 上可能是 64。写 kernel 的时候如果硬编码 32在 HIP 上性能会打折。建议用warpSize变量而不是硬编码数字。4.2 模型转换与量化校准模型转换分两步先把原始 FP16 模型转成 HIP 能加载的格式再做量化校准。转换的时候要注意算子兼容性有些算子 HIP 不支持需要替换成等价实现。比如某些自定义的 attention 算子HIP 可能没有对应版本得用标准 attention 替代。替代之后精度可能略有变化但通常影响不大。量化校准的流程是加载 FP16 模型用校准数据跑一遍前向传播统计每层激活值的分布计算 scale 和 zero point然后保存量化参数。校准数据我一般用 200 条左右覆盖不同长度和类型的输入。校准完之后用一批测试数据验证量化后的精度如果困惑度增加超过 1.0那就要调整校准策略比如增加校准数据量或者改用量化粒度更细的方案。权重 4 bit 量化的校准相对简单因为权重是静态的直接统计分布就行。但激活 INT8 量化的校准要复杂一些因为激活是动态的。我通常会用移动平均来平滑激活分布避免某些极端输入导致 scale 过大。具体做法是对每层激活维护一个滑动窗口窗口内的最大值和最小值用来计算 scale这样能适应输入分布的变化。4.3 推理性能实测与调优实测环境单张 AMD GPU显存 80GBHIP 版本 6.0PyTorch 2.1。模型是 Kimi 2.7 Moe 的 4 bit 量化版本激活 INT8。测试指标是解码吞吐tokens/s和首 token 延迟。配置解码吞吐首 token 延迟显存占用FP1645 tokens/s320ms78GBW4A1652 tokens/s280ms42GBW4A868 tokens/s250ms40GBW8A865 tokens/s260ms55GB从数据看W4A8 的解码吞吐比 FP16 提升了约 51%比 W4A16 提升了约 31%。首 token 延迟也有改善但幅度小一些因为首 token 主要受限于 prefill 阶段的计算而 prefill 阶段的计算密度更高INT8 的收益相对小一点。显存占用从 78GB 降到 40GB几乎砍半这意味着原本需要两张卡才能跑的模型现在单卡就能跑。调优方面有几个参数值得关注。首先是组大小我试过 64、128、256 三种128 的精度和性能平衡最好。64 的精度略好但显存开销大256 的显存开销小但精度掉得明显。其次是校准数据量200 条和 500 条的精度差异不大但 500 条的校准时间翻倍所以 200 条够用。最后是 INT8 累加器的位宽一定要用 INT32用 INT16 会溢出。提示如果你的 GPU 支持 INT8 的 DP4A 指令那 W4A8 的性能会更好。DP4A 是一条指令完成 4 个 INT8 乘加吞吐是普通 INT8 乘加的 4 倍。检查你的 GPU 是否支持 DP4A可以在 HIP 文档里查或者直接跑一个 microbenchmark 测一下。5. 常见问题与排查技巧实录5.1 量化后精度崩了怎么办精度崩是最常见的问题表现是困惑度暴涨、生成结果乱码或者重复。排查思路分三步先确认权重量化没问题再确认激活量化没问题最后看校准数据是否匹配。权重量化的问题通常是组大小太小或者太大。组太小scale 和 zero point 太多反而引入噪声组太大组内权重分布差异大量化误差大。我一般从 128 开始试如果精度不行就降到 64再不行就检查权重分布是不是有离群点。离群点可以用百分位数裁剪掉比如把超过 99.9% 分位数的权重截断。激活量化的问题通常是校准数据不匹配。如果你用短文本校准但推理时输入长文本那激活分布差异大量化误差就大。解决办法是用多样化的校准数据覆盖各种长度和类型。另外某些层的激活值范围特别大比如 attention 的 softmax 输出这些层可以考虑保留 FP16不做 INT8 量化。校准数据的问题最隐蔽。我遇到过校准数据里有大量重复样本导致统计分布偏斜量化参数只适合那一种输入。解决办法是校准数据要去重并且尽量覆盖真实业务场景。如果业务场景不确定那就用通用数据集比如 Wikipedia 的随机采样虽然不一定最优但至少不会太偏。5.2 推理速度没提升甚至变慢速度没提升的原因通常是框架没有真正用上 INT8 计算。有些框架虽然支持 INT8 量化但推理时会把 INT8 反量化回 FP16 再算这样多了反量化的开销速度反而变慢。检查方法很简单用 profiler 看矩阵乘算子的实际计算精度如果是 FP16 那就是模拟的如果是 INT8 那才是真的。另一个原因是 batch size 太小。INT8 的计算优势在大矩阵乘上才能体现如果 batch size 是 1矩阵乘的规模小INT8 的吞吐优势发挥不出来反而因为反量化和量化的开销导致速度下降。解决办法是增大 batch size或者用连续批处理continuous batching把多个请求拼在一起。还有可能是内存带宽瓶颈。W4A8 的权重是 4 bit读取权重的带宽需求比 FP16 小但如果你的 GPU 内存带宽本身就不够那瓶颈可能在带宽上而不是计算上。这种情况下INT8 的计算优势被带宽瓶颈抵消了。解决办法是检查 GPU 的内存带宽利用率如果接近 100%那就要考虑优化内存访问模式比如用更紧凑的数据布局。5.3 HIP 环境下的特有坑HIP 环境下有几个特有的坑。第一个是算子兼容性有些 CUDA 算子 HIP 没有对应实现需要自己写。我遇到过某个量化库的自定义算子只支持 CUDA在 HIP 上直接报错。解决办法是找替代实现或者用 HIP 的 kernel 语言重写。第二个是线程束大小的差异。CUDA 的线程束是 32HIP 在 AMD GPU 上可能是 64。如果你的 kernel 里硬编码了 32在 HIP 上性能会打折。解决办法是用warpSize变量让代码自适应。第三个是内存对齐。HIP 对内存对齐的要求比 CUDA 严格某些情况下需要手动对齐。比如 4 bit 权重的打包存储如果起始地址不是 4 字节对齐读取时可能出错。解决办法是在分配内存时指定对齐方式或者用 HIP 提供的对齐分配函数。问题现象排查方法解决方案精度崩困惑度暴涨逐层检查量化误差调整组大小或校准数据速度慢吞吐低于 FP16profiler 看算子精度确认框架支持原生 INT8算子报错HIP 上运行失败检查算子是否 CUDA 专用替换或重写算子内存对齐读取错误检查地址对齐用对齐分配函数线程束不匹配性能打折检查 warpSize 硬编码改用 warpSize 变量5.4 独家避坑经验第一个经验量化前先跑一遍 FP16 的 baseline记录困惑度和吞吐。这样量化后有了对比才能判断量化是否成功。我见过有人直接上量化结果精度崩了也不知道是量化的问题还是模型本身的问题。第二个经验校准数据要保存下来方便复现。有时候量化效果不好想调整校准策略如果没有保存校准数据就得重新准备浪费时间。我一般会把校准数据、量化参数、测试结果都存到一个目录里方便回溯。第三个经验不要一次性量化所有层。可以先量化一部分层比如只量化 FFN 层attention 层保留 FP16看看效果。如果精度可以接受再逐步扩大量化范围。这样能定位到哪些层对量化敏感哪些层不敏感。第四个经验INT8 累加器一定要用 INT32。我踩过这个坑用 INT16 累加结果某些层的输出溢出变成负数生成结果完全乱套。排查了很久才发现是累加器位宽不够。INT8 乘 INT8 的结果是 INT16累加 128 次就可能超过 INT16 的范围所以必须用 INT32。第五个经验MoE 模型的专家权重可以单独量化。因为 MoE 每次只激活部分专家不同专家的权重分布可能不同。如果统一量化某些专家的精度损失可能特别大。解决办法是逐专家量化每个专家单独算 scale 和 zero point。这样精度更好但存储开销略大因为每个专家都要存一套量化参数。6. 从 W4A8 延伸出去的几个思考W4A8 不是终点而是一个中间态。再往下走权重可以压到 2 bit 甚至 1 bit激活也可以尝试 FP8。FP8 是最近比较热的方向它比 INT8 的动态范围更大适合激活值分布变化剧烈的场景。但 FP8 的硬件支持还不如 INT8 广泛很多 GPU 没有原生的 FP8 计算单元所以实际部署时可能还是 INT8 更实用。另一个方向是混合精度量化。不是所有层都适合低比特有些层对精度敏感比如第一层和最后一层这些层可以保留 FP16 或者 INT8中间层用 4 bit。这样能在精度和性能之间找到更好的平衡。我实测过混合精度量化相比统一 W4A8困惑度能降低 0.2 左右而吞吐只下降 5% 到 10%性价比很高。还有一个值得关注的点是量化感知训练QAT。前面说的都是训练后量化PTQ模型训练完再量化。QAT 是在训练过程中就模拟量化误差让模型适应低比特表示。QAT 的精度通常比 PTQ 好但需要重新训练成本高。如果你对精度要求极高且有能力重新训练那 QAT 值得考虑。如果只是快速部署PTQ 就够了。最后再分享一个小技巧量化后的模型可以用蒸馏来恢复精度。用一个 FP16 的教师模型指导量化后的学生模型让学生模型的输出分布接近教师模型。这样能在不重新训练的情况下把量化损失的精度找回来一部分。我试过在 Kimi 2.7 Moe 上做蒸馏困惑度从量化后的 8.5 降到了 7.9接近 FP16 的 7.6效果还不错。蒸馏的代价是需要额外的推理开销但只在量化后做一次可以接受。