把嵌入表也压到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量化感知训练(Quantization-Aware Training),在训练过程中模拟量化误差,使模型在量化后保持精度。 训练其实已经把 embedding 表也量化到了 4-bit 网格上,只是官方导出时没打包。作者把这个空子钻了,显存直接从 9.8 GiB 干到 2.86 GiB,单用户解码速度翻了 2.3 倍。我比较怀疑的是,八条 prompt 的 token 一致性测试样本太小,实际生产环境里长上下文的表现可能没那么完美。
这篇文章手把手带你走一遍完整流程:把 Google 用 QAT 训练出来的 Gemma 4 E2BGoogle 开源的小型大语言模型,本教程中被部署为推理服务的模型本体,权重挂载路径为 /mnt/models/gemma-4-E2B-it。 压到 4-bit 权重,embedding 表也一起压,然后在一台带 Tesla T4NVIDIA推出的数据中心GPU,基于Turing架构,具有16GB显存。 的 Compute Engine VM 上用 vLLM一个高性能的LLM推理和服务库,为DeepSeek-OCR提供优化的推理能力,支持流式输出和批量处理。 跑起来。我们会拿结果跟 bf16 版本、以及 Google 自己导出的 W4A16权重4位、激活16位的量化方案,常用于压缩模型权重。 版本做对比,同一张卡、同一组 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
核心结论
Google 的 QAT 训练已将 embedding 表量化到 4-bit 网格,但官方导出时未打包;作者通过重打包将 Gemma 4 E2B 的 embedding 表也压到 int4,显存占用从 9.8 GiB 降至 2.86 GiB。
int4 embedding 版本在 Tesla T4 上单用户解码速度达 85.28 tok/s,相比 bf16 的 37.04 tok/s 提升 2.30 倍,相比 Google QAT W4A16 的 62.27 tok/s 提升 37%。
KV cache 容量从 bf16 的 315,974 token 提升至 1,099,362 token,16,384 token 请求的最大并发数达 67.11 倍。
八条贪婪解码测试 prompt 的输出 token 与 bf16 embedding 版本完全一致,但样本量较小,长上下文场景的保真度尚未验证。
加速主要来自输出层:每生成一个 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 GiBembed_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 倍。
版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。
文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。
若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。



