数の表し方 — FP32からFP8・INT4まで
符号・指数・仮数という3つの区画を1から見て、指数部が「範囲」を、仮数部が「精度」を決めることを押さえます。そこからbfloat16が生まれた理由、FP8にE4M3とE5M2の2種類がある理由、INT8/INT4の等間隔量子化で何が失われるのかへ。最後に、学習と推論でどの型を選び、精度劣化をどう検知するかまで。
実数直線に、有限個の目盛りを打つ
コンピュータが扱えるビット数は有限です。だから「実数を表す」とは、実数直線の上に有限個の目盛りを打ち、そのどれかに丸めることに他なりません。自由度は、目盛りをどこに打つかだけです。
打ち方は大きく2通りあります。等間隔に打つのが固定小数点や整数、原点の近くを細かく、遠くを粗くするのが浮動小数点です。後者は「大きい数ほど誤差も大きくてよい」という割り切りで、相対誤差を一定に保つ代わりに絶対誤差を犠牲にします。科学計算で桁が何十も動く量を扱えるのは、この割り切りのおかげです。
浮動小数点の内部を開ける
浮動小数点は、ビット列を3つの区画に切って使います。符号 が1ビット、指数 が ビット、仮数 が ビット。表す値は
は符号(0が正)、 は指数部に入っている符号なし整数、 はバイアスと呼ばれる下駄で 、 は仮数部を小数として読んだ の値です。先頭の1は、正規化された数なら必ず1なので記録しません(ケチ表現)。おかげで仮数は実質1ビット得をしています。
つまりこの式は、符号で向きを決め、2の何乗かで「おおよその大きさ」を決め、残った で「その累乗と次の累乗のあいだのどこに着地するか」を細かく指定している、ということです。 という科学表記を2進数でやっているだけで、指数部が「」に、仮数部が「」に当たります。
この形から、2つの性質がまっすぐ出てきます。
範囲は指数部の幅で決まる。 表せる最大値はおおよそ で、 が1ビット増えるたびに最大値は2乗、つまり表せる桁数が倍になります。
精度は仮数部の幅で決まる。 隣り合う表現可能な数の相対的な間隔は です。これをマシンイプシロンと呼びます。 なら約 、つまり有効数字7桁ぶん。
これは要するに、隣の表せる数までの隙間が、決まった絶対量ではなくその数自身に対する一定の割合(およそ 分の1)だ、ということ。だから仮数を1ビット増やせば相対誤差は半分になり、その効き方は の付近でも の付近でも変わりません。
範囲は指数、精度は仮数。 この2行が本記事の骨格で、以降の話は全部ここから導けます。
FP32とFP16、そして学習で起きる事故
代表的な形式を並べます。
| 形式 | 符号 | 指数 | 仮数 | 表せる最大値の目安 | 相対精度 |
|---|---|---|---|---|---|
| FP32 | 1 | 8 | 23 | ||
| FP16 | 1 | 5 | 10 | ||
| BF16 | 1 | 8 | 7 | ||
| FP8 (E4M3) | 1 | 4 | 3 | ||
| FP8 (E5M2) | 1 | 5 | 2 |
FP16を見てください。指数が5ビットしかないので、上は65504で打ち止め、下も正規化数では 程度までしか表せません。ところが学習中の勾配には、これより小さい値が普通に現れます。 の勾配はFP16では0になって消えます(アンダーフロー)。0になった勾配は逆伝播の途中で全部を殺すので、学習は静かに止まります。
回避策がロススケーリングです。損失に大きな定数(たとえば )を掛けてから逆伝播し、勾配を更新前に同じ数で割って戻す。勾配の分布ごと表現可能な帯域へ平行移動する操作で、実装としては動きますが、スケール値が大きすぎればオーバーフローし、小さすぎればアンダーフローするので、実行時に自動調整する仕掛けが要ります。FP16の面倒さは、精度ではなく範囲に由来しているわけです。
なぜbfloat16が生まれたのか
そこで出てきた発想が、16ビットの予算配分を変えることでした。bfloat16は指数を8ビット、つまりFP32とまったく同じにし、そのぶん仮数を7ビットまで削っています。
得られる性質は明快です。第一に、表せる範囲がFP32と同一。したがってアンダーフローもオーバーフローも起きず、ロススケーリングが要りません。第二に、FP32との変換が仮数の下位16ビットを切り捨てるだけで済む。ハードウェアでもソフトウェアでも変換がほぼ無料です。
代償は精度で、相対誤差は約0.8%。FP16の0.1%と比べて一桁粗い。それでも学習が回るのはなぜか。ここが設計思想の核心です。
- 勾配やAdamの二次モーメントのような量は、層やパラメータによって大きさが何桁も違う。範囲が足りないと値そのものが消滅する(回復不能)
- 一方、重みの更新はミニバッチ内の多数のサンプルの平均であり、丸め誤差は方向がばらつくため統計的に打ち消し合う(致命傷になりにくい)
つまり学習という計算は、範囲の不足には脆く、精度の不足には強い。bfloat16はその非対称性にビット予算を合わせた形式です。
FP8に2種類ある理由
8ビットまで削ると、指数と仮数の綱引きがさらに露骨になります。だからFP8には最初から2つの型が用意されました。
- E4M3(指数4・仮数3):範囲は狭いが精度は相対的に良い。順伝播の活性値や重みのように、値が比較的そろっている量に使う
- E5M2(指数5・仮数2):精度は粗いが範囲が広い。勾配のように何桁もばらつく量に使う
同じ8ビットでも、何を表すかで区画の配分を変える。 「範囲は指数、精度は仮数」という最初の2行が、そのまま製品設計になっている例です。
整数フォーマットと量子化
INT8やINT4は等間隔の目盛りです。実数へ戻すには、スケール とゼロ点 を使って
が格納された整数、 は1目盛りの幅、 は実数の0に対応する整数値です。 を0に固定するのが対称量子化、動かせるようにするのが非対称量子化。 をテンソル全体で1つ持つのがper-tensor、出力チャネルごとに持つのがper-channel、数十要素のグループごとに持つのがgroup-wiseで、後ろに行くほど誤差は減りメタデータは増えます。
つまりこの式がやっているのは、格納された整数を「ものさしの何番目の目盛りか」として読み、実数の0に当たる目盛りの番号を引き、目盛り1つぶんの実寸を掛け直す、という操作です。目盛りを数えて長さに直しているだけで、元の実数についてこの形式が知っている情報は と の2つの定数に全部入っています。
浮動小数点と比べたときの整数の強みは、指数に予算を割かないことです。値が狭い範囲に集中しているなら、等間隔の目盛りのほうが同じビット数で細かく刻めます。ニューラルネットの重みは概ね0付近に集中した分布なので、この条件に合っている。逆に弱点も同じところから来ます。ひとつでも外れ値があると がそれに引っ張られ、大多数の値が数目盛りに潰れます。活性値の外れ値チャネルが量子化の難所として知られているのは、この構造のためです。
量子化で何が失われるのか
(LLMを実際に量子化する手順 — スケールとゼロ点の決め方、PTQとQAT、外れ値の扱い — は LLM量子化を1から解説 で扱います。ここでは「型そのもの」の話に集中します。)
失われるものは2種類あり、トレードオフの関係にあります。
分解能の誤差:刻み幅 の一様量子化では、誤差はおおむね の一様分布になり、その分散は です。ビットを1つ増やすと は半分、誤差の電力は4分の1、SNRにして約6 dB改善する——これは信号処理で古くから知られる「1ビット=6 dB」則です。
クリッピングの誤差:表現範囲の外に出た値を端に丸めることで生じます。範囲を広く取れば分解能が粗くなり、狭く取ればクリッピングが増える。キャリブレーションとは、この2つの誤差の和が最小になる範囲を実データから決める作業にほかなりません。
情報の観点では、量子化は不可逆に情報を捨てる操作です(情報理論)。捨て方が上手なら知覚や予測にはほとんど影響しない、という構図は画像圧縮と共通しています。JPEGのブロックノイズで「ある点までは気づかないが、越えると急に壊れる」振る舞いを体感しておくと、INT4の議論が身体に入ります。
現場ではこう使う
学習でどの型を選ぶか。 既定はBF16の混合精度です。行列積をBF16で実行しつつ、マスター重みはFP32で保持します。理由は明快で、更新量が重みに対して相対的に小さいと、BF16の仮数7ビットでは足し込みが丸められて更新が完全に消えるからです。加えて、総和で桁落ちしやすい演算——正規化層の統計量、softmax、損失計算、勾配のリダクション——はFP32で行うのが定石。PyTorchなら torch.autocast(device_type='cuda', dtype=torch.bfloat16) が既定の入口で、FP16を選ぶ場合のみ torch.amp.GradScaler によるロススケーリングを併用します。
推論でどの型を選ぶか。 判断軸はメモリの壁で見た律速資源です。1トークンずつ生成するデコードは帯域律速なので、重みだけをINT4/INT8にして活性はBF16のまま(weight-only量子化)にすると、転送バイトが減って直接効きます。プロンプトを一括処理するプレフィルは演算律速なので、活性も8ビットにするW8A8のように演算そのものを軽くする手が効く。同じモデルでも段階によって最適な型が違います。
精度劣化に気づく指標。 タスク精度だけを見ていると壊れに気づきません。実務では次を併用します。
- 元モデルの出力分布とのKLダイバージェンス。少数のプロンプトで取れて、感度が高い
- パープレキシティの差。絶対値ではなく量子化前との差分を見る
- 層ごとの出力の余弦類似度。どの層で崩れたかが特定できる
- 活性の最大値・外れ値チャネルの一覧。飽和しているチャネルが劣化の犯人になりやすい
inf/NaNの発生数。学習時は勾配ノルムのヒストグラムも
知らないと事故になる落とし穴
- 量子化したのに速くならない。よくある原因は、演算のたびに整数を浮動小数点へ戻している(デクォンタイズがボトルネック)か、その形式に対応したカーネルが選ばれていないこと。プロファイルを取って確認する
- キャリブレーションデータの分布ズレ。本番と違う入力で範囲を決めると、本番でクリッピングが起きます。長文・多言語・特殊記号など、端の入力を必ず混ぜる
- 二重の量子化。すでに量子化されたモデルをさらに落とすと誤差が累積します。可能なら元の重みからやり直す
- KVキャッシュの量子化は別問題。重みより外れ値が出やすく、長文脈ほど崩れやすい。重みと同じ設定で押し切らない
面接ではこう問われます。「BF16とFP16の違いは?」——ビット配分(指数8対5、仮数7対10)、そこから来る範囲と精度の差、そしてFP16はロススケーリングが要るがBF16は要らないという運用上の帰結まで言えれば十分です。「では推論でどちらを使うか」と続いたら、律速資源とカーネルの対応状況で決める、と答えれば話が噛み合います。
まとめ
- 浮動小数点は 。範囲は指数部、精度は仮数部が決める
- FP16の弱点は精度ではなく範囲。勾配のアンダーフローがロススケーリングを必要にした
- bfloat16は指数をFP32と揃えて範囲を確保し、仮数を削った。学習は範囲不足に脆く精度不足に強い
- FP8にE4M3とE5M2があるのは、順伝播と勾配で必要な範囲が違うから
- 整数量子化は等間隔。誤差は分解能とクリッピングの和で、その最小化がキャリブレーション
コメント
コメントにはログインが必要です