GEOZ

把嵌入表也压到4位:Gemma 4 E2B 在单张 T4 上显存降到 2.86 GiB

2026/10/1
把嵌入表也压到4位:Gemma 4 E2B 在单张 T4 上显存降到 2.86 GiB

AIAI Summary (BLUF)

本文提供了一份完整的教程,指导如何将Google的QAT Gemma 4 E2B模型量化为4位权重(包括嵌入表),并在单张Tesla T4上使用vLLM进行服务。结果显示,int4嵌入版本仅占用2.86 GiB显存,支持109.9万token的KV缓存,解码速度达到109.7 tok/s,且输出与bf16版本完全一致。

核心洞察

这篇文章最有意思的点是:Google 的 QAT 训练其实已经把 embedding 表也量化到了 4-bit 网格上,只是官方导出时没打包。作者把这个空子钻了,显存直接从 9.8 GiB 干到 2.86 GiB,单用户解码速度翻了 2.3 倍。我比较怀疑的是,八条 prompt 的 token 一致性测试样本太小,实际生产环境里长上下文的表现可能没那么完美。


这篇文章手把手带你走一遍完整流程:把 Google 用 QAT 训练出来的 Gemma 4 E2B 压到 4-bit 权重,embedding 表也一起压,然后在一台带 Tesla T4 的 Compute Engine VM 上用 vLLM 跑起来。我们会拿结果跟 bf16 版本、以及 Google 自己导出的 W4A16 版本做对比,同一张卡、同一组 prompt。

int4 embedding 版本加载只占 2.86 GiB,bf16 要 9.8 GiB;KV cache 能塞 1,099,587 个 token,bf16 只有 315,974;单个 512-token 请求的输出速度是 85.28 tok/s,bf16 是 37.04。八条贪婪解码测试 prompt 的输出 token 跟 bf16 embedding 版本完全一致。

代码在这里:https://github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-t4-2b-w4a16


核心结论

  1. Google 的 QAT 训练已将 embedding 表量化到 4-bit 网格,但官方导出时未打包;作者通过重打包将 Gemma 4 E2B 的 embedding 表也压到 int4,显存占用从 9.8 GiB 降至 2.86 GiB。

  2. int4 embedding 版本在 Tesla T4 上单用户解码速度达 85.28 tok/s,相比 bf16 的 37.04 tok/s 提升 2.30 倍,相比 Google QAT W4A16 的 62.27 tok/s 提升 37%。

  3. KV cache 容量从 bf16 的 315,974 token 提升至 1,099,362 token,16,384 token 请求的最大并发数达 67.11 倍。

  4. 八条贪婪解码测试 prompt 的输出 token 与 bf16 embedding 版本完全一致,但样本量较小,长上下文场景的保真度尚未验证。

  5. 加速主要来自输出层:每生成一个 token 需完整读取 262,144 行的 lm_head 矩阵,int4 下仅 0.21 GiB,而 fp16 下为 0.75 GiB;PLE 表打包节省 3.14 GiB 显存但速度不变。

为什么要测这个

第一部分把 Gemma 4 E2B 跑在 T4 上,发现 Google 的 QAT W4A16 checkpoint 单用户解码比 bf16 快 1.79 倍。第二部分搭了能跑起来的最小 VM 和驱动脚本。

QAT checkpoint 量化的是线性层。embedding 表还是 bf16,而 E2B 有两张表:

  • embed_tokens_per_layer,逐层 embedding(PLE)表,E 系列模型靠它把激活尺寸压小:4.375 GiB
  • embed_tokens,token embedding,Gemma 把它跟输出层绑在一起:0.750 GiB

两张表加起来 5.1 GiB,整个纯文本 checkpoint 才 6.11 GiB。输出层每生成一个 token 都要在整个 262,144 词表上跑一遍,bf16 下它是每次解码步骤里读取量最大的部分之一。

QAT 训练时也训了这两张表。两张表里每 32 个采样值一组,都落在跟线性层一样的 4-bit 网格上,bf16 基础模型里没有一组是这样的。所以这两张表可以直接存成 int4,不引入新的量化误差。


开始之前你需要有

  • 第二部分搭好的 Compute Engine VM:n1-standard-2(2 vCPU,7.80 GB 内存),一块 Tesla T4,us-west2-b,数据盘上 16 GB swapfile
  • vLLM 0.29.0,第一部分那个 Turing attention 补丁已经打好并验证过
  • Python 3 加 numpy,重打包用,不需要 PyTorch
  • git clone https://github.com/xbill9/gemma4-dev

第一步:重打包线性层,砍掉视觉和音频塔

google/gemma-4-E2B-it-qat-q4_0-unquantized 存的是 bf16 值,但这些值本来就落在每 32 个一组的 4-bit 网格上。重打包脚本恢复每组的网格步长,写成 compressed-tensors W4A16 格式。第二个脚本砍掉视觉和音频塔,把架构改成 Gemma4ForCausalLM,其他 tensor 一个字节都不动。

cd gemma4-dev/gpu-vllm-t4-2b-w4a16
python3 repack/repack_q4_0.py repack SRC OUT
python3 repack/text_only.py OUT OUT-text

中间产物发布在 xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text:6.11 GiB,int4 线性层,bf16 embedding。


第二步:打包 embedding 表

python3 repack/embed_int4.py OUT-text OUT-text-emb4 --embed-tokens

embed_int4.py 打包 PLE 表,加上 --embed-tokens 就连 token embedding 一起打包。任何一组不在网格上就直接报错退出。

表 bf16 int4
embed_tokens_per_layer 4.375 GiB 1.230 GiB
embed_tokens 0.750 GiB 0.211 GiB
lm_head(解绑后) 绑定 0.211 GiB
整个 checkpoint 6.11 GiB 2.64 GiB

vLLM 绑定输出层的方式是复制 embedding 的 .weight,而打包后的 embedding 没有这个。所以脚本把两者解绑,把同样的 levels 和 scales 再写一份作为 lm_head,vLLM 把它当 int4 线性层跑。模型训练时是绑定的,所以两份拷贝都是训练后的值。

结果发布在 xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text-emb4。


第三步:scales 存成 fp16

每 32 个值一组带一个 scale,也就是网格步长。T4 用 fp16 计算,fp16 比 bf16 多三位尾数,所以 fp16 scale 能更精确地还原源值:

表 bf16 scales:位级一致 fp16 scales:位级一致
PLE 73.80% 74.26%
embed_tokens 73.48% 74.02%

剩下的不一致里,99.9% 的值都在一个 bf16 ULP 以内。这些误差来自源文件自己对 step 乘 level 做的 bf16 舍入。脚本默认用 fp16。


第四步:跑起来

tpu.env 里写了 checkpoint 和所有 serving flag,vllm-t4 读它:

./vllm-t4 start

sweep 服务端启动时的引擎日志:

served xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text-emb4 max_model_len 16384
Model loading took 2.86 GiB memory and 19.400900 seconds
torch.compile took 1.03 s in total
GPU KV cache size: 1,099,587 tokens, Maximum concurrency for 16,384 tokens per request: 67.11x

vLLM 通过 CompressedTensorsEmbeddingWNA16Int 加载打包后的表,int4 线性层日志显示 Using MarlinLinearKernel for CompressedTensorsWNA16。flag 跟第一部分一样:--dtype float16 --gpu-memory-utilization 0.9 --max-model-len 16384 --max-num-seqs 8 --language-model-only。

提示:换模型后重启一次。第一次启动要从头编译,profiling 的激活峰值更大,KV cache 会小一些:冷启动 980,210 token,热启动 1,099,362。


第五步:验证服务正常

./vllm-t4 status
curl -s http://127.0.0.1:8000/v1/chat/completions -H "Content-Type: application/json" \
  -d '{"model":"xbill9/gemma-4-E2B-it-qat-q4_0-w4a16-ct-text-emb4","messages":[{"role":"user","content":"What does the HTTP status code 418 mean? One sentence."}],"temperature":0,"max_tokens":60}' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['choices'][0]['message']['content']); print(json.dumps(d['usage']))"
✅ Serving at http://127.0.0.1:8000 (pid 18273).

VRAM 13581 MiB, 15360 MiB, 0 %

The HTTP status code 418, "I'm a teapot," is a humorous error code indicating that the server refuses to brew coffee because it is a teapot and not a coffee maker.
{"prompt_tokens": 24, "total_tokens": 65, "completion_tokens": 41, "prompt_tokens_details": null, "completion_tokens_details": null}

13,581 MiB 是 vLLM 在 --gpu-memory-utilization 0.9 下的预留;KV cache 填满权重剩下的所有空间。


引擎分配了多少

同一个模型的四个版本,同样的 flag,热编译缓存:

版本 模型加载 KV cache
bf16 参考,google/gemma-4-E2B-it 9.8 GiB 315,974 tokens
Google QAT W4A16,-qat-w4a16-ct 8.02 GiB 519,568 tokens
重打包,纯文本,bf16 embedding 6.33 GiB 711,539 tokens
重打包,纯文本,int4 embedding 2.86 GiB 1,099,362 tokens

两个 Google 版本是按多模态 checkpoint 跑的,所以数字里包含视觉和音频塔。

在这个 serving 配置下,八条最长 16,384 token 的序列,没有哪个版本的 KV cache 会成为瓶颈。省下来的显存可以用来跑更多并发序列或更长的上下文。


输出文本一样吗

八条 prompt,每条 160 token,贪婪解码,跟 bf16 embedding 版本对比:

PROMPT: What is the capital of France? Answer in one word.
SAME TOKENS: True
PROMPT: List the first ten prime numbers, comma separated.
SAME TOKENS: True
PROMPT: What does the HTTP status code 418 mean?
SAME TOKENS: True

八条全部 token 一致。只打包 PLE 表、用 bf16 scales 的版本八条里匹配了七条,在 HTTP 418 回答的第 55 个 token 处开始分叉。这只是抽样检查保真度,两个版本都没跑精度 benchmark。


单流解码

一次一个请求,256 个输出 token:

版本 解码,c=1
重打包,纯文本,bf16 embedding 81.6 tok/s
只打包 PLE 表为 int4 81.2 tok/s
PLE、embed_tokens 和 lm_head 都 int4 109.7 tok/s

打包 PLE 表省了 3.14 GiB,速度没变:每个 token 每层只查它的一行。加速来自输出层。每步一个 token 时,T4 上的解码瓶颈是读了多少字节,而输出层每个 token 都要完整读一遍 262,144 行的矩阵,fp16 下 0.75 GiB,int4 下 0.21 GiB。


压测

用 vllm bench serve,随机 prompt 512 和 4096 token,128 输出 token,并发 1 到 16,每个格子跑三次,贪婪解码。每个格子的 prompt seed 跟第一部分的 bf16 和 QAT 跑的一样,所以三个版本回答的是同一组 prompt。所有格子里最大的运行间波动是 2.5%。


512-token prompt

c bf16 输出 tok/s QAT 输出 tok/s emb4 输出 tok/s emb4 vs QAT emb4 vs bf16
1 37.04 62.27 85.28 +37% 2.30x
4 109.05 157.49 183.08 +16% 1.68x
8 164.62 215.91 239.64 +11% 1.46x
16 164.36 213.79 237.08 +11% 1.44x

单用户收益最大,并发越高收益越小。c=1 时每个解码步骤要读所有

常见问题(FAQ)

Gemma 4 E2B 的 int4 量化后显存占用和 KV 缓存有多大提升?

int4 嵌入版本仅占 2.86 GiB 显存,而 bf16 需要 9.8 GiB;KV 缓存可容纳 1,099,587 个 token,bf16 只有 315,974 个,提升约 3.5 倍。

int4 量化后的模型输出和 bf16 版本完全一致吗?

在八条贪婪解码测试 prompt 上,输出 token 与 bf16 版本完全一致。但样本较小,长上下文生产环境的表现可能不同。

在 Tesla T4 上使用 vLLM 服务 int4 模型,解码速度是多少?

单个 512-token 请求的输出速度为 85.28 tok/s,bf16 为 37.04 tok/s,提升约 2.3 倍。

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

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

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

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