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 パターンに従います。
- 入力に RMSNorm を適用
build_lora_mm()による Q/K/V プロジェクション(LoRA を透過的に処理)- Q と K への RoPE 回転
build_attn()による Attention(KVキャッシュ書き込み、マスク適用、Flash Attention を処理)- 残差接続
ffn_gate_inpの有無に応じた FFN または MoE FFN(107〜143行目)- 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.cpp の LLM_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_result の can_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_base は build_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 ファイルフォーマットを掘り下げます。