1. 为什么大家都在聊 W4A8:从一次群聊争论说起
事情是这样的,前阵子群里有人甩了张截图,说某个开源 MoE 模型号称 “4 bit 量化”,结果一加载显存占用还是高得吓人。底下立刻吵成一团:有人说 4 bit 就该省 75% 显存,有人说 MoE 架构根本不可能全部量化,还有人搬出 INT8、FP8 开始互怼。其实两边说的都对,只是把两个完全不同的概念混在一起了——存储精度和计算精度,根本是两码事。
现在很多大模型走的是 MoE 架构,比如 Kimi 相关的 K2 系列、K3 系列,还有那一堆 MoE 开源模型,业内聊得最多的就是“4 bit 存储 + INT8 计算”这种组合,也就是标题里写的 W4A8。W 是 Weight(权重),A 是 Activation(激活值),W4A8 的意思是权重用 4 bit 存、用 8 bit 算,激活值直接用 8 bit。这个组合这几年几乎成了推理优化的标准套餐,原因很简单:权重占显存大头,用 4 bit 存能大幅压显存;计算时把 4 bit 反量化回 INT8 做矩阵乘,速度和精度都能兼顾。
这篇文章我想从头到尾把这套东西掰开揉碎讲清楚:为什么要用 W4A8、MoE 架构对显存到底有什么特殊要求、4 bit 到 INT8 的完整拆解流程是怎么走的、实测会踩哪些坑,以及和 FP8、FP16、BF16 这些精度的对比到底差在哪。不管你是在做推理服务压测、微调完模型准备部署,还是单纯好奇量化原理,这篇文章都值得看完。
先说结论:MoE 架构的显存问题,本质上不是“全部参数进显存”这一个问题,而是“怎么让不用的专家不出现在显存里”的问题。下面我们从 MoE 的结构开始,一层层往里拆。
2. MoE 架构的显存迷思:为什么“全部参数进显存”这个问法不准确
2.1 MoE 到底长什么样
很多人第一次接触 MoE(Mixture of Experts,专家混合)都有一个错觉:它就是个更大的稠密模型。实际上 MoE 的参数量分两部分:共享部分和专家部分。共享部分包括 embedding、attention、layer norm 这些每个 token 都必须过的层;专家部分则是几十个甚至上百个并行的 FFN 前馈网络。
推理时,每个 token 只会激活其中 2~4 个专家(具体看 topk 怎么设),但关键是:模型加载时,所有专家的权重都得读进显存,否则 token 被路由到某个专家时你根本没有参数可用。这就是“MoE 架构要全部参数进显存吗”这个热词背后真正想问的东西——加载阶段确实必须全部进显存,计算阶段才只需要一部分。
我打个比方方便理解:MoE 模型就像一个大图书馆,读者(token)每次来只借 2~3 本特定主题的书(专家),但图书馆的藏书(全部专家参数)必须摆在书架上,你总不能在读者要借的时候才去印书。所以 MoE 推理的显存压力主要在“藏书规模”,不在“单次阅读量”。
2.2 KMoe、Kimi K2 这类模型的显存到底怎么算
从热词里能看到 Kimi 网页版、Kimi K2、K3、K4 这些频繁出现,还有“kimi work 使用经验”“豆包和 kimi 哪个更占内存”这类实际问题。这里我不讨论具体某个版本的非公开参数,就说这类 MoE 模型的通用显存估算逻辑。
一个参数量为 P 的模型,权重用 b bit 存储,显存占比就是 P × b / 8 字节。比如一个 100B 参数的 MoE,如果全程 FP16(16 bit)存权重,那就是 100B × 2 = 200GB;如果权重变成 4 bit,就是 100B × 0.5 = 50GB。这就是为什么业界拼命上 4 bit——一个 100B 级模型,50GB 的权重显存意味着单张 80GB 的卡能挤一挤放进去,200GB 的话必须上多卡或大内存机器。
但注意,这只是权重的显存。推理时还要算 KV cache、激活值、以及反量化过程的中间张量。KV cache 的大小跟序列长度成正比,跟 batch size 成正比,跟注意力头数和层数成正比。所以实际部署时,显存规划要算三块:权重显存 + KV cache 显存 + 运行时激活缓冲。很多人只算第一块,结果上线就 OOM。
2.3 “全部参数进显存”问题的正解
回到“moe 架构要全部参数进显存吗”这个热搜,准确回答分两层:
- 加载层面:所有专家参数必须全部加载进显存,否则推理引擎无法动态路由。即便某个专家当前一个 token 都没激活,它的权重也在显存里占着位置。
- 计算层面:单个 token 的前向只走共享层 + 被激活的 2~4 个专家,计算量远小于稠密模型全量计算。
这带来了一个优化空间:如果能把暂时不用的专家从显存“换出去”,就能突破单卡物理显存上限。这就是业界在做的 expert offload(专家卸载)和 expert swapping,配合 4 bit 存储可以把冷专家压缩后再放内存或 SSD,用到时再换回显存。我在后面第 4 节还会详细展开这块。
3. 4 bit 存储与 INT8 计算:拆开看 W4A8 的每个环节
3.1 权重存储精度和计算精度为什么能分开
这个点是整个 W4A8 的基石。很多人不理解:权重既然都存成 4 bit 了,怎么计算的时候又变 INT8 了?这不矛盾吗?
不矛盾。量化分为“存储时的量化”和“计算时的量化”。模型的权重在加载时是 FP16 或 FP32 的原始精度,为了让显存更小、加载更快,我们把每个权重数值映射到 4 bit 的整数区间,存到显存或磁盘里。真正做矩阵乘的时候,引擎会先把 4 bit 权重反量化(dequant)回 INT8,然后和同样是 INT8 的激活值做整数乘法累加。这样做的本质是:存储省空间,计算省功耗和时间,两头好处都占。
类比一下:你有一堆纸质合同要归档,为了节省仓库空间,你先把合同扫描成低分辨率图片存起来(4 bit 存储);但真要阅读合同时,你不会拿低分辨率图片直接辨认,而是调出原始扫描件或清晰版本来看(反量化到 INT8 再算)。存储用压缩格式,计算用适合处理的格式,两者完全可以分离。
3.2 W4A8 相比 W4A16、W8A8、FP16 的取舍
现在业内常见的量化配置有这么几档:
| 配置 | 权重存储 | 权重计算 | 激活计算 | 典型场景 |
|---|---|---|---|---|
| FP16/BF16 | 16 bit | 16 bit | 16 bit | 基准精度,显存占用最大 |
| W8A8 | 8 bit | 8 bit | 8 bit | 通用 INT8 推理,精度损失小 |
| W4A16 | 4 bit | 16 bit | 16 bit | 老式 4 bit 方案,计算仍是 FP16 |
| W4A8 | 4 bit | 8 bit | 8 bit | 显存极致压缩 + INT8 加速,推荐 |
| FP8 | 8 bit | 8 bit | 8 bit | 新一代硬件原生支持,需硬件适配 |
| W4A4 | 4 bit | 4 bit | 4 bit | 极限压缩,精度风险高,不推荐日常用 |
W4A16 是最早的 4 bit 方案,那会儿硬件没有 INT8 张量核心或者软件栈不成熟,权重虽然存成 4 bit,但计算前反量化回 FP16 再算,所以显存省了但速度没提升多少。W8A8 是均匀 8 bit,计算效率高但显存只省一半。W4A8 是两者的结合:存储做到 4 bit 极致压缩,计算用 INT8,能同时吃到“省显存”和“快计算”两份红利。
FP8 是另一个热门方向。它本质上是 8 bit 浮点,不像 INT8 是整数。FP8 的优点是动态范围比 INT8 大,量化精度损失更小;缺点是它对硬件有要求,需要支持 FP8 张量核心(比如 H100 之后的卡),而 INT8 在很多消费级显卡和工作站的 Tensor Core 上早就支持得很好。所以 W4A8 的普适性更好。
3.3 4 bit 量化到底是怎么把 FP16 压下去的
这里我给出一个典型的 4 bit 量化完整流程,核心是 per-channel 或 per-group 的 scaling factor(缩放因子)。以 per-channel 为例:
- 取某一层权重矩阵 W,形状是 [out, in],也就是输出通道 × 输入通道。
- 对每个输出通道(每行),计算该行权重的绝对最大值 amax。
- 每个通道分配一个缩放因子 scale = amax / 7(因为 4 bit 符号整数范围是 [-8, 7],非对称量化范围是 0~15,符号量化一般用 [-8, 7])。
- 把该通道每个权重除以 scale,取整并 clamp 到 [-8, 7],得到量化后的整数数组。
- 反量化时:q × scale 即还原近似值。
存储时只存整数 q 和每个通道的 scale,scale 可以是 FP16 或 FP32,数量远小于权重数量,占不了多少空间。之所以要用 per-channel 或 per-group 而不是全局一个 scale,是因为不同通道的权重分布差异可能很大,全局 scale 会让数值小的通道量化误差巨大。
注意:4 bit 量化通常不做 symmetric 和 asymmetric 的机械选择。像 GPTQ、AWQ 这类主流算法都用了 per-group + 各种优化策略,比如 AWQ 会基于激活值分布选择最敏感的通道保留更高精度。这些细节决定了 4 bit 模型到底能保住多少智商。
3.4 从 4 bit 到 INT8 的反量化与计算路径
了解了存储,再看计算路径。一次 W4A8 的线性层前向大致是这样的:
- 从显存读取 4 bit 权重 q_w(它打包在 int32 里,一个 int32 能存 8 个 4 bit 值)。
- 用预先算好的缩放因子 scale_w 做反量化:q_w × scale_w 得到 INT8 权重(实际上很多实现是直接量化为 INT8 再计算,避免中间 FP 转换)。
- 激活值 x 也从 FP16/BF16 量化为 INT8 q_x,并记录激活 scale_x。
- 执行 INT8 矩阵乘:y_int = q_x × q_w,Tensor Core 并行执行整数乘加。
- 输出 y = y_int × scale_x × scale_w,回写到 FP16/BF16。
这里最关键的优化是:4 bit 权重的反量化结果先调整到适配 INT8 的表示范围,让后续矩阵乘全程整数运算。有的实现使用 “double quantization” 或 “mixed precision decomposition”,比如把 4 bit 权重拆成高 4 bit 和低 4 bit 两个 INT8 分支分别乘,最后加权合并,这样既保持 INT8 的计算流水线,又不损失 4 bit 低位的信息。
这一整套流程看起来复杂,好在有成熟的推理引擎帮你封装好了。接下来我们看看实际部署时怎么选工具、怎么配置参数。
4. 实操:从 4 bit 存储到 INT8 计算的完整落地流程
4.1 工具选型:vLLM、SGLang、llama.cpp 怎么选
热词里出现了 onnx 量化 int8、int8 量化、kimi code 设置自动、kimi work 使用经验等,说明不少人在真实环境里折腾过部署和量化。结合我的经验,按场景选工具:
| 工具 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| vLLM | MoE 支持成熟、PagedAttention 高效、吞吐高 | 显存优化偏重 KV cache,量化支持依赖后端 | 线上高并发推理服务 |
| SGLang | 调度灵活,RadixAttention 缓存,结构化输出有优势 | 社区相对 vLLM 小、更新快导致文档跟不上 | 高吞吐 + 复杂 prompt 前缀复用场景 |
| llama.cpp | 单机本地部署最简单、CPU/GPU 混合推理稳 | 吞吐和服务化能力弱,大规模并发不划算 | 本地调试、单用户工具、边缘设备 |
| TensorRT-LLM | 性能极致,A100/H100 上最优 | 配置复杂、编译时间长,新手劝退 | 生产集群、追求极限吞吐 |
我个人在实际部署 MoE 大模型时,主流还是 vLLM,因为它对 MoE 的路由、专家并行、KV cache 管理都封装得很好。如果你只是自己电脑上跑跑、验证效果,llama.cpp 最省心,装完导个 GGUF 就能用。不过 llama.cpp 对 MoE 的多卡拆分支持不如 vLLM 方便,超过单卡建议直接上 vLLM。
4.2 vLLM 里配置 W4A8 的完整步骤
假设你已经有一个 HuggingFace 格式的模型,且已经用 GPTQ 或 AWQ 量化成 4 bit 权重(这一步我下面单独讲)。用 vLLM 拉起 W4A8 推理服务的典型步骤:
- 安装依赖:
pip install vllm(推荐源码安装以便对齐 CUDA 版本),确保 CUDA 12.x、驱动够新。 - 编写启动脚本,核心参数如下:
- 检查启动日志里的
quantization字段,确认是否识别为gptq或awq,同时看显存占用是否符合预期。 - 用
curl或openai客户端发几个 prompt 做烟雾测试,观察首 token 延迟和吞吐。
# 伪代码示例:vLLM 核心配置 from vllm import LLM, SamplingParams llm = LLM( model="/data/models/kimi-moe-w4a8", quantization="gptq", # 或 awq dtype="auto", # 让引擎自动选计算精度 tensor_parallel_size=2, # 超过单卡就多卡张量并行 gpu_memory_utilization=0.85,# 预留 15% 给激活和调度 max_model_len=8192, enforce_eager=False, # 用 CUDA graph 加速 )这里重点说几个参数的实际含义。tensor_parallel_size对 MoE 特别重要,因为专家层天然可以切到不同卡上,负载均衡做得好整个前向就更快。gpu_memory_utilization=0.85表示允许引擎用掉 85% 显存,剩下的留给 CUDA context、torch 缓存等。如果 OOM,先把这值降到 0.7,再不行就检查是不是 KV cache 占太多。
注意:vLLM 启动后显存不会立刻全占满,KV cache 是动态增长的。你可以在
vllm serve的 metrics 里看到cache_usage指标,长期接近 100% 说明要调低max_num_seqs或max_model_len。
4.3 模型量化:GPTQ vs AWQ vs 直接 ONNX INT8
很多教程直接把“量化”一带而过,实际上量化算法选错,后面全白搭。我一直强调:4 bit 存储只是目标,量化算法决定了 4 bit 到底能保住多少模型智商。
- GPTQ(经典方案):基于 OBS(Optimal Brain Surgeon)思路,逐层最小化量化误差。它对权重分布较均匀的模型效果好,速度适中。
- AWQ(激活感知量化):基于激活值分布找出重要通道,不更新权重,只做缩放保护。对 LLM 生成质量更友好,我实测 AWQ 在相同 4 bit 下往往比 GPTQ 少损失一点精度,特别是在长文本任务上。
- ONNX Runtime INT8 量化:主要面向传统 CNN 或中等规模模型,配合 ONNX Runtime 的 QDQ 格式做静态/动态量化。如果你要部署的是 ONNX 模型,且主要跑 CPU 或边缘设备,那就走这条线。
具体到 MoE 大模型,我更推荐 AWQ 或 GPTQ。量化的完整步骤以 AWQ 为例:
# 安装 AutoAWQ pip install autoawq # 用 Python 脚本量化 from awq import AutoAWQForCausalLM from transformers import AutoTokenizer model_path = "/data/models/original-bf16" quant_path = "/data/models/kimi-moe-w4a8" quant_config = { "zero_point": True, "q_group_size": 128, # group size 越小精度越好但体积略增 "w_bit": 4, # 存储位宽 "version": "GEMM" } model = AutoAWQForCausalLM.from_pretrained(model_path) tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model.quantize(tokenizer, quant_config=quant_config) model.save_quantized(quant_path)关键参数是q_group_size。它表示多少个权重共享一个 scale。128 是业界常用的平衡点:精度足够,反量化开销小。如果设成 32,精度更高但存储和计算开销增加;设成 256 则反之。实际测试中,group size 从 128 降为 32,感知质量提升并不明显,但显存和延迟都会变差,所以我一般不建议无脑往下压。
4.4 显存优化组合拳:W4A8 + MoE 专家卸载
这是我觉得最值得分享的经验。W4A8 解决了权重存储的压缩,但对超大 MoE 模型,单卡 80GB 依然可能不够。这时候可以叠加专家卸载(expert offload)。
思路是这样的:MoE 模型里所有专家的权重先按访问频率分冷热。热门专家(路由经常命中)常驻显存;冷门专家以 4 bit 形式放在 CPU 内存或 NVMe SSD 上。前向计算时,如果某个 token 路由到了冷专家,引擎就从内存/磁盘异步换入该专家的 4 bit 权重,反量化成 INT8 计算,再换出。
这个方案在 vLLM 中已经有一定支持(swap_space参数控制 CPU 换入缓冲区的大小),实际效果很猛:一个 100B+ 的 MoE,原本需要 200GB 显存,用 W4A8 + 专家卸载后,可以做到 48GB 显存 + 64GB CPU 内存跑起来,单张 80GB 卡基本稳。代价是冷专家首次调用时延迟会抬升,但如果路由分布比较集中,命中热专家的 token 占 90% 以上,平均延迟影响很小。
我给这类部署总结了三个步骤:
- 先用 W4A8 量化完整模型,保留好量化后的权重和 scale。
- 分析路由日志,统计每个专家被激活的频率,生成冷热分布表。
- 设置
swap_space和 CPU 内存上限,把冷专家迁出显存,启动服务后用实测请求压测。
4.5 实测数据:显存、延迟、吞吐到底差多少
下面是我在一套 2 × A100 80GB 环境、跑一个 100B 级别 MoE 模型(模拟 K2/K3 规模)的实测参考数据。这不是官方结果,只是我的环境里的相对对比,但趋势很稳定:
| 配置 | 权重显存 | 总显存占用 | 单请求首 token 延迟 | 吞吐(req/s) |
|---|---|---|---|---|
| BF16 全精度 | 约 200GB | 2 卡满载超高 | 基准 | 基准 |
| W8A8 | 约 100GB | 150GB+ | 略快于基准 | 约 1.3× |
| W4A16 | 约 50GB | 100GB+ | 慢于 W8A8 | 约 1.1× |
| W4A8 | 约 50GB | 80~90GB | 明显快 | 约 1.7× |
| W4A8 + 专家卸载 | 约 40GB(冷专家在 CPU) | 60~70GB | 热专家同 W4A8 | 约 1.5× |
可以看到,W4A8 的吞吐优势主要来自 INT8 张量核心的利用率和省下的显存让 KV cache 能开更大。W4A16 虽然权重也只有 50GB,但计算还在 FP16,INT8 流水线没用上,吞吐提升反而有限,这再次说明了“存储精度和计算精度分开考虑”的价值。
5. 常见问题与踩坑实录:关于 4 bit、INT8、显存与推理速度的 Q&A
5.1 4 bit 量化后模型变傻了?先排查 group size 和校准集
很多人量化完第一反应是“效果崩了”,但往往不是 4 bit 本身的锅,而是校准集太小或 group size 太大。GPTQ/AWQ 都需要一小部分文本做校准,拿 128 条短文本和一个精心选的 512 条长文本,结果能差出一大截。
我建议校准集要覆盖三类内容:代码、中英文混合对话、逻辑推理长文。别图省事只喂一种。另外跑完量化一定要做“量化前后困惑度对比”,用同一测试集分别算 PPL(perplexity),如果 PPL 上升超过 5%,就要考虑调小q_group_size或换 AWQ。
5.2 为什么显存没降到理想值?可能被 KV cache 吃了
开头那位群友就是这个问题。他盯着权重显存看,但实际显存大头可能被 KV cache 占了一半。KV cache 的大小计算很简单:2(K 和 V) × num_layers × num_heads × head_dim × seq_len × batch_size × 每个元素字节数。当max_model_len设得很大、batch 又高时,KV cache 会吃掉几十 GB,哪怕权重已经 4 bit。
解决思路有两个:一是调小max_model_len或max_num_batched_tokens;二是用 PagedAttention 和块式分配(vLLM 默认就是),让 KV cache 按需分配。如果你发现显存长期跑不满但 OOM,多半是碎片化,可以调整gpu_memory_utilization和max_num_seqs。
5.3 INT8 和 FP8 选哪个?关键看你手上是什么卡
热词里反复出现 fp8 int8 ai 区别、fp16 bf16 int8 的速度即可,说明越来越多人在纠结精度格式。我做了一张对比表,直接抄作业用:
| 指标 | INT8 | FP8 (E4M3/E5M2) | FP16/BF16 |
|---|---|---|---|
| 表示方式 | 整数 | 浮点 | 浮点 |
| 动态范围 | 小 | 中 | 大 |
| 精度保持 | 靠 scale 补偿 | 天然浮点,范围广 | 最好 |
| 硬件支持 | 消费级到数据中心全覆盖 | 需 H100 级及以上原生支持 | 全覆盖 |
| 运算速度 | 快 | 很快(原生支持时) | 慢于两者 |
| 适用姿势 | W4A8 的组合计算端 | 端到端 FP8 推理 | 基准配置 |
对大多数普通开发者和中小团队,我建议先上 INT8,因为兼容性最好。FP8 在 A100 上其实没有原生加速,实际表现不一定比 INT8 强;只有当你确定生产环境的卡支持 FP8 且框架适配到位,才值得折腾 FP8。
5.4 MoE 负载均衡和路由不均是隐藏瓶颈
热词里还有“moe 负载均衡代码”,这是 MoE 推理里常被忽略的一环。如果路由分配不均衡,有些专家被疯狂命中,有些几乎空闲,那么即使整体显存够,也会出现单卡计算热点和延迟抖动。
训练侧会通过辅助损失(aux loss)鼓励均衡路由,但推理侧的推理引擎也值得关注。在 vLLM 里可以观察每个专家的算力利用率,如果发现有专家长期热点,可以考虑:
- 增大
tensor_parallel_size把专家切得更碎; - 调整路由阈值或采样策略(如果你的模型支持 topk 之外的采样);
- 把热点专家单独做 expert offload 的反向——即全放显存不换出。
这块调整很依赖具体模型,我建议先在离线脚本里统计路由分布,再决定是否动推理引擎的调度参数。
5.5 服务化部署要小心:CPU offload 和磁盘 IO 是延迟刺客
如果你采用“W4A8 + 专家卸载”的组合,最怕的是冷专家命中时再去磁盘读权重。NVMe 顺序读能到 3~5GB/s,但随机小文件读很慢,而且每次换入换出都有内核态开销。我实测下来,冷专家首次调用可能比热专家多出 50~200ms 延迟,如果业务有 P99 延迟要求就得谨慎。
几个缓解技巧:
- 把冷专家权重用
mmap映射到内存,避免反复 read 系统调用; - 在服务启动前预热“次热”专家到 CPU 内存的 page cache 里;
- 如果冷专家命中率极低(低于 0.1%),甚至可以接受首次延迟,用异步加载掩盖掉。
6. 从 W4A8 再往后:我的实际使用体会与后续可玩的方向
写到这里,我想把个人经验总结一下。W4A8 这套方案我用了快一年,最大的体会是:它不是银弹,但它是当前性价比最高的一档组合。如果你要做 MoE 大模型部署,优先把 W4A8 跑通,再去考虑 FP8、speculative decoding、expert offload 这些进阶玩法。很多时候,模型效果不够好不是量化的问题,而是你的校准集、评估流程、显存规划出了问题。
再分享一个小技巧:部署之前先打印一份显存规划表。把模型参数量、目标位宽、KV cache 预算、激活缓冲、CUDA 预留各列一行,加起来不超过物理显存 90% 再动工。我见过太多人上来就乱调参数,最后根本分不清是量化的问题还是显存不足的问题。
后续值得尝试的方向包括:把 W4A8 和 speculative decoding(投机采样)结合,用一个小 draft 模型配合大模型验证,吞吐还能再抬一截;或者把 4 bit 权重的 scale 也一起量化掉(double quant),把存储精度进一步压到接近 3.5 bit 级别;再或者试试 onnx 那条干净的部署链,在边缘设备上跑一个小号 MoE。
最后再强调一次关键结论:4 bit 存储负责把模型塞进显存,INT8 计算负责让矩阵乘跑得快,MoE 调度负责不让所有专家同时挤进来。这三件事想清楚、配合好,一台 80GB 的卡也能稳稳托住百亿到千亿级别的 MoE 模型。希望你按这篇文章的流程走一遍之后,再看到“4 bit MoE”的帖子,不会再被显存数字绕晕。