MI300X 上跑 Gemma 4 E2B:fp8 最快,却离原始权重最远
AIAI Summary (BLUF)
本文提供了在单张AMD Instinct MI300X上通过vLLM部署Gemma 4 E2B十种权重格式的逐步指南,并在相同请求数和提示长度下进行了性能测试。结果显示,fp8格式速度最接近bf16,而int8和4位格式较慢。所有日志、报告和脚本均已开源。
核心洞察
这张卡有 192GB 显存,跑一个 9.42GiB 的模型,内存根本不是瓶颈。所以这篇东西真正有意思的地方在于:它把十种量化格式放在同一张卡、同一个镜像、同一天里跑了一遍,然后发现最快的格式(fp88-bit floating-point precision, a numerical format used for efficient training and inference of large neural networks.)恰恰是离 Google 训练权重最远的那个。速度和保真度在这里是一道单选题,没有两全的选项。
另外,4-bit 构建保留了 QAT 的原始网格,精度最高,但在这张卡上跑得最慢,因为 MI300X 没有 int4 乘法单元,每次计算前都得在 kernel 里解包成 bf16。这个细节值得记住。
这篇文章是一份分步指南,讲的是怎么在单张 AMD Instinct MI300XAMD 推出的 GPU 加速卡,基于 CDNA 3 架构,拥有 191.7 GiB HBM3 内存,适用于高性能计算和 AI 推理。 上,通过 vLLM一个高性能的LLM推理和服务库,为DeepSeek-OCR提供优化的推理能力,支持流式输出和批量处理。 部署 Gemma 4 E2BGoogle 开源的小型大语言模型,本教程中被部署为推理服务的模型本体,权重挂载路径为 /mnt/models/gemma-4-E2B-it。 的十种权重格式。所有构建都在同一张卡、同一个镜像、同一天里跑完,覆盖了不同的请求数和提示长度。每一份日志、报告和脚本都已提交。
在 MI300X 上,fp8 是唯一能跟上 bf16 节奏的格式:单请求时是 0.75 倍,8 或 64 并发时能到 1.09 倍。int8 W8A88位整数权重和激活量化,在MI300X上性能较差,且小批量时无法运行。 在 0.29 倍到 0.87 倍之间,4-bit 的 W4A16权重4位、激活16位的量化方案,常用于压缩模型权重。 构建在 0.14 倍到 0.63 倍之间。服务最快的那个格式,恰恰把权重舍入得离 Google 训练值最远,所以这是一场速度和保真度之间的取舍。
代码仓库地址:https://github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-mi300x-2b
核心结论
在单张 AMD MI300X 上部署 Gemma 4 E2B 的十种权重格式中,fp8 是唯一能跟上 bf16 节奏的格式:单请求时为 bf16 的 0.75 倍,8 或 64 并发时最高达 1.09 倍。
速度与保真度呈反向关系:服务最快的 fp8 恰恰离 Google 训练权重最远(2.64% 相对 RMS 误差),而精度最高的 q4w4a16 精确保留 QAT 网格(90.0% 值逐位相同),却因 MI300X 无 int4 乘法单元、需在 kernel 中解包成 bf16,仅跑出 bf16 的 0.14 倍到 0.63 倍。
int8 W8A8 在九个测试格子中有八个低于 fp8,整体跑出 bf16 的 0.29 倍到 0.87 倍,在保真度和速度上均非最优选择。
E4M3 与 E4M3FNUZ 两种 FP8 checkpoint 在 MI300X 上服务表现几乎相同(九个格子中八个为 0.995x 到 1.014x),因为 vLLM 的 compressed-tensors 加载器会在加载时自动转换格式。
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 上的地址:
fp8: https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-fp8-textfp8fnuz: https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-fp8fnuz-textfp8emb4: https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-fp8-text-emb4fp8fnuzemb4: https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-fp8fnuz-text-emb4w8a8: https://huggingface.co/xbill9/gemma-4-E2B-it-qat-w8a8-int8w8a8emb4: https://huggingface.co/xbill9/gemma-4-E2B-it-qat-w8a8-ct-text-emb4q4w4a16: https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-textq4w4a16ple4: https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text-ple4q4w4a16emb4: https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text-emb4
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 上选择权重格式的策略,是通过逐步递增的方法验证的。
参考资料
- 代码、日志、报告和本文证据:https://github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-mi300x-2b
- 同样的 checkpoint 在 TPU v5e 上,含准确率:https://dev.to/gde/gemma-4-qat-on-one-tpu-v5e-what-runs-and-what-doesnt-2gii
- FP8 构建,E4M3:https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-fp8-text
- FP8 构建,E4M3FNUZ:https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-fp8fnuz-text
- 4-bit repack:https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text
- Google 的 QAT 源 checkpoint:https://huggingface.co/google/gemma-4-E2B-it-qat-q4_0-unquantized
- AMD Instinct MI300X:https://www.amd.com/en/products/accelerators/instinct/mi300/mi300x.html
- AMD Developer Cloud:https://devcloud.amd.com
- vLLM on ROCm:https://docs.vllm.ai/en/latest/getting_started/installation/gpu.html
常见问题(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最保真但最慢,这是一道单选题。
版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。
文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。
若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。



