Read OSS

`import transformers` の仕組み:遅延ロードアーキテクチャ

中級

前提知識

  • Python のモジュールシステムの基礎(sys.modules、__getattr__、__init__.py)
  • Python のインポートの仕組みとパッケージ構造への理解
  • frozenset と MutableMapping の基本的な知識

import transformers の仕組み:遅延ロードアーキテクチャ

import transformers と書いた瞬間、Python は 450 以上のモデルアーキテクチャ、数十種類のトークナイザー、画像プロセッサー、そして学習ユーティリティを含むパッケージを読み込みます。これらすべてのモジュールを即座にインポートしようとすれば、起動に数十秒かかるうえ、PyTorch、TensorFlow、JAX、SentencePiece、tokenizers といったオプションのバックエンドがすべてインストールされていることが前提になってしまいます。Transformers はこの問題を、パッケージモジュール全体を sys.modules 内のカスタムクラス _LazyModule に置き換えることで解決しています。実際に使うその瞬間まで、重いインポートはすべて先送りされます。

この記事では、初期化の全経路を追っていきます。__init__.py における TYPE_CHECKING / ランタイムの二分岐から、属性アクセスを横取りする _LazyModule、そして 450 以上のモデルディレクトリを自動探索する define_import_structure() まで、順を追って見ていきましょう。このシステムを理解することは、コードベースを読み解き、新しいモデルをコントリビュートするうえで欠かせません。

二経路による初期化

ルートの src/transformers/__init__.py は、標準的な from typing import TYPE_CHECKING ガードから始まります。これによって、まったく異なる2つのコードパスが生まれます。

flowchart TD
    A["import transformers"] --> B{"TYPE_CHECKING?"}
    B -->|"Yes (mypy/pyright)"| C["Execute real imports<br/>for static analysis"]
    B -->|"No (runtime)"| D["Build _import_structure dict"]
    D --> E["Call define_import_structure()<br/>to scan models/"]
    E --> F["Install _LazyModule<br/>into sys.modules"]
    F --> G["Create module aliases<br/>for backward compat"]

TYPE_CHECKING ブランチ(750〜788 行目)には、数百の from .x import Y 文が並んでいます。これらはランタイムでは一切実行されず、型チェッカーや IDE、補完エンジンがシンボルを解決できるようにするためだけに存在します。一方、ランタイムブランチ(789 行目以降)では _import_structure という辞書を構築し、それを _LazyModule に渡します。

この二経路パターンは、コードベース全体のすべての __init__.py で使われています。重要なのは、静的解析とランタイムインポートがまったく別のコードパスをたどるという点です。両者の同期は、共有データ構造ではなく、慣習と CI チェックによって維持されています。

ヒント: パッケージに新しいシンボルを追加するときは、必ず両方の場所に追記が必要です。_import_structure(またはモデルサブモジュールの __all__)と TYPE_CHECKING ブロック、どちらかを忘れると、補完が壊れるかランタイムインポートが壊れるかのどちらかになります。

_LazyModule クラス

遅延ロードシステムの中核を担うのが _LazyModule です。これは Python の ModuleType のサブクラスで、sys.modules に登録されることで、__getattr__ を通じてすべての属性アクセスを横取りします。

classDiagram
    class ModuleType {
        +__name__: str
        +__file__: str
        +__getattr__(name)
    }
    class _LazyModule {
        +_modules: set
        +_class_to_module: dict
        +_object_missing_backend: dict
        +_objects: dict
        +_import_structure: dict
        +__getattr__(name) Any
        +__dir__() list
        -_get_module(module_name)
    }
    ModuleType <|-- _LazyModule

2039 行目 のコンストラクタは、import_structure パラメータとして、通常の文字列または frozenset をキーに持つ辞書を受け取ります。frozenset キーが肝心な部分で、それぞれのキーは「そのシンボルを使うために必要なバックエンドの集合」を表しています。

{
    frozenset({"torch"}): {
        "models.llama.modeling_llama": {"LlamaModel", "LlamaForCausalLM", ...}
    },
    frozenset({"tokenizers"}): {
        "models.albert.tokenization_albert_fast": {"AlbertTokenizer"}
    },
    frozenset(): {
        "models.llama.configuration_llama": {"LlamaConfig"}
    }
}

空の frozenset は「特別なバックエンド不要」を意味し、config クラスはここに分類されます。コンストラクタ(2060〜2113 行目)では、すべての frozenset キーを走査し、各バックエンドの利用可否を確認します。バックエンドが見つからない場合は _object_missing_backend に記録します。こうすることで、すべてのシンボルが補完の候補として登録されつつも、バックエンドが足りないシンボルにアクセスしたときは、謎めいた ImportError の代わりに分かりやすい Placeholder クラスを返せるようになっています。

getattr のディスパッチ

transformers.LlamaForCausalLM にアクセスすると、__getattr__ が呼び出されます。解決の順序は次のとおりです。

flowchart TD
    A["__getattr__(name)"] --> B{"name in _objects?"}
    B -->|Yes| C["Return cached object"]
    B -->|No| D{"name in _object_missing_backend?"}
    D -->|Yes| E["Return Placeholder class<br/>that raises on use"]
    D -->|No| F{"name in _class_to_module?"}
    F -->|Yes| G["Import the real module<br/>via _get_module()"]
    G --> H["getattr(module, name)"]
    F -->|No| I{"name in _modules?"}
    I -->|Yes| J["Import as submodule"]
    I -->|No| K["Raise AttributeError"]

2174 行目で生成される Placeholder クラスは、特に巧妙な設計です。メタクラスベースのダミーで、isinstance チェックや IDE のイントロスペクションには本物のクラスと同様に見えますが、__init__ 内で requires_backends(self, missing_backends) を呼び出し、次のような明快なエラーを出力します。

ImportError: LlamaForCausalLM requires the PyTorch library but it was not found.

シンボルを単純に省略するよりはるかに優れたアプローチです。ユーザーは謎の AttributeError に戸惑うことなく、何が足りないかをすぐに把握できます。

define_import_structure() による自動モデル探索

450 以上のモデルディレクトリを手作業で管理するのは現実的ではありません。define_import_structure() は、ファイルシステムを走査することでこの問題を解決しています。

flowchart TD
    A["define_import_structure(models/, prefix='models')"] --> B["create_import_structure_from_path()"]
    B --> C["os.scandir() each model dir"]
    C --> D{"Is directory?"}
    D -->|Yes| E["Recurse into subdir"]
    D -->|No| F{"Is .py file?"}
    F -->|Yes| G["Read __all__ from file"]
    G --> H["Infer backend from filename<br/>modeling_*.py → torch<br/>tokenization_*_fast.py → tokenizers"]
    H --> I["Group by frozenset of backends"]
    I --> J["Return nested dict"]

create_import_structure_from_path()models/ ディレクトリを再帰的に走査し、各 .py ファイルに対して以下の処理を行います。

  1. convert_ または modular_ で始まるファイルをスキップする(これらはユーティリティスクリプトで、インポート可能なモジュールではない)
  2. ファイル名のパターンからデフォルトのバックエンドを推定する(例:modeling_*.pytorch を意味する)
  3. ファイルの __all__ リストまたは @require デコレータを読み取り、エクスポートされるシンボルを収集する
  4. 必要なバックエンドの frozenset ごとにまとめる

この設計により、新しいモデルを追加するとき、ルートの __init__.py への登録作業は一切不要です。models/ 配下にディレクトリを作成し、__all__ でエクスポートを定義すれば、スキャナーが自動的に検出してくれます。

各モデルの __init__.py はとてもシンプルです。models/llama/__init__.py の全体を見てみましょう。

if TYPE_CHECKING:
    from .configuration_llama import *
    from .modeling_llama import *
    from .tokenization_llama import *
else:
    import sys
    _file = globals()["__file__"]
    sys.modules[__name__] = _LazyModule(
        __name__, _file, define_import_structure(_file), module_spec=__spec__
    )

各モデルディレクトリもそれぞれ独自の _LazyModule インスタンスをインストールしており、モジュール内のインポートも遅延されます。遅延ロードは、末端まで徹底されているのです。

バックエンドの利用可否チェック

frozenset キーによる構造は、一連の利用可否チェック関数に依存しています。基盤となるのは _is_package_available() で、importlib.util.find_spec() を使い、実際にインポートすることなくパッケージの存在を確認します。

sequenceDiagram
    participant LM as _LazyModule
    participant BM as BACKENDS_MAPPING
    participant IPA as _is_package_available
    participant IU as importlib.util

    LM->>BM: Look up "torch" backend
    BM->>BM: Return (is_torch_available, error_msg)
    LM->>BM: Call is_torch_available()
    BM->>IPA: _is_package_available("torch")
    IPA->>IU: find_spec("torch")
    IU-->>IPA: ModuleSpec or None
    IPA-->>BM: (True, "2.4.0")
    BM-->>LM: True

BACKENDS_MAPPINGOrderedDict で、バックエンド名を (チェック関数, エラーメッセージ) のタプルに対応付けています。torchtokenizers から essentiapretty_midi まで、30 以上のオプションバックエンドをカバーしています。

is_torch_available() はバージョンチェックも兼ねており、PyTorch ≥ 2.4.0 を要求します。また、@lru_cache デコレータにより、spec の重複ルックアップを防いでいます。このキャッシュは重要です。インポート構造の構築中にバックエンドチェックは数千回呼び出されるからです。

ヒント: frozenset キーは Backend クラスを通じてバージョン制約もサポートしています。frozenset({"torch>=2.5"}) のようなキーはバージョン要件を解析し、動的にチェックします。これにより、個々のシンボルがバックエンドの最低バージョンを宣言できます。

後方互換性のためのモジュールエイリアス

初期化の最後のピースが、モジュールエイリアスシステム です。トークナイザーや画像プロセッサーのリファクタリング時のように内部モジュールが名前変更された場合も、古いインポートパスが引き続き動作しなければなりません。

flowchart LR
    A["from transformers.tokenization_utils_fast<br/>import PreTrainedTokenizerFast"] --> B["Alias module in sys.modules"]
    B --> C["__getattr__ redirects to<br/>tokenization_utils_tokenizers"]
    C --> D["Returns real class"]

_create_module_alias() は軽量な types.ModuleType プロキシを生成します。その __getattr__ は遅延的に対象モジュールをインポートし、処理を委譲します。__file__ を明示的に None に設定することで、inspect.py が早期にインポートをトリガーしてしまうのを防いでいます。

設定されているエイリアスは主に 3 つです。

旧パス リダイレクト先
tokenization_utils_fast tokenization_utils_tokenizers
tokenization_utils tokenization_utils_sentencepiece
image_processing_utils_fast image_processing_backends

さらに、826 行目 のループが models/ 配下のすべての image_processing_*.py を走査し、モデルごとの _fast エイリアスを生成します。また、XImageProcessorFast を非推奨警告付きで XImageProcessor にマッピングする __getattr__ ファクトリも作成されます。

ディレクトリマップ

インポートシステムに関わる主要ファイルをまとめておきます。

ファイル 役割
src/transformers/__init__.py ルート init — インポート構造の構築、_LazyModule のインストール、エイリアスの作成
src/transformers/utils/import_utils.py _LazyModuledefine_import_structure()、バックエンドチェック、BACKENDS_MAPPING
src/transformers/models/<model>/__init__.py モデルごとの遅延 init — それぞれが独自の _LazyModule をインストール

スケールで見ると

実感として伝えると、PyTorch がインストールされた環境で import transformers を実行したとき、実際に触れる Python ファイルはおよそ 5 つで、モデルコードはまったくインポートされません。_LazyModule は、450 以上のモデルディレクトリにまたがる約 2,000 シンボルのマッピングを保持しており、すべてファイルシステムのスキャンによって発見されます。transformers.LlamaForCausalLM に初めてアクセスしたその瞬間に、modeling_llama.py が PyTorch や attention の依存関係とともにインポートされます。

このアーキテクチャはライブラリの使いやすさに直結しています。トークナイザーしか必要としない軽量なスクリプトで import transformers しても、PyTorch のインポートコストは一切かかりません。CI ジョブでは GPU 依存なしに config のみのコードをテストできます。そして、新しいモデルは自動的に発見されます。

次の記事では、Auto クラスシステムがこの遅延インポート基盤の上にどう構築されているかを見ていきます。"meta-llama/Llama-2-7b-hf" のようなモデル名から、最後の瞬間まで何もインポートせずに適切な Config、Tokenizer、Model クラスを特定する仕組みです。