同一张AMD Instinct MI300X,同一个vLLM镜像,同一天,把Gemma 4 E2B的十种权重格式挨个跑一遍——结果最快的和最慢的差了约7.8倍。所有日志、报告和脚本都已提交。 在MI300X上, fp8是唯一能跟上bf16的格式 :单请求时是bf16的0.75倍,8个或64个请求并发时最高到1.09倍。int8 W8A8跑在0.29倍到0.87倍之间,4-bit的W4A16构建则在0.14倍到0.63倍之间。跑得最快的格式,也把权重从Google训练时的数值上舍入得最远——速度与保真度之间,只能选一头。 192GB的卡,内存根本不是变量 Gemma 4 E2B在bf16下只占9.42 GiB。一张MI300X有192 GB HBM,所以内存决定不了任何事:bf16留下的空间够放9,045,060个token的KV缓存,最小的构建也只是把这个数字撑到9,475,223。 真正要选的是速度和精度。这张卡对某些数字格式是原生乘法,对另一些则是模拟执行,而下面每一种格式,都以不同的舍入程度保存Google量化感知训练(QAT)出来的权重。 十个构建,同一个起点 每个量化构建都从Google的gemma-4-E2B-it-qat-q4_0-unquantized版本出发,它的权重本来就落在4-bit网格上,每32个值共享一个scale。九个量化版本全部是纯文本模型,都放在Hugging Face上: fp8、fp8fnuz、fp8emb4、fp8fnuzemb4 w8a8、w8a8emb4 q4w4a16、q4w4a16ple4、q4w4a16emb4 int4的词表部分包括embed_tokens、未绑定的lm_head以及逐层嵌入,都按QAT训练时的32值一组网格打包。 动手前需要准备:一个带MI300X实例(gpu-mi300x1-192gb)的AMD Developer Cloud账号和它的SSH密钥、一个Hugging Face token、克隆好的仓库,以及用amd-gputools的make scaffold准备好的实例——它会装好Docker、加上GPU组并拉取vLLM镜像。 第一步:把格式构建出来 每个构建读取Google的QAT检查点,写出一种格式。FP8构建从4-bit重打包版本里取纯文本模型的config和张量清单,所以每个构建量化的都是同样的276个线性层。verify会重新读取两个检查点,把每个量化值和它的QAT原值比对:276个模块、1,876,819,968个值,最大相对误差0.0333,264个被复制且完全一致,相对RMS误差0.0264。 build-on通过把已有int4词表构建的线性层换成FP8、词表原封不动,做出emb4变体。int8构建来自w8a8_from_qat.py,4-bit重打包来自repack_q4_0.py——后者恢复每组训练时的步长,把数值以int4存储,不再重新舍入。 第二步:先给矩阵乘法计时 在服务任何模型之前,gemm_decode_shapes.py先给E2B每token要跑的每一次矩阵乘法计时,分别测1、8、64行,走HIP graph,把启动开销排除在数字之外。 1行:bf16为1966.2微秒/token,fp8为1304.2,fp8是bf16的1.51倍 64行:bf16为2383.6,fp8为1589.4,仍是1.50倍 64行:int8为12146.4,只有bf16的0.20倍 fp8用bf16三分之二的时间做完同样的活。int8在64行时比bf16慢五倍,而且在17行以下,PyTorch的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、开启前缀缓存,并且不加--quantization标志,让vLLM从每个检查点自己读取格式。 每个构建在启动时都会检查vLLM选用的kernel和加载的内存: w8a8:加载7.07 GiB,KV 9025606,kernel为TritonInt8ScaledMMLinearKernel fp8:加载7.07 GiB,KV 9017531,kernel为RowWiseTorchFP8ScaledMMLinearKernel 一个量化构建