AI・開発ツール

ローカルLLMを4モデル実測。MoEと密モデルで3〜4倍の速度差

ベンチマーク 0.0% — 答えを1文字も書いていなかった(ローカルLLM 4モデル実測)
mausu110

「新しく落としてきたローカルLLM、GSM8K(小学校レベルの文章題を解かせるベンチマーク)が0.0%だった」

……こんな数字が出たら、普通は「このモデル、算数がまるでダメじゃないか」と思いますよね。私も思いました。そしてモデルを消しかけました。

でも実際に中身を開いてみたら、モデルは答えを間違えていたのではなく、答えを1文字も出力していなかったのです。250問中207問が空っぽ。出力文字数の中央値は0。それでもベンチマークツールは律儀に「12.8%」という数字を出してくれます。

今回は、AMD Ryzen AI MAX+ 395(いわゆるStrix Halo)のミニPC上でローカルLLMを4モデル実測した記録です。前半は「MoEか密モデルか」で生成速度が3〜4倍変わったという分かりやすい話、後半はベンチマークスコアがいかに簡単に嘘をつくかという話です(生成上限を変えるだけで、同じモデルのスコアが0%と87.5%の間を行き来しました)。正直、あまり気持ちのよくない話です。

読み終わるころには、モデル選びのときに真っ先に見るべき項目と、ネットで見かけるベンチマーク数値をどう疑えばいいかが分かるはずです。

今回の実測環境

まずは環境から。今回は追加で何かを買い足したわけではなく、手元の1台で完結しています。

  • AMD Ryzen AI MAX+ 395 / Radeon 8060S(gfx1151) APUとメモリが一体になったいわゆるStrix Halo世代。今回はGPU側に32GBを割り当てています。30B級のモデルが「専用GPUなしで」動いてしまうのがこの構成の面白いところです。
  • Lemonade 11.6.0(内部のランタイムは llama.cpp b10375) ローカルLLMをHTTP API経由で扱えるようにしてくれるラッパーです。無料。ただし後述するとおり、エラーの真因はLemonade側のAPIレスポンスには出てこず、下位のllama.cppの生ログにしか無いことがあるので、そこは覚えておいてください。
  • 測ったモデル4種(+派生) Qwen3.6-35B-A3B、Gemma-4-26B-A4B、Gemma-4-31B(素/MTP版)、Muse-Glimmer-30B。
  • 実測日: 2026-08-13〜15

ハードよりも、後半の「測り方」のほうが再現性のある話だと思います。

結論:パラメータ数が同じでも、密モデルは3〜4倍遅い

先に結論から出します。

生成速度の比較
生成速度の比較
モデル構成生成速度判定1件の所要
Qwen3.6-35B-A3BMoE(活性3B)53.9 tok/s約30秒
Gemma-4-26B-A4BMoE(活性4B)43.1 tok/s約30秒
Gemma-4-31B-MTP密+MTP19.8 tok/s83〜107秒
Muse-Glimmer-30B密(52層)13.4 tok/s71〜88秒
Gemma-4-31B10.8 tok/s114〜143秒

上から下まで、パラメータ数は26B〜35Bとほぼ横並びです。それなのに生成速度は53.9 tok/s と 10.8 tok/s で5倍開きました。

理由はシンプルで、MoE(Mixture of Experts=混合エキスパート)モデルは、35Bのパラメータを全部積んでいても、1トークン生成するたびに実際に計算するのはそのうち3B分だけだからです。工場に35人の職人がいても、1個の部品には3人しか手を出さない、というイメージですね。対して密(dense)モデルは毎回全員が働きます。賢いかどうかは別として、速度だけを見れば勝負になりません

ですので、ローカルLLMを選ぶときは、まず「35B」「30B」といったサイズを見比べる前に、MoEか密かを確認してください。ここを外すと、体感速度が3〜4倍変わります。実運用で「判定1件に30秒」と「2分」では、使う気になるかどうかがまるで違います。

MTPは1.83倍効いた。それでもMoEには届かない

今回いちばん条件がきれいだったのが、Gemma-4-31Bの「素」と「MTP版」の比較です。MTP(Multi-Token Prediction=複数トークン先読み予測)は、1トークンずつ生成する代わりに先の分まで予測してまとめて確定させる高速化手法です。

この2つはチェックポイント(モデルの重み本体)が同一で、違いは mtp ラベルだけ。狙ってもなかなか作れない、理想的なA/B条件でした。

生成速度
素の31B(旧ランタイム b8668)9.4 tok/s
素の31B(新ランタイム b10375)10.8 tok/s
31B + MTP(新ランタイム)19.8 tok/s

MTPの効果は1.83倍、ランタイム更新の効果は1.15倍でした。

ここで一つ、自分への戒めを書いておきます。旧ランタイムの9.4と新しいMTP版の19.8だけを並べると「2.1倍」に見えます。 でも、その差にはランタイム更新分が混ざっている。片方だけ条件を変えて両方測り直さないと、何が効いたのか永遠に分かりません。面倒でも、変えた条件は1つずつです。

そして、MTPで1.8倍にしたところで19.8 tok/s。MoEの53.9 tok/sの半分にも届きません。投機的な先読みでアーキテクチャの差は覆らない、というのが今回の実感でした。

本題:ベンチマークスコアは簡単に嘘をつく

さて、ここからが本題です。速度が測れたので「じゃあ賢さも測ろう」とlm-evalでベンチマークを回したのですが、ここで冒頭の0%に出くわしました。

生成上限を変えるだけで、スコアが0%↔87.5%に振れた

トークン上限とスコア
トークン上限とスコア

同じモデル・同じ問題で、max_gen_toks(1回の応答で生成できるトークン数の上限)だけを変えた結果がこれです。

max_gen_toksIFEvalGSM8K
204837.5%87.5%
lm-eval標準(gsm8k=256)37.5%0.0%
51212.5%50.0%

87.5%と0.0%。モデルは一切変えていません。

原因はこうでした。--apply_chat_template を付けてチャットモデルとして評価すると、モデルは「Question:」のような停止語を出しません。lm-evalは本来その停止語を見て「はい、ここで解答終わり」と早期に打ち切るのですが、それが働かないので必ず上限まで生成し続けます。そして上限が256トークンだと、思考の途中で強制終了され、肝心の答えにたどり着く前に切られてしまう。

0%は「数学ができない」ではなく「答えを言う前に口を塞がれていた」のです。lm-evalの標準値は少数ショット(few-shot)での短い解答を前提にした値なので、長々と考えてから答えるチャットモデルには短すぎる、というわけですね。設定値はタスクのYAMLに書かれているので、lm-evaluation-harness のリポジトリで自分が回すタスクの値を確認してみてください。

スコアが出ていても、8割が空応答のことがある

もっと厄介なのがこちらです。Qwen3.6-35Bを250問×2タスクで本気で測ったところ、こうなりました。

IFEval 12.8% / GSM8K 31.6%

数字だけ見ると「まあ、そこそこ弱いモデルだな」で終わってしまいます。実際、私も一度そう結論を書きかけました。危なかった。中身を開いたらこうです。

IFEval  空応答 207/250 (83%)  出力文字数の中央値 0
GSM8K   空応答 336/500 (67%)  出力文字数の中央値 0

8割が空応答。出力文字数の中央値が0。 生ログには resps: [['']] が延々と並んでいました。推論モデルが思考だけでトークン上限を使い切り、答えを1文字も吐かずに終わっていたのです。

空応答の割合
空応答の割合

つまりこの12.8%は「モデルの実力」ではなく「たまたま思考が短く済んで答えまで到達できた2割の中での正答率」でしかありません。そしてスコアの数字だけを眺めていても、これには絶対に気づけません。

正直、ここでちょっと背筋が寒くなりました。世の中に公開されているベンチマーク数値の中にも、同じ落とし穴を踏んだまま出ているものがあるはずです。悪意ではなく、単に誰も中身を開いていないというだけで。

どう対策したか

そこで、自分のベンチマークには次の3つを義務づけることにしました。

  • 空応答率を必ず併記する。 これが1割を超えたら、そのスコアは無効と判定して測り直します。スコア単独では公開しない、というルールにしました。
  • 生成上限はモデルごとに設定する。 「全部2048にすれば安全」ではありません。停止語が効かない以上、モデルは必ず上限まで生成します。足りているモデルの上限まで上げると、その分だけ無駄に遅くなります。
  • 「必要トークン数」自体を指標として報告する。 答えを出すまでに何トークン考える必要があるかは、そのまま実運用の待ち時間と電気代に直結します。正答率と同じくらい大事な数字だと思います。

番外編:期待が外れた2つのこと

ついでに、うまくいかなかった話も2つ。

並列化はまったく効かなかった

llama-serverの起動ログに n_parallel = 4 とあったので、「4本同時に投げれば4倍とは言わずとも2倍くらいにはなるだろう」と期待して測りました。結果はこれです。

条件1問あたり
逐次44.5秒
並列447.9秒(むしろ微減)

単一GPUのllama.cppは、1リクエストの時点ですでにGPUを使い切っています。飽和しているところに仕事を増やしても、待ち行列が伸びるだけでスループットは上がりませんでした。設定項目があること=効果があること、ではないという良い教訓です。

拡張子が .gguf でも動くとは限らない

Muse-Glimmer-30B-ROCmFPX-GGUF は、素のllama.cppでは動きません。実験的なFP4/FP8のテンソル形式を使っていて、パッチ済みの専用ビルドが必要だとモデルカードに明記されていました(読め、という話ですね)。

厄介だったのは、標準GGUF版も最初は動かず、しかもエラーメッセージが同じだったことです。

llama-server failed to start

これでは何も分かりません。真因は下位ランタイムの生ログを掘って、ようやく出てきました。

unknown model architecture: 'muse-glimmer'

形式ではなく、llama.cppがそのアーキテクチャにまだ対応していないというだけの話でした。

このとき切り分けの決め手になったのが「失敗までの時間」です。

正常ロード(同等サイズ): 約18秒
失敗:                     1.8秒  ← ファイルを読み始める前に落ちている

数十秒かけて失敗するならメモリ不足や読み込み中の問題、数秒で落ちるなら読み始める前=起動処理そのものの問題。同じエラーメッセージでも、所要時間を見るだけで当たりを付けられます。地味ですが、かなり使える指標です。

つまずいたポイントと、これから測る人へのアドバイス

同じことをやろうとしている方に向けて、今回痛い目を見た順にまとめます。

  • スコアを見たら、まず生ログを開く。 空応答率と出力文字数の中央値を見るまで、スコアは信用しないでください。今回の最大の学びはこれです。数字が出ているという事実は、モデルが答えたという証拠にはなりません。
  • lm-evalの標準値をそのまま使わない。 少数ショット前提の値です。チャットモデル+--apply_chat_template の組み合わせでは早期終了が効かず、上限が短いとスコアがゼロに張り付きます。
  • 変える条件は一度に1つ。 ランタイム更新とMTP有効化を同時に測ると、1.83倍が2.1倍に化けます。「良くなった」と喜んだ分の中身が何なのか、必ず切り分けてください。
  • エラーはラッパーのAPIレスポンスで判断しない。 上位のツールは失敗をきれいに丸めてしまいます。真因は下位ランタイムの生ログにあります。
  • 失敗までの所要時間をメモしておく。 「1.8秒で死ぬ」は、それ自体が強い手がかりです。
  • 設定項目の存在を効果と混同しない。 n_parallel = 4 は、4倍速の約束ではありませんでした。

まとめ

3日間かけて分かったことは、大きく2つでした。

1つは、ローカルLLMを選ぶならサイズより先にMoEか密かを見ること。同じ30B級でも3〜4倍、条件によっては5倍の速度差が出ます。MTPのような高速化手法は確かに効きます(1.83倍)が、アーキテクチャの差までは埋めてくれません。

もう1つは、ベンチマークスコアは、測り方ひとつで0%にも87.5%にもなること。しかも8割が空応答でも、数字自体はしれっと出てきます。今後は空応答率を併記しないスコアは自分でも出さないことにしました。

次にやりたいのは、「必要トークン数」を正式な指標に組み込んだ測定です。正答率が同じなら、少ない思考量で答えにたどり着くモデルのほうが、待ち時間の面でも電気代の面でも実運用では強い。そこまで含めて比べられると、ようやく「うちの用途にはこれ」と言えるようになる気がしています。

……それにしても、消さずに生ログを開いておいて本当によかったです。危うく無実のモデルを消すところでした。

ABOUT ME
やまさん
やまさん
へっぽこITエンジニア
ねずみさんを愛するインフラエンジニア。 とにかく最新が好きで、ココロオドル体験を日夜求め続けています! 直近だとドローンにハートを撃ち抜かれてます。 空を自由に飛びたいな♪
記事URLをコピーしました