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 检查点。
核心结论
在 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 倍。重打包文件将全部 275 个层矩阵和嵌入表统一重写为 Q4_0 格式,文件从 3,349,516,256 字节缩小到 2,620,370,912 字节(减小 21.8%),GPU 驻留显存从 1.31 GiB 降至 1.20 GiB。
质量对比 bf16 基准:重打包版平均 KL 散度为 0.001680,最可能 token 一致率 98.26%,困惑度比为 1.022;而 Google 官方 GGUF 分别为 0.054264、87.18% 和 1.092。
速度提升的核心原因是
token_embd同时充当输出层,每生成一个 token 都需完整读取;该表从 Q6_K(330.3 MB)改为 Q4_0(226.5 MB)后减小 31%,直接降低了每 token 的显存带宽需求。在这块无 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 数、耗时和速度。

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 这套方案,是通过一步步增量验证过的。
参考
- local-llamacpp-1650ti-2b-q4_0 | GitHub
- xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf | Hugging Face
- google/gemma-4-E2B-it-qat-q4_0-gguf | Hugging Face
- google/gemma-4-E2B-it-qat-q4_0-unquantized | Hugging Face
- Gemma 4 on an Old 4 GB Laptop GPU: QAT Takes It From 9.5 GiB to 1.6 | dev.to
- Quantization-Aware Training for Gemma 4 | Google
- llama.cpp | GitHub
常见问题(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磁盘空间。
版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。
文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。
若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。



