1. 这题不是“塞不塞得下”,而是一道显存工程的综合题
一个朋友问了我一个听起来特别离谱的需求:要把 2.8T 总参数量的 Kimi K3 部署到 32 张 H20 上。我第一反应是疯了吧,2.8T 参数用 BF16 存,光权重就得 5.6TB,32 张 H20 哪怕每张 96GB 显存,满打满算 3TB 出头,这账怎么算都平不了。但冷静下来琢磨,这其实是当前大模型落地里非常典型的一类问题:超大 MoE 模型,配上大显存但算力不富裕的推理卡,怎么在物理边界内把一个看似放不下的东西跑起来。
先把最基础的账算明白。Kimi K3 的官方技术细节目前没有完全公开,但“2.8T 总参数”这个量级基本能断定它是 MoE 架构,只有 MoE 才能在总参数这么夸张的情况下,让每次推理的激活参数保持在一个可接受的范围内。所谓 2.8T,指的是模型全部参数合计 2.8 万亿个,这个数字决定了权重文件本身的体积,跟你用哪种精度存储强相关。
| 参数精度 | 每参数字节数 | 2.8T 参数占用 | 32 张 H20(约 3TB 显存)表现 |
|---|---|---|---|
| BF16 | 2 | 约 5.6TB | 完全放不下 |
| FP8 | 1 | 约 2.8TB | 空载勉强,运行时必爆 |
| INT4 | 0.5 | 约 1.4TB | 可落地的起点 |
这里需要提醒一个常见误解:很多人觉得 MoE 模型参数多,显存需求成倍增加,这个直觉没错,但“MoE 省显存”同样是错的。MoE 省的是计算量,不是存储量。2.8T 参数无论哪一次前向推理,不管有多少专家被激活,所有专家权重都必须常驻显存。总参数量就是显存占用的及格线,谁也绕不过去。
所以这道题的第一层答案就已经清楚了:BF16 方案连讨论价值都没有,FP8 方案在“只有权重、没有运行开销”的真空条件下勉强能塞,但稍有 KV cache 和并发就出局。真正能往下走的是 INT4 为主的量化路线。把权重压到 1.4TB 左右后,还有约 1.6TB 的空间给 KV cache、激活值、通信缓冲区和运行时开销,这才有做在线服务的可能性。
“塞进 32 张 H20”这句话的正确理解方式,不是像拷文件一样把权重全部 load 进去就完事,而是要让这么多权重在一个分布式系统里被高效地寻址、切分、交换,最终提供可用的推理服务。这就是接下来要讨论的分布式推理切分问题。
2. H20 的互联结构决定了切分策略:TP 留在节点内,EP 跨节点
算完账只是第一步。1.4TB 的 INT4 权重怎么分布到 32 张卡上,直接决定性能下限。这里必须先看硬件拓扑,因为 H20 的互联结构会狠狠教育每一个不尊重它的人。
32 张 H20 通常意味着 4 个 8 卡节点。单个节点内部,8 张卡通过 NVLink 互联,双向带宽在 900GB/s 量级,非常快;但跨节点一般走 RoCE 或者 InfiniBand,常见配置是 400Gbps 网卡,单向也就 50GB/s 左右。这两个数字差了一个数量级还多。这意味着所有需要高频同步的通信,都必须尽可能被限制在节点内部,跨节点的通信要小心再小心。
Transformer 层的张量并行(TP)恰恰是通信最重的操作。attention 计算里的 QKV 投影、输出投影,每一层都要做 all_reduce,通信量和激活值尺寸成正比。如果 TP 跨了节点,每算一层就有一次全量同步走慢速网络,吞吐量会直接塌方。所以我的切分思路很明确:TP 必须限定在节点内的 8 张卡上。
MoE 层的处理则是另一套逻辑。MoE 的专家 FFN 天然可以按专家维度切分,也就是 expert parallel(EP)。把 2.8T 参数里的专家权重按 4 个节点分片,每个节点各存 1/4 的专家集合;推理时 router 决定每个 token 去哪些专家,然后通过 all_to_all 通信把 token 的 hidden state 发到对应节点,算完再收回来。这个数据量是 token 粒度的,比同步整层激活小得多,跨节点也扛得住。
推荐的世界划分方式就是 world_size=32,TP=8,EP=4。也就是每 8 张卡组成一个 TP 组,4 个 TP 组构成 4 个 EP 分片。attention 部分的通信全部走 NVLink,跨节点的流量只有 expert dispatch 阶段的 all_to_all,把慢速网络的影响压到最低。
| 切分方案 | 优点 | 缺点与适用场景 |
|---|---|---|
| TP=8 + EP=4 | attention 全 NVLink,跨机仅 expert 通信 | 对节点间 all_to_all 质量要求较高 |
| TP=4 + EP=8 | 节点内 TP 压力更小 | 8 个 EP 组跨机通信频次更高 |
| PP=4 | 跨节点通信最少,网络差也能跑 | 流水线气泡明显,长 batch 才划算 |
| 纯 TP=32 | 无 | 每层跨机 all_reduce,直接劝退 |
可能有朋友问,为什么不用 pipeline parallel(PP)替代 EP?PP 确实能把跨节点通信压到最小,因为每个节点只跑一部分层,只要在层边界传一次激活值。但 2.8T 的模型层数极多,PP=4 意味着每个节点要完整加载 1/4 的层,显存分配不均,还伴随流水线 bubble。PP 更适合网络条件很差、带宽只有几十 Gbps 的机房作为退而求其次的方案。网络条件允许时,TP+EP 的组合是 MoE 模型在多机场景下的最优解。
还有一个必须想明白的点:H20 的算力不算强,但单卡 96GB 显存、接近 4TB/s 的显存带宽是它的本钱。MoE 的 decode 阶段是典型的 memory-bound,每个 token 只需要读当前被激活的专家权重,而不是把 2.8T 参数都过一遍。显存带宽越高,并发能力越强,这恰恰是 H20 的甜点区。换句话说,这个部署方案不是硬把模型塞进不合适的卡,而是 MoE 和 H20 的匹配度确实很高,前提是你别把通信结构设计反了。
3. 量化方案:FP8 只是“能塞”,INT4 混合精度才是能用
绕不开的第二个大问题是量化。前面算了,FP8 权重 2.8TB,距离 32 张卡的 3TB 上限只差 200GB 左右。这个 200GB 看着不小,但放到运行时就是个死亡指标:KV cache 是按并发和序列长度线性增长的,激活值、CUDA context、NCCL 通信缓冲、框架内存池都等着要空间。我在不少项目里见过测试环境用 FP8 把模型 load 起来,一上真实流量立刻 OOM,本质就是只算了权重没算运行期开销。
真正能在生产环境跑起来的是 INT4 为主、混合精度兜底的方案。但“INT4”不是简单粗暴地把所有层都压成 4bit,分层处理是必须的。
先说哪些层要保留高精度。Embedding 和 lm_head 在很多 MoE 大模型里是共享的,参数总量几个 B 级别,相对 2.8T 来说占比极小,保留 BF16 完全能接受。Router(gate)层参数更少,但它的作用是决定 token 去哪个专家,对量化误差极其敏感,这层一旦量化过头,模型输出质量会明显劣化。我自己踩过这个坑,只把 router 层量化成 FP8,下游指标就掉了不少,后来这层一律 BF16 起步。Attention 的 qkv/o 投影可以用 FP8,激活分布相对温和;RMSNorm/LayerNorm 这类归一化层必须保留 BF16 甚至 FP32。真正能吃 INT4 的,是权重占比最大的专家 FFN 部分。
INT4 的 group size 选择也有讲究。常见的是 group size 128,空间效率高;如果把 group size 缩到 64,scale 和 zero point 的存储开销会上涨 5% 左右,但量化粒度更细,稳定性明显提升。对 2.8T 这种体量的模型,我倾向于先用 group size 128 跑一遍,如果发现特定 expert 掉点严重,再把这些 expert 单独降级到 group size 64 或者 FP8。
量化校准还有一个 MoE 特有的坑:校准数据要尽量覆盖到足够多的 expert。2.8T 总参数的模型,expert 数量非常庞大,如果校准集只有几百条通用语料,大量冷门 expert 可能在整个校准过程中都没被激活过,它们的量化 scale 靠插值或者默认值,上线后一旦被 router 选中,输出质量就是无法预知的。比较稳的做法是准备 512 到 1024 条混合样本,代码、数学、对话、长文档都要有,跑一次短的 forward 统计激活分布,然后针对每个 expert 单独看量化误差。对异常 expert 做降级处理,而不是一刀切。
执行层面我还有一个习惯:分步量化,一次只动一个变量。先只量化 attention 部分,专家 FFN 保留 BF16,跑一轮评估;再量化专家 FFN 的某一部分,再评估。不要一次性把所有层全部量化完再看效果,否则哪个环节导致掉点根本定位不到。
最后注意推理内核的选择。INT4 权重必须依赖 Marlin、AWQ 这类专门优化的内核才能在 GPU 上高效计算,不是所有框架默认就会走这些内核。有些框架会把 INT4 反量化回 FP16 再算,显存是省下来了,性能却没有任何提升。部署前先在小规模、少节点的环境里确认框架对量化格式的 kernel 支持情况,再上全量,不然启动成功了性能一样没法看。
4. 从权重文件到在线服务:显存预算与启动配置
量化方案定了,切分策略也定了,接下来就是把方案落到机器上。这个阶段最忌讳凭感觉开服务,必须先把显存预算表做出来。
以 INT4 混合精度为例,按 2.8T 参数估算:权重总量大约 1.4 到 1.6TB(取决于 group size 和保留高精度的层占比),摊到 32 张卡上,每卡约 44 到 50GB。KV cache 按一个比较保守的在线配置算:64 路并发、每路 16K 上下文,KV cache 可能吃掉 400GB 左右,每卡约 12.5GB。再加上激活值、CUDA graph、NCCL 通信缓冲,每卡预留 6 到 8GB。合计每卡占用大约 66 到 72GB,离 96GB 还有 24GB 以上的余量,这个方案才谈得上长期稳定跑。
作为对照,FP8 方案每卡光权重就要 87.5GB,加上 KV cache 和运行开销直接超出 96GB,OOM 只是时间问题。所以这里可以下一个更明确的结论:FP8 只能用来做离线评测或者短时间试跑,做不了在线服务。
启动参数方面,我用 vLLM 举例,只是一个思路参考,具体参数名会随框架版本变化:
python -m vllm.entrypoints.openai.api_server \ --model /data/models/kimi-k3-int4 \ --tensor-parallel-size 8 \ --expert-parallel-size 4 \ --max-model-len 16384 \ --max-num-seqs 64 \ --gpu-memory-utilization 0.94 \ --quantization awq \ --kv-cache-dtype fp8 \ --enable-chunked-prefill几个关键参数的解释:tensor-parallel-size 8 对应节点内 8 卡,expert-parallel-size 4 对应 4 个节点,这就是前面说的 TP=8+EP=4 方案的落地映射。gpu-memory-utilization 设 0.94 不是拍脑袋,剩余 6% 是给驱动、CUDA context、NCCL 初始化留的,这些开销看起来小,但多机环境下真会吃掉几个 GB。kv-cache-dtype fp8 能把 KV cache 占用再压一半,对大 MoE 加长上下文的场景非常值得开。enable-chunked-prefill 是为了避免一个大 prompt 进来时 prefill 的显存峰值瞬间把 KV cache 空间打满,分块处理更平滑。
加载慢是紧接着会遇到的第一个现实问题。1.4TB 以上的权重,就算本地 NVMe 读取速度能到 10GB/s,也要 140 秒以上;如果权重放在网络文件系统上而每个节点都要读一遍,启动时间直接按小时算。我的处理方式是把模型 shard 文件按 TP 分片预先切好,每个节点只读自己需要的那一部分,不要让每张卡都去读完整权重再切。另一个办法是转成框架支持的 mmap 格式,启动时直接内存映射,省掉反序列化和格式转换的等待。发布新版本时,先把权重 stage 到各节点本地 NVMe,再重启服务进程,不要所有节点同时去拉 NFS。
服务启动后的冒烟测试也有固定流程。先看 nvidia-smi,确认 32 张卡的显存占用相对均匀。如果某张卡比平均高出 10GB 以上,大概率是切分不均或者通信 buffer 分配出了问题,这种问题越早发现越好修。然后跑一个很短的小请求,确认链路通了,再逐个加大输入长度和并发数。重点观察 first token latency 和 KV cache usage 的变化趋势,而不是一上来就压测吞吐。还有个容易被忽略的动作:冒烟测试阶段关掉不必要的日志上报和 metrics 采集,否则出了问题没法判断是部署问题还是外部依赖拖慢的。
5. 上线后才开始折磨人的三个问题
模型能跑起来,不代表能稳定跑。接下来这三个问题几乎每个多节点 MoE 服务都会遇到,但很少有人第一次就躲过。
第一个是 KV cache 对显存的偷袭。大模型的层数多,每 token 的 KV cache 不能小看。按一个比较典型的配置估算:96 层、8 个 KV heads、head_dim 128、FP16 存储,每 token 每层就是 2 × 8 × 128 × 2 = 4096 字节,96 层加起来单 token 约 384KB。64 路并发、每路 16K 上下文,合计大约 1M token,KV cache 总量就是 384GB 到 400GB。这个数字和权重 1.4TB 比不算大,但如果不设 max-model-len,让模型默认支持超长上下文,KV cache 会快速吞掉余量,最终以 OOM 收场。处理办法是限制 max-model-len、开启 GQA 或 kv-cache-dtype fp8,并且给每路请求设置独立的上下文配额,避免单个请求把整个 KV 池占满。
第二个是 MoE 的 router 热点问题。训练好的 router 并不保证专家负载均衡,线上真实流量分布往往会集中在少量热门 expert 上。现象也很典型:p50 延迟正常,p99 时不时飙高,有时某张卡的显存带宽被全部打满,其他卡闲着。这就是所谓的热点 expert 导致的。最直接的调整是 capacity factor 调大到 1.2 到 1.5,允许 token 在 dispatch 时有一定的缓冲,减少因队列拥塞造成的等待。也可以对 router 的 logits 加一点温度系数,让 top-k 选择更平滑一些,但这会影响模型本身的输出质量,需要权衡。如果热点专家特别固定,还有一个思路是把这个专家复制一份放到另一个节点上,2.8T 模型的一个专家参数相对整体很小,复制成本可接受,却能显著降低热点节点的压力。
第三个是多机通信超时。32 卡跨 4 机,NCCL 初始化偶发超时是高概率事件,而且 inference 跑到一半也可能触发 watchdog timeout。这类问题多数不是框架的 bug,而是底层网络配置没调好。排查顺序我有固定的套路:先看网卡 bonding 是否生效、MTU 是否设置成了 9000;再检查 RoCE 或 IB 的丢包控制,多机 NCCL 通信容忍不了丢包,丢一个包就可能让整个集群卡死;最后才是调整 NCCL 环境变量,比如 NCCL_P2P_LEVEL=LOC 强制跨机走网卡而不尝试 P2P,以及适当调大 NCCL_TIMEOUT。不要一上来就怀疑框架,先用 nccl-tests 测一下节点内和跨节点的实际带宽,把底层网络基线摸清,再谈上层优化。
吞吐调优方面还有一点值得说:H20 的算力有限,prefill 阶段容易打满 compute,decode 阶段则是显存带宽瓶颈更大。启用 chunked prefill 后,prefill 和 decode 可以更平滑地交织在一起,避免某个瞬间计算压力陡增。整体上,2.8T 的 MoE 在 INT4 量化下,decode 阶段每 token 只激活少量专家,memory traffic 比同等参数的 dense 模型小得多,理论上限并不低,但实际能跑到多少完全取决于 kernel 效率、通信质量和显存分配是否合理。
最后说一点我自己的习惯。每次在 32 卡集群上大规模启动前,我都会先用 1/64 的模型切片跑一次完整链路,加载、量化格式、router 行为、KV cache 上限全部验证完,再上全量。2.8T 模型的试错成本太高,一次全量加载就是几分钟到十几分钟,连续几次就能把一个晚上耗光。宁可每一步慢一点,也要保证每个操作都是可重复、可回滚的。部署这种体量的模型,稳比快重要得多。