tk3.biz
ブログ一覧に戻る Back to Blog

ベンチで「効果を見つけた」と思ったら、測定のぶれだった ベンチで「効果を見つけた」と思ったら、測定のぶれだった

AIローカルLLMベンチマークllama.cpp測定

はじめに

第2回で、KV キャッシュの量子化を 使わないと決めました。理由はこう書いています。

q8_0 は f16 と同等の perplexity を示し品質劣化はほぼ無い。 しかし f16 が収まる環境では f16 のほうが速い。毎トークン逆量子化が走るため。

この判断には弱点がありました。根拠が他所の計測だったことです。 「64K 深度で f16 比 55% まで落ちる」という報告を読んで、 自分の環境では測らずに不採用にしていました。

その後コンテキストを 16K から 64K に広げたので、前提を確かめる必要が出てきました。 測りました。そして一度、間違った結論を出しました。その記録です。

何を測るか

llama-bench には -d(n-depth)があります。 KV キャッシュに指定トークン数を積んだ状態から生成を始めるので、 「会話が長くなったとき、生成がどれだけ遅くなるか」を直接測れます。

llama-bench -hf <model> -ngl 99 -fa 1 -b 8192
            -p 0 -n 128 -d 0,4096,16384,32768

-p 0 でプロンプト処理を外し、生成だけを切り出します。

まず f16 で測り、次に -ctk q8_0 -ctv q8_0 で同じことをやりました。

1 回目: 判断が覆った(と思った)

深度f16q8_0差
016.7918.36+9.4%
4,09616.2718.17+11.7%
16,38415.0516.54+9.9%
32,76814.2915.38+7.6%

全深度で q8_0 が速い。 しかもばらつきも小さい。

このとき、説明まで思いついていました。

この機体は一貫して帯域律速だった。密 27B より MoE が 5.7 倍速いのも、 -b は効くが -ub は効かないのも、全部帯域で説明がついた。 KV を量子化すれば読み出しが減る。帯域律速の環境では q8_0 が有利なのではないか。

筋が通って聞こえます。第2回が参照した報告はおそらく dGPU のもので、 そちらは演算律速だから逆量子化のコストがそのまま損になる。 こちらは帯域律速だから得をする——。

もっともらしい説明ほど危ない、という話をこれから書きます。

引っかかった点

表を眺めていて、深度 0 で 9.4% の差がついているのが気になりました。

深度 0 は KV キャッシュが空です。空のキャッシュを量子化しても、 読むものが無いのだから速度は変わらないはずです。

帯域律速の仮説が正しいなら、差は深いところほど大きくなるべきで、 深度 0 ではゼロに近いはずでした。実際の表は逆で、 深度 0 がいちばん差が大きく、深くなるほど縮んでいます。

説明と観測が合っていません。

2 回目: 測り直した

llama-bench は条件をカンマ区切りで受け取れます。 1 回の起動の中で f16 と q8_0 を交互に、各 5 回ずつ測りました。

llama-bench ... -d 0,32768 -ctk f16,q8_0 -ctv f16,q8_0 -r 5

結果が逆になりました。

type_ktype_v深度 0深度 32,768
f16f1618.57 ± 0.5115.65 ± 0.23
f16q8_017.60 ± 0.9914.14 ± 0.41
q8_0f1617.58 ± 0.3515.10 ± 0.35
q8_0q8_017.71 ± 1.0314.06 ± 0.37

f16 が最速です。 深度 32K で q8_0 比 +11.3%(15.65 対 14.06)、 誤差範囲も重なっていません。

そして深度 0 では、差が消えました。 18.57 ± 0.51 と 17.71 ± 1.03 は範囲が重なります。 KV が空なら量子化は効かない、という物理どおりです。

第2回の判断は正しかったわけです。

何が起きていたのか

1 回目と 2 回目で、同じ f16 / 深度 0 の値が違います。

1 本目: 16.79
3 本目: 18.57   ← 同じ設定なのに +10.6%

同じ設定なのに実行するたび10%動く様子。1本目のf16が16.79、2本目のq8_0が18.36で「q8_0が速い」と誤読、3本目の交互計測ではf16が18.57でq8_0が17.71。同じf16のぶれ幅が16.79から18.57まで広がっている

実行間のばらつきが約 10% ありました。 測ろうとしていた差(約 9%)が、そっくりその中に埋もれていたわけです。

1 本目と 2 本目は別プロセスで、順番に走らせました。 その間にウォームアップの状態も、電力もサーマルも変わります。 2 本目が有利な条件で走っただけでした。

「q8_0 のほうが速い」という観測は、条件の差ではなく順番の差を見ていたことになります。

帯域律速の仮説も外れていた

ついでに、私が立てた説明も間違っていました。

KV を量子化すれば読み出しは確かに減ります。 しかし逆量子化のコストのほうが上回っていました。帯域律速の環境でも、です。

細かい発見として、V を量子化するほうが K より損でした。

深度 32,768
K も V も f1615.65
K だけ q8_015.10
V だけ q8_014.14
K も V も q8_014.06

K だけなら劣化 3.5%、V だけだと 9.7%。 q8_0 にするなら K から、というのは覚えておく価値がありそうです。

収穫: 深度による劣化は緩やかだった

誤りは誤りとして、新しく分かったこともあります。

コンテキスト深度による生成速度の劣化。深度0で16.79、4096で16.27、16384で15.05、32768で14.29と、32Kまで積んで15%減に留まる

深度tg128深度 0 比
016.79—
4,09616.27−3%
16,38415.05−10%
32,76814.29−15%

32K まで積んでも 15% 減です。 第2回が参照した「64K 深度で f16 比 55%」という報告に比べると、 傾きがかなり緩いことになります。

長い会話やコードレビューで深いコンテキストを使っても、 速度面の心配は小さいと言えます。

この 4 点は 1 回の起動の中で深度だけを振った値なので、互いに比較できます。 さっき批判した測り方をここではしていません。

教訓

差が 10% 前後なら、まず測定系のばらつきを確かめる。

効果の大きさを測る前に、同じ条件を 2 回測ってどれだけ動くかを見るべきでした。 それをやっていれば、1 回目の表を見た瞬間に「この差はノイズの範囲だ」と分かりました。

別々に起動したベンチは比較できない。

llama-bench に限らず、プロセスを分けた時点で ウォームアップ・電力・サーマル・キャッシュの状態が揃わなくなります。 比較したい条件は、1 回の起動の中で振る。

-ctk f16,q8_0 -ctv f16,q8_0 -r 5

説明が思いついたときこそ疑う。

今回いちばん危なかったのは、「帯域律速だから q8_0 が有利」という説明が すぐ出てきてしまったことです。しかもこれまでの観測と整合していました。

もっともらしい説明は、間違った数字に説得力を与えてしまいます。 助かったのは、説明のほうが別の予測(深度 0 では差が出ないはず)を持っていたからでした。 その予測が外れていたので、測り直す気になりました。

説明できたと思ったら、その説明が他に何を予測するかを確かめる。 そこが合っていなければ、説明か数字のどちらかが間違っています。

まとめ

  • 過去の判断(KV は f16 のまま)を検証したら、一度は逆の結論が出た
  • 深度 0 で差が出るのは物理的におかしいと気づき、測り直したら元の判断が正しかった
  • 原因は実行間のばらつき 10%。測ろうとした差がその中に埋もれていた
  • 設定は変更なし。深度 32K での劣化は 15% と緩やかという収穫は残った
  • q8_0 を使うなら K から。V の量子化のほうが損が大きい

検証して「やっぱり正しかった」で終わる話でしたが、 途中で間違えた過程のほうが、自分には収穫でした。

関連記事 Related Posts

ローカルLLMのベンチマークの取り方(チェックリスト付き) ローカルLLMのベンチマークの取り方(チェックリスト付き)

ローカルLLMの速度や品質を測るときに、結果を信用できるものにする手順をまとめました。同じ条件でも1割ぶれる、順番で結果が変わる、裏の処理で5倍ぶれる、採点器が壊れている。実際に踏んだ失敗から作った、測る前・測っている間・測ったあとのチェックリストです。 ローカルLLMの速度や品質を測るときに、結果を信用できるものにする手順をまとめました。同じ条件でも1割ぶれる、順番で結果が変わる、裏の処理で5倍ぶれる、採点器が壊れている。実際に踏んだ失敗から作った、測る前・測っている間・測ったあとのチェックリストです。

コンテキストを 128K まで伸ばしたら、壁はメモリではなく待ち時間だった コンテキストを 128K まで伸ばしたら、壁はメモリではなく待ち時間だった

ローカルLLMのコンテキストを32Kから128Kまで伸ばして測りました。予想していたメモリ不足は起きず、理由はモデルの構造にありました。代わりに見えた壁はプロンプトの取り込み時間で、128Kを読ませるだけで21〜24分かかります。途中で自分の自動起動に計測を汚された話も含みます。 ローカルLLMのコンテキストを32Kから128Kまで伸ばして測りました。予想していたメモリ不足は起きず、理由はモデルの構造にありました。代わりに見えた壁はプロンプトの取り込み時間で、128Kを読ませるだけで21〜24分かかります。途中で自分の自動起動に計測を汚された話も含みます。

ローカルLLMを13回測って、測定のほうが19回間違っていた ローカルLLMを13回測って、測定のほうが19回間違っていた

ローカルLLM環境の構築と検証を13本の記事に書くあいだに、測定する側が間違っていた場面が19回ありました。比べ方、採点器のバグ、外からの割り込み、前提の取り違え、動いていなかった安全装置。気づいたきっかけを数えると、半分以上は「ありえない値」か「生データ」でした。測る前のチェックリストにまとめます。 ローカルLLM環境の構築と検証を13本の記事に書くあいだに、測定する側が間違っていた場面が19回ありました。比べ方、採点器のバグ、外からの割り込み、前提の取り違え、動いていなかった安全装置。気づいたきっかけを数えると、半分以上は「ありえない値」か「生データ」でした。測る前のチェックリストにまとめます。