先に結論から書きます。RTX 4070 (12GB) で Qwen3.5-35B-A3B を llama.cpp から動かすとき、-ngl 99 --cpu-moe の2フラグだけで手を止めるのはもったいないです。私が同じマシンで追加の3フラグを叩いたところ、34.6 tok/s から 41.2 tok/s まで伸びました。約1.2倍。数字だけ見ると地味ですが、体感の応答時間は明確に短くなります。
「エキスパートを全部CPUに逃がす」という勝ち構成の話は、以前の記事で書きました。あれで確かに Ollama 比 2.8倍にはなります。ただし、その先にはまだ叩けるフラグが残っています。3本です。この記事はそれを実測で追った記録です。
正直に書くと、私は最初 cpu-moe を入れた時点でだいぶ満足していました。「これ以上は量子化を落とすかモデルを変えるかしかないだろう」と決めつけていて、残りのフラグは半分読み流していたのが数週間前です。ある日 llama.cpp のリリースノートを眺めていて、--flash-attn の 4070 対応が明記されているのを見つけ、遊びで叩いてみたら数字が普通に伸びた、というのが今回の記事の出発点です。決めつけで手を止めていた側の記録として読んでもらえるとちょうどよいです。
この記事の位置づけ
区別しておきます。
過去に書いた「cpu-moe で 2.8倍」の記事は、-ngl 99 --cpu-moe -c 4096 という 勝ち構成の入口 の話でした。あれを底として、そこから先を詰めるのが本記事のスコープです。
- 対象:cpu-moe を既に使っている人
- 検証:llama.cpp
llama-benchを-r 3以上で反復、標準偏差付き - 焦点:cpu-moe に追加する3フラグの効き方
- 別軸:量子化レベル(Q4/Q8/FP16)や別モデルの比較はしません
「もっと大きなモデルに乗り換える」「vLLMに変える」といったランタイム替えの話も、今回は扱いません。同じ Qwen3.5-35B-A3B、同じ Q4_K_M、同じRTX 4070 を固定して、フラグだけを動かします。
検証環境(全部そろえたもの)
比較を歪めないために、フラグ以外は全部固定しました。
- マシン:RTX 4070 (VRAM 12GB) / DDR5 64GB / WSL2 Ubuntu 24.04
- CUDA:12.9
- モデル:Qwen3.5-35B-A3B Q4_K_M (20.49 GiB, 34.66B params)
- llama.cpp:2026-07 時点の最新ビルド (CUDA 有効)
-
計測:
llama-bench -n 128 -r 3(tg128、3回反復、標準偏差表記) - 同居プロセス:計測前に VRAM を解放してからスタート
-
ベースライン併走:毎回、素の
-ngl 99 --cpu-moeで 34.6 tok/s ± 0.4 が出ることを確認してから、フラグを足す
計測作法についてはローカルLLM本 (local-llm-qwen-4070) の第4章にまとめました。ここでもその作法をそのまま踏襲します。
一回だけ測って「速くなった」は禁物です。ローカルLLMの計測は、VRAMの空き具合と、思考モードの取り扱いで簡単にブレます。「既知のベースライン値が同じマシンで再現するか」を毎回チェックすると、汚れた環境を最速で見抜けます。
フラグ1:--flash-attn (FA:フラッシュアテンション)
まず1つ目。--flash-attn はアテンション計算をブロック単位でGPUに載せる実装に切り替えます。KVキャッシュのメモリアクセスパターンが変わり、帯域律速のところが速くなります。
私の環境での実測はこうでした。
| 設定 | 生成 tok/s (tg128, r=3) |
|---|---|
-ngl 99 --cpu-moe (base) |
34.6 ± 0.4 |
base + --flash-attn
|
38.9 ± 0.5 |
+4.3 tok/s。伸び幅は約12%です。理由はシンプルです。35B クラスの疎な MoE モデルでも、アテンション自体は密な演算なので、実装差がそのままスループット差に出ます。
注意点は「必ず速くなるとは限らない」ことです。llama.cpp の --flash-attn は、CUDA のバージョンとGPUの世代 (SM) 依存で有効・無効が切り替わります。RTX 4070 (Ada, SM89) では効きましたが、古い GPU では回避が入って速度差がほぼゼロになることがあります。自分の環境で llama-bench を1本走らせて確認するのが早いです。
フラグ2:--cache-type-k q8_0 / --cache-type-v q8_0 (KV量子化)
2つ目は KVキャッシュの量子化です。デフォルトは fp16。生成中に読み書きするだけなら q8_0 で十分に精度を保てます。VRAM の余裕が広がり、その分アテンションの帯域が効きます。
| 設定 | 生成 tok/s (tg128, r=3) |
|---|---|
| base | 34.6 ± 0.4 |
base + --cache-type-k q8_0 --cache-type-v q8_0
|
37.2 ± 0.3 |
+2.6 tok/s (約7.5%)。単独ではフラッシュアテンションほど大きくは伸びませんが、次のバッチサイズと組み合わせると効いてきます。
品質面はどうか。
local-llm-qwen-4070 第5章の標準7問で比較したところ、q8_0 でも全問正解率は落ちませんでした。q4_0 まで落とすと明確に劣化します。境界は q8_0。VRAMギリギリのモデルで cache を軽くしたいときに、私はこの設定を先に入れます。
KVキャッシュのサイズは、コンテキスト長に対して線形に効きます。-c 4096 を -c 8192 に伸ばしたいときは、KV量子化とセットで検討すると、VRAMのあふれが起きにくくなります。
フラグ3:--batch-size 512 --ubatch-size 512 (プロンプト評価の粒度)
3つ目は入力側の話です。生成 (tg128) の tok/s だけを見ると分かりにくいのですが、プロンプトが長くなるほど効きます。--batch-size と --ubatch-size を大きく取ると、prompt eval のスループットが上がり、実際のチャットや agent 用途で「返り始めるまでの体感時間」が縮みます。
llama-bench では pp512 (prompt eval 512トークン) と tg128 の両方を出せます。
| 設定 | pp512 tok/s | tg128 tok/s |
|---|---|---|
| base | 780 ± 20 | 34.6 ± 0.4 |
base + --batch-size 512 --ubatch-size 512
|
1,140 ± 30 | 35.1 ± 0.5 |
pp512 は +46%。生成は誤差の範囲。ただしこれは prompt eval の話で、入力が長いほど差が積み上がります。600トークン程度の入力でも、first-token までの時間の差は体感に出てきます。
--ubatch-size を上げると VRAM 使用量も増えます。RTX 4070 では 512 まではあふれずに済みますが、1024 だと --cpu-moe と組み合わせても限界を超えます。マシン構成ごとに上限は測って探すことになります。
3フラグを全部足したときの実測
ここまでは単独の効きです。全部盛りにするとこうなります。
| 設定 | pp512 tok/s | tg128 tok/s | ベースライン比 |
|---|---|---|---|
| Ollama デフォルト | — | 12.2 ± 0.8 | 0.35倍 |
-ngl 99 --cpu-moe (cpu-moe 単体) |
780 ± 20 | 34.6 ± 0.4 | 1.00倍 |
+ --flash-attn
|
820 ± 15 | 38.9 ± 0.5 | 1.12倍 |
+ --flash-attn + KV q8_0 |
840 ± 20 | 40.4 ± 0.4 | 1.17倍 |
| + 3フラグ全部盛り | 1,190 ± 25 | 41.2 ± 0.4 | 1.19倍 |
生成は 34.6 → 41.2 tok/s。+6.6 tok/s、約1.2倍です。プロンプト評価は 780 → 1,190 tok/s で、こちらは 1.5倍。実運用でのユーザ体感は、この pp512 の伸びのほうが大きく出ます。3秒でチャットが返り始めていたのが、2秒で返ってくる感覚に近いです。
「なぜ雑に真似しても再現しないか」
同じコマンドをコピペしても、この 41.2 tok/s は出ないことがあります。原因はだいたい3つ。
-
VRAMが空いていない:計測の前に Windows 側の VRAM を解放していないと、
--flash-attnを有効にしてもアテンションのブロックが載りきらず、逆に落ちます。私はブラウザを閉じてから測っています。一度、Chrome を開きっぱなしのまま測って「--flash-attnが効かない、環境依存で invisible fallback が入っている」と半日ほど疑っていたのは私です。原因は 200MB 単位で VRAM を持っていく拡張機能でした -
CUDA と llama.cpp のバージョン:CUDA 12.9 と 2026-07時点の llama.cpp の組み合わせで動作確認しました。古い CUDA + 古い llama.cpp では
--flash-attnの分岐が違います - モデルと量子化:Q4_K_M と Q4_0 でもフラッシュアテンションの効き方は微妙に違います。同じ Q4_K_M で測っています
再現できたときにコマンドを添える、再現できないときにはそう書く。これは私が計測の作法として守っている線です。あと、「速いはず」を先に決めつけないこと。Chrome の件で私が身に染みたのはそこでした。
参考:vLLM / mistral.rs はどうか
llama.cpp の外に目を向けると、選択肢は2つです。
- vLLM:GGUF ロードに対応済み。同時リクエスト数が多いサーバ用途なら、連続バッチングでスループットが数倍出ます。ただし単発のチャット用途では起動と VRAM 予約が重く、RTX 4070 の12GBだと 35Bを載せた時点で余裕がほぼないので、私は使い分けています
- mistral.rs:Rust製の推論ランタイム。フラッシュアテンションと ISQ (In-Situ Quantization) を組み合わせやすい。ただし2026-07時点で cpu-moe 相当の柔軟なオフロードは llama.cpp の方が扱いやすいです
「全員 llama.cpp を使え」とは言いません。単発の手元用途で 35B を回すなら、いまのところ llama.cpp + 上記フラグが一番手数少なく速い、というのが私の結論です。
「フラグを叩けばもっと速くなる」の裏側
正直に書いておきます。フラグを積むほど、確実に副作用も増えます。
- KV量子化は精度に影響を出しかねない (q8_0 は 7問で確認できたが、実務コードではもっと難しいタスクで再確認したほうがよい)
- flash-attn は環境依存で invisible に fallback する
- batch-size を上げると VRAM のあふれが近づき、cpu-moe の意味が薄れる
「速くなった」だけを見て採用しないほうがいいです。同じ環境で1本、llama-bench を回して数字を出す。標準偏差を見て、まっすぐ再現するかを確認する。この2つだけで、フラグ選びの精度は跳ね上がります。
私自身、勝ち構成の入口である cpu-moe に到達した時点で、しばらく計測を止めていました。「頭打ちだろう」で満足したところから、こうやってフラグ3つ分の伸びを取りこぼしていたわけです。ローカルLLMは、フラグ1本で局所最適が動きます。定期的に llama-bench を回して、勝ち構成の外側を疑う癖はつけておいたほうが得です。
まとめ
-
-ngl 99 --cpu-moeは最高速ではなく、勝ち構成の入口 -
--flash-attnで +4.3 tok/s (RTX 4070 で確認) -
--cache-type-k q8_0 --cache-type-v q8_0で +2.6 tok/s、コンテキスト長を伸ばす余地も増える -
--batch-size 512 --ubatch-size 512は生成には効かないが、prompt eval が 1.5倍 - 3フラグ全部盛りで生成 41.2 tok/s、pp512 は 1,190 tok/s
-
llama-bench -r 3で標準偏差を見ないと、この差はノイズに埋もれる
私は「速さは3フラグの積み上げで作る」というのを、今回の計測でだいぶ手に馴染ませました。VRAM 12GB の RTX 4070 でも、35B の疎モデルにはまだ伸びしろがあります。手元で llama-bench を1回叩いてみてください。数字が出ると、次にどのフラグを触るかがはっきり見えてきます。面白くいきましょう。
スライドでおさらい
この記事の内容は12枚のスライドにもまとめています。
本文中で参照した計測作法(第4章)と品質確認の標準7問(第5章)は、拙著のローカルLLM本にまとめています。Ollama比 2.8倍の勝ち構成に至るまでの全手順もこちらです。
RTX 4070でQwen 35Bを2.8倍速くする