9月11日、xbillが「Gemma 4 on an 2021 Era 4 GB Laptop GPU: QAT Takes It From 9.5 GiB to 1.6」と題した記事を公開した。9.5GiBのモデルを1.6GiBに収めて、2021年製の4GB GPUで動かす——そんなことが本当に可能なのか、と思う読者も多いだろう。この記事は、GTX 1650 Ti搭載のラップトップという制約の中でGemma 4を実際に動かすまでの手順と、それを可能にした仕組みを詳しく解説している。
9.5GiBのモデルを1.6GiBに押し込む
Googleが公開したGemma 4 E2Bのbfloat16モデルは9.5GiBある。GTX 1650 Ti(4GB)では、int8に変換しても約4.7GiBとなりまだ収まらない。クラウドを使わず手元のラップトップで動かそうとすると、ここで詰む。
それを解決したのがQAT(Quantization-Aware Training)だ。通常の量子化が「学習済みモデルを事後的に圧縮する」のに対し、QATは「4ビットで保存されることを前提として学習させる」手法である。量子化誤差を訓練段階で吸収するため、PTQ(Post-Training Quantization)と比べて精度劣化が小さい点が特徴だ。Googleのモデルカードには次のように記されている。
This model card is for the new versions of the Gemma 4 family optimized with Quantization-Aware Training (QAT), which allows preserving similar quality to bfloat16 while dramatically reducing the memory requirements to load the model.
結果として、ディスク上のファイルサイズは3.35GB。しかし実際にGPUに乗るのは1.31GiBだけで済み、GTX 1650 Tiの4GBに余裕で収まる。デコード速度は73.75 tok/sに達した。
Gemma 4 E2Bとは
本記事で対象とする「Gemma 4 E2B」は、GoogleがGemma 4ファミリーとして公開した約20億パラメータの小型モデルだ。エッジデバイスやオンデバイス推論を想定した設計で、Gemma 4の公式発表ページでもその位置づけが紹介されている。
モデル名の"E"はper-layer embedding(層別埋め込み)を指す独自アーキテクチャに由来する。通常のモデルでは全層が共通の埋め込みテーブルを参照するが、E2Bでは各層が独自のembedding重みを持つ。この構造が、後述するlazy loadingによるメモリ削減の鍵になる。
なぜ3.35GBのファイルが1.31GiBで動くのか
ここが最も面白い部分だ。make infoでGGUFのテンソルテーブルを確認すると、以下の構成になっている。
gemma-4-E2B_q4_0-it.gguf
tensors: 541
total: 3.334 GB
largest tensors:
1926.8 MB per_layer_token_embd.weight Q6_K [8960, 262144] <- LAZY, host-resident
330.3 MB token_embd.weight Q6_K [1536, 262144]
27.5 MB per_layer_model_proj.weight F16 [1536, 8960]
10.6 MB blk.34.ffn_up.weight Q4_0 [1536, 12288]
lazy (never on GPU): 1.927 GB (58% of file)
must be resident: 1.407 GB = 1.31 GiB
ファイルの58%を占める1.93GBのテンソル(per_layer_token_embd)は、GPUに載せる必要がない。E2Bアーキテクチャのper-layer embeddingテンソルはmatmulではなく行ルックアップとして使われるため、llama.cppはこれをTENSOR_READ_LAZYとして扱い、ホスト側のmmapから必要な行だけを読み出す仕組みだ。
QATによる圧縮とこのlazy loadingが組み合わさることで、1トークンのデコードに実際にストリームするデータ量はbfloat16の4.514GBから1.407GBへ、3.2倍の削減になる。
この仕組みを壊す設定が2つある。記事では特にこの2点を強調している。
-nglを下げてはいけない。 フル・オフロードにすると2GiB以上が空きになる。中途半端に下げると実際のmatmul重みがCPUに移り、何も改善しない。--no-mmapを渡してはいけない。 lazy readingはmmapに依存している。これを無効にすると1.93GBのテーブルが実体化され、ロードに失敗する。
実際のGPUメモリ使用量
nvidia-smiで確認した実際の消費は1618 MiB。8192トークンのコンテキストとCUDAのオーバーヘッドを含んでこの数字だ。
nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv,noheader
37073, /home/xbill/llama.cpp/build/bin/llama-server, 1618 MiB
KVキャッシュはわずか60MiBに収まる。llama.cppがGemma 4のスライディングウィンドウ層をコンテキストサイズに関わらず1024トークンウィンドウで上限を設けているためだ。
GTX 1650 Tiの「罠」
記事はGTX 16シリーズ特有の問題も指摘している。コンピュート能力は7.5だが、テンソルコアが搭載されていない。T4も同じ7.5だが、T4にはテンソルコアがある。llama.cppはロード時に自動で検出し、以下の警告を出す。
The following devices will have suboptimal performance due to a lack of tensor cores:
Device 0: NVIDIA GeForce GTX 1650 Ti with Max-Q Design
ベンチマーク結果からは、テンソルコアなし環境でのチューニングの傾向も見えてくる。
- **Flash attentionは無料の+4.8%**(70.39 → 73.74 tok/s)。有効化を推奨。
- KVキャッシュの量子化(q8_0)は逆効果。 デコードで11〜12%、プリフィルで30〜40%の速度低下。テンソルコアがない環境では脱量子化のコストが隠蔽されず、しかもKVキャッシュは60MiBしかなくメモリ節約の旨みもない。
GGML_CUDA_FORCE_MMQも不要。 llama.cppがこのカード向けに提案するが、実測ではむしろわずかに遅い。
MCPでの管理
記事ではllama.cppサーバーを管理するPython製MCPサーバー(server.py)も紹介されている。gpu_status、model_info、start_model_server、stop_model_serverなど7つのツールを持つ単一ファイル構成で、Claude Codeから直接操作できる。
本題のQAT推論とは独立した話題だが、ローカルLLMをMCPエージェントから制御するパターンの実例として参考になる。なお途中でMCP Python SDK 2.xへのアップデートによりクラス名が変わり(FastMCP→MCPServer)サーバーが起動しなくなるトラブルも発生している。複数プロジェクトで同一のシステムPythonを使っている場合は注意が必要だ。
ソースコードはGitHubリポジトリで公開されている。
詳細はGemma 4 on an 2021 Era 4 GB Laptop GPU: QAT Takes It From 9.5 GiB to 1.6を参照していただきたい。