GEOZ

MI300X 上跑 Gemma 4 E2B:fp8 最快,却离原始权重最远

2026/10/9
MI300X 上跑 Gemma 4 E2B:fp8 最快,却离原始权重最远

AIAI Summary (BLUF)

本文提供了在单张AMD Instinct MI300X上通过vLLM部署Gemma 4 E2B十种权重格式的逐步指南,并在相同请求数和提示长度下进行了性能测试。结果显示,fp8格式速度最接近bf16,而int8和4位格式较慢。所有日志、报告和脚本均已开源。

核心洞察

这张卡有 192GB 显存,跑一个 9.42GiB 的模型,内存根本不是瓶颈。所以这篇东西真正有意思的地方在于:它把十种量化格式放在同一张卡、同一个镜像、同一天里跑了一遍,然后发现最快的格式(fp8)恰恰是离 Google 训练权重最远的那个。速度和保真度在这里是一道单选题,没有两全的选项。

另外,4-bit 构建保留了 QAT 的原始网格,精度最高,但在这张卡上跑得最慢,因为 MI300X 没有 int4 乘法单元,每次计算前都得在 kernel 里解包成 bf16。这个细节值得记住。


这篇文章是一份分步指南,讲的是怎么在单张 AMD Instinct MI300X 上,通过 vLLM 部署 Gemma 4 E2B 的十种权重格式。所有构建都在同一张卡、同一个镜像、同一天里跑完,覆盖了不同的请求数和提示长度。每一份日志、报告和脚本都已提交。

在 MI300X 上,fp8 是唯一能跟上 bf16 节奏的格式:单请求时是 0.75 倍,8 或 64 并发时能到 1.09 倍。int8 W8A8 在 0.29 倍到 0.87 倍之间,4-bit 的 W4A16 构建在 0.14 倍到 0.63 倍之间。服务最快的那个格式,恰恰把权重舍入得离 Google 训练值最远,所以这是一场速度和保真度之间的取舍。

代码仓库地址:https://github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-mi300x-2b


核心结论

  1. 在单张 AMD MI300X 上部署 Gemma 4 E2B 的十种权重格式中,fp8 是唯一能跟上 bf16 节奏的格式:单请求时为 bf16 的 0.75 倍,8 或 64 并发时最高达 1.09 倍。

  2. 速度与保真度呈反向关系:服务最快的 fp8 恰恰离 Google 训练权重最远(2.64% 相对 RMS 误差),而精度最高的 q4w4a16 精确保留 QAT 网格(90.0% 值逐位相同),却因 MI300X 无 int4 乘法单元、需在 kernel 中解包成 bf16,仅跑出 bf16 的 0.14 倍到 0.63 倍。

  3. int8 W8A8 在九个测试格子中有八个低于 fp8,整体跑出 bf16 的 0.29 倍到 0.87 倍,在保真度和速度上均非最优选择。

  4. E4M3 与 E4M3FNUZ 两种 FP8 checkpoint 在 MI300X 上服务表现几乎相同(九个格子中八个为 0.995x 到 1.014x),因为 vLLM 的 compressed-tensors 加载器会在加载时自动转换格式。

  5. int4 词表可将权重内存减半(如 fp8emb4 从 7.07 GiB 降至 3.61 GiB),代价仅为 1% 到 5% 的速度损失,同时 KV 缓存增加约 4%,适合显存受限的场景。

为什么要在 192 GB 的卡上比较格式?

Gemma 4 E2B 以 bf16 加载时占 9.42 GiB。一张 MI300X 有 192 GB HBM,所以内存在这里决定不了什么:bf16 留给 KV 缓存的空间是 9,045,060 个 token,最小的构建也只是把这个数字拉到 9,475,223。

剩下的选择维度就是速度和精度。这张卡对某些数字格式有原生乘法支持,对另一些则是模拟,下面每一种格式存储 Google 量化感知训练(QAT)权重时的舍入程度都不一样。


十种构建

每一个量化构建都从 Google 的 gemma-4-E2B-it-qat-q4_0-unquantized 发布版开始,它的权重本来就落在 4-bit 网格上,每 32 个值一组共享一个 scale。九个量化版都是纯文本,都在 Hugging Face 上:

构建 线性层 词表 制作脚本
bf16 bf16,google/gemma-4-E2B-it bf16 —
fp8 FP8 E4M3,FP8 激活按 token bf16 fp8_text.py
fp8fnuz FP8 E4M3FNUZ,MI300X 原生 FP8 bf16 fp8_text.py --fnuz
fp8emb4 FP8 E4M3 int4 fp8_text.py build-on
fp8fnuzemb4 FP8 E4M3FNUZ int4 fp8_text.py build-on --fnuz
w8a8 int8 按通道,int8 激活按 token bf16 w8a8_from_qat.py
w8a8emb4 int8 按通道 int4 w8a8.py
q4w4a16 int4 精确保留 QAT 网格,bf16 激活 bf16 repack_q4_0.py、text_only.py
q4w4a16ple4 同 q4w4a16 仅逐层嵌入用 int4 embed_int4.py
q4w4a16emb4 同 q4w4a16 int4 embed_int4.py

Hugging Face 上的地址:

int4 词表包括 embed_tokens、未绑定的 lm_head 和逐层嵌入,都按 QAT 训练时的 32 个值一组打包。


开始之前你需要准备

  • 一个 AMD Developer Cloud 账号,带 MI300X 实例(gpu-mi300x1-192gb),以及对应的 SSH 密钥
  • 一个 Hugging Face token
  • 克隆仓库:git clone https://github.com/xbill9/gemma4-dev
  • 用 amd-gputools 里的 make scaffold 准备好实例,它会装好 Docker、加上 GPU 用户组、拉取 vLLM 镜像

第一步:构建各种格式

每个构建读取 Google 的 QAT checkpoint,写出一种格式。FP8 构建从 4-bit repack 里拿到纯文本模型的 config 和张量列表,所以每个构建量化的都是同样的 276 个线性层:

hf download google/gemma-4-E2B-it-qat-q4_0-unquantized --local-dir src
hf download xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text config.json model.safetensors.index.json --local-dir ct-text
python3 repack/fp8_text.py build src ct-text/config.json ct-text/model.safetensors.index.json fp8fnuz-text --fnuz
python3 repack/fp8_text.py verify src fp8fnuz-text --fnuz

verify 会重新读取两个 checkpoint,把每个量化值和它的 QAT 原始值做比较:

{
 "modules": 276,
 "values": 1876819968,
 "max_rel_err": 0.03333339840173721,
 "copied": 264,
 "copied_identical": 264,
 "relative_rms_error": 0.026415727046737517
}

build-on 用来做 emb4 变体,把已有 int4 词表构建的线性层换成 FP8,词表不动。int8 构建来自 w8a8_from_qat.py,4-bit repack 来自 repack_q4_0.py,后者恢复每组训练时的步长,把数值存成 int4,不做重新舍入。


第二步:测这张卡的矩阵乘法

在部署之前,gemm_decode_shapes.py 会测 E2B 每 token 跑的每一个矩阵乘法,分别用 1、8、64 行,跑在 HIP graph 里,把启动开销排除在数字之外:

| M | dtype | us/token | x bf16 |
| 1 | bf16 | 1966.2 | 1.00 |
| 1 | fp8 | 1304.2 | 1.51 |
| 64 | bf16 | 2383.6 | 1.00 |
| 64 | fp8 | 1589.4 | 1.50 |
| 64 | int8 | 12146.4 | 0.20 |

fp8 做同样的工作只花 bf16 三分之二的时间。int8 在 64 行时比 bf16 慢五倍,而且低于 17 行时 PyTorch 的 int8 乘法根本拒绝运行:

control_8192 M=1 int8: `RuntimeError: self.size(0) needs to be greater than 16, but got 1`

MI300X 没有 int4 乘法,所以 4-bit 构建在每次乘法前,要在 kernel 里把权重量解包成 bf16。


第三步:在同一个固定镜像上部署所有构建

dtype_sweep.py 依次从各自的 rig 目录部署每个构建,用同一个镜像 digest,相同的服务参数:--max-model-len 32768、--gpu-memory-utilization 0.90、开启 prefix caching、不加 --quantization 标志,让 vLLM 自己从 checkpoint 里读格式:

python3 dtype_sweep.py --droplet debian-gpu-mi300x1-192gb-devcloud-atl1 \
  --image vllm/vllm-openai-rocm@sha256:ec62abecc13923172cf1225c0270b8cea7d84b652eacd3c8dffe2dd35dd73e37 \
  --run-id 2026-10-07-dtype-sweep-mi300x

每个构建在启动时都会检查 vLLM 选了哪个 kernel、加载了多少内存:

=== [3/8] w8a8 (gpu-vllm-mi300x-2b-w8a8)
    ok: loading 7.07 GiB, KV 9025606, kernels ['TritonInt8ScaledMMLinearKernel']

=== [4/8] fp8 (gpu-vllm-mi300x-2b-fp8)
    ok: loading 7.07 GiB, KV 9017531, kernels ['RowWiseTorchFP8ScaledMMLinearKernel']

如果一个量化构建加载出来接近 bf16 的 9.42 GiB,说明它在加载时被解包成了 bf16。一个都没有。


第四步:检查每个构建能不能正常回答

verify_capabilities 给每个构建发一个文本问题、一个思考提示和一个工具调用,答案都是已知的:

✅ 3/3 capabilities verified on `debian-gpu-mi300x1-192gb-devcloud-atl1`.

| text | ✅ | The AMD MI300X GPU is based on the AMD CDNA 3 architecture |
| thinking | ✅ | 1704 chars, 587 reasoning tokens |
| tool calling | ✅ | tool_calls → get_weather{"city": "Reykjavik"} |

九个量化构建全部通过三项检查。bf16 也通过同样三项,但拒绝图像检查,因为它和其他构建一样是纯文本部署的。


第五步:扫请求数和提示长度

每个构建分别用 1、8、64 并发请求,提示长度 128、1,024、8,192 个 token,每个输出 512 个 token,每个格子重复三次,每次运行都用新的随机种子,确保没有提示从 prefix cache 里命中。


每种格式有多快?

1,024 token 提示下,1、8、64 并发请求的输出 token 每秒,以及所有九个格子里相对 bf16 的范围:

构建 权重 1 / 8 / 64 请求 相对 bf16 范围
bf16 9.42 GiB 321 / 1,795 / 8,951 1.00x
fp8 7.07 GiB 242 / 1,827 / 9,700 0.75x – 1.08x
fp8fnuz 7.07 GiB 245 / 1,819 / 9,739 0.75x – 1.09x
fp8emb4 3.61 GiB 234 / 1,742 / 9,450 0.72x – 1.08x
w8a8 7.07 GiB 98 / 757 / 4,717 0.29x – 0.87x
w8a8emb4 3.60 GiB 97 / 746 / 4,680 0.29x – 0.79x
q4w4a16 6.32 GiB 47 / 367 / 3,288 0.14x – 0.63x
q4w4a16emb4 2.85 GiB 47 / 365 / 3,285 0.14x – 0.63x

fp8 在单请求时落后 bf16 四分之一,8 或 64 并发时跑到 1.00x 到 1.08x,最嘈杂的那个格子除外。int8 和 4-bit 构建每个格子都比 bf16 慢。

单请求时每个输出 token 的耗时是:bf16 2.89 ms,fp8 3.83 ms,int8 9.94 ms,4-bit 20.91 ms。4-bit 构建加载的字节数是 bf16 的三分之二,每个 token 却要花七倍时间,所以是解包 kernel 决定了它的节奏。提示变长后,4-bit 和 int8 构建会缩小差距,因为每一步里越来越多的计算花在提示和 attention 上,那里权重格式不改变工作量:64 并发、8,192 token 提示下 q4w4a16 能到 0.63x。


它们离训练权重有多近?

每个构建都离线对照它制作时的 QAT 值做了检查:

构建 相对 QAT 权重的误差
q4w4a16 网格上无误差:58,650,624 组,0 个偏离网格,90.0% 的值逐位相同,其余在 scale 舍入范围内偏差不超过 1.1%
w8a8 每个张量 0.59% 到 1.71% 相对误差,平均 0.90%
fp8 2.64% 相对 RMS 误差,最差的值是该行最大值的 3.57%
fp8fnuz 2.64% 相对 RMS 误差,最差的值是该行最大值的 3.33%

这个顺序和速度表正好相反。4-bit 构建精确保留训练网格,int8 把每个值舍入到每行 255 个级别之一,FP8 的三个尾数位舍入得最远。这些 checkpoint 文件在分类套件、GSM8K 和工具调用上的准确率,在下面链接的 TPU v5e 文章里;这次扫描只测了速度。


小提示:E4M3 还是 E4M3FNUZ,都能用

MI300X 的 FP8 是 E4M3FNUZ:和大多数公开 FP8 checkpoint 用的 E4M3 有同样的 3 位尾数,最大值是 240 而不是 448,没有负零。原生 E4M3 矩阵乘法在这张卡上会抛 HIPBLAS_STATUS_NOT_SUPPORTED,但 E4M3 checkpoint 能正常服务,因为 vLLM 的 compressed-tensors 加载器在加载时做了转换:

Selected RowWiseTorchFP8ScaledMMLinearKernel for CompressedTensorsW8A8Fp8

用 E4M3FNUZ 原生构建的同样权重,加载进同一个 kernel,同样的 7.07 GiB,同样的 9,017,531 token 缓存,九个格子里有八个跑到 E4M3 构建的 0.995x 到 1.014x。两个文件在这张卡上都能用。


小提示:int4 词表几乎不花代价

emb4 构建把词表打包成 int4,词表在 E2B 权重里占很大一块。和同格式但 bf16 词表的版本相比,九个格子里有八个:

对比 权重 输出 tok/s
fp8emb4 / fp8 3.61 / 7.07 GiB 0.954x – 0.974x
w8a8emb4 / w8a8 3.60 / 7.07 GiB 0.984x – 0.992x
q4w4a16emb4 / q4w4a16 2.85 / 6.32 GiB 0.990x – 0.999x

一半的权重内存换 1% 到 5% 的速度,KV 缓存还涨了大约 4%。在这张卡上内存不缺;换一张小卡,这就是该选的构建。


对比一览

构建 权重 单请求 64 请求,1,024 token 相对 QAT 保真度
🥇 fp8 / fp8fnuz 7.07 GiB 259 9,700 / 9,739 2.64% RMS
🥈 fp8emb4 3.61 GiB 247 9,450 2.64% RMS
🥉 bf16 9.42 GiB 343 8,951 Google 的 bf16 发布版
w8a8 7.07 GiB 100 4,717 0.59% – 1.71%
q4w4a16 6.32 GiB 48 3,288 精确网格
q4w4a16emb4 2.85 GiB 47 3,285 精确网格

单请求数字用的是 128 token 提示;奖牌按 64 请求的输出排名。


那到底选哪个?

单个用户用 bf16:它是单请求下最快的构建,343 输出 token 每秒,而且在这张卡上内存不要钱。多用户用 fp8 或 fp8fnuz,8 到 64 并发时能到 bf16 的 1.09 倍,如果卡是共享的,用 fp8emb4,权重只有一半。想要最接近 Google 训练权重的,用 q4w4a16,在这张卡上单请求速度大约是 bf16 的七分之一。int8 在保真度上介于两者之间,速度上除了最嘈杂的那个格子外每个格子都低于 fp8,所以在这里从来不是最佳选择。


收尾

把实例关机并不会停止计费。扫描结束后去 AMD Developer Cloud 控制台销毁它;在那之前 MI300X 实例按每小时 1.99 美元计费。


总结

这篇文章的目标是为单张 AMD MI300X 上的 Gemma 4 E2B 找到最佳权重格式。解决方案的关键是在同一个固定 vLLM 镜像上用相同参数部署每个构建,在启动时检查选中的 kernel 和加载的内存,然后在同一个网格上给它们计时。结果如下:

  • 🟢 fp8 是唯一跟上 bf16 节奏的格式:单请求 0.75 倍,8 或 64 并发时最高 1.09 倍
  • 🟢 E4M3 和 E4M3FNUZ checkpoint 服务表现相同,通过 vLLM 在加载时转换
  • 🟢 int4 词表把权重减半,代价是 1% 到 5% 的速度
  • 🟢 每个量化构建都能通过文本、思考和工具调用检查
  • ⚠️ 4-bit 构建精确保留 QAT 网格,但通过一个解包成 bf16 的 kernel,跑出 bf16 的 0.14 倍到 0.63 倍
  • ⚠️ fp8 离训练权重舍入最远:2.64% 相对 RMS 误差
  • ❌ int8 W8A8 跑出 bf16 的 0.29 倍到 0.87 倍,九个格子里有八个低于 fp8

范围说明:AMD Developer Cloud atl1 区域的一张 MI300X 实例,vLLM 0.31.1rc1.dev23+g43b4aaea3,镜像 vllm/vllm-openai-rocm@sha256:ec62abec…,十个构建都在 2026-10-07 和 2026-10-08 部署,每个格子重复三次。bf16 构建是 Google 的 gemma-4-E2B-it,九个量化构建来自 Google 的 QAT 发布版,所以 bf16 在权重和格式上都不同。64 请求、8,192 token 提示的那个格子重复间波动高达 15.7%,已从成对范围里排除;其他格子波动都在 4.1% 以内,除了一个 fp8emb4 格子是 10.7%。这张卡上没有跑准确率基准。重新打包的 checkpoint 是非官方的,基于 Google 在 Apache 2.0 下的发布版。部分分析和写作借助了 AI(Claude);每个数字都来自已提交的输出文件。

为 Gemma 4 在 AMD MI300X 上选择权重格式的策略,是通过逐步递增的方法验证的。


参考资料

常见问题(FAQ)

在MI300X上部署Gemma 4 E2B,哪种量化格式速度最快?

fp8格式速度最接近bf16,单请求时是bf16的0.75倍,8或64并发时可达1.09倍。int8和4位格式较慢,4-bit W4A16仅为bf16的0.14到0.63倍。

为什么4-bit格式在MI300X上运行最慢?

因为MI300X没有int4乘法单元,每次计算前需在kernel里将int4解包成bf16,导致额外开销。尽管4-bit构建保留了QAT原始网格,精度最高,但速度最慢。

在192GB显存的MI300X上比较量化格式有什么意义?

显存不是瓶颈,bf16模型仅占9.42GiB。比较十种格式是为了权衡速度和保真度:fp8最快但舍入离训练值最远,4-bit最保真但最慢,这是一道单选题。

Roger深圳
本文由 Roger 审核,最后更新于 2026年10月9日
联系编辑 →
← 返回文章列表
分享到:微博

版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。

文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。

若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。