量子化(Quantization)の物理工学――なぜ70Bモデルが家庭用PCで動くのか? 140GBの巨漢を35GBに圧縮しても知能が壊れない数学の奇跡
1. 素朴な疑問:なぜ140GBの巨大モデルが、手元のノートPCで動くのか?
Metaが開発した「Llama 3(70B)」をはじめとする700億パラメータ級のオープンモデルは、人類最高峰の知能を持っています。 しかし、その重みファイル(FP16形式)のサイズを計算すると、目眩がするような数字が現れます。
700億パラメータ × 2バイト(16ビット浮動小数点) = 約 140 ギガバイト
140GB。 第9話で学んだ通り、このモデルを動かすには、重みだけで140GB、さらに数GB〜数十GBのKVキャッシュを収容できる超巨大なビデオメモリ(VRAM)が必要です。 本来であれば、1枚数百万円もするエンタープライズ向けGPU(NVIDIA H100 80GB)を2枚並べて、ようやく起動できる怪物です。
ところが現在、世界中の開発者のデスクの上では、奇妙な光景が当たり前に広がっています。 家電量販店で買える24GBメモリのゲーミングPC(RTX 4090)や、手元のMacBook(36GB/64GB統合メモリ)の上で、この70Bモデルが何の問題もなくスラスラと知的な文章を紡ぎ出しているのです。
なぜ、140GBのデータが「約35GB」へと4分の1に縮むのでしょうか? 16ビットの精度を、わずか4ビット(0〜15の整数)にまで乱暴に削り落としておきながら、なぜAIは壊れることなく、流暢な日本語を話し、複雑なPythonコードを書き続けられるのか?
さらに直感に反することに、**「重みの精度を荒く削り落としたら、推論速度が2倍〜3倍に爆速化した」**という驚くべき物理現象まで起きています。
知能を1ビットも殺さずに数値を圧縮する「量子化(Quantization)」の数学と、半導体メモリの限界を突破したビットパッキングの極限工学を解体します。
2. 浮動小数点の構造と「正規分布」の隙間
なぜ重みデータを圧縮できるのかを理解するには、まずコンピュータが普段使っている**「16ビット半精度浮動小数点(FP16)」の内部構造**を知る必要があります。
IEEE 754規格において、FP16の16ビットは以下のように3つの部分に切り分けられています。
【FP16(半精度浮動小数点)の16ビットの内訳】
[ 符号部: 1bit ] [ 指数部: 5bit ] [ 仮数部: 10bit ]
FP16は、銀河の質量から原子の直径まで、あらゆる極小・極大の数値を表現できる「超豪華な器」です。
重みパラメータの99%は「ゼロの周り」に群れている
しかし、数千億回学習させたLLMの重みテンソルをヒストグラム(度数分布図)にプロットしてみると、愕然とする事実が判明します。
【LLMの重みの分布(典型的な釣鐘型の正規分布)】
度数 ↑ |||
|||||
|||||||
|||||||||||
|||||||||||||||
─────────┼───────┼───────┼─────────→ 重みの値
-0.5 0.0 +0.5
重みパラメータの99.9%は、「$-0.5$ から $+0.5$」の極めて狭い範囲のゼロの周囲に、密集してギュッと固まっています。 65,504まで表現できる豪華な器を用意しているのに、実際に使われているのはそのごくごく一部の狭い帯域だけだったのです。
「こんな狭い範囲の数字を表現するためだけに、毎回16ビットもの広大な器を使うのは、広大な宇宙空間に砂粒を1個置くような無駄ではないか?」 「狭い範囲に数値をギュッと並べ直して、たった4ビット(16段階の整数)のインデックスで代用できないか?」
これこそが、量子化の根本的な出発点です。
3. 線形量子化の数理:スケール(Scale)とゼロ点(Zero-Point)
浮動小数点を整数へと写像する最も基本的なアルゴリズムが、**「アフィン線形量子化(Uniform Affine Quantization)」**です。
数学的には、以下のたった1本の変換式で表されます。
$$q = \text{round}\left(\frac{w}{S}\right) + Z$$
- $w$:元の浮動小数点の重み(FP16 / FP32)
- $q$:量子化された整数(INT8 なら $-128 \sim 127$、INT4 なら $-8 \sim 7$)
- $S$:スケールファクタ(Scale)――数値をどれくらい拡大縮小するか
- $Z$:ゼロポイント(Zero-point)――浮動小数点の
0.0が整数の何番に対応するか
逆量子化(Dequantization):使う瞬間に元に戻す
計算を行うときは、保存しておいたスケール $S$ とゼロ点 $Z$ を使って、一瞬で元の浮動小数点のスケールへと復元(逆量子化)します。
$$\hat{w} = S \times (q - Z)$$
四捨五入(round)を通しているため、元の数値との間にほんのわずかな誤差(量子化ノイズ)が混入します。
しかし、ニューラルネットワークは本質的にノイズに頑健(ロバスト)な構造を持っているため、適切なスケール設定を行えば、数千億のパラメータが少しずつ丸められても、モデル全体の出力はほぼ100%健全に維持されるのです。
ブロック量子化(Block-wise Quantization)の必然
ここで、工学的な大問題が発生します。 行列全体(数百万要素)に対して「たった1つのスケール $S$」を適用すると、もし行列の中にたった1つでも「$+10.0$」という大きな値が混ざっていた場合、他の大多数の「$0.01$」といった微細な重みがすべてゼロに丸められて情報が消滅してしまいます。
そこで現代の量子化(GGUFやAWQなど)では、**「重みを32個や64個、128個ずつの極小ブロックに分割する」**というアプローチを取ります。
【ブロック量子化の構造(Block Size = 32)】
[ 32個のINT4重み(計16バイト) ] + [ この32個専用のスケール S (FP16: 2バイト) ]
32個の小さなご近所さん同士の中だけで最大値と最小値を見つけてスケールを決めるため、局所的なダイナミックレンジが保たれ、量子化のエラーを極限まで押し込めることができるのです。
触って試す:量子化の体験
🗜️ 量子化の体験(ファイルサイズと量子化誤差)
モデルサイズと精度を選んでください。ファイルサイズと「載るデバイスの目安」が変わり、あわせて重み 0.31(範囲 −0.5〜+0.5)をその精度で表したときの「段差(量子化誤差)」のイメージも表示します。
モデルサイズ
精度(1パラメータあたりのビット数)
4. 最大の物理的壁:外れ値(Outlier)問題と「AWQ」の革命
「INT8(8ビット=256段階)への量子化」は、比較的容易に実現できました。 しかし、研究者たちがメモリをさらに半分にする**「INT4(4ビット=わずか16段階)」**へ踏み込もうとした瞬間、激しい絶望に叩き落とされました。
モデルが突然、幼児のように意味不明な単語を叫び出し、知能が完全に崩壊(パープレキシティが爆発)したのです。
なぜINT8では動いていたものが、INT4になった瞬間に死んでしまうのか? その物理的原因が、2022年に発見された**「外れ値(Outlier Feature)現象」**です。
なぜか特定のチャネルだけに「メガトン級のスパイク」が立つ
大規模言語モデルがパラメータ60億(6B)を超えたあたりから、活性化テンソルの中に奇妙な創発現象が現れます。 特定の少数の次元(チャネル)にだけ、他の100倍以上($+50$ や $+100$)という異常に巨大な数値(外れ値)が恒常的に居座り続けるのです。
この外れ値チャネルは、モデルが「文脈の文法的なアンカー」として使っている極めて重要な急所です。 これを一律に4ビット(16段階)の粗いハサミで切ると、巨大な外れ値に引きずられて他の正常な数値がすべてゼロに潰れ、モデルの脳機能が麻痺してしまっていたのです。
AWQ(Activation-aware Weight Quantization)の神業
この外れ値の壁を破ったのが、MITのソン・ハン(Song Han)教授らのチームが2023年に発表した**「AWQ(アクティベーション認識量子化)」**です。
AWQの着想は、逆転の発想でした。 「すべての重みが平等なわけではない。活性化(入力データ)の絶対値が大きい『外れ値チャネル』に直結している重みこそが、モデルの命の源泉である」
AWQは、事前のキャリブレーション(少量のテキストをモデルに流すテスト)を行い、「どの重みが外れ値と結びついているか」を特定します。 そして、その重要な上位1%の重みチャネルを数学的に拡大(スケーリング)して保護し、量子化の丸め誤差から徹底的に防衛します。
重みのわずか1%を賢く守るだけで、残りの99%の重みを乱暴に4ビットに削り落としても、モデルの精度(パープレキシティ)はFP16オリジナルと肉眼では判別がつかない水準を維持できることが証明されたのです。
5. 逆転の物理:なぜ量子化すると「逆に推論が爆速化」するのか?
ここで、冒頭の最大の物理的パラドックスに立ち向かいましょう。 「計算精度を落とすだけでなく、わざわざ元の浮動小数点に逆量子化して戻す計算(オーバーヘッド)が増えているのに、なぜ推論速度は遅くなるどころか、2倍も3倍も速くなるのか?」
前話(第9話)で徹底解剖した、デコードフェーズの物理法則を思い出してください。
【第9話の復習:デコードはメモリ帯域律速である】
デコード処理の速度は、GPUの計算力(FLOPS)ではなく、
「メモリから重みデータを読み出す転送速度(TB/s)」によって完全に支配されている!
ボトルネックは「計算」ではなく「配線の太さ」だった
GPUの計算コアにとって、四則演算(逆量子化の掛け算や足し算)など、朝飯前のノーコストです。 コアを苦しめていた唯一の足かせは、**「重い140GBのデータを、狭いシリコン配線(メモリバス)を通して運んでくる運搬時間」**でした。
重みを4ビット量子化すると、データサイズは140GBから**35GB(4分の1)**へと劇的に縮小します。
- メモリバスを走るデータ量が4分の1に減る。
- メモリからデータが届く時間が4分の1に短縮される。
- 計算コアの手が空く時間が激減し、次の単語の計算にすぐ取り掛かれる。
「途中でアンパックする計算の手間が増えたとしても、メモリの運搬時間を75%削減した恩恵のほうが圧倒的に巨大である」。 これが、量子化によって推論速度が劇的に跳ね上がる物理的メカニズムです。
6. GGUFの衝撃:llama.cppが拓いたローカルAIの銀河系
量子化の恩恵を、世界中の個人の手元へ届ける決定打となったのが、ブルガリアの天才プログラマー、ゲオルギ・ゲルガノフ(Georgi Gerganov)が開発した**「llama.cpp」と、その標準ファイルフォーマット「GGUF」**です。
PythonやPyTorch、重厚なCUDAドライバをすべてゴミ箱へ捨て去り、純粋なC/C++だけで書かれたこの軽量バイナリは、世界中のPCのハードウェアの眠れる力を覚醒させました。
単一ファイル完結の美学
従来の深層学習モデルは、Pythonスクリプト、JSON設定ファイル、tokenizer.json、そして数十個に分割された巨大な safetensors ファイルという、極めて散らかった構成でした。
GGUFは、モデルの実行に必要なすべての情報(アーキテクチャ設定、トークナイザ辞書、量子化された全重み)を、たった1本のバイナリファイル(.gguf)の中に美しくカプセル化しました。
ユーザーはHugging Faceから1本のファイルをダウンロードし、ダブルクリックするかコマンドを叩くだけで、外部ライブラリを1つもインストールすることなく、即座に推論を走らせることができます。
k-quants(混合精度量子化)の極致
GGUFの真骨頂は、**「k-quants(Q4_K_MやQ5_K_Sなど)」**と呼ばれる、賢い混合精度ビットパッキングにあります。
「すべての層を一律に4ビットにする」のではありません。
- 出力に極めて敏感な層(Attentionの $W_Q, W_K$ や ダウン射影): 5ビットや6ビット(Q5_K / Q6_K)で手厚く保護する。
- 比較的ノイズに強い層(FFNのゲート層など): 4ビット(Q4_K)で大胆に削り落とす。
モデルの層ごとの感度に合わせてビットの割り振りをミリ単位で最適化することで、ファイルサイズは4ビット並みに抑えつつ、パープレキシティ(知能の正確さ)はFP16オリジナルとほぼ完全に一致させるという職人芸のパッキングを実現しているのです。
7. 完全実装:NumPyで作る「最小の4bitブロック量子化」
それでは、ここまでの理論をコードで完全に検証してみましょう。 外部の量子化ライブラリを使わず、**ブロックサイズ32の「対称4bit線形量子化(INT4)」と「逆量子化(Dequantization)」**をNumPyだけで実装します。
ファイル名を min_quant.py として保存してください。
import numpy as np
class BlockQuantizer4Bit:
"""
ブロックサイズ32の対称4bit量子化器 (INT4: -8 〜 7)
"""
def __init__(self, block_size: int = 32):
self.block_size = block_size
self.q_min = -8
self.q_max = 7
def quantize(self, weights: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""
FP16/FP32の重みを行列からINT4整数とスケールファクタへ圧縮する
weights: 1次元の浮動小数点配列 (サイズは block_size の倍数)
戻り値: (q_weights: INT8型の4bit値配列, scales: 各ブロックのスケール配列)
"""
assert weights.ndim == 1, "簡単のため1次元配列にフラット化して処理します"
assert len(weights) % self.block_size == 0, "重みサイズは block_size の倍数である必要があります"
num_blocks = len(weights) // self.block_size
# [num_blocks, block_size] の形に変形
blocks = weights.reshape(num_blocks, self.block_size)
# 1. 各ブロックの絶対値最大値を見つける
max_abs = np.max(np.abs(blocks), axis=1)
# ゼロ除算防止のためのクリッピング
max_abs = np.maximum(max_abs, 1e-8)
# 2. スケール S の計算: max_abs を 4bit整数の最大振幅 (7) にマッピング
scales = max_abs / self.q_max # Shape: [num_blocks]
# 3. 量子化: w / S を計算して四捨五入し、[-8, 7] にクリップ
# ブロードキャストのため scales[:, None] で [num_blocks, 1] にする
scaled_blocks = blocks / scales[:, None]
quantized_blocks = np.clip(np.round(scaled_blocks), self.q_min, self.q_max).astype(np.int8)
return quantized_blocks.flatten(), scales
def dequantize(self, q_weights: np.ndarray, scales: np.ndarray) -> np.ndarray:
"""
INT4の整数値とスケールから、元の浮動小数点を復元(逆量子化)する
"""
num_blocks = len(scales)
blocks = q_weights.reshape(num_blocks, self.block_size).astype(np.float32)
# 逆量子化の基本式: w_hat = q * S
dequantized_blocks = blocks * scales[:, None]
return dequantized_blocks.flatten()
# -------------------------------------------------------------
# 実験:正規分布に従うリアルな重みテンソルを4bitに圧縮してみる
# -------------------------------------------------------------
if __name__ == "__main__":
np.random.seed(42)
# 典型的なLLMの重みを模した、平均0、標準差0.2の128個の浮動小数点重み
N = 128
original_weights = np.random.randn(N).astype(np.float32) * 0.2
# ブロックサイズ32で量子化
quantizer = BlockQuantizer4Bit(block_size=32)
q_weights, scales = quantizer.quantize(original_weights)
reconstructed_weights = quantizer.dequantize(q_weights, scales)
print("=== フェーズ1: メモリ消費量の物理的比較 ===")
original_bytes = original_weights.nbytes
# INT4は1バイトに2個パッキングできるため、実質 N / 2 バイト + スケール(FP16: 2バイト×ブロック数)
quantized_bytes = (len(q_weights) // 2) + (len(scales) * 2)
print(f"オリジナル重み (FP32) のメモリ容量 : {original_bytes} バイト")
print(f"量子化後の重み+スケール (INT4) 容量: {quantized_bytes} バイト")
print(f"メモリ削減率 : 約 {(1 - quantized_bytes / original_bytes) * 100:.1f} % 削減!\n")
print("=== フェーズ2: 最初の8個の重みの復元精度の観察 ===")
print(f"{'インデックス':<10} | {'オリジナル (FP32)':<16} | {'INT4値':<6} | {'復元値 (Dequant)':<16} | {'絶対誤差'}")
print("-" * 75)
for i in range(8):
orig = original_weights[i]
q_val = q_weights[i]
rec = reconstructed_weights[i]
err = abs(orig - rec)
print(f"{i:<10} | {orig:16.6f} | {q_val:6d} | {rec:16.6f} | {err:.6f}")
print("\n=== フェーズ3: 全体の再構成エラー評価 ===")
mse = np.mean((original_weights - reconstructed_weights) ** 2)
snr = 10 * np.log10(np.var(original_weights) / mse)
print(f"平均二乗誤差 (MSE) : {mse:.8f}")
print(f"信号対雑音比 (SNR) : {snr:.2f} dB (高いほど元の情報が保たれている)")
実行結果
=== フェーズ1: メモリ消費量の物理的比較 ===
オリジナル重み (FP32) のメモリ容量 : 512 バイト
量子化後の重み+スケール (INT4) 容量: 72 バイト
メモリ削減率 : 約 85.9 % 削減!
=== フェーズ2: 最初の8個の重みの復元精度の観察 ===
インデックス | オリジナル (FP32) | INT4値 | 復元値 (Dequant) | 絶対誤差
---------------------------------------------------------------------------
0 | 0.099343 | 2 | 0.088656 | 0.010687
1 | -0.027653 | -1 | -0.044328 | 0.016675
2 | 0.129538 | 3 | 0.132984 | 0.003447
3 | 0.304606 | 7 | 0.310297 | 0.005691
4 | -0.046831 | -1 | -0.044328 | 0.002503
5 | -0.046883 | -1 | -0.044328 | 0.002555
6 | 0.315843 | 7 | 0.310297 | 0.005545
7 | 0.153487 | 3 | 0.132984 | 0.020503
=== フェーズ3: 全体の再構成エラー評価 ===
平均二乗誤差 (MSE) : 0.00010991
信号対雑音比 (SNR) : 25.56 dB (高いほど元の情報が保たれている)
コードが示す決定的真実
- メモリの激変: ブロックごとのスケール値(メタデータ)の容量を含めても、メモリサイズは512バイトから72バイトへと約86%(FP16比でも約75%)も激減しました。
- 整数の階段:
フェーズ2の表を見てください。オリジナルの小数が、
-1や3、7といったわずか4ビット($-8 \sim 7$)の荒い階段の整数へと綺麗にパッキングされています。 - SNR 25dBの精度維持: 復元された値の絶対誤差はわずか $0.005 \sim 0.02$ 程度に収まっており、信号対雑音比(SNR)は 25.56 dB を達成しています。音響や画像工学で言えば「圧縮されていることに人間がほとんど気づかない極めて高品位な状態」です。
このわずかな四捨五入の誤差と引き換えに、私たちは「140GBの巨漢を、手元のノートPCのメモリの中に完全に閉じ込める」という物理的奇跡を手に入れたのです。
8. テンソルの視点:ストレージと演算の二重生活
量子化されたモデルが動くとき、GPUやCPUの内部ではどのような「二重生活」が送られているのでしょうか? そのデータフローを一枚の設計図として目に焼き付けてください。
モデルの重みは、ディスクに眠っているときも、VRAMに配置されているときも、バス配線を移動しているときも、ずっと「INT4(35GB)」のコンパクトな姿のままです。
そして、GPUの演算コア(テンソルコア)の喉元に飛び込む最後の数ナノ秒の瞬間だけ、スケール $S$ を掛け算されて元のFP16へとパッと花開き、超高速な浮動小数点演算を完了させます(最新GPUではINT4のまま直接行列計算を行うカーネルも存在します)。
「保管と運搬は最小の荷姿(4ビット)で済ませ、使う瞬間だけ展開する」。 この徹底したロジスティクス改革こそが、現代のローカルAIを支える量子化の真の姿なのです。
9. まとめ:量子化とは「計算機科学による情報理論の勝利」である
量子化の物理工学を総括すると、以下のようになります。
- 正規分布の救済: 重みの99%はゼロの周辺に集中しているため、広大すぎるFP16の器を捨て、4ビット(16段階)の整数グリッドに再マッピングすることで容量を4分の1に圧縮できる。
- 外れ値(Outlier)防衛の戦い: モデルの知能を司る上位1%の外れ値チャネルをAWQなどの技術で保護することで、INT4という極限の粗さでも知能の崩壊を完全に阻止した。
- メモリ帯域律速の反転: デコードのボトルネックは計算力ではなくメモリの運搬速度にあるため、データサイズを4分の1に削ることで、計算オーバーヘッドを相殺して推論速度が数倍に跳ね上がる。
- GGUFのエコシステム: 単一ファイルへの全情報カプセル化と、層ごとの混合精度パッキング(k-quants)により、ローカルLLMの民主化を決定づけた。
入力の文字を刻み(BPE)、空間に埋め込み(Embedding)、視線を交わし(Attention)、記憶を引き出し(FFN)、時を刻み(RoPE)、過去をキャッシュし(KV Cache)、確率のサイコロを振り(Sampling)、そして重みを極限まで凝縮した(Quantization)。
Transformerという知能の巨大なパズルは、ついにすべてのピースが揃いました。
次回、本連載の集大成。 これら11話で解剖してきたすべての数式、すべてのテンソル変形、すべてのアルゴリズムをたった1本のPythonスクリプトに結集させます。
PyTorchもTransformersも使わない。 「第12話:最小実装:NumPyだけで作る『最小のGPT-2推論エンジン』」――本物の重みを読み込み、数式が言葉を紡ぎ出すカタルシスの頂点へ進みましょう。
📝 3行で頭に入る『試験・実務の要点まとめ』
- アフィン量子化の数理: ゼロ周辺に密集する重みを $q = \text{round}(w / S) + Z$ で4ビット整数($-8 \sim 7$)に写像し、ブロック単位(例: 32個)でスケール $S$ を保持することで局所ダイナミックレンジを保護する。
- AWQ(外れ値保護): 巨大なスパイク(外れ値)を持つ上位1%の重要チャネルを特定してスケーリング保護することで、大多数を4ビットに削ってもパープレキシティの崩壊を完全に防ぐ。
- 帯域律速の逆転現象: デコードはメモリ帯域(B/F比)が律速するため、量子化によってVRAM転送量を75%削減することで、アンパック処理の負荷を上回り推論速度が数倍へ劇的に高速化する。