この記事は tk3.biz のブログ からの転載です。
はじめに
第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 回目: 判断が覆った(と思った)
| 深度 | f16 | q8_0 | 差 |
|---|---|---|---|
| 0 | 16.79 | 18.36 | +9.4% |
| 4,096 | 16.27 | 18.17 | +11.7% |
| 16,384 | 15.05 | 16.54 | +9.9% |
| 32,768 | 14.29 | 15.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_k | type_v | 深度 0 | 深度 32,768 |
|---|---|---|---|
| f16 | f16 | 18.57 ± 0.51 | 15.65 ± 0.23 |
| f16 | q8_0 | 17.60 ± 0.99 | 14.14 ± 0.41 |
| q8_0 | f16 | 17.58 ± 0.35 | 15.10 ± 0.35 |
| q8_0 | q8_0 | 17.71 ± 1.03 | 14.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% ありました。**測ろうとしていた差(約 9%)が、そっくりその中に埋もれていたわけです。
1 本目と 2 本目は別プロセスで、順番に走らせました。その間にウォームアップの状態も、電力もサーマルも変わります。2 本目が有利な条件で走っただけでした。
「q8_0 のほうが速い」という観測は、条件の差ではなく順番の差を見ていたことになります。
帯域律速の仮説も外れていた
ついでに、私が立てた説明も間違っていました。
KV を量子化すれば読み出しは確かに減ります。しかし逆量子化のコストのほうが上回っていました。帯域律速の環境でも、です。
細かい発見として、V を量子化するほうが K より損でした。
| 深度 32,768 | |
|---|---|
| K も V も f16 | 15.65 |
| K だけ q8_0 | 15.10 |
| V だけ q8_0 | 14.14 |
| K も V も q8_0 | 14.06 |
K だけなら劣化 3.5%、V だけだと 9.7%。q8_0 にするなら K から、というのは覚えておく価値がありそうです。
収穫: 深度による劣化は緩やかだった
誤りは誤りとして、新しく分かったこともあります。
| 深度 | tg128 | 深度 0 比 |
|---|---|---|
| 0 | 16.79 | — |
| 4,096 | 16.27 | −3% |
| 16,384 | 15.05 | −10% |
| 32,768 | 14.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 の量子化のほうが損が大きい
検証して「やっぱり正しかった」で終わる話でしたが、途中で間違えた過程のほうが、自分には収穫でした。