とことんDevOps | 日本仮想化技術のDevOps技術情報メディア

DevOpsに関連する技術情報を幅広く提供していきます。

日本仮想化技術がお届けする「とことんDevOps」では、DevOpsに関する技術情報や、日々のDevOps業務の中での検証結果、TipsなどDevOpsのお役立ち情報をお届けします。
主なテーマ: DevOps、CI/CD、コンテナ開発、IaCなど
読者登録と各種SNSのフォローもよろしくお願いいたします。

LLMでMTPを試す

少し出遅れた感はありますが、LLMのMTPを試すことにしました。

次のOllama公式アカウントの投稿を見たのが、LLMのMTPを試そうと思ったきっかけです。

この投稿はGemma 4 MLXモデルでMTPを用いることで約2倍も高速に動くようになったという投稿でした(私が普段使っているのはM4 Mac miniなので、MLXモデルが高速化されるのは素直に喜ばしいことではありますが)。では、Appleシリコン以外の環境でも使えるGGUFでも高速になるのかなと思って、ベンチマーク比較することにしました。

MTPとは

LLM(大規模言語モデル)の分野におけるMTP(Multi-Token Prediction:複数トークン予測)とは、「次の言葉だけでなく、その先の複数の言葉まで同時に予測する技術」のことです。
従来のLLMが「1文字ずつ(1トークンずつ)」考えて出力していたのに対し、MTPは「数文字先までまとめて」予測するため、処理が効率化されます。

「MTP、良さそうじゃないか、じゃあ全部のモデルでMTPを採用すればいいじゃないか」と言いたいところですが、そういう訳にもいかないようで、どんな構成であってもMTPがささるという訳では無いようです。

ネットでの情報によると、単一ユーザー・batch 1・メモリ帯域制約の強いローカル環境ではMTPのメリットは効きにくいそうです。

MTPは、1回の読み込みで「2〜3文字まとめて予測」しようとします。

たくさんの人が同時に使っている(batch数が多い)ため、メモリからデータを1回読み込む間に、全員分の計算をギチギチに詰め込んで処理できます。MTPで先読みする余裕(余った計算力)が大量にあります。

自分1人しか使っていない場合は、メモリからデータを読み込む「待ち時間」が大半を占め、パソコンの計算能力自体はかなり暇を持て余して(余って)います。

「じゃあ、余った計算力でMTPの先読みをすればいいのでは?」と思うかもしれませんが、MTPで先読みを多くすると、今度は「先読み用のデータ」も追加でメモリから読み込まなければならなくなり、すでに遅いメモリというボトルネックに、先読み用のデータまで追加で要求する形になり、かえって遅延の原因になることになります。

結果として、「1文字ずつ出すのと、MTPで2文字まとめて出すので、全体のスピードがほとんど変わらない(むしろデータの出し入れが増えて遅くなる)」という現象が起きやすいため、「効きにくい」と言われています。

MTPはどのように高速化しているのか

通常のLLMは、1トークン生成→次のトークンを予測→1トークン生成という流れを繰り返しています。一方MTPでは、まず「ドラフトモデル(Draft Model)」が数トークン先まで一気に予測します。

ドラフトモデル
「たぶん次は A B C D と続く」
        ↓
本体モデル
「A B C はそのまま採用」
「D の予測は違ったので、ここから新しく生成」

ドラフトモデルが先読みした結果を、本体モデルがそのまま使える部分だけ採用し、違ったところから続きを生成します。 つまり、ドラフトトークンの採用率が高いほど、生成に要する時間を短縮できる傾向があります。

実際試してみる

次の二つのモデルで比較してみます。どちらもGGUFのQ4_K_M量子化バージョンで比較します。 前情報が正しければ、「MTPがめっちゃ速い」みたいな結果にはならないはずです。

お題はいつものやつを試してみましょうか。

Golangで動くSnake gameを作成。ebitenを利用。たまに画面上に星がランダムに画面内に5つ表示されて、その星をとると100点がもらえる。5000点取ると1UP。敵の蛇はランダムに動いて欲しい。敵の蛇にぶつかると1機消滅。3機消滅でゲームオーバー。ハイスコアを記録。

コード生成に要した時間と動作可否

評価のポイントとして、まずThinkingからコードの生成、コード生成後の利用方法の解説までの出力に要した時間を生成スピードと生成トークン数から算出します。なお、生成されたコードが一発で通るかも一応評価のポイントとします。

一発で動かなかったからローカルLLMはダメだと言っている訳では無くて、仮に動いたらすごいな程度で考えています。

その結果は次の通りです。MTPモデルを採用することで生成時間が半分になるという結果になりました。この結果は先の情報と一見矛盾するように見えますが、ドラフトトークンの採用率が57.6%と高かったことが、生成時間を短縮した最大の要因と考えられます。

モデル 要した時間 生成スピードと生成数 動作可否
Qwen3.5 9B 約 8 分 39 秒 16.57 Token/s 8604 Token 不可
Qwen3.5 9B MTP 約 4 分 11 秒 16.24 Token/s 4083 Token, 57.6% draft tokens accepted 不可

表の要した時間は「全トークン数 ÷ 一秒あたりのトークン数」で算出しています。

今回の場合はドラフトトークンのうち、57.6% と言うかなりの割合で本採用されたことが結果的に成果物の生成が半分の時間で済むようになったという結果につながったようです。

コード生成は構文や定型パターンが多く、次に来るトークンを当てやすい場面があるため、MTPと相性が良かった可能性が考えられます。

一方で分岐の多い推論、創作、長い日本語の説明などでは採用率が落ちる可能性が高いため、このようなものにはMTPがささらないようです。

コード生成が速度向上した一方、トレードオフもありました。

まず生成されたコードはいずれも動作しませんでしたが、コードの差分を分析してもらうとMTPモデルでは次のような問題がありました。

  • ebiten.RunMain(update) は現在のEbitenの通常APIとして不自然で、ebiten.RunGame(game) 形式にする必要があります
  • vector.DrawFilledText はEbiten/vectorには存在しないはずです。文字描画は ebiten/v2/texttext/v2 を使う必要があります
  • for _, enemy := range enemies の中で enemies[i] = nil としており、i が未定義です。
  • livesmaxLives で初期化されていません。初期値0なので、開始直後からゲームオーバー扱いになります
  • プレイヤーの入力処理がありません。蛇が動きません
  • 星を取る処理がありません。スコアが増えません
  • ハイスコアは読み込みだけで、保存処理がありません
  • 敵を nil にした後、描画側で enemy.Body にアクセスするのでpanicする可能性があります

(MTPの高速化の裏側で)今回の検証では、MTPモデルの方がコードの完成度は低い結果となりました。今回のようなコード生成では、MTPにより出力が短く・早く終わった一方で、完成度はむしろ下がった可能性があります。

今回の検証のまとめとしては、MTPで実行時間はほぼ半分になったが、生成されたコードはコンパイル不能に近く、速度向上とコード品質は別問題だったと結論づけることができそうです。

ただ、ここから賢いモデルを使ってコードのバグ修正をすれば、最初のコードの生成が速かった分、完成版の作成が速くなる可能性はあります。品質を取るか時間の節約を取るか、コーディングにMTPを用いるかは悩ましいところですね。

しかし少なくとも今回の検証では、「ローカル環境ではMTPは効きにくい」という一般論とは異なり、コード生成では十分な効果を確認できました。もちろん用途によって結果は変わるでしょうが、「ローカルだから意味がない」と決めつけるのは早そうです。