Read OSS

llama.cpp はモデルの重みをどのように計算へと変換するか

上級

前提知識

  • 第1回:アーキテクチャの概要とコードナビゲーション
  • Transformer のフォワードパスの理解(自己注意機構、FFN、レイヤー正規化、KVキャッシュ、RoPE)

llama.cpp はモデルの重みをどのように計算へと変換するか

第1回では、すべてのデコード呼び出しの中心に model.build_graph() があることを確認しました。しかし 120 以上のモデルアーキテクチャに対して「グラフを構築する」とは具体的にどのような処理なのでしょうか。その答えは llama.cpp が持つ最も洗練された設計パターンにあります。共有のブロックセットから LEGO を組み立てるように、モデル作者がフォワードパスを組み立てる再利用可能な構成要素のツールキットです。

この記事では、そのシステムを詳しく見ていきます。llm_graph_context 基底クラスとそのツールキットメソッド、具体的なモデルビルダー(LLaMA)の実装、ディスパッチ機構を順に解説します。さらにグラフの再利用による冗長処理の回避と、Mamba や RWKV といった非 Transformer アーキテクチャが同じフレームワークにどう収まるかも見ていきます。

グラフコンテキストのツールキット

llama.cpp のすべてのモデルビルダーは、src/llama-graph.h で定義された llm_graph_context を継承しています。この基底クラスは2つのものを提供します。ひとつは、ハイパーパラメータ・コンテキストパラメータ・現在の ubatch から事前に初期化された豊富なメンバー変数群。もうひとつは、計算グラフの断片を構築する反復的な処理を担うビルダーメソッドのツールキットです。

主なビルダーメソッドは以下の通りです。

メソッド 役割
build_inp_embd() トークン埋め込みルックアップの入力を生成
build_inp_pos() 位置入力テンソルを生成
build_norm() LayerNorm または RMSNorm を適用
build_ffn() フィードフォワードネットワークを構築(SiLU/GELU/ReLU とゲーティング)
build_moe_ffn() Mixture-of-Experts FFN を構築
build_attn() KVキャッシュの読み書きを含む Attention を構築(5種類のオーバーロード)
build_lora_mm() LoRAとテンソルごとのスケーリングに対応した行列積
build_cvec() ステアリング用のコントロールベクターを適用
build_inp_out_ids() 出力フィルタリング(要求されたトークンのみロジットを計算)

これらのメソッドは src/llama-graph.h で定義されています。build_attn() だけで5種類のオーバーロードがある点に注目してください。それぞれ、キャッシュなし(BERT)、KVキャッシュあり、Kのみキャッシュ、インターリーブドスライディングウィンドウ(iSWA)、クロスアテンションに対応しています。

classDiagram
    class llm_graph_context {
        +arch: llm_arch
        +hparams: llama_hparams&
        +cparams: llama_cparams&
        +ubatch: llama_ubatch&
        +n_embd, n_layer, n_head...
        +ctx0: ggml_context*
        +gf: ggml_cgraph*
        +res: llm_graph_result*
        +build_inp_embd()
        +build_inp_pos()
        +build_norm()
        +build_ffn()
        +build_moe_ffn()
        +build_attn() ×5
        +build_lora_mm()
        +build_cvec()
    }
    class llm_build_llama {
        Constructor builds full graph
    }
    class llm_build_qwen2 {
        Constructor builds full graph
    }
    class llm_build_bert {
        Constructor builds full graph
    }
    llm_graph_context <|-- llm_build_llama
    llm_graph_context <|-- llm_build_qwen2
    llm_graph_context <|-- llm_build_bert

設計の哲学は「設定より規約(Convention over Configuration)」です。モデルビルダーは、KVキャッシュのスロットインデックス管理、アテンションマスクの構築、LoRAの適用といった処理を手動で行う必要はありません。それらはすべてツールキットが内部で処理します。モデル作者が集中すべきなのは、フォワードパスのトポロジーです。すなわち、どのノーム、どのプロジェクション、どの活性化関数を、どの順番で適用するか、という設計の部分です。

具体的なモデル:LLaMA グラフビルダー

src/models/llama.cpp のリファレンス実装を追ってみましょう。フォワードパス全体は llm_build_llama のコンストラクタ内で構築されます。

flowchart TD
    EMB["build_inp_embd(tok_embd)"] --> POS["build_inp_pos()"]
    POS --> ATTN_INP["build_attn_inp_kv()"]
    ATTN_INP --> LOOP_START["For each layer il = 0..n_layer"]
    
    LOOP_START --> NORM1["build_norm(attn_norm, RMS)"]
    NORM1 --> QKV["Q/K/V projections via build_lora_mm"]
    QKV --> ROPE["ggml_rope_ext (Q and K)"]
    ROPE --> ATTN["build_attn(inp_attn, wo, Q, K, V)"]
    ATTN --> ADD1["residual add"]
    ADD1 --> NORM2["build_norm(ffn_norm, RMS)"]
    NORM2 --> FFN_CHECK{MoE layer?}
    FFN_CHECK -->|No| FFN["build_ffn(SiLU, PAR)"]
    FFN_CHECK -->|Yes| MOE["build_moe_ffn(...)"]
    FFN --> ADD2["residual add + build_cvec"]
    MOE --> ADD2
    ADD2 --> NEXT["Next layer"]
    
    NEXT --> FINAL_NORM["build_norm(output_norm, RMS)"]
    FINAL_NORM --> LM_HEAD["build_lora_mm(output, cur)"]
    LM_HEAD --> DONE["ggml_build_forward_expand(gf, cur)"]

ビルダーはまず埋め込み入力と位置テンソルを生成し(13〜16行目)、続いてアテンション入力をセットアップします。この処理では KVキャッシュのインデックスとマスクテンソルが構築されます。テンプレートパラメータ <embed> を使うことで、生成モードと埋め込みモードの両方に同じコードを使い回せます。C++ テンプレートの巧みな活用例です。

レイヤーループ内(31〜153行目)では、各イテレーションが標準的な Transformer パターンに従います。

  1. 入力に RMSNorm を適用
  2. build_lora_mm() による Q/K/V プロジェクション(LoRA を透過的に処理)
  3. Q と K への RoPE 回転
  4. build_attn() による Attention(KVキャッシュ書き込み、マスク適用、Flash Attention を処理)
  5. 残差接続
  6. ffn_gate_inp の有無に応じた FFN または MoE FFN107〜143行目
  7. 2つ目の残差接続 とコントロールベクターの適用

全レイヤーを処理し終えると、出力は正規化され、言語モデルヘッドを通じて語彙サイズに射影されます(156〜171行目)。

ヒント: コード中の cb(cur, "attn_norm", il) 呼び出しは、単なるデバッグ用ラベルではありません。バックエンドスケジューラが演算の配置やオフローディングを決定する際に使用するグラフコールバックを起動しています。

アーキテクチャのディスパッチ

llama_context::process_ubatch()model.build_graph() を呼び出すと、src/llama-model.cpp 内の巨大な switch 文にたどり着きます。

ggml_cgraph * llama_model::build_graph(const llm_graph_params & params) const {
    std::unique_ptr<llm_graph_context> llm;

    switch (arch) {
        case LLM_ARCH_LLAMA:
            llm = std::make_unique<llm_build_llama<false>>(*this, params);
            break;
        case LLM_ARCH_QWEN2:
            llm = std::make_unique<llm_build_qwen2>(*this, params);
            break;
        // ... 120+ cases
    }
}

arch フィールドは llm_arch 型の enum 値で、モデルロード時に GGUF ファイルの general.architecture キーを読み取ることでセットされます。GGUF の文字列名から enum 値へのマッピングは、src/llama-arch.cppLLM_ARCH_NAMES に定義されています。

static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
    { LLM_ARCH_LLAMA,  "llama"  },
    { LLM_ARCH_QWEN2,  "qwen2"  },
    { LLM_ARCH_MAMBA,  "mamba"  },
    // ...
};

llm_arch enum には現在125のアーキテクチャ識別子が定義されています。これはコードベースの中でも最も活発に追加が続いている部分であり、新しいモデルアーキテクチャが定期的に加わっています。

flowchart LR
    GGUF["GGUF file\ngeneral.architecture = 'llama'"] --> PARSE["llm_arch_from_string()"]
    PARSE --> ENUM["LLM_ARCH_LLAMA"]
    ENUM --> SWITCH["build_graph() switch"]
    SWITCH --> BUILDER["llm_build_llama<false>"]
    BUILDER --> GRAPH["ggml_cgraph"]

グラフ再利用の最適化

計算グラフの構築はコストのかかる処理です。GGML テンソルのアロケーション、演算の結線、バックエンドスケジューラのアロケーションパス実行が伴います。自己回帰的な生成処理では、連続するデコード呼び出しが同一のグラフトポロジーを生成することがよくあります(同じバッチサイズ、同じシーケンス構造、同じモデル設定)。llama.cpp はこの性質を活かして、グラフ再利用の最適化を実装しています。

process_ubatch() では、前回のグラフを再利用できるかどうかを次のように確認します。

if (!graph_reuse_disable && res->can_reuse(gparams)) {
    n_reused++;
} else {
    res->reset();
    gf = model.build_graph(gparams);
    ggml_backend_sched_alloc_graph(sched.get(), gf);
}
res->set_inputs(&ubatch);
graph_compute(res->get_gf(), ...);

llm_graph_resultcan_reuse() メソッドは、新しい llm_graph_params が前回と同じグラフトポロジーを生成するかどうかを検証します。再利用の条件は allow_reuse() に定義されており、ubatch のサイズ(n_tokens、n_seqs、n_seq_tokens)、入力モード(トークンか埋め込みか)、出力数、各種設定フラグが一致している必要があります。

再利用が成功した場合は set_inputs() のみが呼び出され、テンソルのデータだけが更新されます。グラフ構造、バックエンドのアロケーション、テンソルのメモリはすべてそのまま維持されます。各 llm_graph_input_i サブクラスも独自の can_reuse() チェックを実装しており、入力固有の状態(KVキャッシュインデックスなど)がグラフを無効化するような形で変化していないかを検証します。

ヒント: 環境変数 LLAMA_GRAPH_INPUT_DEBUG=1 を設定すると、グラフ再利用の成否に関するデバッグ出力が得られます。パフォーマンスのボトルネックを特定したいときに役立ちます。

入力システム

計算グラフにはデータが必要です。トークンID、位置情報、アテンションマスク、KVキャッシュインデックスといったデータは、src/llama-graph.h で定義された llm_graph_input_i の継承階層で管理されています。

class llm_graph_input_i {
public:
    virtual void set_input(const llama_ubatch * ubatch) = 0;
    virtual bool can_reuse(const llm_graph_params & params) { return false; }
};

各具体的な入力クラスは1つ以上の GGML テンソルを生成し、ubatch からそれらを埋める方法を知っています。主な入力型を以下に示します。

  • llm_graph_input_embd — トークンIDまたは生の埋め込みベクトル
  • llm_graph_input_pos — 位置インデックス(複数の位置次元を持つ M-RoPE にも対応)
  • llm_graph_input_attn_kv — 標準的な Transformer 向けの KVキャッシュスロットインデックスとアテンションマスク
  • llm_graph_input_attn_kv_iswa — インターリーブドスライディングウィンドウアテンション向けの同等の入力
  • llm_graph_input_attn_no_cache — BERTスタイルのモデル向けのフルアテンションマスク
  • llm_graph_input_rs — Mamba/RWKV 向けの再帰状態コピーインデックス
  • llm_graph_input_mem_hybrid — Jamba などのハイブリッドモデル向けの Attention と再帰の複合入力
classDiagram
    class llm_graph_input_i {
        <<interface>>
        +set_input(ubatch)*
        +can_reuse(params)*
    }
    class llm_graph_input_embd {
        tokens: ggml_tensor*
        embd: ggml_tensor*
    }
    class llm_graph_input_attn_kv {
        self_k_idxs: ggml_tensor*
        self_v_idxs: ggml_tensor*
        self_kq_mask: ggml_tensor*
    }
    class llm_graph_input_rs {
        s_copy: ggml_tensor*
    }
    class llm_graph_input_mem_hybrid {
        inp_attn: llm_graph_input_attn_kv
        inp_rs: llm_graph_input_rs
    }
    llm_graph_input_i <|-- llm_graph_input_embd
    llm_graph_input_i <|-- llm_graph_input_attn_kv
    llm_graph_input_i <|-- llm_graph_input_rs
    llm_graph_input_i <|-- llm_graph_input_mem_hybrid

この設計により、グラフのトポロジーと入力データのライフサイクルが分離されています。モデルビルダーが build_attn_inp_kv() を呼び出すと、アテンション入力オブジェクトが生成されます。このオブジェクトはプレースホルダーテンソルを作成するとともに、後の set_inputs() 呼び出し時にデータを充填するよう自身を登録します。

非Transformerアーキテクチャ

llama.cpp の設計上の優れた点のひとつは、非Transformerモデルを同じフレームワークに収めていることです。鍵となるのは、src/models/models.h で宣言された特殊化された基底クラスです。

llm_build_mamba_basebuild_mamba_layer()build_mamba2_layer() メソッドを提供し、SSM(状態空間モデル)の計算グラフを構築します。入力プロジェクション、畳み込み、セレクティブスキャン、出力プロジェクションといった処理が含まれます。Mamba ビルダーの構造は Transformer ビルダーと驚くほど似ています。レイヤーのループ処理、正規化、SSM ブロックの適用、残差接続という流れは共通しており、異なるのは build_attn_inp_kv() の代わりに build_rs_inp() を、build_attn() の代わりに build_mamba_layer() を使う点だけです。

llm_build_rwkv7_base は、RWKVアーキテクチャの線形Attention変種向けに build_rwkv7_time_mix()build_rwkv7_channel_mix() を提供します。

llm_build_delta_net_base は、Delta Net 線形Attention機構のチャンクベースと自己回帰の両実装を提供します。

Jamba のようなハイブリッドアーキテクチャは、これら両方のパターンを組み合わせています。llm_graph_input_mem_hybrid を使用することで、Attention KV 入力と再帰状態入力の両方をラップし、各レイヤーのループ内でそのレイヤーが Attention か再帰かを判定しながら処理を切り替えます。

classDiagram
    class llm_graph_context {
        <<base>>
        build_norm()
        build_ffn()
        build_attn()
    }
    class llm_build_mamba_base {
        <<base>>
        build_mamba_layer()
        build_mamba2_layer()
    }
    class llm_build_rwkv7_base {
        <<base>>
        build_rwkv7_time_mix()
        build_rwkv7_channel_mix()
    }
    class llm_build_delta_net_base {
        <<base>>
        build_delta_net()
    }
    class llm_build_mamba {
        Constructor builds SSM graph
    }
    class llm_build_rwkv7 {
        Constructor builds RWKV graph
    }
    class llm_build_jamba {
        Constructor builds hybrid graph
    }
    llm_graph_context <|-- llm_build_mamba_base
    llm_graph_context <|-- llm_build_rwkv7_base
    llm_graph_context <|-- llm_build_delta_net_base
    llm_build_mamba_base <|-- llm_build_mamba
    llm_build_rwkv7_base <|-- llm_build_rwkv7
    llm_graph_context <|-- llm_build_jamba

メモリシステム(第4回で詳説)もこの階層を反映しています。llama_memory_recurrent が SSM の状態を管理し、llama_memory_hybrid が KVキャッシュと再帰状態を組み合わせ、create_memory() ファクトリがアーキテクチャに応じて適切なものを選択します。

次回予告

モデルアーキテクチャが再利用可能な構成要素を通じて GGML の計算グラフへと変換される仕組みを理解できました。では、GGML とは何者でしょうか。遅延評価テンソルライブラリはどのように動作し、CPU・CUDA GPU・Metal・Vulkan をまたいで同じグラフを実行するのでしょうか。次回は、GGMLのコア抽象、バックエンドの vtable システム、そして GGUF ファイルフォーマットを掘り下げます。