GEOZ

4GB 显存跑 Gemma 4:重打包 QAT 权重让解码快 1.12 倍

2026/10/3
4GB 显存跑 Gemma 4:重打包 QAT 权重让解码快 1.12 倍

AIAI Summary (BLUF)

本文手把手教你在只有4GB显存的笔记本GPU(GTX 1650 Ti)上,用重打包的QAT量化GGUF文件运行Gemma 4 E2B模型。通过llama.cpp和CUDA,模型仅占1.2GB显存,解码速度达76 token/秒,比官方GGUF快1.12倍,且完全本地运行,无需云端。

核心洞察

这篇教程最让人意外的地方在于,一台 2021 年的笔记本、一块 4 GB 的 GTX 1650 Ti,居然能跑到 76 tok/s 以上。关键不在于硬件,而在于那个重新打包的 QAT 权重文件——它把 Google 训练时就按 4-bit 网格对齐的权重原样塞进 GGUF,没有二次舍入。我比较怀疑的是这种精确重打包能否推广到其他模型,因为前提是 Google 恰好发布了未量化的 QAT 检查点。


核心结论

  1. 在 4 GB 显存的 GTX 1650 Ti Max-Q 笔记本 GPU 上,重打包的 Gemma 4 E2B QAT 模型实现了 76.14–77.14 tok/s 的生成速度,llama-bench 实测为 81.75 tok/s,比 Google 官方 GGUF 的 73.26 tok/s 快约 1.12 倍。

  2. 重打包文件将全部 275 个层矩阵和嵌入表统一重写为 Q4_0 格式,文件从 3,349,516,256 字节缩小到 2,620,370,912 字节(减小 21.8%),GPU 驻留显存从 1.31 GiB 降至 1.20 GiB。

  3. 质量对比 bf16 基准:重打包版平均 KL 散度为 0.001680,最可能 token 一致率 98.26%,困惑度比为 1.022;而 Google 官方 GGUF 分别为 0.054264、87.18% 和 1.092。

  4. 速度提升的核心原因是 token_embd 同时充当输出层,每生成一个 token 都需完整读取;该表从 Q6_K(330.3 MB)改为 Q4_0(226.5 MB)后减小 31%,直接降低了每 token 的显存带宽需求。

  5. 在这块无 tensor core 的显卡上,将 KV 缓存量化为 q8_0 会损失 12% 解码速度和 20–40% 预填充速度,开启 GGML_CUDA_FORCE_MMQ 反而略慢,降低 -ngl 也得不偿失——三项常见优化在此硬件上均无效。

这个项目想做什么

这个仓库里其他配置都是从租来的硬件上跑 Gemma 4。这一个不一样,它跑在桌子底下那台笔记本上,给一个人对话用。演示的重点是响应速度:短回答的首字在半秒内出现在屏幕上,长回答的流式输出比人阅读还快。

做到这一点靠两件事。QAT 让模型在训练阶段就适应 4-bit 网格,体积因此足够小。把这些训练好的 4-bit 值原样打包进文件、不做任何重新舍入,则让它在速度和精度上都超过官方转换版。先看演示步骤,重打包的原理放在后面讲。

开始之前你需要准备

  • 一块 Nvidia GPU,空闲显存 2 GB 以上。这里用的是 GTX 1650 Ti Max-Q:计算能力 7.5,4096 MiB,40 W 功耗上限,没有 tensor core。
  • 针对你显卡架构编译好的 llama.cpp(仓库里的 make build 会为 sm_75 编译)。
  • 从 Hugging Face 下载重打包的 GGUF:xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf。
  • 大约 3 GB 磁盘空间。

硬件

product_version: Yoga 9 15IMH5
Model name:                              Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
Mem:            15Gi       7.8Gi       1.0Gi       890Mi       7.9Gi       7.4Gi
NVIDIA GeForce GTX 1650 Ti with Max-Q Design, 4096 MiB, 7.5
            Current Power Limit                        : 40.00 W

这台笔记本最早的评测是 2021 年 1 月发的。GTX 16 系列和 T4 数据中心卡共享计算能力 7.5,但 Nvidia 在这块芯片上砍掉了 tensor core,所以每次矩阵乘法都跑在普通 CUDA 核心上。

为什么它能塞进去

Gemma 4 E2B 权重 能塞进 4 GiB?
bf16 9.5 GiB 否
int8 ~4.8 GB 否
Google QAT GGUF,驻留显卡 1.31 GiB 是
重打包 QAT GGUF,驻留显卡 1.20 GiB 是

模型的两个特性补上了这个差距。

QAT。 Google 训练 Gemma 4 这个版本的时候就清楚它会被存成 4 bit,所以 4-bit 副本的质量接近 bf16。从 9.5 GiB 缩下来的大部分体积来自这里。

逐层嵌入留在主机内存。 E2B 里的 E 是一张很大的逐层嵌入表 per_layer_token_embd,llama.cpp 懒加载它:它一直以内存映射方式待在系统内存里,每个 token 只读一行。这张表占了重打包文件的一半,而且完全不占显卡。

第一步:下载重打包的 GGUF

hf download xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf gemma-4-E2B-it-q4_0-exact.gguf \
  --local-dir ~/models/gemma-4-E2B-it-qat-q4_0-exact-v2
sha256sum ~/models/gemma-4-E2B-it-qat-q4_0-exact-v2/gemma-4-E2B-it-q4_0-exact.gguf
419db9a6bf3bc15d770c85ccf9216827aa88c64fe8f3d72fbe5674f48efe2dc8  gemma-4-E2B-it-q4_0-exact.gguf

仓库里还有 gguf_exact.py,这个脚本从 Google 的两个 Hugging Face 下载构建出这个文件,任何人都可以重建并对比哈希。

第二步:配置服务端

tpu.env 是这个配置的配置文件。演示用的就是这些值:

MODEL_PATH=/home/xbill/models/gemma-4-E2B-it-qat-q4_0-exact-v2/gemma-4-E2B-it-q4_0-exact.gguf
MODEL_SHA256=419db9a6bf3bc15d770c85ccf9216827aa88c64fe8f3d72fbe5674f48efe2dc8
N_GPU_LAYERS=99
CONTEXT_SIZE=8192
KV_CACHE_TYPE=f16
FLASH_ATTENTION=1
PARALLEL_SLOTS=1
REASONING=off
设置 原因
-ngl 99 所有 transformer 层都放显卡上
-fa 1 flash attention,在这块卡上解码快 4.8%
-ctk f16 -ctv f16 KV 缓存用全精度;q8_0 在这里会损失 12% 的解码速度
--parallel 1 一个用户独占整块卡
--reasoning off 回答立刻开始;单个请求仍然可以要求思考

make serve 就是用这些参数启动 llama-server。

第三步:启动

演示用了两个小的 shell 封装:gpu 负责启停服务端并检查它跑在哪个设备上,ask 从终端流式发起对话。两个都从 tpu.env 读取所有值。

gpu start
ask -q "hi"
gpu: starting /home/xbill/models/gemma-4-E2B-it-qat-q4_0-exact-v2/gemma-4-E2B-it-q4_0-exact.gguf on 127.0.0.1:8080 (-ngl 99)
gpu: pid 132745, log /home/xbill/gemma4-dev/local-llamacpp-1650ti-2b-q4_0/run/llama-server.log
gpu: waiting up to 180s for http://127.0.0.1:8080/health

gpu: healthy after 4s -- http://127.0.0.1:8080
gpu: device=gpu · pid=132745 · -ngl 99 · mapped: ggml-cuda, libcublas, libcuda, libcudart · /home/xbill/llama.cpp/build/bin/llama-server
Hi! How can I help you today? 😊

4 秒就健康了,因为 llama.cpp 是内存映射文件的。那句 hi 是热身,免得当着观众问第一个问题时还要等磁盘。再开一个终端跑 nvtop,演示第三步要用。

演示第一步:即时回答

ask What is the capital of Australia? One sentence.
The capital of Australia is Canberra.
[wall 0.38 s]

端到端 0.38 秒,来自一块 4 GB 的笔记本 GPU。

演示第二步:接入真实工作流

ask 把标准输入当作素材、把参数当作指令,所以它能直接插进 shell 管道:

git -C ~/gemma4-dev log --oneline -12 | ask summarize what this project has been working on, in 3 bullets
Here is a 3-bullet summary of what this project has been working on:

* **Article Refinement and Content Updates:** Significant effort has been dedicated to rewriting, retitling, and restructuring articles related to v5e QAT builds, ...
* **Benchmark and Index Maintenance:** The project has been actively working on updating and regenerating various benchmarks, indices, and documentation files ...
* **Resource and Environment Configuration:** The work also involves managing and referencing specific hardware/software configurations, ...
[wall 4.26 s]

演示第三步:展示占用

nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv
nvidia-smi --query-gpu=name,memory.used,memory.total,power.draw,temperature.gpu --format=csv
gpu arm
pid, process_name, used_gpu_memory [MiB]
7978, /home/xbill/llama.cpp/build/bin/llama-server, 1488 MiB
name, memory.used [MiB], memory.total [MiB], power.draw [W], temperature.gpu
NVIDIA GeForce GTX 1650 Ti with Max-Q Design, 1493 MiB, 4096 MiB, 40.43 W, 47
device=gpu · pid=7978 · -ngl 99 · mapped: ggml-cuda, libcublas, libcuda, libcudart · /home/xbill/llama.cpp/build/bin/llama-server

服务端占用 1488 MiB,是整块卡的 36%,功耗 40 W。gpu arm 从运行中的进程读取设备信息:加载的 CUDA 库和真实的 -ngl。这个配置有一个纯 CPU 的孪生版本,用同一个文件监听同一个端口,所以光看健康检查是分不出到底哪个设备在回答的。

演示第四步:连续对话

ask 不带参数会打开一个保持上下文的会话:

  • “Explain what quantizing a model means, in three sentences, for an engineer.”
  • “Give me an everyday analogy for it.”
  • “Now as a haiku.”

同一个会话里的第二个回答:

Imagine you have a super detailed blueprint for a house (the high-precision model), where every single measurement is recorded with extreme accuracy (32-bit precision). Quantizing is like printing that blueprint on a smaller, simpler sketch (like an 8-bit drawing), where you only record the most essential measurements, sacrificing a tiny bit of microscopic detail to make the drawing much easier and faster to read and reproduce.

! 清空对话,Ctrl-D 退出。问题尽量问通用话题:小模型被问到自己的训练细节时会自信地编答案。

演示第五步:打开思考模式

ask -T Which is larger, 9.11 or 9.9? Explain.
**9.9 is larger than 9.11.**

Here is the explanation based on comparing the place values:

1. **Whole Numbers:** Both numbers have a whole number part of **9**.
2. **Tenths Place:**
   * In 9.**1**1, the digit in the tenths place is **1**.
   * In 9.**9**, the digit in the tenths place is **9**.
3. **Comparison:** Since 9 is greater than 1, the number 9.9 is larger than 9.11.
[wall 9.36 s]

在终端里,模型的推理过程会先以暗色流式输出,然后才是答案。这也是思考模式默认关闭的原因:这个问题开着思考花了 9.36 秒,而关掉之后一个短回答远不到一秒。

演示第六步:打开浏览器聊天界面

llama-server 自带聊天页面。在同一台机器的浏览器里打开 http://127.0.0.1:8080:不用安装、不用账号,每条回答下面会显示 token 数、耗时和速度。

llama-server 内置聊天页面正在回答

462 个 token 用了 6.0 秒,77.35 token 每秒,跑在这块 4 GB 卡上。

实测速度是多少

聊天页面的数字和服务端自己的计时一致。llama-server 每次响应都会报告这些数据。五次请求,要求 250 词的解释,思考关闭:

{"run":1,"finish":"stop","completion_tokens":302,"predicted_per_second":76.1367071253587}
{"run":2,"finish":"length","completion_tokens":400,"predicted_per_second":76.3564025991796}
{"run":3,"finish":"stop","completion_tokens":366,"predicted_per_second":76.20224702680754}
{"run":4,"finish":"stop","completion_tokens":239,"predicted_per_second":76.87420440850953}
{"run":5,"finish":"stop","completion_tokens":285,"predicted_per_second":77.1396194517872}

服务期间 76.14 到 77.14 token 每秒。llama-bench 不走 HTTP,直接测生成,同一个文件在这块卡上是 81.75 tok/s,Google 的 GGUF 是 73.26。

重打包是怎么做的

QAT 存了什么。 Google 把 QAT 权重发布了两份:一份是现成的 GGUF,一份是解包成 bf16 safetensors 的(google/gemma-4-E2B-it-qat-q4_0-unquantized)。在解包副本里,每一行每 32 个值组成一个块,块内最多有 16 个不同的值:一个从 -8 到 7 的整数级别乘以每块一个步长。这正是 Q4_0 的布局,4-bit 级别加每 32 个值一个 fp16 缩放因子。所以一个 QAT 权重矩阵放进 Q4_0 里,除了步长舍入到 fp16 之外没有任何损失。

Google 的 GGUF 对它做了什么。 它的 transformer 层是 Q4_0,但用的是 llama.cpp 默认步长,也就是块内最大值除以 8。只要某个块的最大值低于级别 8,这个步长就偏离了训练时的步长,值就离开了网格。它的两张嵌入表存成了 Q6_K,完全是另一种格式,而且占了文件的大部分:

gemma-4-E2B_q4_0-it.gguf
  by tensor type (the slot-5 token is q4_0; the file mostly is not):
    Q6_K      2.257 GB  (67.7%)
    Q4_0      1.048 GB  (31.4%)
    F16       0.028 GB  ( 0.8%)
    F32       0.001 GB  ( 0.0%)

重打包做了什么。 gguf_exact.py 逐字节复制 Google 的 GGUF 头部、分词器、聊天模板和设置。它把全部 275 个层矩阵、两张嵌入表和 per_layer_model_proj 全部重写为 Q4_0。对每 32 个值的块,它找到这个块训练时用的步长,也就是让所有值都落在整数级别上的第一个 amax/m(m 从 1 到 8),再用最小二乘精修。如果某个块找不到这样的步长,构建就停下。

gemma-4-E2B-it-q4_0-exact.gguf
  lazy (never on GPU):        1.321 GB  (51% of file)
  must be resident:           1.283 GB = 1.20 GiB

  by tensor type (the slot-5 token is q4_0; the file mostly is not):
    Q4_0      2.603 GB  (100.0%)
    F32       0.001 GB  ( 0.0%)

重建的 46.3 亿个值里,96.84% 解码回完全相同的源值,其余的都在缩放因子的 fp16 舍入范围内。文件从 3,349,516,256 字节缩到 2,620,370,912 字节,小了 21.8%。

为什么重打包更快

生成一个 token 要把所有常驻权重读一遍,所以在这块卡上解码速度取决于每 token 读取的字节数。token_embd 同时充当输出层,所以每生成一个 token 都要读整张表。作为 Q6_K 它是 330.3 MB,作为 Q4_0 是 226.5 MB,小了 31%。

llama-bench,完全卸载到显卡,四轮交替文件顺序,每轮前把卡冷却到 50 °C:

tok/s,4 轮均值 Google GGUF 重打包
生成(tg128) 73.26 81.75
预填充(pp512) 338.26 344.64

生成快 1.12 倍,每一轮都保持在 1.111x 到 1.121x 之间。重打包文件还让显卡轻了 134 MiB:服务端进程占 1480 MiB,而 Google 版是 1614 MiB。

为什么重打包更准

质量检查把每个文件的下一个 token 预测和 bf16 模型对比,用的是 wikitext-2,16 个 512 token 的块,在显卡上评分。KL 散度衡量两个概率分布差多远,零表示完全相同。

Mean PPL(Q)/PPL(base)         :   1.021563 ±   0.005079
Mean    KLD:   0.001680 ±   0.000072
99.0%   KLD:   0.016059
Same top p: 98.260 ± 0.205 %
对比 bf16 Google GGUF 重打包
平均 KL 散度 0.054264 0.001680
最可能 token 相同 87.18% 98.26%
困惑度 / bf16 困惑度 1.092 1.022

Google 的文件大约每八次就有一次选出和 bf16 不同的最可能下一个 token。重打包文件不到五十分之一。QAT 训练出来的权重,就是显卡实际服务的权重。

提示:在这块卡上看起来像加速的设置

在这块卡上实测过,三个都保持关闭:

  • 把 KV 缓存量化成 q8_0 会损失 12% 的解码速度和 20–40% 的预填充速度。没有 tensor core 时反量化藏不住,而且 E2B 在 8192 token 下 KV 缓存本来就小。
  • GGML_CUDA_FORCE_MMQ,llama.cpp 自己建议在没有 tensor core 的卡上开启,实测反而略慢。
  • 降低 -ngl 来省内存 会把真正的矩阵乘法移到 CPU 上,换来的内存这块卡根本不缺。

提示:演示输入保持简短

上下文是 8192 token,预填充大约 345 token 每秒,所以粘 4000 个 token 进去就是 11 秒以上的沉默才开始出第一个字。往 ask 里管道传 commit 摘要,别传大 diff。

对比

在 GTX 1650 Ti 上 Google QAT GGUF 重打包 QAT GGUF
文件大小 3.35 GB 2.62 GB
以 Q4_0 存储的权重占比 31.4% 100%
服务端显卡占用 1614 MiB 1480 MiB
生成,llama-bench 73.26 tok/s 81.75 tok/s
相对 bf16 的平均 KL 散度 0.054 0.0017

那选哪个

对这台笔记本来说,选重打包文件。它在同一个 llama.cpp 构建里用同样的参数加载,唯一的变化是 tpu.env 里的路径。它更小、更快、更接近 bf16,而这块卡本来就没多少余量。

重打包依赖 Google 的解包 QAT 检查点保持发布状态,而且只在源权重训练时就对齐到 4-bit 网格的情况下有效。gguf_exact.py 会拒绝任何块不在网格上的张量。

总结

这篇文章的目标是在一台 2021 年的笔记本、一块 4 GB GPU 上把 Gemma 4 E2B 跑成一个响应迅速的单用户助手,并一步步展示演示过程。方案的关键是 Hugging Face 上的 QAT 发布版本,经过重打包让每个训练好的 4-bit 值原样落进文件。实测结果:

  • 服务期间 76.14 到 77.14 tok/s,llama-server 五次请求测得,聊天页面 77.35 tok/s;llama-bench 里 81.75 tok/s。
  • 一句话回答 0.38 秒,端到端,冷启动 4 秒。
  • 4096 MiB 的卡占用 1488 MiB,功耗 40 W。
  • 生成比 Google GGUF 快 1.12 倍,输出表小了 31%。
  • 按平均 KL 散度比 bf16 近 32 倍;最可能 token 相同率 98.26%,对比 87.18%。
  • 思考模式要花几秒:一个短推理问题 9.36 秒,所以默认关闭。
  • KV 缓存量化和 FORCE_MMQ 在这块卡上更慢。

范围:一台 Lenovo Yoga 9 15IMH5(Core i7-10750H,GTX 1650 Ti Max-Q,4096 MiB,40 W 上限),llama.cpp f95b0d9 为 sm_75 编译,CUDA 13.4,单用户,除注明外思考关闭。演示步骤于 2026-10-02 重跑以获取这里的输出和截图。llama-bench 和 KL 散度数据来自 2026-09-29 的四轮交替运行和 16 × 512 token 的 wikitext-2。KL 散度之外的任务准确率没有测量。

在 4 GB 笔记本 GPU 上用精确重打包的 QAT GGUF 跑 Gemma 4 这套方案,是通过一步步增量验证过的。

参考

常见问题(FAQ)

4GB显存的笔记本GPU真的能跑Gemma 4 E2B吗?

可以。GTX 1650 Ti(4GB)通过重打包的QAT量化GGUF文件,模型仅占1.2GB显存,解码速度达76 token/秒,完全本地运行。

重打包的QAT GGUF为什么比官方GGUF快?

因为它将Google训练时按4-bit网格对齐的权重原样塞进GGUF,没有二次舍入,避免了精度损失和额外计算,因此速度更快。

运行这个模型需要哪些准备?

需要Nvidia GPU(空闲显存2GB以上)、编译好的llama.cpp、从Hugging Face下载重打包的GGUF文件,以及约3GB磁盘空间。

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

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

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

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