はじめに
AWS EC2 の NVIDIA L4 で、Qwen3-8B の BF16 モデルと、OneComp で GPTQ 4bit量子化したモデルを比較しました。推論エンジンには llama.cpp、測定には llama-bench を使用しました。
結果として、今回の条件では量子化モデルの性能は以下となりました。
- Decode: BF16 の約 2.73 倍
- Prefill: BF16 の約 1.31 倍
本記事では備忘録として、実験条件と結果を纏めます。
比較対象
今回の実験では、GPTQ で生成した重みを GGUF に変換し、llama.cpp で実行します。
| 比較対象 | モデルの作成方法 | llama.cpp での形式 |
|---|---|---|
| BF16 Model | 元の Qwen3-8B を GGUF へ変換 | BF16 GGUF |
| GPTQ 4bit | OneComp で GPTQ 量子化後、GGUF へ直接変換 | 主に Q4_0 の GGUF |
GPTQ は量子化手法、Q4_0 は llama.cpp で使用する量子化データ形式です。
実際に使用されたカーネルや演算精度は確認の範囲外です。
実験環境
| 項目 | 内容 |
|---|---|
| AWS インスタンス | EC2 g6.2xlarge |
| GPU | NVIDIA L4 |
| GPU メモリ | 24 GB |
| vCPU | 8 |
| ホストメモリ | 32GB |
| ホスト OS | Ubuntu 24.04 LTS |
| 実行環境 | Docker (img: nvidia/cuda:13.0.2-devel-ubuntu24.04) |
| モデル | Qwen/Qwen3-8B |
| 量子化フレームワーク | OneComp(OneCompression) |
| 推論エンジン | llama.cpp |
| ベンチマーク | llama-bench |
比較モデル詳細
BF16 モデル
元の Qwen3-8B を llama.cpp により BF16 GGUF に変換します。
python /opt/llama.cpp/convert_hf_to_gguf.py /workspace/models/Qwen3-8B \
--outtype bf16 --outfile /workspace/models/Qwen3-8B-BF16.gguf
--outtype bf16 により GGUF の主要な重みを BF16 形式で保存しています。
GPTQ 量子化モデル
同じ元モデルを OneComp で量子化します。
| 項目 | 内容 |
|---|---|
| 量子化手法 | GPTQ |
| 重み bit 数 | 4 |
| group size | 128 |
| 対称量子化 | True |
| actorder | False |
| block size | 128 |
| percdamp | 0.01 |
| 明示的な除外層 | lm_head |
| QEP | 無効 |
| LPCD | 無効 |
| 読み込み精度 | FP16 |
| 校正データ | WikiText-2(サンプル数=128, 系列長=512, concat_rand) |
| seed | 0 |
今回の量子化では、BF16 チェックポイントを FP16 で読み込んだため、その過程でキャストが発生しています。
量子化後は OneComp の convert_gptq_to_gguf で直接 GGUF へ変換しました。
from onecomp.cpu import convert_gptq_to_gguf
summary = convert_gptq_to_gguf(
quantized_dir="/workspace/models/Qwen3-8B-GPTQ-W4-G128",
out_gguf="/workspace/models/Qwen3-8B-GPTQ-W4-G128-Q4_0.gguf",
original_model="/workspace/models/Qwen3-8B",
)
print(summary)
量子化後のテンソル型の確認
変換後の GGUF について、Q4_0 以外で保存されている行列テンソルを確認しました。
from collections import Counter
from gguf import GGUFReader, GGMLQuantizationType
reader = GGUFReader(
"/workspace/models/Qwen3-8B-GPTQ-W4-G128-Q4_0.gguf"
)
counts = Counter(
GGMLQuantizationType(t.tensor_type).name
for t in reader.tensors
)
print("Tensor counts:", dict(counts))
print("\nNon-Q4_0 matrix tensors:")
for t in reader.tensors:
dtype = GGMLQuantizationType(t.tensor_type).name
if len(t.shape) >= 2 and dtype != "Q4_0":
print(t.name, dtype, [int(n) for n in t.shape])
Q4_0 以外の行列テンソルは、次の2つでした。
Non-Q4_0 matrix tensors:
token_embd.weight F16 [np.uint64(4096), np.uint64(151936)]
output.weight F16 [np.uint64(4096), np.uint64(151936)]
つまり、今回のモデルは入力埋込みと出力層を FP16 で保持し、それ以外の行列テンソルを Q4_0 で保存したものです。ちなみに FP16 で残った2つの行列で重みの容量は約2.3GBとなります。
llama-bench 測定方法
BF16 モデルと GPTQ Q4_0 モデルで、条件を以下に揃えました。
llama-bench -m /workspace/models/Qwen3-8B-BF16.gguf \
-ngl 99 -p 512 -n 128 -b 512 -ub 512 \
-t 8 -fa on -ctk f16 -ctv f16 -r 10 -o json \
> /workspace/results/bf16-pp512-tg128.json \
2> /workspace/logs/bf16-pp512-tg128.log
| 項目 | 設定 |
|---|---|
| GPU配置 | 全層GPU配置(-ngl 99) |
| Prefill | 512トークン |
| Decode | 128トークン |
| 事前コンテキスト長 | 0 |
| batch / ubatch | 512 / 512 |
| CPUスレッド数 | 8 |
| Flash Attention | 有効 |
| KVキャッシュ | K・VともにFP16 |
| 反復回数 | 10 |
| ウォームアップ | 有効 |
測定結果・考察
各条件の平均 token/s の比較
| test | BF16 tok/s | GPTQ Q4_0 tok/s | ratio |
|---|---|---|---|
| tg128@d0 | 16.90 | 46.20 | 2.734 |
| pp512@d0 | 2421.37 | 3177.55 | 1.312 |
今回の条件では、Prefill・Decode ともに量子化モデルの方が高速でした。特に Decode では、1トークンあたりの処理時間が、約 59.2ms から約 21.6ms へ短縮されました。
Decode での重みの読み出し時間を削減できた可能性が高いと考えています。
一般的に、Decode では入力トークンを処理するためにモデルの重みを繰り返し参照するため、重みを 16bit -> 4bit に圧縮したことでデータの転送量を削減できる可能性があります。 一方 Prefill は複数トークンを纏めて処理するため、行列演算の効率や演算カーネル性能が支配的となります。今回 Prefill は Decode よりも高速化の割合が小さくなっており、この推察と合致します。ただし、カーネル単位の分析はできていないため原因の特定はできていません。
今回考慮できていない点
- 処理時間のばらつきの考慮
- コンテキスト長の影響
- 制度の比較
- 使用された演算カーネルの確認