ローカルLLMを実用していると、モデルサイズを上げたときにまず気になるのは「回答が出始めるまで」「最後まで出力し終わるまで」の待ち時間です。
この記事では、llama.cpp の MTP ドラフター構成を使い、gemma-4-31B-it 系のGGUFモデルで推論速度がどの程度変わるかを測定しました。
検証環境は Intel GPU + SYCL ですが、主題はハードウェア紹介ではありません。NVIDIA環境ならCUDA、AMD環境ならROCm/HIPやVulkanなど、利用するGPUに合わせてバックエンドを読み替えれば、考え方自体は応用できます。
この記事で扱うこと
- MTP / Speculative Decoding の概要
- llama.cppでMTPドラフターを使う起動方法
- MTPなし / MTPありの同一プロンプト比較
- Intel GPU + SYCLでの実測値
- Vision対応モデルを使う場合の注意点
この記事で扱わないこと
- OpenCodeなど、特定のエージェントソフトウェアへの組み込み
- iGPUとdGPUの役割分担
- Vision入力そのもののベンチマーク
- モデル品質の詳細評価
OpenCodeやiGPU活用の話は面白いテーマですが、MTP構成とは読者層も論点も少し違います。この記事では、まずMTPの構築と効果に絞ります。
MTP / Speculative Decoding とは
通常のLLM推論では、モデルは基本的に1トークンずつ次のトークンを生成します。
MTPやSpeculative Decodingでは、メインモデルとは別にドラフトモデルを使います。ドラフトモデルが先に複数トークンの候補を生成し、メインモデルがそれを検証します。候補が採用されれば、メインモデル単体で1トークンずつ進めるよりも速く生成できます。
ざっくり言えば、次のような構成です。
user prompt
|
v
main model <---- verifies ---- draft model
|
v
accepted tokens
|
v
response
ただし、常に速くなるわけではありません。ドラフトモデルの品質、メインモデルとの相性、ドラフト候補の受理率、GPUバックエンドの性能、コンテキスト長などで効果が変わります。
参考:
- llama.cpp speculative decoding: https://github.com/ggml-org/llama.cpp/blob/master/docs/speculative.md
検証環境
今回の検証では、メインモデルを31Bに固定しました。
全体のハードウェア構成は次の通りです。
| 項目 | 内容 |
|---|---|
| CPU | AMD Ryzen 7 8845HS |
| MEM | DDR5 64GB |
| GPU1 | AMD Radeon 780M Graphics |
| iGPU memory | 8GB割当 |
| GPU2 | Intel Arc Pro B70 |
| dGPU VRAM | 32GB |
| dGPU power limit | 150W |
B70の最大消費電力は、/usr/local/bin/set-b70-tdp-limit.sh をsystemdから呼び出して150Wに制限しています。
理由は、夏場の気温上昇に対する安全マージンを取るためです。PCゲームのように瞬間的な最高性能を狙う用途ではなく、ローカルLLMを長時間安定して動かす用途なので、過大な電力を投入するよりも、発熱と安定性のバランスを優先しています。
私の環境では、スクリプトの中身は次のようなものです。
#!/bin/bash
PCI_ADDR="0000:03:00.0"
TARGET_FILE=$(find /sys/bus/pci/devices/${PCI_ADDR}/ -name "power1_cap" -print -quit 2>/dev/null)
if [ -n "${TARGET_FILE}" ] && [ -f "${TARGET_FILE}" ]; then
echo "Setting TDP limit to 150W for Arc Pro B70 at ${TARGET_FILE}"
echo 150000000 > "${TARGET_FILE}"
exit 0
else
echo "[ERROR] Arc Pro B70 hwmon power1_cap not found under PCI ${PCI_ADDR}" >&2
exit 1
fi
power1_cap はマイクロワット単位なので、150Wは 150000000 として指定します。
PCI_ADDR は環境ごとに変わります。card0 / card1 や hwmon の番号は起動ごとにずれることがあるため、固定の hwmonX を直接書かず、PCIデバイス配下から power1_cap を探すようにしています。
ソフトウェアとモデルの条件は次の通りです。
| 項目 | 内容 |
|---|---|
| llama.cpp | b10073 |
| commit | 91d2fc387 |
| build | IntelLLVM 2026.1.0 for Linux x86_64 |
| oneAPI | 2026.1 |
| backend | SYCL |
| device | SYCL0: Intel(R) Arc(TM) Pro B70 Graphics |
| main model | gemma-4-31B-it-qat-UD-Q4_K_XL.gguf |
| draft model | mtp-gemma-4-31B-it.gguf |
| context | 98304 |
| KV cache | q8_0 / q8_0 |
| threads | 8 |
| flash attention | enabled |
パスは記事用にマスクしています。
/home/<user>/.local/opt/llm/models/gemma-4-31B-it-qat-UD-Q4_K_XL.gguf
/home/<user>/.local/opt/llm/models/mtp-gemma-4-31B-it.gguf
なお、今回B70を選んだ主な理由は次の2点です。
- VRAMが32GBあり、30B級モデルを扱いやすい
- 同程度のVRAM容量を持つNVIDIA / AMD GPUと比べてコストパフォーマンスがよい
もちろん、ソフトウェア対応や周辺ツールの情報量ではNVIDIA CUDA環境の方が有利な場面もあります。今回は、ローカルLLM用途で「大きめのVRAMを比較的安価に確保する」ことを優先しました。
バックエンドの考え方
今回はIntel GPUなのでSYCLを使いました。
llama.cppでは複数のバックエンドが使えます。GPUごとの大まかな選択肢は次のようになります。
| GPU | 候補 |
|---|---|
| Intel GPU | SYCL |
| NVIDIA GPU | CUDA |
| AMD GPU | ROCm/HIP、Vulkan |
| 汎用 | CPU、BLAS、Vulkan |
この記事のコマンドはSYCL向けですが、MTPの考え方はバックエンド固有ではありません。別GPUでは、ビルドオプションとデバイス指定を読み替える必要があります。
参考:
- llama.cpp SYCL backend: https://github.com/ggml-org/llama.cpp/blob/master/docs/backend/SYCL.md
- Intel oneAPI DPC++/C++ Compiler: https://www.intel.com/content/www/us/en/developer/tools/oneapi/dpc-compiler-documentation.html
ビルド方針
SYCL向けのビルドでは、oneAPI環境を読み込んだ上で、llama.cppをSYCL有効でビルドします。
実際の検証環境では、CIで次のような方針を取っています。
-
origin/masterを取得 - oneAPI 2026.1 の
icx/icpxを使用 GGML_SYCL=ONGGML_SYCL_F16=ONGGML_SYCL_DNN=ONGGML_SYCL_GRAPH=ON- Level Zero API対応を有効化
- ビルド成果物は本番バイナリとは分離
概念的には次のようなCMake設定です。
cmake -S llama.cpp -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=/opt/intel/oneapi/2026.1/bin/icx \
-DCMAKE_CXX_COMPILER=/opt/intel/oneapi/2026.1/bin/icpx \
-DGGML_NATIVE=ON \
-DGGML_SYCL=ON \
-DGGML_SYCL_F16=ON \
-DGGML_SYCL_DNN=ON \
-DGGML_SYCL_GRAPH=ON \
-DGGML_SYCL_HOST_MEM_FALLBACK=ON \
-DGGML_SYCL_SUPPORT_LEVEL_ZERO_API=ON
起動前の環境変数
本番起動時と同じく、oneAPI環境を読み込み、SYCL / Level Zero向けの環境変数を設定しています。
source /opt/intel/oneapi/2026.1/oneapi-vars.sh --force
export ONEAPI_DEVICE_SELECTOR=level_zero:gpu
export GGML_SYCL_ENABLE_OPT=1
export GGML_SYCL_USE_LEVEL_ZERO_API=1
export GGML_SYCL_ENABLE_VMM=1
export GGML_SYCL_ENABLE_DNN=1
export GGML_SYCL_ENABLE_FLASH_ATTN=1
export GGML_SYCL_ENABLE_GRAPH=0
export UR_L0_ENABLE_RELAXED_ALLOCATION_LIMITS=1
export LD_LIBRARY_PATH=/home/<user>/.local/opt/llm/bin-sycl/lib:${LD_LIBRARY_PATH:-}
デバイスは llama-server --list-devices で確認します。
Available devices:
SYCL0: Intel(R) Arc(TM) Pro B70 Graphics
MTPなしの起動例
まずは比較用に、ドラフトモデルなしで起動します。
llama-server \
-m /home/<user>/.local/opt/llm/models/gemma-4-31B-it-qat-UD-Q4_K_XL.gguf \
--alias local-text \
--host 127.0.0.1 \
--port 18081 \
-c 98304 \
-ctk q8_0 \
-ctv q8_0 \
--jinja \
-ngl all \
-sm none \
-t 8 \
--device SYCL0 \
--parallel 1 \
-fa 1 \
--metrics
MTPありの起動例
MTPを使う場合は、メインモデルに加えてドラフトモデルを指定します。
llama-server \
-m /home/<user>/.local/opt/llm/models/gemma-4-31B-it-qat-UD-Q4_K_XL.gguf \
-md /home/<user>/.local/opt/llm/models/mtp-gemma-4-31B-it.gguf \
--spec-type draft-mtp \
--spec-draft-n-max 4 \
--spec-draft-p-min 0.8 \
--spec-draft-device SYCL0 \
--spec-draft-ngl all \
--alias local-text-mtp \
--host 127.0.0.1 \
--port 18081 \
-c 98304 \
-ctk q8_0 \
-ctv q8_0 \
--jinja \
-ngl all \
-sm none \
-t 8 \
--device SYCL0 \
--parallel 1 \
-fa 1 \
--metrics
重要なのは以下です。
| オプション | 意味 |
|---|---|
-m |
メインモデル |
-md |
ドラフトモデル |
--spec-type draft-mtp |
MTPドラフターを使う |
--spec-draft-n-max 4 |
ドラフト候補の最大数 |
--spec-draft-p-min 0.8 |
ドラフト候補のしきい値 |
--spec-draft-device SYCL0 |
ドラフトモデルを動かすデバイス |
--spec-draft-ngl all |
ドラフトモデルをGPUへ載せる |
ドラフトモデルを重くしすぎたり、n-max を上げすぎたりすると、逆に遅くなる可能性があります。ここは環境ごとに調整が必要です。
ベンチマーク方法
合成ベンチとして llama-bench も使えますが、この記事では実運用に近い比較として、OpenAI互換APIへ同一プロンプトを投げました。
条件は固定しています。
| 項目 | 値 |
|---|---|
| prompt | 固定 |
| temperature | 0 |
| seed | 42 |
| max_tokens | 1024 |
| thinking | disabled |
| stream | false |
| 実行回数 | 5回 |
| 集計対象 | 2〜4回目 |
| 捨てた値 | 1回目と5回目 |
1回目はウォームアップの影響を受けやすく、5回目は終了前の状態変化を避けるため、今回は2〜4回目だけを平均しました。
プロンプトは以下です。
Intel Arc GPUでローカルLLMを運用する際、性能と安定性を両立するための重要事項を5項目で簡潔に説明してください。
リクエスト例です。
{
"model": "local-text-mtp",
"messages": [
{
"role": "user",
"content": "Intel Arc GPUでローカルLLMを運用する際、性能と安定性を両立するための重要事項を5項目で簡潔に説明してください。"
}
],
"temperature": 0,
"seed": 42,
"max_tokens": 1024,
"chat_template_kwargs": {
"enable_thinking": false
},
"stream": false
}
結果
2〜4回目の平均値です。
| 条件 | 平均wall time | 生成速度 | prompt eval | completion tokens | prompt tokens |
|---|---|---|---|---|---|
| MTPなし | 19.21 sec | 20.52 tok/s | 43.20 tok/s | 390 | 45 |
| MTPあり | 11.19 sec | 35.37 tok/s | 46.10 tok/s | 389 | 45 |
MTPを有効にしたことで、生成速度は約1.72倍になりました。
35.37 / 20.52 = 1.72
wall timeは約41.7%短縮されています。
(19.21 - 11.19) / 19.21 = 0.417
今回の条件では、MTPの効果はかなりはっきり出ました。
個別実行結果
参考として、5回分の実行結果も載せます。
| 条件 | run | wall time | 生成速度 | completion tokens | prompt cache |
|---|---|---|---|---|---|
| MTPなし | 1 | 20.85 sec | 20.68 tok/s | 389 | 0 |
| MTPなし | 2 | 19.24 sec | 20.54 tok/s | 390 | 40 |
| MTPなし | 3 | 19.25 sec | 20.52 tok/s | 390 | 40 |
| MTPなし | 4 | 19.13 sec | 20.50 tok/s | 390 | 40 |
| MTPなし | 5 | 19.13 sec | 20.51 tok/s | 390 | 40 |
| MTPあり | 1 | 13.82 sec | 34.08 tok/s | 401 | 0 |
| MTPあり | 2 | 11.23 sec | 35.40 tok/s | 389 | 40 |
| MTPあり | 3 | 11.24 sec | 35.36 tok/s | 389 | 40 |
| MTPあり | 4 | 11.11 sec | 35.36 tok/s | 389 | 40 |
| MTPあり | 5 | 11.13 sec | 35.31 tok/s | 389 | 40 |
1回目はプロンプトキャッシュが効いていないため、他の回より遅くなっています。比較に使うなら、ウォームアップ後の値を見る方が実態に近いです。
draft acceptance
MTPありの2〜4回目では、ドラフト受理率は安定していました。
draft acceptance = 0.65385
221 accepted / 338 generated
mean len = 2.64
ドラフトモデルが生成した候補のうち、約65%が採用されています。この受理率が低いと、ドラフト生成のコストだけが増えて効果が出にくくなります。
今回の構成では、ドラフトモデルとメインモデルの相性がよく、MTPの恩恵を受けられたと判断できます。
llama-benchの参考値
同じ31Bモデルで、llama-bench の合成ベンチも取得しています。
| test | tokens/sec |
|---|---|
| pp512 | 451.18 tok/s |
| tg128 | 21.71 tok/s |
tg128 の値は、MTPなしAPIベンチの生成速度 20.52 tok/s と近い値になっています。合成ベンチだけでなく、実際のAPI呼び出しでも近い傾向が出ている点は確認材料になります。
VRAMが少ない場合の参考: 26B A4B
31BモデルはVRAMに余裕がある環境向けです。VRAMが16GB〜24GB程度の場合は、より小さいモデルを選ぶ方が現実的なことがあります。
そこで参考値として、次の26B A4B構成でも同じベンチを取りました。
main : gemma-4-26B-A4B-it-qat-UD-Q4_K_XL.gguf
draft: mtp-gemma-4-26B-A4B-it.gguf
測定条件は31Bと同じです。
- 同一プロンプト
temperature: 0seed: 42max_tokens: 1024stream: false- 5回実行し、1回目と5回目を除外
- 2〜4回目の平均を採用
結果は次の通りです。
| 条件 | 平均wall time | 生成速度 | prompt eval | completion tokens | prompt tokens |
|---|---|---|---|---|---|
| 26B MTPなし | 10.15 sec | 51.60 tok/s | 87.22 tok/s | 519 | 45 |
| 26B MTPあり | 10.13 sec | 63.18 tok/s | 85.94 tok/s | 634 | 45 |
26Bでも、MTPありでは生成速度が約1.22倍になりました。
63.18 / 51.60 = 1.22
一方で、wall timeはほぼ同じです。これはMTPありの方が出力トークン数が多かったためです。
MTPなし: 519 completion tokens
MTPあり: 634 completion tokens
同一プロンプト、同一seedでも、MTP有無で完全に同じ文章になるとは限りません。そのため、wall timeだけを見ると誤解します。このケースでは、1秒あたりの生成トークン数は伸びていますが、生成されたトークン数も増えたため、総時間としてはほぼ同等になりました。
MTPありの2〜4回目では、ドラフト受理率は次の通りです。
draft acceptance = 0.66163
350 accepted / 529 generated
mean len = 2.72
26B A4Bは、31Bより小さいモデルを使いたい場合の候補になります。ただし、今回の測定では「MTPでwall timeが大きく短縮された」とは言い切れません。生成速度は伸びたものの、出力長の差で相殺された、という見方が妥当です。
Vision対応モデルで使う場合
Vision対応モデルを使う場合、テキストモデル本体とは別に mmproj が必要になることがあります。
今回の環境では、次のようなファイルを使います。
/home/<user>/.local/opt/llm/models/gemma-4-31B-mmproj/mmproj-BF16.gguf
Vision対応で起動する場合は、MTPありの起動例に --mmproj を追加します。
llama-server \
-m /home/<user>/.local/opt/llm/models/gemma-4-31B-it-qat-UD-Q4_K_XL.gguf \
-md /home/<user>/.local/opt/llm/models/mtp-gemma-4-31B-it.gguf \
--spec-type draft-mtp \
--spec-draft-n-max 4 \
--spec-draft-p-min 0.8 \
--spec-draft-device SYCL0 \
--spec-draft-ngl all \
--mmproj /home/<user>/.local/opt/llm/models/gemma-4-31B-mmproj/mmproj-BF16.gguf \
--alias local-vision \
--host 127.0.0.1 \
--port 18081 \
-c 98304 \
-ctk q8_0 \
-ctv q8_0 \
--jinja \
-ngl all \
-sm none \
-t 8 \
--device SYCL0 \
--parallel 1 \
-fa 1
注意点があります。
MTPは画像理解そのものを高速化する仕組みではありません。画像入力では、画像の前処理や埋め込み生成、projector処理が別途発生します。
一方で、画像入力後のテキスト回答生成フェーズでは、モデルとllama.cpp側の実装が対応していれば、MTPの恩恵を受けられる可能性があります。
この記事ではVisionの性能比較はしていません。あくまで、Vision対応モデルを運用する場合の起動オプションとして紹介するに留めます。
参考:
- llama.cpp multimodal / mtmd: https://github.com/ggml-org/llama.cpp/blob/master/tools/mtmd/README.md
ハマりどころ
メインモデルとドラフトモデルの対応
MTPでは、メインモデルとドラフトモデルの対応関係が重要です。
今回の構成では次の組み合わせに固定しています。
main : gemma-4-31B-it-qat-UD-Q4_K_XL.gguf
draft: mtp-gemma-4-31B-it.gguf
別サイズや別ファミリのドラフトモデルを雑に組み合わせると、起動できなかったり、受理率が落ちたり、速度が出なかったりします。
--spec-draft-n-max は大きければよいわけではない
--spec-draft-n-max は、ドラフト側が先読みする最大トークン数です。
値を大きくすれば常に速くなる、というものではありません。ドラフト候補の検証コストや不採用時の無駄も増えます。
今回の検証では 4 を使いました。
ドラフトモデルの配置デバイス
今回のようにメインモデルとドラフトモデルを同じGPUに載せる場合、次の指定を使いました。
--device SYCL0
--spec-draft-device SYCL0
--spec-draft-ngl all
複数GPU構成やiGPU/dGPU構成では、ドラフトモデルを別デバイスに逃がす設計も考えられます。ただし、その場合はデータ転送やバックエンド差も含めて測定が必要です。
llama.cpp master追従時の挙動変化
llama.cppは開発が速いプロジェクトです。特にSYCL、multimodal、speculative decoding周辺はバージョン差の影響を受ける可能性があります。
記事や検証結果を読むときは、必ず以下を確認した方がよいです。
- llama.cpp build number
- commit hash
- backend
- compiler / CUDA / oneAPI / ROCm などのバージョン
- モデルとドラフトモデルの組み合わせ
まとめ
gemma-4-31B-it 系モデルでMTPドラフターを使ったところ、今回の固定プロンプトでは生成速度が約1.72倍になりました。
| 条件 | 生成速度 | wall time |
|---|---|---|
| MTPなし | 20.52 tok/s | 19.21 sec |
| MTPあり | 35.37 tok/s | 11.19 sec |
MTPは単純な「有効化すれば必ず速くなる」機能ではありません。ドラフトモデルとの相性、受理率、GPUバックエンド、起動オプションに依存します。
ただし、条件が合えばローカルLLMの体感速度を大きく改善できます。特に31Bクラスのモデルでは、生成待ち時間の短縮がそのまま使い勝手に効きます。
次回は、OpenCode環境でiGPUを補助LLMに使い、タイトル生成やRAG処理をdGPUから逃がした話を書く予定です。