量子化とダブル量子化: LLMを効率的に圧縮する方法

スポンサーリンク

量子化とダブル量子化: LLMを効率的に圧縮する方法

  • Bitsandbytes
  • ダブル量子化
  • INT4
  • INT8
  • LLM効率化
  • メモリ最適化
  • モデル圧縮
  • モデル量子化
  • NF4
  • QLoRA
  • 量子化ブロック
  • スケールファクター
  • Ready Tensor
  • Mohamed Abdelhamid
  • Wasif Mehmood

🏠
ホーム – 全てのレッスン

⬅️
前へ – ディープラーニングにおけるデータ型

➡️
次へ – LoRAとQLoRA

しかし、ここで実践的な質問があります: すでにFP32のモデルがある場合、メモリを節約するためにそれを実際に4ビットに変換できるのでしょうか?

はい—それが量子化です。訓練済みモデルを高精度の浮動小数点からコンパクトな整数に圧縮するプロセスです。FP32で28GBを必要とする7Bモデルは、4ビット量子化で4GB未満に収まります。それは「強力なクラウドマシンが必要」と「ノートPCのGPUで動作」の違いです。

このレッスンでは、量子化がどのように機能するかを学びます: スケールファクターとゼロポイント、なぜブロックベースの量子化が外れ値問題を解決するのか、そしてダブル量子化がメタデータ自体を圧縮して追加の10%-15%の節約を実現する方法。

最後には、訓練済みモデルを効率的に圧縮する方法を正確に理解できます—今週後半に使用するQLoRAの基礎です。

量子化が重要な理由

最初のニューラルネットワークを訓練したとき、おそらく各数値が何ビット使用するかについて深く考えなかったでしょう。FP32がデフォルトでした—単に機能しました。

しかし、モデルが成長するにつれて、その前提は崩れ始めました。
今日では、大規模モデルのパラメータすべてを保存するだけで、単一の訓練ステップが実行される前にほとんどのGPUのメモリを使い果たすことができます。

量子化は、根本的でありながらシンプルなアイデアを取り入れることでこれを修正します:
重みを表現するために常に32ビット精度が必要というわけではありません。
代わりに、モデルの動作を意味のある形で変更することなく、INT8やINT4のようなより小さな整数形式に圧縮できます。

それが量子化の核心です: 限られたハードウェアで大規模モデルを訓練可能にする数学的なショートカットです。

核心的なアイデア: 浮動小数点を整数にマッピングする

直感から始めましょう。

モデルの重みが-2.5から+3.0の間の連続した数値だと想像してください。
量子化は、その滑らかな範囲を少数の離散的なビン—はしごの最も近いステップに数値を丸めるようなもの—にマッピングします。

最もシンプルなレベルでは、そのマッピングは2つの量によって支配されます:

  • スケール: 各ビンの幅
  • ゼロポイント: 実際のゼロに対応する整数

これらが一緒になって、浮動小数点値が整数になる方法と、それを後で再構築する方法を定義します。

短い例でこれを見てみましょう。

シンプルな量子化の例

量子化が実際にどのように機能するかを具体的な例で見ていきましょう。

次のようなモデルの重みの小さなセットがあるとします:

weights = [0.7, -1.4, 2.5, -0.8, 1.9, -1.0, 0.3, 2.1, -0.5, 0.0]

これらを8ビット整数に量子化します。つまり、各数値は-128から+127の範囲内に収める必要があります。

ステップ1 – 範囲を見つける

まず、値の範囲を知る必要があります:

min_val = -1.4
max_val = 2.5

量子化後、-1.4から2.5の範囲は-128から127の範囲にマッピングされます。これは、値-1.4が整数-128にマッピングされ、値2.5が整数127にマッピングされることを意味します。

ステップ2 – スケールファクターとゼロポイントを計算

次に、スケールとゼロポイントを計算します。

scale = (max_val - min_val) / (127 - (-128))
scale = (2.5 - (-1.4)) / 255
scale = 3.9 / 255
scale ≈ 0.0153

スケールファクター0.0153は、量子化スケールでの1単位のステップ (たとえば-128から-127) が元のスケールでの0.0153のステップ (-1.4から-1.3847) に対応することを意味します。

ゼロポイントは、量子化スケール内のどの数値が元のデータの実際の値ゼロに対応するかを決定します:

zero_point = round(-128 - (min_val / scale))
zero_point = round(-128 - (-1.4 / 0.0153))
zero_point = round(-128 - (-91.5))
zero_point = round(-36.46)
zero_point = -36

ステップ3: 各重みを量子化

次に、次の式を使用して各浮動小数点を整数に変換します:

quantized_value = round(weight / scale + zero_point)

いくつか量子化してみましょう:

# 重み: 0.7
quantized = round(0.7 / 0.0153 + (-36))
quantized = round(45.8 - 36) = round(9.8) = 10

# 重み: -1.4 (最小値)
quantized = round(-1.4 / 0.0153 + (-36))
quantized = round(-91.5 - 36) = round(-127.5) = -128

# 重み: 2.5 (最大値)
quantized = round(2.5 / 0.0153 + (-36))
quantized = round(163.4 - 36) = round(127.4) = 127

# 重み: 0.0
quantized = round(0.0 / 0.0153 + (-36))
quantized = round(0 - 36) = -36

10個の重みすべてを量子化:

元の値: [ 0.7, -1.4, 2.5, -0.8, 1.9, -1.0, 0.3, 2.1, -0.5, 0.0]
量子化値: [ 10, -128, 127, -88, 88, -101, -16, 101, -69, -36]

ステップ4: 品質チェックのための逆量子化

推論中にこれらの重みを使用するには、それらを浮動小数点に変換し直します:

dequantized_value = (quantized - zero_point) × scale

最初の重みを逆量子化してみましょう:

# 量子化値: 10
dequantized = (10 - (-36)) × 0.0153
dequantized = 46 × 0.0153
dequantized ≈ 0.70

元の値: 0.7
逆量子化値: 0.70
誤差: 0.00 (優秀!)

別のものをチェックしてみましょう:

# 元の値: -1.4、量子化値: -128
dequantized = (-128 - (-36)) × 0.0153
dequantized = -92 × 0.0153
dequantized ≈ -1.41

元の値: -1.4
逆量子化値: -1.41
誤差: 0.01 (わずかな丸め誤差)

すべての逆量子化値は次のとおりです:

精度損失は最小限です—ニューラルネットワークに必要なものそのものです。これらの小さな誤差に対して頑健です。

達成したこと:

メモリ節約を見てみましょう:

元の値 (FP32):

量子化後 (INT8):

  • 10個の重み × 1バイト = 10バイト
  • 1つのスケールファクター (FP32) = 4バイト
  • 1つのゼロポイント (FP32) = 4バイト
  • 合計: 18バイト

メモリ削減: 40バイト → 18バイト = 2.2倍小さい

さて、この例には10個の重みしかないため、スケールとゼロポイントが顕著なオーバーヘッドを追加します。しかし、100万個の重みを持つ実際のモデル層を想像してください:

  • 1,000,000個の重み × 1バイト = 1MB
  • スケール + ゼロポイント = 8バイト (無視できる!)
  • 合計: ~1MB

メモリ削減: 4MB → 1MB = 4倍小さい

そして、この例では8ビット量子化を使用しました。4ビット量子化 (INT4) では、代わりに8倍の圧縮が得られます—その4MBをわずか0.5MBに削減します。それが、コンシューマーハードウェアで70Bモデルのファインチューニングを可能にする圧縮レベルです。

外れ値問題: 1つのスケールではすべてに合わない理由

ここまでは順調です。しかし、落とし穴があります。

テンソル全体に1つのグローバルスケールとゼロポイントを使用すると、外れ値が重大な精度損失を引き起こす可能性があります。

ほとんどが小さな重みの行列で、1つの大きなスパイクを除いた場合を想像してください:

ブロック1: [-0.05, 0.03, -0.02, 0.01, ...]
ブロック2: [-0.08, 0.04, 8.5, -0.06, ...]

その単一の8.5が範囲を劇的に広げます。
今、すべての小さな値—学習のほとんどが起こる場所—がいくつかのビンに押し込まれ、精度を失います。

解決策は? すべてに1つのスケールを使用しないことです。

ブロックワイズ量子化: ローカルコンテキストのためのローカルスケール

1つのスケールでテンソル全体を量子化する代わりに、より小さなチャンク (通常、ブロックあたり64または128値) に分割します。
各ブロックは独自のスケールとゼロポイントを取得し、そのローカル範囲に適応します。

視覚的には、大きな行列をタイルにスライスすることを想像できます—各タイルには独自のミニチュア量子化スキームがあります。

これにより、モデルの一部の外れ値が残りの部分に影響を与えないため、精度が劇的に向上します。

bitsandbytesを含むほとんどの最新ライブラリは、デフォルトでブロックワイズ量子化を使用します。

隠れたコスト: メタデータのオーバーヘッド

ブロックワイズ量子化は精度問題を解決しますが、新しい問題を導入します。

各ブロックには独自のスケールとゼロポイントが必要であり、これらの数値もメモリを消費します。
それらは小さいですが、数百万個あります。

7Bモデルの数値を計算してみましょう。

70億パラメータ
ブロックサイズ = 64
→ 1億900万ブロック

各ブロックのスケールとゼロポイントは32ビット浮動小数点として保存されます。
したがって、次のものを保存する必要があります:

  • 1億900万個のスケールファクター × 4バイト = 436MB
  • 1億900万個のゼロポイント × 4バイト = 436MB
  • 合計: 872MB

量子化定数を保存するためだけに約0.9GBのオーバーヘッドがあります。

それでは、これらも圧縮できるでしょうか?

ダブル量子化: 圧縮器を圧縮する

賢い洞察は次のとおりです:
これらのスケールとゼロポイントの値は単なる数値です—なぜそれらも量子化しないのでしょうか?

それがまさにダブル量子化が行うことです。

2回目の量子化パスを適用し、これらのスケールファクターとゼロポイントを32ビットから8ビット精度に削減し、総メモリでさらに10-15%節約します。

ステップバイステップで分解してみましょう。

ステップバイステップ: 2層の量子化

  • 第1層: モデルの重みを量子化 (例: 4ビット)。
    → ブロックごとにスケールとゼロポイントを生成します。
  • 第2層: すべてのスケールとゼロポイントの値を収集し、再び量子化 (通常8ビット)。
    → 保存コストを劇的に削減します。

圧縮を重ねた圧縮です。

簡単な図は次のとおりです:

単一量子化 → 4ビット重み + (32ビットスケール、32ビットゼロ) ブロックごと
ダブル量子化 → 4ビット重み + (8ビットスケール、8ビットゼロ) ブロックごと
 + (32ビットメタスケール、32ビットメタゼロ) ブロックグループごと

7Bモデルのメモリ節約を計算してみましょう:

セットアップ:

  • ブロックサイズ: ブロックあたり64パラメータ
  • ブロック数: 7B ÷ 64 = ~1億900万ブロック

元のモデル (BF16):

重み: 7B × 2バイト (BF16) = 14GB

単一量子化 (4ビット):

重み: 7B × 0.5バイト (4ビット) = 3.5GB
スケールファクター: 1億900万 × 4バイト (FP32) = 436MB
ゼロポイント: 1億900万 × 4バイト (FP32) = 436MB
────────────────────────────────────────────────────
合計: ~4.37GB

ダブル量子化 (4ビット + 8ビットメタデータ):

次に、これら1億900万個のスケールファクターとゼロポイントを量子化します。それらを256のブロックにグループ化します:

重み: 7B × 0.5バイト = 3.5GB
スケールファクター (8ビット): 1億900万 × 1バイト = 109MB ⬇️
ゼロポイント (8ビット): 1億900万 × 1バイト = 109MB ⬇️

第2レベルブロック: 1億900万 ÷ 256 = ~42万6000ブロック
メタスケール: 42万6000 × 4バイト = 1.7MB
メタゼロ: 42万6000 × 4バイト = 1.7MB
────────────────────────────────────────────────────────
合計: ~3.72GB

合計圧縮:

  • 元の値 (BF16): 14GB
  • 単一量子化: 4.37GB (69%削減)
  • ダブル量子化: 3.72GB (73%削減)

節約されたメモリ: 単一量子化からダブル量子化で650MB (15%の追加節約)

7Bモデルの場合、14GB (BF16) から3.72GB (4ビット + ダブル量子化) になり、73%の削減です。

NF4: ニューラルネットワーク向けの4ビット量子化の最適化

量子化がどのように機能し、ダブル量子化がメモリを節約する方法を説明しました。しかし、4ビット量子化をはるかに効果的にするもう1つの最適化があります: NF4 (NormalFloat4)。

前のレッスンでNF4について学びました—これは、ニューラルネットワークの重みの正規分布に最適化された不均一なビン間隔を持つ4ビット形式です。標準のINT4は、その16個の値を均等に配置しますが (-8、-7、-6、…、6、7のように)、ニューラルネットワークの重みはゼロ周辺にクラスタ化します。NF4は、ほとんどの重みが存在するゼロ付近により多くのビンを割り当て、末端には少なく割り当てます。

量子化にとってそれが重要な理由は次のとおりです:

NF4:

ビン: [-1.0, -0.70, -0.53, -0.39, -0.28, -0.18, -0.09, 0,
 0.08, 0.16, 0.25, 0.34, 0.44, 0.56, 0.72, 1.0]
 ゼロ付近で密 → 重要な場所で解像度を最大化

標準INT4:

ビン: [-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7]
 等間隔 → まばらな領域で解像度を無駄に使う

ニューラルネットワークを4ビットに量子化する場合、標準のINT4の代わりにNF4を使用すると、通常、品質損失が3-5%から1-2%に減少します—メモリコストがゼロで大幅な改善です。

これが、QLoRAがベースモデルの重みにデフォルトでNF4を使用する理由です。ブロックワイズ量子化 + ダブル量子化 + NF4の組み合わせにより、48GBのVRAMで70Bモデルをファインチューニングし、出力品質の損失を最小限に抑えることが可能になります。

実践での実装

これらすべての技術がコードでどのように組み合わさるかを見てみましょう。

すべてを手作業で構築する必要はありません。bitsandbytesのようなライブラリは、モデルをロードするときに量子化を自動的に処理します。

次のようになります:

from transformers import AutoModelForCausalLM, BitsAndBytesConfig
import torch

bnb_config = BitsAndBytesConfig(
 load_in_4bit=True, # 4ビット量子化を有効化
 bnb_4bit_quant_type="nf4", # NormalFloat4形式を使用
 bnb_4bit_use_double_quant=True, # ダブル量子化を有効化
 bnb_4bit_compute_dtype=torch.bfloat16 # 安定性のためにBF16で計算
)

model = AutoModelForCausalLM.from_pretrained(
 "meta-llama/Llama-2-7b-hf",
 quantization_config=bnb_config,
 device_map="auto"
)

print("4ビットNF4量子化とダブル量子化を有効にしてモデルをロードしました。")

3つの重要な設定に注目してください:

  • load_in_4bit=True → ブロックワイズアプローチで4ビット量子化を有効化
    load_in_4bit=True
  • bnb_4bit_quant_type=”nf4″ → ニューラルネットワーク向けに最適化されたビン間隔を使用
    bnb_4bit_quant_type="nf4"
  • bnb_4bit_use_double_quant=True → 量子化メタデータを圧縮
    bnb_4bit_use_double_quant=True

この構成により、BF16で通常約14GBを必要とする7Bモデルが、品質損失を最小限に抑えて4GB未満のVRAMに快適に収まります。

まとめと次のステップ

量子化はLLM Engineeringプロジェクトにおいて不可欠な技術です。これが大規模なLLMファインチューニングを可能にするものです。

これまでに、次のことを理解しました:

  • 量子化が連続した重みを整数にマッピングする方法
  • ブロックワイズ量子化が外れ値問題を解決する理由
  • ダブル量子化がメタデータを圧縮する方法
  • そしてNF4がニューラル重みに最適な4ビット表現を提供する方法

これらの技術を組み合わせることで、モデルサイズを最大8倍削減し、それ以外では小さすぎるGPUで70Bパラメータモデルのファインチューニングを可能にします。

次に、この効率性がLoRAとどのように組み合わさってQLoRAを作成するかを見ていきます—アダプタベースの更新と量子化されたベース重みの両方を使用して大規模モデルをファインチューニングする技術です。

言い換えれば: 大きなモデルを収める方法を学びました。次に、それらを効率的にファインチューニングする方法を学びます。

続けましょう。

  • 大規模モデルを控えめなGPUに収める

コメント

タイトルとURLをコピーしました