データ型はなぜこんなに増えたのか

数年前まで、ディープラーニングの数値は float32 一つで十分だった。ところが最近の論文やモデルカードを見ると、FP16, BF16, FP8 (E4M3/E5M2), INT8, INT4, FP4, NF4… とあらゆるデータ型が溢れている。名前だけでは何が何やら、なぜこんなに種類があるのか分かりにくい。

理由は単純だ。低ビット演算はとにかく安いからである。45nmプロセスでの演算1回あたりのエネルギーコストはおおよそ次のとおり。

演算エネルギー (pJ)
8-bit int ADD0.03
32-bit int ADD0.1
32-bit float ADD0.9
8-bit int MULT0.2
32-bit float MULT3.7

8ビット整数の乗算は32ビットfloatの乗算より 約18倍 安く、加算は 30倍 安い。さらに、メモリから値を1つ読み出すコストは乗算・加算1回の数百倍にもなる。だから同じモデルでも 数値をより少ないビットに収めるほど、電力もメモリも帯域も節約できる。上に並んだ数々のデータ型は、結局「何ビットを、どこに使うか」という問いにそれぞれ違う答えを出した結果物だ。

問題は、ビットを減らすと表現できる数値の種類が減り、その分だけ精度や範囲を失うことだ。だからフォーマットごとに 符号・指数・仮数へビットをどう配分するか で性格が分かれる。この記事では、その配分ルール — ビットが実際にどんな数値に解釈されるのか — を一つずつ分解していく。

各データ型ごとに ビットを直接クリックできるウィジェット を用意した。0と1を押して反転させると、計算式と結果がリアルタイムで変わる。説明を目で読むより、自分でビットを触ってみる方がずっと早く腑に落ちる。


1. 整数 (Integer)

符号なし整数 (Unsigned Integer)

最も単純だ。$n$個のビットそれぞれが $2^k$ の桁値を持ち、オンのビット($=1$)の桁値をすべて足す。

$$\text{value} = \sum_{i=0}^{n-1} b_i \cdot 2^i$$

表現範囲は $[0,\ 2^n - 1]$。8ビットなら0から255まで。

下のウィジェットでビットを押してみよう。デフォルトの 00110001 は $2^5 + 2^4 + 2^0 = 49$ だ。

符号付き整数 (Signed Integer)

負数を表すには符号が要る。方法は二つある。

符号-絶対値 (Sign-Magnitude) — 先頭ビットを符号に使い($0$=正、$1$=負)、残りで大きさを表す。直感的だが致命的な欠点がある。0000000010000000どちらも0(+0と−0)なのだ。0が二つあるとハードウェアが厄介になる。範囲は $[-2^{n-1}+1,\ 2^{n-1}-1]$。

2の補数 (Two’s Complement) — 現代のコンピュータが実際に使う方式だ。アイデアは単純で、先頭ビットの桁値を負にする。つまりMSBの重みが $+2^{n-1}$ ではなく $-2^{n-1}$ になる。

$$\text{value} = -b_{n-1}\cdot 2^{n-1} + \sum_{i=0}^{n-2} b_i \cdot 2^i$$

こうすると0は 00000000 一つだけになり、範囲 $[-2^{n-1},\ 2^{n-1}-1]$ は負側が一つ広い。下の 11001111 は $-2^7 + 2^6 + 2^3 + 2^2 + 2^1 + 2^0 = -49$ だ。MSBをオフにするとどう変わるか押してみよう。


2. 固定小数点 (Fixed-Point)

整数だけでは小数を扱えない。最も簡単な拡張は 小数点の位置を固定 することだ。ビット列をそのまま2の補数整数として読み、決められた分だけ $2^{-f}$ でスケールする。

$$\text{value} = (\text{整数として読んだ値}) \times 2^{-f}$$

下は8ビットを整数部4ビット・小数部4ビット($f=4$)に分けたものだ。各ビットの重みが $2^3, 2^2, \dots, 2^0, 2^{-1}, \dots, 2^{-4}$ と続く。00110001 は整数として読むと49、$49 \times 2^{-4} = 3.0625$ だ。

固定小数点は単純だが限界が明確だ。小数点の位置が固定なので 表現できる大きさの幅(range)が狭い。非常に大きい数と非常に小さい数を同時に扱うのが苦手だ。これを解くのが次の主役、浮動小数点である。


3. 浮動小数点 (Floating-Point) — IEEE 754

浮動小数点は名前のとおり 小数点が浮動する。科学的記数法($1.5 \times 10^3$)の2進数版だと思えばいい。ビットを三つに分ける。

  • Sign(符号) — 1ビット
  • Exponent(指数) — 大きさの規模を決める → 表現範囲(range) を決定
  • Fraction(仮数部) — 細かい値を決める → 精度(precision) を決定

正規(normal)数の解釈式はこうだ。

$$\text{value} = (-1)^{\text{sign}} \times (1 + \text{Fraction}) \times 2^{\text{Exponent} - \text{bias}}$$

ここで bias は指数を負まで表すためのオフセットで $2^{e-1}-1$($e$=指数ビット数)だ。そして仮数の前の $(1 + \cdots)$ に注目。正規数は常に先頭に暗黙の1が付く(「implicit leading 1」と呼ぶ)。

ちょっと用語整理 — 仮数 / Fraction / Mantissa / Significand

この部分の用語は文献ごとに少しずつ混ざって使われ、紛らわしい。上の式の $1.\text{Fraction}$ 全体、つまり 有効数字の部分 を指す正式名称は Significand(有効数字) だ。

  • Fraction(仮数部) — ビットに実際に 格納される小数部分 だけを指す。上のウィジェットで黄色く塗られた23ビットがこれ。小数点以下の値 $0.\text{b}_1\text{b}_2\cdots$ だ。
  • Significand(有効数字) — 暗黙の1まで含む $1.\text{Fraction}$ 全体。実際に掛けられる有効数字だ。
  • Mantissa(仮数) — 歴史的には対数表の小数部分を指した語で、浮動小数点では普通 Fractionと同じ意味 でゆるく使われる。IEEE 754規格自体は「Significand」を正式用語とし「mantissa」を推奨しないが、現場では今も「仮数/mantissa」がよく使われる。

まとめると Significand = 1 + Fraction、そして日常的に「仮数(mantissa)」と言えば大抵は格納されるFractionを指す。この記事ではビットに収まるフィールドを Fraction(仮数部) と呼ぶ。

下は32ビット単精度(FP32)だ。符号1 + 指数8 + 仮数23 = 32ビット、biasは127。デフォルトは $0.265625 = (1 + 0.0625) \times 2^{125-127}$ を表す。指数ビットを一つずつ押して、値が2倍ずつ跳ねるのを確かめよう。

特別な値: 0、無限大、NaN、そしてsubnormal

式を見ると奇妙な点が一つある。$(1 + \text{Fraction})$ のせいで 正規数では0を表せない。そこでIEEE 754は指数フィールドを特別な信号として使う。

指数(Exponent)Fraction = 0Fraction ≠ 0解釈
00…0 (=0)$\pm 0$subnormal$(-1)^s \times \text{Fraction} \times 2^{1-\text{bias}}$
00…1 ~ 11…0normalnormal$(-1)^s \times (1+\text{Fraction}) \times 2^{\text{Exp}-\text{bias}}$
11…1 (=max)$\pm\infty$NaN

肝は 指数がすべて0のとき だ。このとき暗黙の1を外し($1+\text{Fraction} \to \text{Fraction}$)、指数を $2^{1-\text{bias}}$ に固定する。こうしてできるのが subnormal(非正規)数 で、0付近の非常に小さな値を細かく埋めてくれる。逆に 指数がすべて1のとき は無限大(仮数0)とNaN(仮数≠0)になる。

上のFP32ウィジェットで自分で作ってみよう。

  • 指数をすべて1に0 11111111 0…0)→ +∞
  • そこから 仮数のどれかのビットをオンにするとNaN
  • すべて0に0
  • 指数を0のままにして 仮数を少しオンにすると → 非常に小さなsubnormal値

指数幅が広いほど表現範囲が広がり、仮数幅が広いほど精度が上がる。Exponent → Range, Fraction → Precision. この一行が、以降のすべての低精度フォーマット設計の核心的なトレードオフだ。


4. 半分のサイズへ: FP16とBF16

FP32は正確だが32ビットも食う。ディープラーニングはそこまでの精度を必要としない場合が多いので、16ビットのフォーマットを使う。同じ16ビットでも ビットをどこに配分するか で分かれる。

FP16 (IEEE 754 Half Precision)

指数5 + 仮数10、biasは15。精度(仮数)により多く投資した配分だ。下は $1\,10001\,1100000000$、すなわち $-(1+0.75)\times 2^{17-15} = -7.0$ だ。

BF16 (Google Brain Float)

指数8 + 仮数7、biasは127。FP16と総ビット数は同じだが、指数をFP32と同じ8ビットに 保つ。つまり 表現範囲はFP32と同一 で、代わりに精度を犠牲にした。学習中の勾配のように値の規模(scale)が大きく揺れる状況でもオーバーフローの心配がなく、人気が高い。

下は $2.5 = (1 + 0.25)\times 2^{1}$ をBF16で表したものだ($0\,10000000\,0100000$)。

二つのウィジェットで指数ビット数の違いを感じてみよう。BF16は指数セルが8個あるので非常に大きい/小さい数まで届くが、仮数が7個しかないので値がまばらだ。FP16はその逆だ。


5. さらに下へ: FP8とFP4

FP8 (E4M3 / E5M2)

8ビット浮動小数点は最新のハードウェア(例: Nvidia Hopper/Blackwell)がサポートする。二つの配分が事実上の標準として使われる。

  • E4M3 — 指数4 + 仮数3。精度優先。順伝播(forward)の重み・活性値に主に使う。INFがなく S.1111.111 のみをNaNに使う。表現可能な最大正規値は $448$。
  • E5M2 — 指数5 + 仮数2。範囲優先。逆伝播(backward)の勾配のように規模の大きい値に使う。IEEEのようにINFとNaNを持つ。

まずE4M3。biasは7。デフォルト 0 0111 000 は $(1+0)\times 2^{7-7} = 1.0$ だ。

E5M2。bias 15。指数5個でずっと広い範囲を収めるが、仮数が2個しかないので値の間隔がまばらだ。

INT4とFP4

最も極端だ。4ビットで表現できる値は わずか16個 だけ。この16個をどう配置するかがフォーマットごとに異なる。

INT4 — 2の補数整数。$-8$ から $7$ まで 均等な間隔 で並ぶ。

FP4 は指数/仮数の配分によって値の分布が変わる。指数ビットを増やすほど0付近は密に、外側はまばらに、非均等(non-uniform) に広がる。

  • E1M2 — 指数1 + 仮数2。整数に最も近い(bias 0)。0111 = $(1+0.75)\times 2^{1-0} = 3.5$。
  • E2M1 — 指数2 + 仮数1(bias 1)。0111 = $(1+0.5)\times 2^{3-1} = 6$。
  • E3M0 — 指数3 + 仮数0(bias 3)。仮数がないので事実上2の冪だけを表す。0111 = $(1+0)\times 2^{7-3} = 16$。randomボタンを押して、値が $\dots, 4, 8, 16$ のように 指数的に 広がるのを確かめよう。

ここで面白い観察が一つ出てくる。ニューラルネットの重みの分布は大抵 0付近に集まり、裾が長い。だとすれば、値を均等に並べるINTより、0付近を密に埋めるFP系の方が、同じ4ビットでも実際の値をよりよく合わせられる。どの分布にどのフォーマットが合うか — データ型の選択がそのまま精度につながる理由だ。


おわりに

ここまでが、ディープラーニングで出会うデータ型が実際に何を意味するかの話だ。まとめると、

  • 整数 / 固定小数点 — 値が均等な間隔。単純だが範囲が狭い。
  • 浮動小数点 — 指数で範囲を、仮数で精度を分け合う。0付近は密で外側はまばら。
  • ビットを減らすということ = 表現できる値の個数を減らすということ。FP32の約43億個からFP4の16個まで。

これで FP16, BF16, FP8 E4M3, INT4 のような名前を見れば、頭の中にビット配置が浮かぶはずだ。ディープラーニングにおいて「どのデータ型を使うか?」はもはや些細な実装のディテールではなく、モデルをどれだけ小さく速く、しかも正確に動かせるかを分ける設計上の選択になった。