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?

LLM量子化 Day 5: 良いソフトウェア設計の原則

Posted at

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

LLM量子化 Day 5: 良いソフトウェア設計の原則

なぜ設計原則を学ぶのか

量子化ツールを開発する際、最初は「とにかく動くものを作ろう」と考えがちです。しかし、ソフトウェアは作って終わりではありません。

  • 新しい量子化形式が登場したら対応したい
  • バグを見つけたら直したい
  • パフォーマンスを改善したい
  • チームメンバーが理解できるようにしたい

これらの要求に応えるには、変更しやすい設計が必要です。今日は、llm-quantizeの設計を通じて、ソフトウェア設計の基本原則を学びましょう。

単一責任の原則:一つのことをうまくやる

原則の説明

「一つのモジュールは、一つの責任だけを持つべきである」

これはUNIX哲学の「一つのことをうまくやる(Do One Thing Well)」と同じ考え方です。

悪い例:全部入りのクラス

想像してみてください。「量子化器」クラスに以下のすべてが詰め込まれているとしたら:

  • モデルの読み込み
  • 設定の解析
  • 量子化の実行
  • 進捗の表示
  • ファイルの保存
  • エラーメッセージの表示

このクラスを修正するとき、どこに影響が出るか予測できるでしょうか?進捗表示を変えたいだけなのに、量子化ロジックを壊してしまうかもしれません。

良い例:責任の分離

llm-quantizeでは、責任を明確に分離しています:

cli/         → ユーザーインターフェースの責任
  main.py       コマンドライン引数の解析、結果の表示

lib/         → ビジネスロジックの責任
  quantizers/   量子化処理の実行
  checkpoint.py 状態の保存と復元
  progress.py   進捗の追跡と報告

models/      → データ表現の責任
  source_model.py        入力モデルの情報
  quantized_model.py     出力モデルの情報
  quantization_config.py 量子化設定

この分離により、例えば「進捗表示を改善したい」場合はprogress.pyだけを見れば良いのです。

実践的なヒント

  • 「このクラス/関数は何をするか」を一文で説明できますか?
  • 「〜と〜をする」のように「と」が入る場合、分割を検討しましょう
  • ファイル名がそのまま責任を表すようにする

開放閉鎖の原則:拡張に開き、修正に閉じる

原則の説明

「新しい機能を追加するとき、既存のコードを変更せずに済むようにする」

これは一見矛盾しているように聞こえますが、実現可能です。

具体的な問題

新しい量子化形式(例:ONNX)をサポートしたいとします。もし以下のようなコードだったら:

# 悪い例
def quantize(model, format):
    if format == "gguf":
        return quantize_gguf(model)
    elif format == "awq":
        return quantize_awq(model)
    elif format == "gptq":
        return quantize_gptq(model)
    # ONNX を追加するには、このファイルを修正する必要がある

新しい形式を追加するたびに、この関数を修正する必要があります。修正は常にバグのリスクを伴います。

解決策:レジストリパターン

llm-quantizeでは、「レジストリパターン」を採用しています:

# 量子化器を登録するレジストリ
_QUANTIZERS = {
    OutputFormat.GGUF: GGUFQuantizer,
    OutputFormat.AWQ: AWQQuantizer,
    OutputFormat.GPTQ: GPTQQuantizer,
}

# 新しい形式を追加する場合
def register_quantizer(format, quantizer_class):
    _QUANTIZERS[format] = quantizer_class

新しい形式を追加するには:

  1. 新しい量子化器クラスを作成
  2. register_quantizerで登録

既存のコードは一切変更しません。

なぜレジストリパターンか

  • 疎結合:CLIは具体的な量子化器クラスを知る必要がない
  • 拡張性:新形式追加時にCLIコード変更不要
  • テスタビリティ:テスト用のモック量子化器を簡単に登録できる

依存性逆転の原則:抽象に依存する

原則の説明

「上位モジュールは下位モジュールに依存してはならない。両者は抽象に依存すべきである」

具体的な問題

CLIが直接GGUFQuantizerを呼び出すとしたら:

# 悪い例
from llm_quantize.lib.quantizers.gguf import GGUFQuantizer

def quantize_command(model, level):
    quantizer = GGUFQuantizer(model, level)  # 具体クラスに依存
    return quantizer.quantize()

この場合、CLIはGGUFQuantizerの実装詳細を知る必要があります。テストも難しくなります。

解決策:抽象への依存

# 良い例
from llm_quantize.lib.quantizers import get_quantizer
from llm_quantize.lib.quantizers.base import BaseQuantizer

def quantize_command(model, format, level):
    quantizer_class = get_quantizer(format)  # 抽象インターフェースを取得
    quantizer: BaseQuantizer = quantizer_class(model, level)
    return quantizer.quantize()  # 共通インターフェースを使用

CLIは「量子化器」という抽象概念とだけやり取りし、それがGGUFなのかAWQなのかは関知しません。

依存関係の図解

    CLI(上位モジュール)
          │
          ↓ 依存
    BaseQuantizer(抽象)
          ↑
          │ 実装
   ┌──────┼──────┐
   │      │      │
GGUF    AWQ    GPTQ(下位モジュール)

上位モジュール(CLI)と下位モジュール(具体的な量子化器)の両方が、抽象(BaseQuantizer)に依存しています。

テンプレートメソッドパターン:共通の骨格を定義する

パターンの説明

「アルゴリズムの骨格を定義し、詳細はサブクラスに任せる」

適用例

すべての量子化器には共通の処理があります:

  • チェックポイントの設定
  • 進捗の報告
  • 結果の検証

これらを毎回実装するのは無駄です。

class BaseQuantizer(ABC):
    # 共通処理(サブクラスは変更不要)
    def setup_checkpoint(self, total_layers):
        """チェックポイントの設定"""
        checkpoint_dir = Path(self.config.checkpoint_dir or "./.checkpoint")
        self._checkpoint = Checkpoint(checkpoint_dir, self.config)
        return self._checkpoint

    def try_resume(self):
        """中断からの再開を試みる"""
        if not Checkpoint.can_resume(checkpoint_dir):
            return None, 0
        # ...共通の再開ロジック

    # 抽象メソッド(サブクラスで必ず実装)
    @abstractmethod
    def quantize(self) -> QuantizedModel:
        """量子化の実行(形式固有の実装)"""
        pass

サブクラス(GGUFQuantizer、AWQQuantizerなど)は、共通処理をそのまま使いながら、quantize()だけを実装すればよいのです。

CLIツールの設計原則

終了コードを設計する

UNIX/Linux の慣例に従い、終了コードを設計します:

コード 意味 使用例
0 成功 量子化が正常に完了
1 一般エラー 予期しないエラー
2 引数エラー 無効なオプション
3 モデル未発見 指定されたモデルが存在しない
5 メモリ不足 OOMエラー

これにより、シェルスクリプトでの自動化が容易になります:

llm-quantize quantize model gguf
case $? in
    0) echo "成功" ;;
    3) echo "モデルが見つかりません" ;;
    5) echo "メモリ不足です。小さいバッチを試してください" ;;
esac

出力形式を選択可能にする

人間向けとプログラム向けの両方に対応:

  • Human mode: カラー表示、進捗バー、読みやすいメッセージ
  • JSON mode: パース可能な構造化データ、自動化に最適
# 人間向け
llm-quantize quantize model gguf

# プログラム向け
llm-quantize --format json quantize model gguf | jq '.file_size'

Tips: 設計を改善するための問いかけ

設計を見直すとき、以下の質問を自分に投げかけてみてください:

責任について

  • このクラスの責任は何か、一文で説明できるか?
  • このクラスが変更される理由は複数あるか?

依存関係について

  • このモジュールをテストするとき、他のどのモジュールが必要か?
  • 依存関係を図に描いたとき、循環していないか?

拡張性について

  • 新しい機能を追加するとき、既存のコードを変更する必要があるか?
  • 「if-else」の連鎖が増えていないか?

シンプルさについて

  • この抽象化は本当に必要か?
  • 将来のために作ったコードが、実際に使われる可能性はどれくらいか?

次回予告

Day 6では「データモデルの設計」について解説します。入力モデル(SourceModel)、設定(QuantizationConfig)、出力(QuantizedModel)という3つのデータ構造が、どのように設計され、なぜそうなっているのかを紐解きます。


良い設計は、コードを書く速度を一時的に遅くするかもしれません。しかし、長期的には変更の容易さとバグの少なさという形で報われます。「急がば回れ」は設計にも当てはまります。

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?