MTP(Multi-Token Prediction、マルチトークン予測)は、言語モデルに「次の1トークン」だけでなく「その先の数トークン」も予測させる学習手法です。2024年にMetaのGloeckleらが論文で効果を示し、同年12月のDeepSeek-V3が671B規模の事前学習に採用しました。その後、GLM-5系、Qwen3.5〜3.8、DeepSeek-V4-Flashなど多くの公開モデルがMTP層を同梱しています。
学習で得た追加の予測層は、投機的デコーディングのドラフトに流用すれば別のドラフトモデル無しで生成を速められます。本記事では、2つの実装方式、効く条件、受理率(acceptance rate)の読み方、vLLM・SGLang・llama.cppでの設定を論文とソースコードで確認し、llama.cpp v0.9.0 と Qwen3.5-2B で --spec-draft-n-max を変えて測った速度と受理率も載せます。
なお、CPUの仕様表で「TDP/MTP」と並ぶMTPは、Intelが第12世代Core以降で使っている電力の項目 Maximum Turbo Power(最大ターボ電力)で、本記事のMTPとは無関係です。
まとめ:MTPの要点
- MTPは「次トークン予測の損失」に「数トークン先を当てる損失」を足す学習目標。推論で使うかどうかは別の選択
- 方式は2系統。Metaは共有トランクの上に独立ヘッドを並列に置き、DeepSeek-V3は1トークンずつ逐次につなぐモジュールで因果の連鎖を保つ
- 学習効果は規模とタスクで変わる。Metaの実験では小さいモデルで逆効果、13BでHumanEval・MBPPが12〜17%改善
- 推論ではMTP層をドラフトにする。DeepSeek-V3は2トークン目の受理率85〜90%で1.8倍のTPSを報告
- 使うには学習済みのMTP層が要る。config.json の
num_nextn_predict_layersなどで見分け、vLLMは"method":"mtp"、llama.cppは--spec-type draft-mtpで有効にする - llama.cpp v0.9.0・Qwen3.5-2B(CPU)の実測では、受理率はコード76〜95%、日本語の説明文21〜58%。CPUでは日本語で最大0.53倍に遅くなり、コードも明確には速くならなかった
手元のモデルでMTPを使えるかを知りたいだけなら「MTP層を同梱する公開モデル」と「vLLM・SGLang・llama.cppの設定」の章、速度の目安が知りたいなら「llama.cppでの速度と受理率の実測」の章から読めば足ります。
MTPの定義:次トークン予測に「数トークン先」の損失を足す学習目標
通常のLLMは、位置 i までの文脈から位置 i+1 のトークンだけを当てるように学習します。MTPは同じ位置から i+2、i+3…も予測させ、その誤差を学習信号に加えます。1つの位置から得られる学習信号が増えるので、DeepSeek-V3の技術報告は狙いを「学習信号を密にしてデータ効率を上げる」「将来のトークンを見越した表現を前もって作らせる」の2点と説明しています。
混同されやすいのが投機的デコーディングとの関係です。MTPは学習の目標で、投機的デコーディングは推論のアルゴリズムです。MTPで学習した追加の予測層は、推論時に捨てても本体はそのまま動きます。捨てずにドラフト生成に回すと、本体だけで「下書きと検証」を回す自己投機的デコーディングになります。「mtp speculative decoding」「mtp draft speculation」と検索されているのは、この後者の使い方です。投機的デコーディングそのものの仕組みと速くなる条件はSpeculative Decoding(投機的デコーディング)の解説記事で扱っています。
MTPヘッドの2方式:Metaの並列独立ヘッドとDeepSeek-V3の逐次モジュール
並列独立ヘッド(Gloeckle et al. 2024):共有トランク+n本の出力ヘッド
Metaの論文(arXiv:2404.19737、2024年4月30日)は、Transformer本体(共有トランク)の最終表現の上に、n本の独立した出力ヘッドを横並びに置きます。1本目が次トークン、2本目が2トークン先…を担当し、すべてのヘッドが同じ隠れ状態から同時に予測します。語彙への射影行列(unembedding)は全ヘッドで共有します。
実装上の壁はメモリです。語彙数 V は隠れ次元 d よりはるかに大きいため、n本分のロジットと勾配を同時に持つと GPU メモリを圧迫します。論文はヘッドごとに順伝播と逆伝播を順番に済ませ、次のヘッドへ進む前にロジットの勾配を解放する手順を取り、ピーク使用量を O(nV+d) から O(V+d) に抑えました。学習時間は増えないと報告しています。
逐次MTPモジュール(DeepSeek-V3):深さごとの因果関係の保持
DeepSeek-V3(arXiv:2412.19437)は、追加トークンを並列ではなく逐次に予測します。深さ1のモジュールは、本体が位置 i で出した隠れ状態と、位置 i+1 の正解トークンの埋め込みをそれぞれ正規化して連結し、線形射影でもとの次元に戻してから Transformer ブロック1層に通し、位置 i+2 を予測します。深さ2を足すなら、深さ1の出力を同じ手順でもう一段つなぎます。埋め込み層と出力ヘッドは本体と共有します。
報告書は、各深さで完全な因果の連鎖を保つ点がGloeckleらとの違いで、発想はEAGLEに近いと明記しています。DeepSeek-V3の設定は深さ D=1(次トークンに加えて1トークン先だけ)で、MTP損失の重み λ は最初の10Tトークンで0.3、残り4.8Tトークンで0.1でした。
Metaの方式は正解トークンを入力に使わず、論文の最良設定はコードで n=4 でした。現在の公開モデルの多くはDeepSeek-V3型の逐次モジュールを1層だけ持つ構成です。後述の config.json のキー名 num_nextn_predict_layers や、GGUFのテンソル名 blk.N.nextn.* も DeepSeek 系の「NextN」という呼び名に由来します。
PyTorchで書く深さ1のMTPモジュールと損失の合成
DeepSeek-V3型の深さ1モジュールについて、順伝播・損失計算・逆伝播を1回だけ行う最小例です(オプティマイザーによる重み更新は含みません)。報告書ではRMSNormを使いますが、ここではPyTorch 2.2系でも動くようLayerNormに置き換えています。
import torch
import torch.nn as nn
import torch.nn.functional as F
V, H, T, B = 1000, 64, 16, 2 # 語彙・隠れ次元・系列長・バッチ
torch.manual_seed(0)
def block():
return nn.TransformerEncoderLayer(H, nhead=4, dim_feedforward=4 * H, batch_first=True)
class MainModel(nn.Module):
def __init__(self):
super().__init__()
self.embed = nn.Embedding(V, H)
self.trunk = nn.ModuleList([block() for _ in range(2)])
self.norm = nn.LayerNorm(H)
self.head = nn.Linear(H, V, bias=False)
def forward(self, x, mask):
h = self.embed(x)
for blk in self.trunk:
h = blk(h, src_mask=mask, is_causal=True)
return h, self.head(self.norm(h))
class MTPModule(nn.Module):
"""深さ1のMTPモジュール。埋め込みと出力ヘッドは本体と共有する"""
def __init__(self):
super().__init__()
self.norm_h = nn.LayerNorm(H)
self.norm_e = nn.LayerNorm(H)
self.proj = nn.Linear(2 * H, H, bias=False)
self.block = block()
def forward(self, h_prev, next_tok, main, mask):
e = main.embed(next_tok) # 位置 i+1 のトークン埋め込み
z = self.proj(torch.cat([self.norm_h(h_prev), self.norm_e(e)], dim=-1))
z = self.block(z, src_mask=mask, is_causal=True)
return main.head(main.norm(z)) # 位置 i+2 を予測する
main, mtp = MainModel(), MTPModule()
tokens = torch.randint(0, V, (B, T))
L = T - 2 # 2トークン先まで正解がある長さ
mask = nn.Transformer.generate_square_subsequent_mask(L)
h, logits = main(tokens[:, :L], mask)
loss_main = F.cross_entropy(logits.reshape(-1, V), tokens[:, 1:L + 1].reshape(-1))
mtp_logits = mtp(h, tokens[:, 1:L + 1], main, mask)
loss_mtp = F.cross_entropy(mtp_logits.reshape(-1, V), tokens[:, 2:L + 2].reshape(-1))
lam = 0.3 # DeepSeek-V3は最初の10Tトークンで0.3
loss = loss_main + lam * loss_mtp
loss.backward()
print(f"main={loss_main.item():.3f} mtp={loss_mtp.item():.3f} total={loss.item():.3f}")
print("logits", tuple(logits.shape), "mtp_logits", tuple(mtp_logits.shape))
print("MTP params", sum(p.numel() for p in mtp.parameters()))
PyTorch 2.2.2で実行した出力は次のとおりです(乱数シード0・学習前の重み)。
main=7.092 mtp=6.937 total=9.173
logits (2, 14, 1000) mtp_logits (2, 14, 1000)
MTP params 58432
MTPモジュールの入力には「位置 i+1 の正解トークン」を入れ、2トークン先まで正解がある位置だけを使うため、本体へ入力する位置は先頭14トークン分に切り詰めています(系列長16に対して予測位置は14)。損失は本体と同じ交差エントロピーを λ 倍して足すだけで、逆伝播は1回です。追加パラメータは埋め込みと出力ヘッドを共有するぶん小さく、この例では射影とブロック1層の約5.8万個に収まります。
学習効果の実測:MTPが効く規模と効かないタスク
MTPは入れれば必ず賢くなる手法ではありません。両論文の数字を並べると、効く条件がかなり限定されていることが分かります。
Metaは300Mから13Bまで6サイズのコードモデルを同じ計算量で比べ、小さいモデルではMTPが次トークン予測を下回り、規模が大きくなるほど上回る結果を得ました。13Bでは比較対象よりHumanEvalで12%、MBPPで17%多く問題を解いています。一方、数学推論のGSM8K(8-shot)では4トークン予測が全条件で劣り、多肢選択や尤度ベースのベンチマークでも大きな改善は出ていません。
DeepSeekは2つのMoEベースモデルに深さ1のMTPを足して比較しています(推論時はMTPモジュールを捨てるので推論コストは同一)。
| ベンチマーク(スコアは%) | 小型MoE 基準 | 小型MoE +MTP | 大型MoE 基準 | 大型MoE +MTP |
|---|---|---|---|---|
| 規模(総/活性) | 15.7B/2.4B・1.33Tトークン | 228.7B/20.9B・540Bトークン | ||
| HumanEval | 20.7 | 26.8 | 44.5 | 53.7 |
| MBPP | 35.8 | 36.8 | 61.6 | 62.2 |
| GSM8K | 25.4 | 31.4 | 72.3 | 74.0 |
| MMLU | 50.0 | 53.3 | 67.5 | 66.6 |
| NaturalQuestions | 22.7 | 22.3 | 27.2 | 28.5 |
出典はDeepSeek-V3技術報告の表4です。HumanEval・MBPPはPass@1、その他はEM、shot数はHumanEval 0・MBPP 3・GSM8K 8・MMLUとNaturalQuestions 5です。コード生成(HumanEval)の伸びが最も大きく、大型MoEで+9.2ポイントでした。逆にMMLUは大型で0.9ポイント下がり、NaturalQuestionsは小型で0.4ポイント下がっています。
判断はこうです。小規模モデルの事前学習にMTPを入れるなら、対象タスクで次トークン予測と比較してから決める。Metaの実験で悪化したのは小さいモデル側ですが、同論文は30M以下の合成課題でのinduction能力の改善も報告しており、一律に不利とは言えません。知識問題では効果が一貫せず、表4のMMLUは小型で改善、大型で低下しています。
推論での使い方:MTP層をドラフトにする自己投機的デコーディング
Meta型は複数のヘッドが並列に、DeepSeek-V3型はMTPモジュールが本体の隠れ状態と次トークンから候補を作ります。どちらも次の順伝播で本体が候補を検証し、一致した分だけ1回で複数トークン進みます。検証は本体の出力分布に基づくので、正しく実装されていれば出力の品質は通常の生成と変わりません。小型モデルをドラフトに立てる方式と違い、ドラフト用の別モデルを選ぶ必要も、語彙をそろえる必要もありません。
DeepSeek-V3の速度実測と受理トークン数の期待値
DeepSeek-V3の報告では、MTPで予測した2トークン目の受理率は生成トピックをまたいで85〜90%で、TPS(1秒あたりのトークン数)は1.8倍でした。Metaの7B・4トークン予測モデルでは、コードで3提案中平均2.5トークンが受理され3.0倍、テキストで2.7倍です。
先読み数を k、先行候補がすべて受理された条件で次の候補も受理される確率を各位置で一律 α と置くと、1回の検証で進むトークン数の期待値は (1−αk+1)÷(1−α) です。α=0.85・k=1なら1回あたり1.85トークンです。これはトークン数であって速度倍率ではありません。速度に換算するには通常生成と検証・ドラフト生成の所要時間が必要で、DeepSeekの実測1.8倍との差の内訳は報告されていません。
| 受理率 α | k=1 | k=2 | k=3 |
|---|---|---|---|
| 0.60 | 1.60 | 1.96 | 2.18 |
| 0.75 | 1.75 | 2.31 | 2.73 |
| 0.85 | 1.85 | 2.57 | 3.19 |
| 0.90 | 1.90 | 2.71 | 3.44 |
この表は上限の目安で、実際には先の位置ほど受理率が下がります。MTP層の数と、うまく先読みできる段数も一致しません。Qwen3-NextはMTP層1層ですが、モデルカードは num_speculative_tokens に2を推奨しています。一方、vLLMのドキュメントは1から始めるよう勧めています。k を上げるのは、位置ごとの受理率を測って2番目以降も十分に通っていると確認してからにしてください。後述の実測でも、k を上げるほど1候補あたりの受理率は下がりました。
vLLMのログで読む受理率の定義
「acceptance rate」は論文やツールで定義が違うため、数字を比べる前に何を割った値かを確認します。vLLM 0.33.0 の vllm/v1/spec_decode/metrics.py が出すログ項目は次の3種類です。
Avg Draft acceptance rate:受理されたドラフトトークン数 ÷ 提案したドラフトトークン数(百分率で表示)Mean acceptance length:1 + 受理トークン数 ÷ ドラフト回数。本体が必ず1トークン出す分(ボーナストークン)を含むので、1回の検証で何トークン進んだかに相当Per-position acceptance rate:各位置で受理されたトークン数 ÷ ドラフト回数。手前の候補が棄却された影響も含むので、期待値の式の条件付き確率 α とは別物。k=2以上で後ろの位置が落ちていないかはここで見る
DeepSeekの「2トークン目の受理率85〜90%」は k=1 の Per-position の値に相当し、上の期待値の式の α として読めます。Prometheus では vllm:spec_decode_num_accepted_tokens_total を vllm:spec_decode_num_draft_tokens_total で割ると平均受理率になります。llama.cpp の llama-server は応答の timings に draft_n(提案数)と draft_n_accepted(受理数)を返すので、割れば vLLM の Avg Draft acceptance rate と同じ定義の値が出ます。
ドラフトモデル・Medusa・EAGLEとMTPの違い
「本体に予測ヘッドを足して先読みする」手法はMTP以外にもあります。違いは、その追加部分をいつ・何に対して学習するかです。
| 方式 | 追加部分 | 学習のタイミング | 本体の重み |
|---|---|---|---|
| 小型ドラフトモデル | 別モデル1本 | 既存モデルを流用 | 変えない |
| Medusa-1 | 複数の予測ヘッド | 本体完成後 | 凍結 |
| Medusa-2 | 複数の予測ヘッド | 本体完成後 | 一緒に微調整 |
| EAGLE系 | 特徴量を予測する軽量層 | 本体完成後 | 変えない |
| MTP | MTPヘッド/モジュール | 事前学習と同時 | MTP損失込みで学習 |
Medusa(arXiv:2401.10774)とEAGLE(arXiv:2401.15077)は完成済みモデルに追加部分を学習させる方式で、対象モデルに対応した実装と追加学習が必要です。MTPの弱点は裏返しで、学習済みのMTP層が無ければ設定変更だけでは使えない点です。次トークン予測で学習済みのモデルにMTPヘッドを足す研究(arXiv:2502.09419)も、本体の隠れ層が次トークン予測に特化しているため適応は容易でないと報告しています。
既存のMTPヘッドを微調整するFastMTP
MTP層を最初から持つモデルなら、その層だけを追加学習して先読みの精度を上げる道があります。FastMTP(arXiv:2509.18362、2025年9月16日)は、1つのMTPヘッドを位置間で重みを共有したまま再帰的に回す使い方に合わせ、モデル自身が生成したデータでヘッドだけを微調整します。論文の表1(MiMo-7B-RL・7タスク平均・3トークン先読み)では、通常の生成31.55トークン/秒に対し、素のMTPが38.04(1.21倍)、自己生成データでの微調整のみが57.01(1.81倍)、さらにドラフト側の語彙を言語に応じて絞る圧縮を加えると64.12(2.03倍)でした。要旨の「素のMTPより82%速い」は、この表1の同条件から計算できる値(約69%)とは一致しないので、引用するときは表の値を使う方が確実です。
vLLMのドラフト学習ライブラリ speculators(v0.9.0、2026年10月19日)はこの方式をMTPの微調整として取り込んでおり、MTP層だけを学習対象にして埋め込みと出力ヘッドは凍結し、学習後に元のチェックポイントへ書き戻す手順をMTPのドキュメントで案内しています。対象はQwen3-NextやQwen3.5のようにMTP層を同梱するモデルです。なお、同ドキュメント末尾の引用欄は論文名と著者がarXivの登録内容と異なるため、引用するときはarXivの書誌を使ってください。
MTP層を同梱する公開モデルとconfig.json・GGUFでの見分け方
config.json のキーによるMTP層の有無の判定
手元のモデルがMTP層を持つかは、Hugging Face の config.json を見れば大半は判定できます。2026年11月13日時点で各公式リポジトリの config.json を確認した結果です。
| モデル | 確認できる場所 | 値 |
|---|---|---|
| DeepSeek-V3 / V3.2 / V4-Flash | num_nextn_predict_layers | 1 |
| GLM-4.5 / GLM-5 / GLM-5.3 | num_nextn_predict_layers | 1 |
| Ling-3.1-flash-1019 | num_nextn_predict_layers | 1 |
| Step-5-Preview | text_config.num_nextn_predict_layers | 3 |
| Qwen3.5 / Qwen3.6 / Qwen3.8 | text_config.mtp_num_hidden_layers | 1 |
| Qwen3-Next-80B-A3B | 重みの mtp.* テンソル | configにキー無し |
| Gemma 4 | 別リポジトリの assistant チェックポイント | 本体には無し |
| Kimi-K2-Instruct | num_nextn_predict_layers | 0(MTP無し) |
落とし穴は2つあります。Kimi K2はDeepSeek-V3と同じ設定項目を持ちますが値は0で、MTP層がありません。逆にQwen3-Nextは config.json に該当キーが無く、model.safetensors.index.json に mtp.fc.weight などのテンソルが並んでいることで確認できます。Gemma 4のMTPは本体の層ではなく、gemma-4-31B-it-assistant のような別チェックポイント(model_type は gemma4_assistant)を小型ドラフトとして組み合わせる形式です。
パラメータ数の表記にも影響します。DeepSeek-V3のリポジトリは本体671BとMTPモジュール14Bを合わせて配布しており、重みファイルの総量からモデル規模を逆算すると本体より大きく見えます。この点はDeepSeek-V3の解説記事で詳しく扱っています。
GGUFは「本体に同梱」と「mtp- 別ファイル」の2形態
llama.cpp 用の GGUF では、MTP層の置き場所が配布元によって違います。2つのリポジトリの GGUF ヘッダーを読んで比べました。
| ファイル | block_count | MTPのテンソル | サイズ |
|---|---|---|---|
| unsloth Qwen3.5-2B-Q4_K_M.gguf | 25 | blk.24.nextn.*(同梱) | 1.33GB |
| ggml-org Qwen3.6-27B-Q4_K_M.gguf | 64 | 無し | 19.4GB |
| ggml-org mtp-Qwen3.6-27B-Q4_0.gguf | 65 | blk.64.nextn.* のみ | 1.68GB |
unsloth の Qwen3.5-2B は、24層の本体の後ろに25層目としてMTP層を足し、nextn_predict_layers=1 を1ファイルに収めています(公式 config.json も num_hidden_layers: 24・mtp_num_hidden_layers: 1)。unslothはQwen3.5の0.8Bから397B-A17Bまで8サイズでこの同梱版を配布しており、各サイズの必要メモリはQwen3.5のモデル一覧の記事にまとめています。ggml-org の Qwen3.6-27B は本体64層のファイルにMTPを含めず、65層目だけを入れた mtp- ファイルを別に置いています。別ファイル側は埋め込みと出力層も持つので、MTP層1層にしては大きくなります。
同梱型は --spec-type draft-mtp を付けるだけで動き、付けなければMTP層を読まずに通常のモデルとして動きます(起動ログに unused tensor blk.24.nextn... と出ます)。別ファイル型は本体だけではMTPを使えませんが、-hf で取得するときに --spec-type draft-mtp を指定していれば mtp- ファイルも一緒に落ちます。
vLLM・SGLang・llama.cppでMTPを有効にする設定
vLLM 0.33.0:speculative-config の method に mtp を指定
vLLMはMTP対応のモデル系列なら、ドラフトモデルを指定せず方式名だけで有効になります。公式ドキュメントの例はMiMo-7B-Baseです。
vllm serve XiaomiMiMo/MiMo-7B-Base \
--tensor-parallel-size 1 \
--speculative-config '{"method":"mtp","num_speculative_tokens":1}'
モデルカードによっては "method":"qwen3_next_mtp" のようなモデル固有の名前が書かれていますが、vLLM 0.33.0(PyPI公開は2026年11月2日)の vllm/config/speculative.py では、deepseek_mtp・qwen3_5_mtp・glm5_next_mtp・gemma4_mtp など27種類の固有名がすべて非推奨で、警告を出したうえで mtp に読み替えられます。新しく書く設定は mtp にそろえてください。Gemma 4はMTP層が別配布の assistant チェックポイントなので、"method":"mtp" に加えて "model" にそのチェックポイントを指定します。vLLMの全体像はvLLMの仕組みと使い方の記事を参照してください。
SGLang 0.5.23:NEXTN は EAGLE と同じ経路
SGLangでは --speculative-algorithm NEXTN を指定します。v0.5.23(2026年11月2日)の spec_info.py でも NEXTN は予約された別名として EAGLE に解決され、MTP層をドラフトとして回します。公式クックブックの Qwen3.6-35B-A3B-FP8(H200)と DeepSeek-V3.2-Exp の起動例は、いずれも EAGLE を指定して3段先読みにしています。次は Qwen3.6 の例から主な引数を抜き出したものです。
SGLANG_ENABLE_SPEC_V2=1 sglang serve \
--model-path Qwen/Qwen3.6-35B-A3B-FP8 \
--reasoning-parser qwen3 \
--speculative-algorithm EAGLE \
--speculative-num-steps 3 \
--speculative-eagle-topk 1 \
--speculative-num-draft-tokens 4
--speculative-num-steps が先読みの段数、--speculative-num-draft-tokens は検証に回すトークン数(topk=1なら段数+1)です。後ろの段ほど受理率は落ちるので、自分のワークロードでは1段から測り始めてください。
llama.cpp v0.9.0:–spec-type draft-mtp
llama.cpp は2026年11月4日公開のv0.9.0で、--spec-type の値として draft-mtp を持ちます(ほかに draft-simple・draft-eagle3・draft-dflash・ngram-* 系)。--spec-type mtp と書くと起動できません。MTP対応がプルリクエストの段階だった2026年5月ごろの解説には --spec-type mtp と書いたものがあるので注意してください。
llama-server -hf ggml-org/Qwen3.6-27B-GGUF:Q4_K_M \
--spec-type draft-mtp --spec-draft-n-max 2 \
-ngl 99
--spec-draft-n-max は1回に先読みするトークン数の上限で、既定値は3です(common/common.h)。MTP層が1層でも、その層を繰り返し回して上限まで先読みします。
Ollama v0.40.16 は、MTPテンソルを含むモデルを llama.cpp ランナーで動かすとき draft-mtp として起動し、Modelfile の draft_num_predict を --spec-draft-n-max に渡します。公式ライブラリーの qwen3.6:27b-mtp-q4_K_M は draft_num_predict 3 で配布されています。Qwen3.6のタグ構成はQwen3.6-35B-A3Bの記事とQwen3.6-27Bの記事、LM StudioでGemma 4のMTPドラフターを使う手順はLM Studio×Gemma 4の記事にまとめています。
llama.cppでの速度と受理率の実測:–spec-draft-n-max 1〜4の比較
MTPを有効にすれば必ず速くなるわけではありません。受理率と速度を次の条件で計測しました。
- 実行日:2026年11月13日。llama.cpp v0.9.0 を CPU 向けにビルド(Metal 無効)
- マシン:Intel Core i9-9880H(8コア16スレッド)・メモリ16GB。GPUは使わず
-t 8 - モデル:unsloth/Qwen3.5-2B-MTP-GGUF の Q4_K_M(MTP層を同梱した1.33GBのファイル)
- 設定:
llama-server -c 4096 -np 1 --reasoning off、温度0、最大160トークン。MTP無しは--spec-typeを付けない同じファイル - プロンプト:コード生成「Pythonで、整数nを受け取りフィボナッチ数列の最初のn項をリストで返す関数fibと、そのpytestのテスト関数を3つ書いてください。コードだけを出力してください。」と、日本語の説明文「投機的デコーディングとは何か、仕組みと利点と注意点を日本語の文章で説明してください。箇条書きは使わないでください。」
- 回数:各条件3回を、設定の順番を逆にしてもう1巡。速度は計6回の中央値
同じMacでは別の処理も動いており、計測中のロードアベレージは約4〜10でした。速度は同じ条件でも最大1.6倍ほどぶれたので、小さな差は誤差として読んでください。受理率と平均受理長は温度0なら毎回同じ値になり、負荷の影響を受けません。
受理率と平均受理長:コード76〜95%、日本語の説明文21〜58%
| –spec-draft-n-max | コード 受理率 | コード 平均受理長 | 日本語 受理率 | 日本語 平均受理長 |
|---|---|---|---|---|
| 1 | 95.1%(77/81) | 1.95 | 58.0%(58/100) | 1.58 |
| 2 | 87.8%(101/115) | 2.74 | 38.8%(69/178) | 1.78 |
| 3 | 84.4%(114/135) | 3.53 | 26.5%(70/264) | 1.79 |
| 4 | 76.3%(119/156) | 4.05 | 20.6%(71/344) | 1.82 |
受理率は llama-server が返す draft_n_accepted ÷ draft_n、平均受理長はログの mean len(1+受理数÷検証回数)で、どちらも vLLM の Avg Draft acceptance rate・Mean acceptance length と同じ定義です。
コードでは、先読みを深くしても受理率の落ち方が緩やかで、1回の検証で進むトークン数は n-max=4 で4.05まで伸びました。日本語の説明文は n-max=2 で1.78に達したあとほぼ横ばいです。n-max=4 では344トークンを先読みして受理は71トークンで、追加した先読みの大半が捨てられています。同じモデル・同じMTP層でも、受理率は出力の予測しやすさで大きく変わります。
生成速度とメモリ:今回のCPU環境での日本語の文章の速度低下
| –spec-draft-n-max | コード(トークン/秒) | 日本語(トークン/秒) | サーバーのRSS(MiB) |
|---|---|---|---|
| MTP無し | 5.85(1.00倍) | 5.79(1.00倍) | 1,983 |
| 1 | 5.35(0.92倍) | 5.05(0.87倍) | 2,156 |
| 2 | 6.28(1.07倍) | 4.47(0.77倍) | 2,178 |
| 3 | 5.63(0.96倍) | 3.72(0.64倍) | 2,203 |
| 4 | 7.04(1.20倍) | 3.08(0.53倍) | 2,222 |
日本語の説明文は、n-max を上げるほど遅くなりました。n-max=2 以上では6回のうち最も速い回でも、MTP無しの中央値を下回っています。コードは0.92〜1.20倍で、6回の値の幅(n-max=4 でも5.12〜7.47トークン/秒)がMTP無しの5.63〜6.18と重なるため、この環境で速くなったとは言えません。メモリは、MTP用のドラフトコンテキストの分だけ増え、-c 4096 で173〜239MiBでした。コンテキストを長くするほどこの差は広がります。
今回の計測は処理ごとの時間を分けて測っていないため、遅くなった原因は特定できていません。考えられるのは投機的デコーディングの前提とのずれです。投機的デコーディングが速くなるのは、1トークンずつの生成がメモリ帯域で律速され、数トークンをまとめて検証しても1回あたりの時間がほとんど増えない場合です。今回のCPU環境では、検証するトークン数とMTP層の順伝播の分だけ時間が増え、受理の得を打ち消したと見ています。受理率95%のコードでも、n-max=1 では速くなりませんでした。GPUでQwen3.6-27Bなどを動かした第三者の計測ではMTPで生成が速くなった報告が多く、効果はハードウェアに強く依存します。
温度0でのMTP有無による出力文字列の相違
温度0(貪欲デコーディング)で同じプロンプトを流しても、MTPを有効にした出力はMTP無しの出力と一致しませんでした。日本語の説明文は20〜68文字目、コードは140〜226文字目で分岐しています(n-max の設定で位置が違います)。MTP無しの6回は毎回同じ文字列で、MTP有りも同じ設定なら毎回同じ文字列でした。n-max=1と2、3と4がそれぞれ同じ出力になっています。検証で複数トークンをまとめて計算するため、1トークンずつ計算するときと数値がわずかにずれ、僅差の候補で選ぶトークンが入れ替わったと考えられます。投機的デコーディングは理論上は出力分布を変えませんが、量子化モデルの実装では文字列まで同じになるとは限りません。回帰テストで出力の完全一致を見ているなら、MTPの有無を固定してから比べてください。
–spec-draft-n-maxの決め方
実測から言える手順は次のとおりです。まず --spec-draft-n-max 1 で起動し、llama-server のログに出る draft acceptance と mean len を自分のプロンプトで確認します。n-max を1つ上げて mean len がほとんど伸びなければ(上の日本語の例では2→3で1.78→1.79)、それ以上の先読みは計算の無駄なので戻します。コードや定型の文書のように受理率が80%を超える用途では、n-max を2〜3まで上げる価値があります。最後は必ずMTP無しと生成速度を比べ、速くならなければ無効にします。llama.cpp の既定値は3ですが、会話や日本語の文章が中心なら1〜2から始める方が安全です。
MTPを採用すべき場面・見送る場面
推論でMTP層を使うのは、学習済みのMTP層があるモデル(同梱型か、Gemma 4のような別配布型)を vLLM・SGLang・llama.cpp で自前で動かしており、1リクエストあたりの生成速度を上げたい場合です。追加の重みはモデル提供者が一緒に学習済みで、ドラフトモデルを選ぶ手間も語彙の不一致もありません。MTP無効時と --spec-draft-n-max 1〜2 で、受理率・生成速度・プロンプト処理速度・メモリ使用量を同じ負荷条件で比べ、改善を確認できたら採用します。社内でLLMを動かす推論基盤の選定から相談したい場合は、生成AI導入支援で構成の比較からお手伝いしています。
見送るべき場面は次のとおりです。
- 自分の環境とプロンプトでMTP無しより速くならない:今回のCPU(Core i9-9880H)・Qwen3.5-2Bの計測では、日本語の説明文で0.53〜0.87倍に遅くなり、受理率95%のコードでも明確には速くならなかった。GPUや別のモデルでは結果が変わるので、必ず自分の条件で比べる
- 出力の完全一致を前提にしたテストがある:温度0でもMTP有無で文字列が変わった。テストの条件をそろえてから採用する
- MTP層の無いモデル(Llama系・Kimi K2など)を追加学習なしで速くしたい:MTPは設定変更だけでは有効にできない。EAGLE系の学習済みヘッドか小型ドラフトモデルを選ぶ
- Hugging Face Transformersで推論している:Qwen3-Nextのモデルカードは、TransformersではMTPが使えないため速度が要るならvLLMかSGLangを使うよう案内している
推論基盤全体のどこにMTPが位置づくかはLLM推論の仕組みと高速化手法の記事で整理しています。
よくある質問
LLMのMTPとは何の略ですか?CPUの「TDP/MTP」とは違いますか?
LLMのMTPはMulti-Token Prediction(マルチトークン予測)の略で、言語モデルに次の1トークンだけでなく2トークン先、3トークン先も予測させる学習目標を指します。CPUの仕様表で「TDP/MTP」と並ぶMTPはIntelの電力項目 Maximum Turbo Power で、まったく別のものです。
MTPと投機的デコーディングは何が違いますか?
MTPは学習の目標で、投機的デコーディングは推論のアルゴリズムです。追加の予測層は推論時に捨ててもよく、ドラフトに回せば別のドラフトモデル無しで先読みと検証ができます。vLLMではこれを "method":"mtp"、llama.cppでは --spec-type draft-mtp と呼んでいます。
MTPヘッド(MTP head)とは何ですか?
本体の隠れ状態から数トークン先を予測するために追加された層のことです。Metaの方式では独立の出力ヘッド、DeepSeek-V3の方式では射影とTransformerブロック1層からなるモジュールを指し、DeepSeek-V3では本体671Bに対してMTPモジュールは14Bです。GGUFでは blk.N.nextn.* という名前のテンソルとして、本体に同梱されるか mtp- で始まる別ファイルで配布されます。
MTPの受理率(acceptance rate)はどれくらいですか?
タスクとツールで大きく変わります。DeepSeek-V3の報告では2トークン目の受理率が85〜90%で、生成速度は1.8倍でした。llama.cpp v0.9.0とQwen3.5-2Bで測ると、先読み1トークンの受理率はコード生成で95.1%、日本語の説明文で58.0%で、先読みを4トークンにすると76.3%と20.6%まで下がりました。比べるときは、受理数÷提案数なのか、1回に進むトークン数なのか、定義をそろえてください。
DeepSeekのMTPは推論で使えますか?
使えます。DeepSeek-V3・V3.2・V4-Flashの config.json はいずれも num_nextn_predict_layers が1で、公開重みにMTPモジュールが含まれています。vLLMなら --speculative-config '{"method":"mtp","num_speculative_tokens":1}'、SGLangなら --speculative-algorithm EAGLE(または別名の NEXTN)で有効になります。