結論

LLM の量子化は、重みを「小さな整数+スケール(倍率)」で持ち、使うときに掛け戻す技術です。精度があまり落ちないのは、丸めの誤差が重みと無関係な小さな雑音としてふるまい、内積と LayerNorm で薄まり、答えが入れ替わるのは「もともと迷っていた位置」に限られるからです。numpy で学習した小さな言語モデルで測ると、int8 ではパープレキシティがほぼ変わらず(20.628 → 20.622)、int4 でもグループ単位なら 0.1% の悪化(20.657)でした。崩れるのは 2〜3 ビットまで下げたときと、外れ値の重み・活性化があるときです。

この記事でわかること

  • テンソル単位・チャンネル単位・グループ単位(32/64/128)で int8・int4 にしたときの容量・パープレキシティ・次の文字の一致率・生成の一致率(実測表)
  • 外れ値の重み・活性化が量子化を壊す様子を、関数としては同じモデルで再現し、外れ値だけ別に持つ・スケールを分ける・SmoothQuant 風の移し替えで直した結果
  • GPTQ・AWQ・SmoothQuant・LLM.int8()・GGUF の型(Q4_K_M など)の違い(論文の abstract と公式ドキュメントの範囲)と、選び方の判断表

2026年11月4日更新|numpy 2.5.3 で文字単位の言語モデル(53.5万パラメータ)を自作・学習し、量子化を自作して実測

この記事で実際に動かしたもの:numpy だけで書いた 2 層の Transformer(文字単位の言語モデル)の学習、RTN(四捨五入)・GPTQ・AWQ 風・外れ値分離・活性化の量子化・SmoothQuant 風の自作コード、int4 を 1 バイトに 2 個詰めて保存するコード。

量子化の解説では「4 ビットまでは体感できる劣化はない」とよく書かれますが、なぜそうなるのか、どこから崩れるのかを数字で見る機会は多くありません。この記事では、学習済みの重みを外から取ってくる代わりに、自分で小さな言語モデルを学習し、その重みを自分で量子化して、損失・一致率・誤差の伝わり方を順に測ります。小さなモデルなので数字そのものは大きな LLM と同じにはなりませんが、仕組みと崩れ方の傾向はそのまま確かめられます。

flowchart TD
    A[FP32 の重み] --> B[入力方向に 32 個ずつ区切る]
    B --> C[最大の絶対値 ÷ 7 = スケール]
    C --> D[重み ÷ スケールを四捨五入し -8〜7 に]
    D --> E[(4 ビット整数 + fp16 のスケール)]
    E --> F[使うとき 整数 × スケール で戻す]
    F --> G[行列積]

よく調べられることの答えを先にまとめます。

知りたいこと 答え 詳しい節
LLM 量子化の仕組み 重みを整数に丸め、グループごとのスケールと一緒に保存し、計算の直前に掛け戻す 量子化の仕組み
量子化で精度が劣化しない理由 誤差は重みと無相関の小さな雑音で、出力に伝わるうちに薄まる。答えが変わるのは 1 位と 2 位が僅差の位置だけ なぜ精度があまり落ちないのか
int8 と int4 の違い int8 は容量 1/4 でほぼ無劣化。int4 は 1/7 前後で、スケールの単位を細かくすると劣化が小さい テンソル・チャンネル・グループ単位
何ビットまで下げられるか 手元では 4 ビットまでは損失の悪化 0.1%、3 ビットで 1.4%、2 ビットで 22% と急に崩れた ビット数を下げていくと崩れる点
外れ値の影響 大きな値が 1 つあるだけでスケールが引き伸ばされ、ほかの重みの目盛りが粗くなる 外れ値の重みが量子化を壊す
GPTQ と AWQ の違い GPTQ は丸めた誤差を残りの重みで埋め合わせる。AWQ は活性化から大事なチャンネルを見つけ、拡大してから量子化する GPTQ と AWQ の違い
Q4_K_M とは GGUF の 4 ビット K 系(32 個ずつのブロック×8 の super-block)。M は S より一部を大きく持つ混合で、Llama 3.1 8B では 4.89 ビット/重み GGUF の型の読み方
量子化で速くなるか 専用の計算カーネルがあれば、読み込む重みが減るぶん生成が速くなる。numpy の整数行列積は逆に遅い(手元で 150 倍) 容量の計算と速さ

確認環境:Ubuntu 24.04.5 LTS(Intel Xeon 2.10GHz・CPU 2 コア・メモリ 8GB、ほかの処理と共用)の Python 3.13.18 と numpy 2.5.3。OPENBLAS_NUM_THREADS=1 で 1 スレッドに固定しました。学習データは、この技術ブログの記事 247 本の本文(コードブロックとタグを除いた日本語の文章)で、235 本・448 万文字を学習に、残りの 12 本・22.8 万文字を評価に使っています。文字の種類は出現の多い 511 字+「その他」の 512 種類(学習データの 97.3% を覆う)。モデルは埋め込み 128 次元・2 層・4 ヘッド・文脈 64 文字の Transformer(53.5 万パラメータ)で、逆伝播も numpy で書き、数値微分との相対誤差 9.4×10-6 で正しさを確かめてから、Adam で 1,000 ステップ(213.7 秒)学習しました。評価は、評価用の文章 12,288 文字でのパープレキシティ、FP32 のモデルとの次の文字の 1 位の一致率と KL ダイバージェンス、16 個の書き出しから貪欲法で 64 文字ずつ生成した文字列の一致です。GPTQ・AWQ・SmoothQuant・LLM.int8()・QLoRA・BitNet b1.58 は arXiv の論文の abstract、GGUF の型は Hugging Face Hub のドキュメント、Q4_K_M の大きさは llama.cpp の quantize の README で確かめた範囲です。各手法の公式の実装は動かしておらず、この記事の GPTQ・AWQ は論文の考え方を自分で書いた簡略版です。図は mermaid で描画を確かめています(確認日 2026年11月4日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「LLMの量子化とは|int8・int4で精度が落ちにくい理由を自作して実測」

LLM の量子化とは:重みを少ないビットで持つこと

この節では、量子化が何を小さくする技術か、言葉の範囲(重みだけか活性化もか、学習後か学習中か)が分かります。

LLM の量子化は、ふつう 16 ビットの浮動小数点で持つ重みを、8 ビットや 4 ビットの整数で持ち直す技術です。7B(70 億パラメータ)のモデルなら、FP16 で約 14GB の重みが int8 で約 7GB、int4 で約 3.5〜4GB になります。GPU のメモリ(VRAM)に載るかどうかがこの差で決まることが多く、ローカル LLM を動かすときの前提になっています。必要な VRAM の計算は、関連記事「VRAMとは|確認方法・必要量の目安とLLMの計算式をPythonで解説」で扱っています。

1重みだけ(weight-only)

重みを int4・int8 で保存し、計算は戻した 16 ビットで行う。GPTQ・AWQ・GGUF の多くはこれ。生成 1 文字ごとに読む重みが減る。

2重みと活性化(W8A8 など)

層への入力(活性化)も 8 ビットにして、整数のまま行列積をする。SmoothQuant・LLM.int8() が扱う。活性化の外れ値が難所。

3学習後(PTQ)と学習中(QAT)

学習済みの重みをあとから丸めるのが PTQ。量子化した状態を前提に学習するのが QAT。この記事の実験はすべて PTQ。

浮動小数点数がどう値を持っているか(指数と仮数)は、関連記事「浮動小数点数とは|仕組み・誤差・0.1+0.2の理由を4言語で実演」で扱っています。FP16 と BF16 は同じ 16 ビットでも指数と仮数の配分が違いますが、量子化の整数は「目盛りの数」が決まっているだけで、値の幅はスケールが受け持ちます。

量子化の仕組み:スケール・ゼロ点・対称と非対称を実物で

この節では、重みが整数に変わり、また戻るまでの計算を、学習したモデルの実際の重み 8 個で追えます。

対称量子化では「最大の絶対値 ÷ 整数の最大値」をスケールにし、重み ÷ スケールを四捨五入します。戻すときは整数 × スケールです。学習したモデルの 1 層目の重み(0.w1 の先頭 8 個)で計算した結果です。

元の重み 8個 [ 0.0258 -0.0431  0.0038 -0.1174  0.0456  0.0066 -0.0942  0.0354]
int8: scale=0.000925 q=[  28  -47    4 -127   49    7 -102   38]
      戻した値=[ 0.0259 -0.0435  0.0037 -0.1174  0.0453  0.0065 -0.0943  0.0351]
      誤差の最大=0.00036 (scale/2=0.00046)
int4: scale=0.016774 q=[ 2 -3  0 -7  3  0 -6  2]
      戻した値=[ 0.0335 -0.0503  0.     -0.1174  0.0503  0.     -0.1006  0.0335]
      誤差の最大=0.00772 (scale/2=0.00839)

int8 は目盛りが 255 段あるので、戻した値はほぼ元どおりです。int4 は 16 段しかないため、0.0038 と 0.0066 はどちらも 0 になり、0.0258 と 0.0354 は同じ 0.0335 になりました。誤差は必ず「スケールの半分」以下に収まります。量子化の誤差を小さくするには、ビット数を増やすか、スケールを小さくする(1 つのスケールが受け持つ範囲を狭くする)しかありません。

対称(ゼロ点なし)

スケール = 最大の絶対値 ÷ 7(int4)

0 はちょうど 0 に戻る

分布が 0 を中心に左右対称なら無駄が少ない

非対称(ゼロ点あり)

スケール = (最大 − 最小) ÷ 15、ゼロ点 = −最小 ÷ スケール

偏った分布でも 16 段を使い切れる

ゼロ点のぶん保存量が少し増える

手元で実際に 4 ビットで保存した大きさも確かめました。0.w1(128×512=65,536 個)を 32 個ずつのグループで int4 にし、1 バイトに 2 個詰めて .npy で保存しています。

w = W.reshape(n // 32, 32, o)
s = np.abs(w).max(1, keepdims=True) / 7                    # グループごとのスケール
q = (np.clip(np.round(w / s), -8, 7).astype(np.int8) + 8).astype(np.uint8).reshape(-1)
packed = (q[0::2] | (q[1::2] << 4)).astype(np.uint8)        # 1 バイトに 2 個
np.save('w1_q4.npy', packed); np.save('w1_s16.npy', s.astype(np.float16))
w1_fp32.npy 262272
w1_q4.npy 32896
w1_s16.npy 4224
最大差(自作の詰め方 vs rtn) 0.000104025006  相対誤差 0.09571671

FP32 の 262KB が、整数 32.9KB+スケール 4.2KB になりました(ヘッダー 128 バイトを含む)。重み 1 個あたり 4 ビット+スケール 16 ビット ÷ 32 個=4.5 ビットです。取り出して戻した値の相対誤差は 9.57% でした。重みが 1 割近くずれているのに、モデル全体の損失はほとんど変わりません。その理由を次から測ります。

実験の準備:numpy で小さな言語モデルを 5 分以内に学習する

この節では、量子化の対象にしたモデルの作りと、学習の経過、基準になる数字が分かります。

量子化の効き方を確かめるには、ランダムな行列ではなく、学習で意味を持った重みが要ります。学習済みの LLM の重みはこの環境から取得できないため、文字単位の言語モデルを numpy で書いて学習しました。前置き LayerNorm・因果マスク付きの注意・GELU の MLP という、LLM と同じ部品の Transformer です。ニューラルネットワークと Attention の基本は、関連記事「ニューラルネットワークとは|仕組み・学習・AttentionをNumPyで実演」で扱っています。

$ OPENBLAS_NUM_THREADS=1 python3 train.py 1000
step     1 loss 6.2823 lr 0.00003    0.5s
step   100 loss 3.6492 lr 0.00293   22.9s
step   200 loss 3.4757 lr 0.00274   45.9s
step   400 loss 3.3530 lr 0.00207   96.2s
step   600 loss 3.1579 lr 0.00123  139.3s
step   800 loss 2.9741 lr 0.00056  174.2s
step  1000 loss 2.9073 lr 0.00030  213.7s
params 535040 time 213.7s
20.63評価用の文章のパープレキシティ直前 1 文字だけ見る bigram は 29.03、文字の頻度だけなら 176.5
39.3万量子化の対象にした重み各層の Q・K・V・O と MLP の 2 行列(2 層で 12 個)
214秒学習時間(1 スレッド)16 本×64 文字のバッチを 1,000 回

パープレキシティは「次の 1 文字を平均して何択まで絞れているか」です。20.63 は bigram より良く、文脈を使えています。ただし生成文は小さなモデルらしく繰り返しになります(例:' overlay ネットワークMTU と必要なポ' → 'ートの Python の Python の Python の …')。この記事は文章の質ではなく、FP32 のときと比べてどれだけ変わったかだけを測ります。

途中でつまずいた点もそのまま載せます。最初の版は 1 ステップ 0.6 秒かかり、調べると勾配がすべて float64 になっていました。np.sqrt(hd) が返す numpy の float64 スカラーで float32 の配列を割ると、numpy 2 では結果が float64 になります。

>>> x = np.ones(3, np.float32)
>>> (x / np.sqrt(64)).dtype, (x / float(np.sqrt(64))).dtype
(dtype('float64'), dtype('float32'))

Python の float に直すと float32 のままで、1 ステップ 0.25 秒になりました。量子化の実験でも、float16 に落としたつもりが float64 で計算していた、という食い違いは起きやすいので、dtype を出力して確かめる習慣が役に立ちます。

テンソル単位・チャンネル単位・グループ単位で int8・int4 にした実測

この節では、スケールを「どの範囲で 1 つ持つか」で、容量と精度がどう変わるかが分かります。

int8 はどの単位でもパープレキシティが変わらず、int4 では単位を細かくするほど劣化が小さくなりました。テンソル単位は行列全体で 1 つ、チャンネル単位は出力チャンネル(列)ごとに 1 つ、グループ単位は出力チャンネルの中をさらに入力方向に 32・64・128 個ずつ区切って 1 つずつスケールを持ちます。この小さなモデルでは入力が 128 次元の行列が多く、group128 はそれらの行列ではチャンネル単位と同じになります(MLP の 2 つ目の行列だけ入力が 512 次元)。

方式(12 行列) 容量 パープレキシティ 1 位の一致(KL) 生成 16 本の一致
FP32(基準)1,536KB20.628100%(0)16/16
FP16768KB20.628100%(0.0000)16/16
int8 テンソル単位384.0KB20.62899.6%(0.0000)9/16
int8 チャンネル単位388.5KB20.62299.7%(0.0000)13/16
int8 group64396.0KB20.62499.7%(0.0000)15/16
int8 group32408.0KB20.62799.8%(0.0000)12/16
int4 テンソル単位192.0KB20.82791.5%(0.0102)1/16
int4 チャンネル単位196.5KB20.80094.6%(0.0037)2/16
int4 group128198.0KB20.76994.9%(0.0035)3/16
int4 group64204.0KB20.72095.2%(0.0031)1/16
int4 group32216.0KB20.65795.9%(0.0025)2/16

読み方の要点は 3 つです。1 つ目に、int8 は容量 1/4 でパープレキシティの差が 0.03% 以内です(ばらつきで基準より僅かに良い値も出ます)。2 つ目に、int4 のテンソル単位は 0.96% 悪化しますが、group32 にすると 0.14% まで縮まり、そのぶん容量はスケールの分だけ 24KB 増えます。3 つ目に、生成の一致は損失よりはるかに敏感で、int8 テンソル単位でもパープレキシティが同じなのに 16 本中 7 本がどこかで別の文字列になりました。

埋め込みと出力層まで量子化すると、悪化が増えました。

int8 channel +emb/out   517.8KB ppl 20.626 top1 99.2% KL 0.0001 全一致 11/16
int4 group32 +emb/out   288.0KB ppl 20.815 top1 90.6% KL 0.0109 全一致 1/16

int4 では線形層だけのとき(20.657)より悪く、次の文字の 1 位の一致も 95.9% から 90.6% に落ちています。出力層は語彙の数だけ答えの候補を直接並べる行列なので、ここの誤差はそのまま確率の並びに出ます。GGUF の配布ファイルでも出力層や埋め込みを別の型にする設定があり(llama.cpp の quantize には --tensor-type などテンソルごとに型を変える指定があります)、層によってビット数を変える考え方はここから来ています。

なぜ LLM の量子化で精度があまり落ちないのか(誤差の伝わり方を実測)

この節では、重みが 1 割ずれても損失がほとんど変わらない理由を、4 つの測定で分解して確かめられます。

理由は「誤差が無相関の雑音であること」「伝わるうちに薄まること」「答えが変わるのは僅差の位置だけであること」「重みの分布が素直であること」の 4 つです。group32 で量子化したときの誤差を、重み → 層の出力 → 最終の logits と追いかけました。

int8 g32: 0.w1 重み相対誤差 0.53% 出力相対誤差 0.37% logits相対誤差 0.17%
          1位が変わった位置 0.2%  その位置の1位-2位の差 中央値 0.003 / 全体 0.596
          誤差と重みの相関 0.002
int4 g32: 0.w1 重み相対誤差 9.57% 出力相対誤差 6.61% logits相対誤差 3.15%
          1位が変わった位置 4.1%  その位置の1位-2位の差 中央値 0.043 / 全体 0.596
          誤差と重みの相関 0.008
int3 g32: 0.w1 重み相対誤差 22.34% 出力相対誤差 15.28% logits相対誤差 7.46%
          1位が変わった位置 9.3%  その位置の1位-2位の差 中央値 0.087 / 全体 0.596
          誤差と重みの相関 0.019
1

誤差は雑音としてふるまう

丸め誤差と元の重みの相関は 0.002〜0.019。int4 の誤差をランダムに並べ替えて足した「ただの雑音」でも、パープレキシティは 20.698 と、量子化そのもの(20.657)とほぼ同じだった。

特定の方向に偏らない

2

内積で薄まる

重みの誤差 9.57% が、層の出力では 6.61%、最終の logits では 3.15%。128 個の積の和では、符号のばらばらな誤差が打ち消し合い、残差接続と LayerNorm が大きさをそろえ直す。

誤差は増えずに減った

3

変わるのは迷っていた位置

int4 で 1 位が入れ替わった 4.1% の位置は、元のモデルで 1 位と 2 位の差の中央値が 0.043。全体の中央値 0.596 の 1/14 で、もともと五分五分だった所だけが入れ替わった。

自信のある答えは動かない

4

分布が素直

12 行列の「最大の絶対値 ÷ 標準偏差」は 3.9〜7.3、尖度は 0.00〜2.50。極端な値が無いので、スケールが一部の値のために引き伸ばされない。

外れ値があると崩れる

同じ大きさの誤差を「並べ替えた雑音」として足した比較の全体です。量子化はスケールの範囲内で一様に近い誤差を足す操作と見なせて、雑音と同じ効き方をしています。

int8 g32 対称               ppl 20.627 top1 99.8% KL 0.0000
int8 g32 の誤差を並べ替えた雑音   ppl 20.629 top1 99.8% KL 0.0000
int4 g32 対称               ppl 20.657 top1 95.9% KL 0.0025
int4 g32 の誤差を並べ替えた雑音   ppl 20.698 top1 95.9% KL 0.0023
int3 g32 対称               ppl 20.923 top1 90.7% KL 0.0138
int3 g32 の誤差を並べ替えた雑音   ppl 20.886 top1 89.9% KL 0.0133

3 つ目の点は、生成の一致が損失より敏感だった理由でもあります。貪欲法の生成は毎回 1 位を選ぶので、僅差の位置で 1 回入れ替わると、その後の文脈が丸ごと変わります。一方で、入れ替わった位置はもともと 2 つの候補がほぼ同じ確率だった所なので、損失(正解の文字に付けた確率)はほとんど動きません。「生成文が変わった」ことと「質が落ちた」ことは別で、評価には損失・KL と、仕事の課題での正答率を組み合わせます。LLM の評価の組み立て方は、関連記事「LLMの評価方法と評価指標一覧|テストケース表のテンプレート付き」で扱っています。

ビット数を下げていくと崩れる点:8・6・5・4・3・2 ビットの実測

この節では、何ビットまで下げると急に崩れるのかと、対称・非対称の差が分かります。

group32 で 8 ビットから 1 ビットずつ下げると、4 ビットまでは損失の悪化が 0.15% 以内、3 ビットで 1.4%、2 ビットで 22% と、最後に崖があります。誤差はビットを 1 つ減らすごとに約 2 倍になるので、logits の誤差が 1 位と 2 位の差の典型値(0.6)に近づいたところで、自信のある答えまで入れ替わり始めます。

ビット(group32) 容量 対称 ppl(1 位の一致) 非対称 ppl(1 位の一致)
8408.0KB/420.0KB20.627(99.8%)20.627(99.7%)
6312.0KB/321.0KB20.630(99.0%)20.633(99.1%)
5264.0KB/271.5KB20.646(98.1%)20.649(98.3%)
4216.0KB/222.0KB20.657(95.9%)20.595(96.6%)
3168.0KB/172.5KB20.923(90.7%)20.930(92.8%)
2120.0KB/123.0KB25.200(67.1%)21.668(81.4%)

2 ビットで非対称が大きく勝つのは、対称の 2 ビットは −2・−1・0・1 の 4 段のうち、絶対値の最大で決めたスケールでは実質 −1・0・1 の 3 段しか使えないためです。非対称は最小から最大までを 4 段に割り付けるので、目盛りを無駄にしません。GGUF の Q4_K・Q5_K・Q2_K が「スケール+最小値(block_min)」の形、Q6_K・Q3_K が「スケールだけ」の形になっているのも、ビット数ごとにこの得失が違うからだと読めます(型の式は後の節の表)。

生成文を並べると、崩れ方が目で分かります。2 ビットでも書き出しによっては同じ繰り返しになり、別の書き出しでは意味の無い綴りが増えます。

PROMPT ' IT用語□典 e-Words と Weblio'      (□は語彙に無い文字)
  FP32         'ws の Python の porer の stale の stale の st'
  int8 channel 'ws の Python の porer の stale の stale の st'
  int4 g32     'ws の pale の pate の pate stale の stale の '
  int2 g32     'ws の ache の steande の stal の shetesterio'

「4 ビットなら劣化しない」の受け取り方

誤解4 ビットはどのモデル・どの方式でも無劣化で、3 ビットや 2 ビットも同じ調子で使える
→
実測から言えること4 ビットは「スケールを細かく持てば」損失の悪化が小さい。3 ビット以下は誤差が答えの差に届き始め、方式(GPTQ・非対称・重要度)の差が大きく出る

GPTQ の論文も「3〜4 ビットで精度の低下がごく小さい」と書き、2 ビット・3 値は「それなりの精度」と分けて書いています

外れ値の重みが量子化を壊す様子と、外れ値だけ別に持つ・スケールを分ける対策

この節では、大きな値が 1 つあるだけで量子化が崩れる理由と、3 つの直し方の効き目が分かります。

スケールは「最大の絶対値」で決まるため、外れ値が 1 つあると、同じスケールを使うほかの重みの目盛りが粗くなります。学習したモデルには目立つ外れ値が無かったので、関数としてはまったく同じまま外れ値を作りました。各層の LayerNorm の出力の 5 番目のチャンネルを 1/30 倍し、それを受ける Q・K・V の重みの 5 行目を 30 倍しています。掛けて割っているだけなので、FP32 のパープレキシティは 20.628 のまま、生成も 16/16 一致です。

方式 容量 元の重み ppl(1 位の一致) 外れ値入り ppl(1 位の一致)
int8 テンソル単位384.0KB20.628(99.6%)20.657(94.4%)
int8 チャンネル単位388.5KB20.622(99.7%)20.646(98.1%)
int4 チャンネル単位196.5KB20.800(94.6%)26.120(66.7%)
int4 group32216.0KB20.657(95.9%)21.455(84.6%)
int4 チャンネル+大きい 0.5% を 16 ビット208.0KB20.761(95.5%)20.791(93.9%)
int4 group32 AWQ 風220.5KB20.673(95.8%)20.686(95.7%)

int4 のチャンネル単位は、外れ値が入っただけでパープレキシティが 20.800 から 26.120 に悪化し、2 ビットより悪くなりました。各列の中に 30 倍の値が 1 つあり、その列の残り 127 個がほぼすべて 0 に丸められたためです。直し方の効き目は次のとおりでした。

  1. 1スケールを細かく分ける(group32):26.120 → 21.455外れ値と同じグループに入る 31 個だけが巻き込まれる。被害は狭まるが残る
  2. 2外れ値だけ 16 ビットで別に持つ:26.120 → 20.791絶対値の上位 0.5% を値(fp16)と位置(int32)で保存。容量は 11.5KB 増える。LLM.int8() が活性化で行う「外れ値の次元だけ 16 ビット」を重みに当てはめた形
  3. 3入力の大きさに合わせて行を拡大・縮小する(AWQ 風):21.455 → 20.686外れ値の行は入力側が 1/30 なので、入力の平均の大きさの α 乗で行をそろえてから量子化すると、外れ値が消える。元のモデルとほぼ同じ精度に戻った

元の重み(外れ値なし)では、外れ値を別に持つ方法も AWQ 風も、普通の group32 と差がほとんどありません。対策の効き目は、外れ値がどれだけあるかで決まります。大きな LLM では外れ値が多いことが論文で報告されているため(次の節)、こうした工夫が標準になっています。

活性化の外れ値と W8A8:SmoothQuant と LLM.int8() の考え方を再現

この節では、重みだけでなく層への入力(活性化)も 8 ビットにするときの難しさと、2 つの論文の直し方が分かります。

活性化はトークンごとに値が変わるうえ、特定のチャンネルだけが大きくなりやすく、重みより量子化しにくい値です。LLM.int8() の論文は、大きなモデルで系統的に現れる外れ値の特徴の次元が注意と予測の性能を左右すると述べ、SmoothQuant の論文は「重みは量子化しやすく、活性化はしにくい」という観察から出発しています。この小さなモデルの活性化は、チャンネルごとの最大値の比が 1.5〜6.4 倍と穏やかだったので、ここでも関数を変えずに外れ値を作りました(LayerNorm のゲインの 5 番目を 30 倍、次の重みの 5 行目を 1/30)。1 層目の Q・K・V への入力の最大値は 3.55 から 61.17 になります。

重み int8(チャンネル単位)+活性化 int8                          元の重み       活性化に外れ値
W8A8 活性化 per-tensor(全体で1つのスケール)                       ppl 20.618    ppl 21.334(1位 86.4%)
W8A8 活性化 per-token(トークンごとのスケール)                      ppl 20.623    ppl 20.685(1位 96.2%)
W8A8 + |x|>=6 を含む次元だけ16ビット(LLM.int8() の考え方)         ppl 20.623    ppl 20.629(1位 99.0%)
W8A8 SmoothQuant 風(α=0.5)で移してから per-tensor                ppl 20.618    ppl 20.618(1位 98.5%)
1

そのまま per-tensor で 8 ビット

外れ値 61 に合わせたスケールでは、ほかのチャンネル(2 前後)が数段の目盛りにつぶれ、20.618 → 21.334 に悪化した。

崩れる

2

トークンごとにスケール

LLM.int8() の vector-wise(内積ごとに別の正規化定数)に近い。外れ値はどのトークンにも出るので、改善は 20.685 まで。

半分戻る

3

外れ値の次元だけ 16 ビット

LLM.int8() の混合精度の分解。論文の abstract では 99.9% 以上の値を 8 ビットで掛けるとしている。手元でも 20.629 とほぼ戻った。

ほぼ戻る

4

難しさを重みへ移す

SmoothQuant の考え方。チャンネル j の入力を sj で割り、重みの j 行目を sj 倍する(数学的に同じ変換)。sj = 入力の最大の 0.5 乗 ÷ 重みの最大の 0.5 乗。外れ値の有無に関係なく 20.618 になった。

外れ値が消える

SmoothQuant の変換は前の LayerNorm のゲインに畳み込めるので、推論時の追加の計算がありません。手元の再現では、畳み込める入力(Q・K・V と MLP の 1 つ目)にだけ当て、残りは per-tensor のまま量子化しました。論文の abstract では、W8A8 で最大 1.56 倍の推論の高速化と 2 倍のメモリ削減を、精度の低下がほぼ無い状態で達成したとしています。

GPTQ と AWQ の違い:論文の要点と自作の簡略版での比較

この節では、代表的な 4 ビット量子化の 2 方式が何をしているか、手元で書いた簡略版でどれだけ差が出たかが分かります。

GPTQ は「丸めた誤差を、まだ丸めていない重みで埋め合わせる」方式、AWQ は「大事なチャンネルを活性化から見つけて、拡大してから丸める」方式です。どちらも学習し直さない PTQ で、少量の較正データ(実際の入力)を使います。

項目 GPTQ AWQ
論文Frantar ら、arXiv 2210.17323(2022年10月)Lin ら、arXiv 2306.00978(2023年6月、MLSys 2024 Best Paper)
考え方二次の情報(入力から作るヘッセ行列の近似)を使い、1 回の処理で重みを量子化する重みの 1% の重要な部分を守るだけで誤差が大きく減る。重要なチャンネルは活性化の分布から決め、拡大で守る
abstract の主張175B を約 4 GPU 時間で 3〜4 ビットに。FP16 比で A100 約 3.25 倍、A6000 約 4.5 倍の推論速度逆伝播も再構成も使わないので較正データに過学習しにくい。TinyChat で FP16 の実装比 3 倍超の高速化
この記事の簡略版1 列ずつ丸め、誤差 ÷ 逆ヘッセ行列の対角を残りの列に配る(ブロック化・並べ替えは省略)入力の平均の大きさの α 乗で行を拡大して量子化し、割り戻す。α は 0〜1 を 0.05 刻みで探す(クリップの探索は省略)

較正データには学習用の文章から 64 本×64 文字(4,096 文字)を流し、各線形層への入力を集めました。量子化した前の層の出力ではなく FP32 の入力を使う簡略化をしています。

設定 RTN(四捨五入) GPTQ(自作) AWQ 風(自作)
int4 チャンネル単位20.800(94.6%)20.696(96.8%)―
int4 group12820.769(94.9%)20.685(96.9%)20.773(94.8%)
int4 group3220.657(95.9%)20.657(97.5%)20.673(95.8%)
int3 group3220.923(90.7%)20.779(93.8%)20.934(90.4%)
int3 チャンネル単位21.035(88.4%)20.777(92.5%)―

数字はパープレキシティ(かっこ内は FP32 との 1 位の一致)です。GPTQ は同じ容量のまま、すべての設定で RTN 以上でした。特に 3 ビットのチャンネル単位では、RTN の 21.035 が 20.777 になり、RTN の 3 ビット group32 より良くなっています。KL ダイバージェンスも int4 group32 で 0.0025 → 0.0009 と 1/3 になり、生成の一致は 16 本の平均で最初のずれが 18.8 文字目から 35.8 文字目へ伸びました。12 行列の量子化にかかった時間は 0.56〜1.02 秒です。

AWQ 風はこのモデルではほとんど効きませんでした。探索で選ばれた α は 0〜0.55 と小さく、多くの行列で「拡大しないのが最善」になっています。前の節で見たとおり、活性化の外れ値が作られたモデルでは AWQ 風が 21.455 → 20.686 と大きく効いたので、AWQ は「大事なチャンネルが目立つ」モデルほど効く方式だと分かります。大きな LLM で AWQ が効くと報告されているのは、そうしたチャンネルがあるからです。

手法の名前としては、ほかに次のものがよく並びます。いずれも公式の説明の範囲です。

  • bitsandbytes(LLM.int8()・NF4):読み込むときにその場で量子化する。Transformers のドキュメントの表では on-the-fly の量子化に対応し、4/8 ビット。NF4 は QLoRA の論文で「正規分布する重みに対して情報理論的に最適」とされた 4 ビットのデータ型で、量子化の定数をさらに量子化する double quantization と組み合わせる。
  • HQQ:Transformers のドキュメントでは、較正データが要らず、8・4・3・2・1 ビットでその場で量子化できる方式とされている。
  • BitNet b1.58:重みを −1・0・1 の 3 値にしたモデル。論文の abstract では、同じ大きさ・同じ学習トークン数の FP16/BF16 のモデルとパープレキシティと課題の性能で並ぶとし、学習の手順(recipe)とスケーリング則も新しく示している。学習後に丸める PTQ とは別の道。

GGUF の型の読み方:Q8_0・Q4_0・Q4_K_M・IQ 系

この節では、llama.cpp の GGUF ファイル名に付く型の記号が、ここまでの実験のどれに当たるかが分かります。

GGUF の型は「ビット数+ブロックの作り方」の名前です。Hugging Face Hub の GGUF のドキュメントにある表をもとに、この記事の実験との対応を付けました。

型 ビット/重み 作り(公式の表) この記事の実験との対応
Q8_08 ビット+スケール32 個ずつのブロック、w = q × block_scale(以前からある型)int8 group32 対称
Q4_0/Q4_14 ビット+スケール32 個ずつ、Q4_1 は block_minimum も持つ(以前からある型)int4 group32 の対称/非対称
Q6_K6.562516 個×16 ブロック、8 ビットのブロックスケールint6 対称に近い
Q5_K5.532 個×8 ブロック、6 ビットのスケールと最小値int5 非対称に近い
Q4_K4.532 個×8 ブロック、6 ビットのスケールと最小値int4 group32 非対称(スケールも量子化)
Q3_K3.437516 個×16 ブロック、6 ビットのスケールint3 対称に近い
Q2_K2.62516 個×16 ブロック、4 ビットのスケールと最小値int2 非対称に近い
IQ4_XS〜IQ1_S4.25〜1.56256 個の super-block、重要度の行列(importance matrix)を使う重要度で重み付けする点は AWQ・GPTQ と同じ発想

K 系の「スケールそのものも 6 ビットで持つ」作りは、この記事の group32(fp16 のスケールで 4.5 ビット/重み)と同じ 4.5 ビットで、より細かい 32 個単位のスケールと最小値を持てる工夫です。Q4_K_M の「_M」と「_S」は、llama.cpp の quantize の README に内訳の説明は無く、Llama 3.1 8B の表では Q4_K_S が 4.6672 ビット/重み・4.36GiB、Q4_K_M が 4.8944 ビット/重み・4.58GiB です。Q4_K の 4.5 より大きいことから、一部のテンソルをより大きな型で持つ混合だと読めます。前の節で出力層まで量子化すると悪化が増えたことと、同じ方向の工夫です。--imatrix は「重要度の行列のデータを量子化の最適化に使う」オプションで、README は精度の低下を「適切な imatrix ファイルで小さくできる」としています。

llama.cpp での変換と実行の手順、各型の大きさと速さの表、重みの誤差の実測は、関連記事「llama.cppとは|使い方・ビルド・GGUF量子化とllama-server」で扱っています。この記事は、その誤差がなぜ損失にあまり響かないのかを受け持ちます。

容量の計算と速さ:行列積の速さは numpy では測れない

この節では、7B モデルの容量をビット数から計算する方法と、量子化で生成が速くなる仕組み、numpy で測ると逆の結果になる理由が分かります。

容量は「パラメータ数 × ビット/重み ÷ 8」です。スケールの分はビット/重みに含めて数えます(group32 に fp16 のスケールなら 4 + 16÷32 = 4.5)。

形式 ビット/重み 7B の重みの容量 帯域 100GB/s での上限
FP16・BF161614.0GB7.1 トークン/秒
int8 チャンネル単位約 8.07.0GB14.3 トークン/秒
int4 group1284.1253.61GB27.7 トークン/秒
int4 group32・Q4_K4.53.94GB25.4 トークン/秒
Q4_K_M(Llama 3.1 8B の値で換算)4.894.28GB23.4 トークン/秒
Q2_K2.6252.30GB43.5 トークン/秒

右の列は「1 トークン生成するたびに全部の重みを 1 回読む」として、メモリの帯域を 100GB/s と仮に置いた計算上の上限です。バッチ 1 の生成は計算よりも重みの読み込みで時間が決まることが多く、重みが小さくなれば、そのぶん速くなる余地があります。KV キャッシュや活性化のメモリは別に要ります。

ただし、それは「4 ビットのまま読み込み、計算の直前に戻す」専用のカーネルがある場合の話です。numpy で測ると、次のように逆になります(64×n と n×n の行列積、ほかの処理と共用のマシンなので数値はぶれます)。

n=512:  float32 0.28ms / int8→int32 行列積 42.4ms  / 毎回 int8→float32 に戻して積 0.51ms
n=1024: float32 2.91ms / int8→int32 行列積 446.6ms / 毎回 int8→float32 に戻して積 2.08ms

numpy の整数の行列積は BLAS を使わないため、float32 の 150 倍遅くなりました。戻してから掛ける方法も、戻す処理のぶん速くはなりません。この記事の実測は容量と精度に限り、速さは専用の実装(GPU のカーネル、llama.cpp など)の公式の数字で判断します。なお int8 同士を int8 のまま掛けると、numpy は警告なしにあふれます。

>>> a = np.array([[100, 100]], np.int8); b = np.array([[100], [100]], np.int8)
>>> a @ b, (a @ b).dtype
(array([[32]], dtype=int8), dtype('int8'))      ← 本当は 20000
>>> a.astype(np.int32) @ b.astype(np.int32)
array([[20000]], dtype=int32)

量子化を自分で試すときのよくあるエラー・つまずき

この節では、量子化のコードを書いたり評価したりするときに、手元で実際に出たメッセージと、その直し方が分かります。

症状・原文 原因 直し方
RuntimeWarning: invalid value encountered in divide のあと値が nan全部 0 のグループで、スケールが 0 になり 0÷0スケールが 0 のときは 1 に置き換える(np.where(s == 0, 1, s))
int8 の行列積の結果が小さすぎる(20000 のはずが 32)int8 のまま積を取り、桁あふれ。警告は出ないint32 に広げてから掛け、最後にスケールを掛ける
RuntimeWarning: overflow encountered in scalar add、127+1 が −128numpy の int8 のスカラー同士の足し算のあふれ四捨五入のあと np.clip で範囲に収めてから int8 にする
勾配や logits の dtype が float64 で、計算が 2 倍遅いnumpy 2 では float64 のスカラー(np.sqrt(64) など)との演算で float64 に広がるfloat() で Python の数に直す。dtype を出力して確かめる
パープレキシティは同じなのに生成文が変わる1 位と 2 位が僅差の位置で入れ替わり、貪欲法ではその後がすべて変わる損失・KL・課題の正答率で比べる。生成文の一致だけで良し悪しを決めない
int4 にしたら急に崩れた(2 ビット並みの損失)外れ値の重みでスケールが引き伸ばされたグループを細かくする、外れ値だけ別に持つ、AWQ・GPTQ を使う
W8A8 にしたら損失が悪化した活性化のチャンネルの外れ値で、per-tensor のスケールが粗いトークンごとのスケール、外れ値の次元を 16 ビット、SmoothQuant で重みへ移す

「自分で確かめる」ための 1 行も載せます。手元の重み行列 W について、外れ値の目安(最大の絶対値 ÷ 標準偏差)と、int4 group32 の相対誤差を出します。比が 10 を超える行列は、量子化で崩れやすい候補です。

print(np.abs(W).max() / W.std(), np.linalg.norm(rtn(W, 4, 'group', 32)[0] - W) / np.linalg.norm(W))

活性化の側は、層への入力 X(トークン数×チャンネル)で cm = np.abs(X).max(0); print(cm.max() / np.median(cm)) とし、チャンネルごとの最大値の比を見ます。この記事のモデルは 1.5〜6.4 倍、外れ値を作ったモデルは 27.6 倍(61.17 ÷ 2.22)でした。

LLM の量子化の判断表:int8・int4・GPTQ・AWQ・GGUF の選び方

この節では、手持ちの条件からどの量子化を選ぶかが、実測と公式の説明をもとに分かります。

条件 選ぶもの 根拠
精度を落としたくない、メモリは半分になれば足りるint8(重みだけ、チャンネル単位)手元で損失の差 0.03% 以内。LLM.int8() の abstract も 175B まで劣化なしとする
VRAM に載せたい、損失の悪化は少しなら許せる4 ビット+細かいスケール(group32〜128)テンソル単位 0.96% に対し group32 は 0.14%
4 ビット以下でも精度を保ちたいGPTQ・AWQ など較正データを使う方式自作 GPTQ で int3 が 21.035 → 20.777
CPU や Mac で llama.cpp・Ollama を使うGGUF(迷ったら Q4_K_M、余裕があれば Q5_K・Q6_K・Q8_0)K 系はブロックごとにスケールと最小値を持つ 4.5〜6.6 ビットの型
GPU で推論サーバーを立てる使う推論エンジンが対応する形式(GPTQ・AWQ・FP8 など)対応は版ごとに変わるため、エンジンの公式の一覧で確かめる
活性化も 8 ビットにして整数の行列積で速くしたいSmoothQuant などの W8A8abstract では最大 1.56 倍・メモリ 2 倍削減
2〜3 ビットまで小さくしたい重要度を使う型(IQ 系)や較正データを使う方式、非対称手元では対称 2 ビットが 25.200、非対称で 21.668

量子化したモデルを仕事で使う前には、次の点を確かめます。

  • 1同じ課題で元のモデルと比べるパープレキシティ・KL に加え、自分の用途のテストケースの正答率。生成文の一致だけで判断しない
  • 2どの層を何ビットにしたかを記録する埋め込み・出力層を別の型にしているか。手元では出力層まで int4 にすると 1 位の一致が 5 ポイント落ちた
  • 3較正データの中身を確かめるGPTQ・AWQ・imatrix は較正データの入力で結果が変わる。日本語で使うなら日本語の文章が含まれているか
  • 4元のモデルのライセンスを確かめる量子化版も元のモデルの利用条件の下で配られるのが基本。配布ページの表記を読む

よくある質問

Q. LLM を量子化すると、どのくらい精度が落ちますか。

手元の小さなモデルでは、int8 でパープレキシティの差が 0.03% 以内、int4 の group32 で 0.14%、テンソル単位の int4 で 0.96%、3 ビットで 1.4%、2 ビットで 22% でした。大きな LLM では外れ値が多く、方式による差も大きいため、数字はモデルごとに測ります。

Q. int8 と int4 はどちらを選べばよいですか。

メモリが足りるなら int8、載らないなら 4 ビットです。4 ビットはスケールを細かく持つ方式(group128 以下、GGUF の K 系)か、GPTQ・AWQ を選ぶと劣化が小さくなります。

Q. なぜ 1 割も誤差があるのに答えが変わらないのですか。

誤差が重みと無相関の雑音で、内積と LayerNorm で薄まり(重み 9.57% → logits 3.15%)、1 位が入れ替わるのは元から僅差だった位置に限られるからです。並べ替えた雑音を足しても同じ結果になることで確かめました。

Q. GPTQ と AWQ はどちらが良いですか。

手元の簡略版では、外れ値の無いモデルで GPTQ が常に RTN 以上、AWQ 風はほぼ差なし、外れ値のあるモデルでは AWQ 風が大きく効きました。実際のモデルでの優劣は論文や推論エンジンの公式の評価と、自分の課題での比較で決めます。

Q. Q4_K_M の K と M は何ですか。

K は super-block の中にブロックごとのスケール(と最小値)を持つ K 系の型、M は S より一部のテンソルを大きく持つ混合です。Llama 3.1 8B では Q4_K_S が 4.67、Q4_K_M が 4.89 ビット/重みです(llama.cpp の README の表)。

Q. 量子化すると速くなりますか。

専用の計算カーネルがあれば、読み込む重みが減るぶん生成が速くなります。numpy のような汎用の計算では、整数の行列積はかえって遅く、手元では 150 倍遅くなりました。

Q. 量子化・蒸留・プルーニングは何が違いますか。

量子化は重みの数はそのままで 1 個あたりのビット数を減らします。蒸留は大きなモデルの出力をまねる小さなモデルを学習し直します。プルーニングは重みや構造の一部を取り除きます。組み合わせることもできます。

Q. 学習の段階から量子化を考える方法はありますか。

QAT(量子化を考慮した学習)があり、重みを −1・0・1 の 3 値にした BitNet b1.58 は、同じ大きさの FP16/BF16 のモデルと並ぶ性能を abstract で報告しています。学習済みのモデルをあとから丸める PTQ とは別の方法です。

Q. QLoRA の 4 ビットは何が違いますか。

QLoRA は 4 ビットに量子化して固定した元のモデルに、追加の小さな重み(LoRA)を学習する方法です。4 ビットの型には、正規分布する重みに合わせた NF4 を使い、量子化の定数も量子化します(論文の abstract)。

Q. この記事のコードを大きなモデルにそのまま使えますか。

量子化の関数(RTN・グループ単位・GPTQ の列ごとの更新)は行列の大きさに依存しないのでそのまま動きますが、数十億パラメータでは numpy の速さとメモリが足りません。実際に配る・動かすモデルは、GPTQ・AWQ・llama.cpp などの公式の実装で量子化します。

まとめ

  • 量子化は、重みを整数+スケールで持ち、使うときに掛け戻す技術。誤差は必ずスケールの半分以下で、スケールを細かく持つほど小さい。
  • 精度があまり落ちないのは、誤差が無相関の雑音としてふるまい、伝わるうちに薄まり(9.57% → 3.15%)、入れ替わるのが僅差の位置だけだから。手元で int8 は差 0.03% 以内、int4 group32 は 0.14%。
  • 崩れるのは 2〜3 ビットと外れ値。外れ値 1 つで int4 チャンネル単位が 26.120 まで悪化し、外れ値を別に持つ・AWQ 風に行をそろえると元に戻った。
  • 活性化の外れ値は W8A8 の難所。LLM.int8() の「外れ値の次元だけ 16 ビット」と SmoothQuant の「難しさを重みへ移す」で、手元でもほぼ元どおりになった。
  • GPTQ は誤差を埋め合わせる、AWQ は大事なチャンネルを守る、GGUF の K 系はブロックごとのスケールと最小値。速さは専用のカーネルで決まり、numpy では測れない。

参考・出典

いずれも 2026年11月4日に確かめました。

  • Frantar ら「GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers」arXiv:2210.17323:https://arxiv.org/abs/2210.17323
  • Lin ら「AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration」arXiv:2306.00978:https://arxiv.org/abs/2306.00978
  • Xiao ら「SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models」arXiv:2211.10438:https://arxiv.org/abs/2211.10438
  • Dettmers ら「LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale」arXiv:2208.07339:https://arxiv.org/abs/2208.07339
  • Dettmers ら「QLoRA: Efficient Finetuning of Quantized LLMs」arXiv:2305.14314:https://arxiv.org/abs/2305.14314
  • Ma ら「The Era of 1-bit LLMs: All Large Language Models are in 1.58 Bits」arXiv:2402.17764:https://arxiv.org/abs/2402.17764
  • Hugging Face Hub ドキュメント「GGUF」:https://huggingface.co/docs/hub/gguf
  • llama.cpp「tools/quantize/README.md」:https://github.com/ggml-org/llama.cpp/blob/master/tools/quantize/README.md
  • Hugging Face Transformers ドキュメント「Quantization(Overview・HQQ)」:https://huggingface.co/docs/transformers/main/en/quantization/overview

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「LLMの量子化とは|int8・int4で精度が落ちにくい理由を自作して実測」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

成長のヒミツ、 ぜんぶ丸わかり