0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

科学と神々株式会社アドベントカレンダー 2025

LLM量子化 Day 7: 抽象化の力

なぜ抽象化が必要なのか

llm-quantizeは3つの異なる量子化形式(GGUF、AWQ、GPTQ)をサポートしています。それぞれの内部実装は大きく異なりますが、ユーザーから見れば「モデルを量子化する」という同じ操作です。

この「異なるものを同じように扱う」能力が、抽象化の本質です。

コンセントに例えると

日本のコンセントは同じ形状をしています。テレビ、冷蔵庫、パソコン——どれも同じコンセントに差し込めます。

これは「電化製品の種類」と「電力供給の方法」を分離した結果です。コンセントという「インターフェース」を定義することで、電力会社は電化製品の種類を気にせずに電力を供給でき、メーカーは電力供給の仕組みを気にせずに製品を作れます。

ソフトウェアでも同じことができます。BaseQuantizerという「インターフェース」を定義することで、CLIは量子化の種類を気にせずに命令を出せ、各量子化器は CLIの仕組みを気にせずに実装できます。

抽象基底クラス:共通のルールを定める

Pythonでは、ABC(Abstract Base Class)を使って抽象クラスを定義します。

from abc import ABC, abstractmethod

class BaseQuantizer(ABC):
    """すべての量子化器の基底クラス"""
    pass

BaseQuantizerを継承するクラスは、「量子化器である」という性質を持ちます。そして、特定のメソッドを必ず実装することが求められます。

抽象メソッド:契約として機能する

契約とは

「量子化器」を名乗るなら、以下のことができなければならない:

  1. 量子化を実行できるquantize()
  2. 対応する量子化レベルを答えられるget_supported_levels()
  3. 出力サイズを推定できるestimate_output_size()

これが「契約」です。この契約を守る限り、内部の実装方法は自由です。

class BaseQuantizer(ABC):
    @abstractmethod
    def quantize(self) -> QuantizedModel:
        """量子化を実行し、結果を返す"""
        pass

    @classmethod
    @abstractmethod
    def get_supported_levels(cls) -> list[str]:
        """対応する量子化レベルのリストを返す"""
        pass

    @abstractmethod
    def estimate_output_size(self) -> int:
        """出力ファイルのサイズを推定する"""
        pass

@abstractmethodデコレータが付いたメソッドは、サブクラスで必ず実装しなければなりません。実装しないとエラーになります。

各量子化器の実装

GGUFQuantizer

class GGUFQuantizer(BaseQuantizer):
    @classmethod
    def get_supported_levels(cls) -> list[str]:
        return ["Q2_K", "Q3_K_M", "Q4_K_M", "Q5_K_M", "Q6_K", "Q8_0"]

    def estimate_output_size(self) -> int:
        # GGUF固有の計算式
        bits_per_weight = GGUF_QUANT_TYPES[self.config.quantization_level]["bits"]
        original_size = self.source_model.parameter_count * 2
        return int((original_size * bits_per_weight / 16) * 1.1)

    def quantize(self) -> QuantizedModel:
        # llama.cppを使った量子化処理
        ...

AWQQuantizer

class AWQQuantizer(BaseQuantizer):
    @classmethod
    def get_supported_levels(cls) -> list[str]:
        return ["4bit"]

    def estimate_output_size(self) -> int:
        # AWQ固有の計算式
        original_size = self.source_model.parameter_count * 2
        return int((original_size * 4 / 16) * 1.15)

    def quantize(self) -> QuantizedModel:
        # llm-awqライブラリを使った量子化処理
        ...

GPTQQuantizer

class GPTQQuantizer(BaseQuantizer):
    @classmethod
    def get_supported_levels(cls) -> list[str]:
        return ["2bit", "3bit", "4bit", "8bit"]

    def estimate_output_size(self) -> int:
        # GPTQ固有の計算式
        bits = GPTQ_QUANT_TYPES[self.config.quantization_level]["bits"]
        original_size = self.source_model.parameter_count * 2
        return int((original_size * bits / 16) * 1.12)

    def quantize(self) -> QuantizedModel:
        # GPTQModelライブラリを使った量子化処理
        ...

共通メソッド:繰り返しを避ける

抽象クラスには、具体的な実装を持つメソッドも定義できます。これらはすべてのサブクラスで共通して使える機能です。

class BaseQuantizer(ABC):
    def validate_config(self) -> list[str]:
        """設定のバリデーション(共通処理)"""
        errors = []
        supported = self.get_supported_levels()  # 抽象メソッドを呼び出し
        if self.config.quantization_level not in supported:
            errors.append(f"Unsupported level: {self.config.quantization_level}")
        return errors

    def setup_checkpoint(self, total_layers: int) -> Checkpoint:
        """チェックポイントの設定(共通処理)"""
        checkpoint_dir = Path(self.config.checkpoint_dir or "./.checkpoint")
        self._checkpoint = Checkpoint(checkpoint_dir, self.config)
        return self._checkpoint

    def try_resume(self) -> tuple[Optional[Checkpoint], int]:
        """中断からの再開(共通処理)"""
        # すべての量子化器で同じロジック
        ...

サブクラスは、これらの共通メソッドをそのまま使えます。車輪の再発明をする必要がありません。

ポリモーフィズム:同じ操作、異なる振る舞い

ポリモーフィズム(多態性)とは、「同じメソッド呼び出しが、オブジェクトの種類によって異なる振る舞いをする」ことです。

CLIでの活用

CLIは、具体的な量子化器の種類を知る必要がありません:

def run_quantization(source: SourceModel, config: QuantizationConfig):
    # 形式に応じた量子化器を取得(GGUF, AWQ, GPTQ のどれか)
    quantizer_class = get_quantizer(config.target_format)

    # BaseQuantizerとして扱う
    quantizer: BaseQuantizer = quantizer_class(source, config)

    # バリデーション(すべての量子化器で同じ呼び出し)
    errors = quantizer.validate_config()
    if errors:
        raise ValueError(errors)

    # サイズ推定(中身は形式によって異なる)
    estimated = quantizer.estimate_output_size()
    print(f"推定出力サイズ: {estimated} bytes")

    # 量子化実行(中身は形式によって異なる)
    result = quantizer.quantize()

    return result

この設計の利点

  1. CLIコードがシンプルに: 形式ごとの分岐が不要
  2. 新形式の追加が容易: CLIを変更せずに新しい量子化器を追加できる
  3. テストが容易: モック量子化器を作ってテストできる

リスコフの置換原則

「サブクラスのインスタンスは、親クラスのインスタンスとして使えなければならない」

これは重要な原則です。BaseQuantizerを期待する場所に、GGUFQuantizerAWQQuantizerを渡しても、正しく動作しなければなりません。

守るべき契約

# すべてのQuantizerに対して成り立つ
def process_any_quantizer(quantizer: BaseQuantizer):
    # 1. バリデーションは必ずリストを返す
    errors = quantizer.validate_config()
    assert isinstance(errors, list)

    # 2. サイズ推定は必ず正の整数を返す
    size = quantizer.estimate_output_size()
    assert isinstance(size, int) and size > 0

    # 3. quantize()は必ずQuantizedModelを返す
    result = quantizer.quantize()
    assert isinstance(result, QuantizedModel)

この契約を守っている限り、どの具体的な量子化器を渡しても、コードは正しく動作します。

テスタビリティの向上

抽象化により、テスト用のモック実装が簡単に作れます:

class MockQuantizer(BaseQuantizer):
    """テスト用のモック量子化器"""

    @classmethod
    def get_supported_levels(cls) -> list[str]:
        return ["mock_level"]

    def estimate_output_size(self) -> int:
        return 1000  # 固定値

    def quantize(self) -> QuantizedModel:
        return QuantizedModel(
            output_path="/mock/path",
            format="mock",
            file_size=1000,
            compression_ratio=0.5,
            source_model_path=self.source_model.model_path,
        )

このモックを使えば、実際のモデルや長時間の処理なしに、CLIやワークフローのテストができます。

新しい量子化形式を追加するには

将来、ONNX形式のサポートを追加したいとしましょう。必要な作業は:

1. 新しい量子化器クラスを作成

class ONNXQuantizer(BaseQuantizer):
    @classmethod
    def get_supported_levels(cls) -> list[str]:
        return ["int8", "uint8"]

    def estimate_output_size(self) -> int:
        # ONNX固有の計算
        ...

    def quantize(self) -> QuantizedModel:
        # ONNX固有の処理
        ...

2. OutputFormatに追加

class OutputFormat(Enum):
    GGUF = "gguf"
    AWQ = "awq"
    GPTQ = "gptq"
    ONNX = "onnx"  # 追加

3. レジストリに登録

register_quantizer(OutputFormat.ONNX, ONNXQuantizer)

これだけで、CLIから使えるようになります:

llm-quantize quantize model onnx -q int8

既存のコード(CLI、他の量子化器)には一切手を加える必要がありません。

Tips: 抽象化のバランス

過度な抽象化は避ける

抽象化は強力ですが、やりすぎると逆効果です:

  • 必要以上に深い継承階層
  • 使われないインターフェース
  • 「将来のため」の過剰な設計

実用的なガイドライン

  • 3つ以上の具体実装があるときに抽象化を検討する
  • 実際に共通コードがある場合にのみ基底クラスを作る
  • 今必要なものだけを抽象化する(YAGNI原則)

llm-quantizeの場合

  • GGUF、AWQ、GPTQという3つの具体実装がある
  • チェックポイント処理、バリデーションなど共通コードがある
  • すべて「量子化」という同じ概念を表している

これらの条件が揃っているため、BaseQuantizerの抽象化は妥当です。

次回予告

Day 8からは「GGUF形式編」として、llama.cppエコシステムとGGUF形式の詳細を5日間にわたって解説します。Q4_K_Mなどの謎めいた名前の意味も明らかになります。


抽象化は「複雑さを隠す」技術です。うまく使えば、システムの各部分が独立して進化できるようになります。ただし、抽象化それ自体も複雑さを持つことを忘れずに。必要最小限の抽象化を心がけましょう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?