JA EN
体系アクセラレータ
·無料·11分で読めます

数の表し方 — FP32からFP8・INT4まで

符号・指数・仮数という3つの区画を1から見て、指数部が「範囲」を、仮数部が「精度」を決めることを押さえます。そこからbfloat16が生まれた理由、FP8にE4M3とE5M2の2種類がある理由、INT8/INT4の等間隔量子化で何が失われるのかへ。最後に、学習と推論でどの型を選び、精度劣化をどう検知するかまで。

対象textタスクquantization

実数直線に、有限個の目盛りを打つ

コンピュータが扱えるビット数は有限です。だから「実数を表す」とは、実数直線の上に有限個の目盛りを打ち、そのどれかに丸めることに他なりません。自由度は、目盛りをどこに打つかだけです。

打ち方は大きく2通りあります。等間隔に打つのが固定小数点や整数、原点の近くを細かく、遠くを粗くするのが浮動小数点です。後者は「大きい数ほど誤差も大きくてよい」という割り切りで、相対誤差を一定に保つ代わりに絶対誤差を犠牲にします。科学計算で桁が何十も動く量を扱えるのは、この割り切りのおかげです。

浮動小数点の内部を開ける

浮動小数点は、ビット列を3つの区画に切って使います。符号 ss が1ビット、指数 EEee ビット、仮数 ffmm ビット。表す値は

x=(1)s×2Eb×(1+f)x = (-1)^s \times 2^{E-b} \times (1 + f)
(1)

ss は符号(0が正)、EE は指数部に入っている符号なし整数、bb はバイアスと呼ばれる下駄で b=2e11b = 2^{e-1}-1ff は仮数部を小数として読んだ 0f<10 \le f < 1 の値です。先頭の1は、正規化された数なら必ず1なので記録しません(ケチ表現)。おかげで仮数は実質1ビット得をしています。

つまりこの式は、符号で向きを決め、2の何乗かで「おおよその大きさ」を決め、残った 1+f1+f で「その累乗と次の累乗のあいだのどこに着地するか」を細かく指定している、ということです。1.23×1041.23 \times 10^{4} という科学表記を2進数でやっているだけで、指数部が「×104\times 10^{4}」に、仮数部が「1.231.23」に当たります。

この形から、2つの性質がまっすぐ出てきます。

範囲は指数部の幅で決まる。 表せる最大値はおおよそ 22e12^{2^{e-1}} で、ee が1ビット増えるたびに最大値は2乗、つまり表せる桁数が倍になります。

精度は仮数部の幅で決まる。 隣り合う表現可能な数の相対的な間隔は 2m2^{-m} です。これをマシンイプシロンと呼びます。m=23m = 23 なら約 1.2×1071.2 \times 10^{-7}、つまり有効数字7桁ぶん。

ε=2m\varepsilon = 2^{-m}
(2)

これは要するに、隣の表せる数までの隙間が、決まった絶対量ではなくその数自身に対する一定の割合(およそ 2m2^m 分の1)だ、ということ。だから仮数を1ビット増やせば相対誤差は半分になり、その効き方は 103010^{-30} の付近でも 103010^{30} の付近でも変わりません。

範囲は指数、精度は仮数。 この2行が本記事の骨格で、以降の話は全部ここから導けます。

FP32とFP16、そして学習で起きる事故

代表的な形式を並べます。

形式 符号 指数 仮数 表せる最大値の目安 相対精度 2m2^{-m}
FP32 1 8 23 3.4×10383.4 \times 10^{38} 1.2×1071.2 \times 10^{-7}
FP16 1 5 10 6.5×1046.5 \times 10^{4} 9.8×1049.8 \times 10^{-4}
BF16 1 8 7 3.4×10383.4 \times 10^{38} 7.8×1037.8 \times 10^{-3}
FP8 (E4M3) 1 4 3 4.5×1024.5 \times 10^{2} 1.3×1011.3 \times 10^{-1}
FP8 (E5M2) 1 5 2 5.7×1045.7 \times 10^{4} 2.5×1012.5 \times 10^{-1}

FP16を見てください。指数が5ビットしかないので、上は65504で打ち止め、下も正規化数では 6×1056 \times 10^{-5} 程度までしか表せません。ところが学習中の勾配には、これより小さい値が普通に現れます。10810^{-8} の勾配はFP16では0になって消えます(アンダーフロー)。0になった勾配は逆伝播の途中で全部を殺すので、学習は静かに止まります。

回避策がロススケーリングです。損失に大きな定数(たとえば 2152^{15})を掛けてから逆伝播し、勾配を更新前に同じ数で割って戻す。勾配の分布ごと表現可能な帯域へ平行移動する操作で、実装としては動きますが、スケール値が大きすぎればオーバーフローし、小さすぎればアンダーフローするので、実行時に自動調整する仕掛けが要ります。FP16の面倒さは、精度ではなく範囲に由来しているわけです。

なぜbfloat16が生まれたのか

そこで出てきた発想が、16ビットの予算配分を変えることでした。bfloat16は指数を8ビット、つまりFP32とまったく同じにし、そのぶん仮数を7ビットまで削っています。

得られる性質は明快です。第一に、表せる範囲がFP32と同一。したがってアンダーフローもオーバーフローも起きず、ロススケーリングが要りません。第二に、FP32との変換が仮数の下位16ビットを切り捨てるだけで済む。ハードウェアでもソフトウェアでも変換がほぼ無料です。

代償は精度で、相対誤差は約0.8%。FP16の0.1%と比べて一桁粗い。それでも学習が回るのはなぜか。ここが設計思想の核心です。

つまり学習という計算は、範囲の不足には脆く、精度の不足には強い。bfloat16はその非対称性にビット予算を合わせた形式です。

FP8に2種類ある理由

8ビットまで削ると、指数と仮数の綱引きがさらに露骨になります。だからFP8には最初から2つの型が用意されました。

同じ8ビットでも、何を表すかで区画の配分を変える。 「範囲は指数、精度は仮数」という最初の2行が、そのまま製品設計になっている例です。

整数フォーマットと量子化

INT8やINT4は等間隔の目盛りです。実数へ戻すには、スケール ss とゼロ点 zz を使って

xs(qz)x \approx s\,(q - z)
(3)

qq が格納された整数、ss は1目盛りの幅、zz は実数の0に対応する整数値です。zz を0に固定するのが対称量子化、動かせるようにするのが非対称量子化。ss をテンソル全体で1つ持つのがper-tensor、出力チャネルごとに持つのがper-channel、数十要素のグループごとに持つのがgroup-wiseで、後ろに行くほど誤差は減りメタデータは増えます。

つまりこの式がやっているのは、格納された整数を「ものさしの何番目の目盛りか」として読み、実数の0に当たる目盛りの番号を引き、目盛り1つぶんの実寸を掛け直す、という操作です。目盛りを数えて長さに直しているだけで、元の実数についてこの形式が知っている情報は sszz の2つの定数に全部入っています。

浮動小数点と比べたときの整数の強みは、指数に予算を割かないことです。値が狭い範囲に集中しているなら、等間隔の目盛りのほうが同じビット数で細かく刻めます。ニューラルネットの重みは概ね0付近に集中した分布なので、この条件に合っている。逆に弱点も同じところから来ます。ひとつでも外れ値があると ss がそれに引っ張られ、大多数の値が数目盛りに潰れます。活性値の外れ値チャネルが量子化の難所として知られているのは、この構造のためです。

量子化で何が失われるのか

(LLMを実際に量子化する手順 — スケールとゼロ点の決め方、PTQとQAT、外れ値の扱い — は LLM量子化を1から解説 で扱います。ここでは「型そのもの」の話に集中します。)

失われるものは2種類あり、トレードオフの関係にあります。

分解能の誤差:刻み幅 Δ\Delta の一様量子化では、誤差はおおむね [Δ/2,Δ/2][-\Delta/2, \Delta/2] の一様分布になり、その分散は Δ2/12\Delta^2/12 です。ビットを1つ増やすと Δ\Delta は半分、誤差の電力は4分の1、SNRにして約6 dB改善する——これは信号処理で古くから知られる「1ビット=6 dB」則です。

クリッピングの誤差:表現範囲の外に出た値を端に丸めることで生じます。範囲を広く取れば分解能が粗くなり、狭く取ればクリッピングが増える。キャリブレーションとは、この2つの誤差の和が最小になる範囲を実データから決める作業にほかなりません。

情報の観点では、量子化は不可逆に情報を捨てる操作です(情報理論)。捨て方が上手なら知覚や予測にはほとんど影響しない、という構図は画像圧縮と共通しています。JPEGのブロックノイズで「ある点までは気づかないが、越えると急に壊れる」振る舞いを体感しておくと、INT4の議論が身体に入ります。

FIG 1これはJPEGのDCT係数の量子化だが、「刻みを粗くしていくと、ある点までほとんど気づかず、越えると急激に壊れる」という振る舞いはINT8→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のように演算そのものを軽くする手が効く。同じモデルでも段階によって最適な型が違います。

精度劣化に気づく指標。 タスク精度だけを見ていると壊れに気づきません。実務では次を併用します。

知らないと事故になる落とし穴

面接ではこう問われます。「BF16とFP16の違いは?」——ビット配分(指数8対5、仮数7対10)、そこから来る範囲と精度の差、そしてFP16はロススケーリングが要るがBF16は要らないという運用上の帰結まで言えれば十分です。「では推論でどちらを使うか」と続いたら、律速資源とカーネルの対応状況で決める、と答えれば話が噛み合います。

まとめ

コメント

コメントにはログインが必要です