量子化モデル紫陽花は、
サイズを削っても中身は変わるのか

  • 紫陽花は青葉と同じ構造をint8量子化した配布用モデルで、目的は学習の高速化ではなく配布サイズの削減です。
  • weights.binは75.0パーセント小さくなりましたが、同じcheckpoint・同じseedでもfloat32とはlogitsが一致しませんでした。
  • 向日葵は白梅の約4倍規模を狙う量子化モデルの候補で、CPU benchmarkとsmoke runはまだ実行していません。
  • int8への変換はtensorごとにscaleを保存する方式で、Web側は読み込み時にfloat32へ復元してから既存の推論コードをそのまま使います。

関連記事

1. 紫陽花と向日葵、二つの量子化モデルの狙い

最近、モデル規模の計画書へ量子化モデルの系統を書き足しています。 量子化と聞くと学習を速くする話だと思いがちですが、今回はそうではありません。 最初から学習高速化には使わず、推論と配布用のcheckpoint変換、bundleサイズの削減だけを目的にすると決めました。

紫陽花は、青葉相当のサイズをpost-training int8量子化したモデルです1。 学習済みの重みを、あとから8bit整数へ変換する方式のこと。 向日葵は、白梅の約4倍規模を狙う量子化モデルとして位置づけました。

向日葵の第一候補構成は、embed 320、heads 8、blocks 3、MLP 1280で、パラメータ数は3,798,080です。 この数字はまだ確定ではありません。 正式採用の前に、CPU benchmarkとsmoke runで確認する予定です。

紫陽花のprofile定義には、少し引っかかる仕組みを入れました。 紫陽花は青葉とshapeが同じなので、通常のshape自動判定では青葉として扱われます。 量子化bundleをexportするときだけ、profileを明示的に指定して紫陽花として扱う。 同じ形のモデルに、用途によって違う名前を与えるようなものです。

2. int8への変換はどんな仕組みか

2.1. tensorごとにscaleを持たせる

Web bundle exportへ、int8_symmetric形式を追加しました。 float32の重みをそのまま8bitへ丸めると、値の分布によっては誤差が大きくなります。 そこで、tensorごとにscaleを保存し、量子化と復元のときにそのscaleを使う方式にしました2。

2.2. Web推論はfloat32へ戻してから計算する

Web側の推論では、int8の重みを読み込み時にfloat32へ復元します。 復元した後は、既存のJavaScript推論計算をそのまま使う。 推論のコード自体を量子化用に書き換える必要はありませんでした。 このあたりは、腕の見せ所というより、既存資産を壊さない設計の話です。

青葉の20260708-100k-aobaのrecovery checkpoint、step 24,000から、紫陽花bundle20260708-100k-aoba-recovery-024000-ajisai-int8を作成し、Web catalogへ追加しました。

3. サイズは75パーセント減ったのに、生成結果は一致しない

3.1. logitsの差分を数値で見る

紫陽花bundleのweights.binは546,048 bytesでした。 同じcheckpointのfloat32 exportは2,184,192 bytes。 紫陽花は75.0パーセント小さくなった計算です3。

サイズだけ見れば成功ですが、では中身の計算結果はどうなのか。 同一checkpointのfloat32とint8で、JavaScript推論のlogits差分を測定しました。 token IDs [0]ではmax abs diffが0.1007、[0,4,5]では0.1779、[0,4,5,6,7]では0.2688でした。 token列が長くなるほど、差分が積み上がっていくのが見えます。

3.2. 同じseed、同じ温度で生成を比べる

固定temperature 0.8、固定seed 42で、空prompt、「あきの」、「はるの」の生成比較を行いました。 紫陽花int8 bundleでも生成自体はできることを確認しています。 ただ、量子化による差のせいで、同じseedでも青葉float32とは出力が一致しません4。 サイズを削る処理のつもりが、出力そのものまで変えてしまう。 量子化は圧縮ではなく、別の計算経路を作る操作なのだと、あらためて感じました。

4. 向日葵はまだ動かしていない

向日葵については、候補構成を記録した段階で止まっています。 理由は単純で、100,000 step学習キューが今も動いているからです。 CPU競合を避けるため、benchmarkとsmoke runは保留にしました。

学習キューの監視には、1時間ごとに状態を確認するscripts/watch-100k-training-hourly.shを追加しました。 学習プロセスがない場合だけ、未完了runを再開する冪等なスクリプトです。 tmux session waka-100k-hourly-watchで監視を起動したところ、初回の確認時点ですでに既存の学習プロセスが動いていました5。

残っているのは、紫陽花int8の生成品質を追加promptと人手評価で確認することと、向日葵のCPU benchmarkとsmoke runを実行することです。 どちらも、今動いている学習が一段落してからになりそうです。 サイズを削った先に何が起きるかは、数字を並べただけでは半分しかわからない。 実際に動かして、聞き比べてみるしかないのだと思います。

  1. PyTorch公式ドキュメントでは、post-training quantizationは学習済みの重みをあらかじめ量子化しておき、活性化のscaleは較正データから事前に計算する方式だと説明されています。 – Quantization — PyTorch documentation
  2. PyTorch公式ブログでは、per-tensor量子化はテンソル全体に同じscaleを使う方式で、symmetric量子化はzero-pointを0に固定してscaleだけで値を表すと説明されています。 – Practical Quantization in PyTorch
  3. PyTorchの量子化設計ドキュメントでは、8bit整数tensorを使うことでモデルサイズとメモリ帯域を75%削減できるとされています。 – torch_quantization_design_proposal
  4. post-training quantizationを扱う論文では、FP32からINT8への変換は精度低下が小さいか無視できる場合が多いものの、量子化ノイズ自体は残ると指摘されています。 – Post-Training Sparsity-Aware Quantization
  5. tmuxの公式マニュアルでは、セッションはデタッチしても裏で動き続け、切断後も再アタッチできる永続的な仕組みだと説明されています。 – tmux(1) – Linux manual page