Read OSS

llama.cpp に新しいモデルアーキテクチャを追加する

上級

前提知識

  • 記事 1〜5
  • 少なくとも 1 つの Transformer アーキテクチャのフォワードパスへの理解
  • コンバーターステップのための基本的な Python の知識

llama.cpp に新しいモデルアーキテクチャを追加する

このシリーズを通じて、llama.cpp の内部構造を体系的に学んできました。2 層のライブラリスタック、グラフコンテキストツールキット、GGML のバックエンドシステム、デコードパイプライン、そしてアプリケーション層。今こそ、その知識を実際の作業に活かす番です。この記事では、llama.cpp に新しいモデルアーキテクチャを追加する手順を実践的に解説します。変更が必要なファイルと、その作業順序をすべてカバーします。

モデルアーキテクチャの追加には、5〜6 個のファイルを決まった順序で変更する必要があります。本記事では、ほとんどのアーキテクチャが手本とする標準的な実装例として、既存の LLaMA モデルを参照実装として使用します。

モデル追加の全体フロー

最初の変更から PR のマージまで、全体の流れを示します。

flowchart TD
    S1["Step 1: Architecture Registration\nllama-arch.h + llama-arch.cpp"] --> S2["Step 2: Python Converter\nconvert_hf_to_gguf.py"]
    S2 --> S3["Step 3: Tensor Loading\nllama-model.cpp\n(load_hparams + load_tensors)"]
    S3 --> S4["Step 4: Graph Builder\nsrc/models/mymodel.cpp"]
    S4 --> S5["Step 5: Registration\nllama-model.cpp\n(build_graph + create_memory)"]
    S5 --> S6["Step 6: Build & Test\nCMakeLists.txt + validation"]
ステップ 変更ファイル 目的
1 src/llama-arch.h, src/llama-arch.cpp アーキテクチャ識別子とテンソル名を登録する
2 convert_hf_to_gguf.py HuggingFace 形式からの Python コンバーターを書く
3 src/llama-model.cpp ハイパーパラメーターとテンソルの読み込みを追加する
4 src/models/{name}.cpp(新規ファイル) グラフビルダーを実装する
5 src/llama-model.cpp build_graph()create_memory() のディスパッチエントリを追加する
6 src/CMakeLists.txt 新しいソースファイルをビルドに追加する

Step 1: アーキテクチャの登録

最初のステップは、src/llama-arch.hllm_arch enum に新しいアーキテクチャを追加することです。

enum llm_arch {
    // ... existing entries ...
    LLM_ARCH_MY_MODEL,
    LLM_ARCH_UNKNOWN, // keep this last
};

続いて、src/llama-arch.cpp に文字列マッピングを追加します。

static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
    // ... existing entries ...
    { LLM_ARCH_MY_MODEL, "my-model" },
};

ここで指定する文字列 "my-model" は、Python コンバーターが GGUF ファイルに書き込む general.architecture の値と一致している必要があります。

次に、llama-arch.cpp のテンソル名マッピングも登録します。これはローダーが GGUF 名からテンソルを検索するための大きなマップ(LLM_TENSOR_NAMES)です。記述例を示します。

{ LLM_ARCH_MY_MODEL, {
    { LLM_TENSOR_TOKEN_EMBD,      "token_embd" },
    { LLM_TENSOR_OUTPUT_NORM,     "output_norm" },
    { LLM_TENSOR_OUTPUT,          "output" },
    { LLM_TENSOR_ATTN_NORM,      "blk.%d.attn_norm" },
    { LLM_TENSOR_ATTN_Q,         "blk.%d.attn_q" },
    // ... all weight tensors
}},

ヒント: LLaMA アーキテクチャのエントリをテンプレートとして活用しましょう。ほとんどの Transformer モデルは同じテンソルセットを使用しており、主な違いは名前付けと、どのオプションテンソル(バイアス、ゲートプロジェクション、MoE の重みなど)が存在するかという点だけです。

Step 2: Python コンバーター

コンバーター convert_hf_to_gguf.py は、各モデルアーキテクチャが専用のコンバータークラスを持つクラス階層になっています。新しいクラスを追加しましょう。

@ModelBase.register("MyModelForCausalLM")
class MyModelModel(TextModel):
    model_arch = gguf.MODEL_ARCH.MY_MODEL
    
    def set_gguf_parameters(self):
        super().set_gguf_parameters()
        # Write architecture-specific hyperparameters
        self.gguf_writer.add_block_count(self.hparams["num_hidden_layers"])
        self.gguf_writer.add_embedding_length(self.hparams["hidden_size"])
        self.gguf_writer.add_head_count(self.hparams["num_attention_heads"])
        # ... etc
    
    def modify_tensors(self, data_torch, name, bid):
        # Map HuggingFace tensor names to GGUF names
        # Return list of (new_name, tensor) pairs
        return [(self.map_tensor_name(name), data_torch)]

@ModelBase.register("MyModelForCausalLM") デコレーターは、HuggingFace の config.json に含まれる architectures フィールドに基づいてコンバーターを登録します。model_arch 属性は、gguf-py/gguf/constants.py に追加する enum 値と一致させてください。

gguf-py の定数ファイルにもアーキテクチャを追加する必要があります。

# In gguf-py/gguf/constants.py
class MODEL_ARCH(IntEnum):
    # ...
    MY_MODEL = auto()

同じファイルにテンソル名マッピングも追加します。

flowchart LR
    HF["HuggingFace Checkpoint\nconfig.json + model.safetensors"] --> CONV["convert_hf_to_gguf.py"]
    CONV --> ARCH["MyModelModel class"]
    ARCH --> PARAMS["set_gguf_parameters()\nWrite hyperparameters"]
    ARCH --> TENSORS["modify_tensors()\nRename + transform weights"]
    PARAMS --> GGUF["Output: model.gguf"]
    TENSORS --> GGUF

Step 3: ハイパーパラメーターとテンソルの読み込み

C++ 側では、GGUF のメタデータを読み取ってテンソルを読み込む方法を llama.cpp に伝える必要があります。この処理は src/llama-model.cpp で行います。

ハイパーパラメーターの読み込みload_hparams() メソッドを見つけ、その switch 文に新しいアーキテクチャのケースを追加します。ここでは GGUF のメタデータキーを読み取り、hparams 構造体に値を格納します。

case LLM_ARCH_MY_MODEL:
    ml.get_key(LLM_KV_BLOCK_COUNT,       hparams.n_layer);
    ml.get_key(LLM_KV_EMBEDDING_LENGTH,  hparams.n_embd);
    ml.get_key(LLM_KV_ATTENTION_HEAD_COUNT, hparams.n_head());
    // ... model-specific hyperparameters
    break;

テンソルの読み込みload_tensors() メソッドを見つけ、各重みに対応する ggml_tensor ポインターを生成するケースを追加します。パターンは llama_layer に倣います。

case LLM_ARCH_MY_MODEL:
    model.tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD), {n_embd, n_vocab});
    for (int i = 0; i < n_layer; ++i) {
        auto & layer = model.layers[i];
        layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, i), {n_embd});
        layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, i), {n_embd, n_embd});
        // ... all per-layer tensors
    }
    break;

ヒント: create_tensor() の呼び出しはデータをアロケートしません。モデルローダーが GGUF ファイルから値を埋めるために、テンソルの形状と名前を登録するだけです。実際のデータのアロケートとデバイスへの割り当て(CPU か GPU か)は、n_gpu_layers の設定に基づいてローダーが処理します。

Step 4: グラフビルダーの実装

新規ファイル src/models/mymodel.cpp を作成します。ここでは、記事 2 で解説した llm_graph_context ツールキットのメソッドを使ってモデルのフォワードパスを実装します。

LLaMA のリファレンス実装 src/models/llama.cpp を出発点として、自分のアーキテクチャのトポロジーに合わせて変更していきましょう。全体のパターンは次のとおりです。

#include "models.h"

struct llm_build_my_model : public llm_graph_context {
    llm_build_my_model(const llama_model & model, 
                       const llm_graph_params & params) 
        : llm_graph_context(params) {
        
        ggml_tensor * cur;
        ggml_tensor * inpL = build_inp_embd(model.tok_embd);
        ggml_tensor * inp_pos = build_inp_pos();
        auto * inp_attn = build_attn_inp_kv();
        ggml_tensor * inp_out_ids = build_inp_out_ids();
        
        for (int il = 0; il < n_layer; ++il) {
            // 1. Pre-attention norm
            cur = build_norm(inpL, model.layers[il].attn_norm, 
                           NULL, LLM_NORM_RMS, il);
            
            // 2. Q/K/V projections + RoPE
            // ... (specific to your architecture)
            
            // 3. Attention
            cur = build_attn(inp_attn, model.layers[il].wo, NULL,
                           Qcur, Kcur, Vcur, NULL, NULL, NULL, 
                           kq_scale, il);
            
            // 4. Residual + FFN
            cur = ggml_add(ctx0, cur, inpL);
            // ... FFN via build_ffn() or build_moe_ffn()
            
            inpL = cur;
        }
        
        // Final norm + LM head
        cur = build_norm(inpL, model.output_norm, NULL, 
                        LLM_NORM_RMS, -1);
        res->t_embd = cur;
        cur = build_lora_mm(model.output, cur);
        res->t_logits = cur;
        
        ggml_build_forward_expand(gf, cur);
    }
};

src/models/models.h にもビルダーの宣言を追加する必要があります。

struct llm_build_my_model : public llm_graph_context {
    llm_build_my_model(const llama_model & model, 
                       const llm_graph_params & params);
};
flowchart TD
    subgraph "Toolkit methods you compose"
        A["build_inp_embd()"]
        B["build_inp_pos()"]
        C["build_norm(RMS/LayerNorm)"]
        D["build_lora_mm(Q/K/V projections)"]
        E["ggml_rope_ext(RoPE)"]
        F["build_attn(attention + KV cache)"]
        G["build_ffn(SiLU/GELU/ReLU)"]
        H["build_moe_ffn(MoE)"]
    end
    
    A --> C
    B --> E
    C --> D
    D --> E
    E --> F
    F --> C
    C --> G
    C --> H

Step 5: 登録とメモリ選択

src/llama-model.cpp にあと 2 つのエントリを追加します。

build_graph() への追加8288 行目付近の switch 文 に以下を追加します。

case LLM_ARCH_MY_MODEL:
    llm = std::make_unique<llm_build_my_model>(*this, params);
    break;

create_memory() への追加 — 標準的な Transformer モデルであれば、create_memory()default ケースがすでに対応しており、llama_kv_cache が自動的に作成されます。以下のような特殊なケースに該当する場合のみ、明示的なケースを追加してください。

  • キャッシュ不要(BERT スタイル) → nullptr を返す
  • リカレント状態(Mamba/RWKV) → llama_memory_recurrent
  • ハイブリッド → llama_memory_hybrid
  • スライディングウィンドウ → hparams.swa_type != NONE であれば自動処理

最後に、src/CMakeLists.txt に新しいソースファイルを追加します。

add_library(llama
    # ... existing files ...
    models/mymodel.cpp
)

テストとコントリビューションのワークフロー

ビルドが完了したら、実装を検証しましょう。

1. モデルの変換:

python convert_hf_to_gguf.py /path/to/hf-model --outfile model.gguf

2. テンソル形状の確認: gguf-py を使ってファイルをダンプし、すべてのテンソルが正しい形状を持っているか確認します。

python -m gguf.scripts.gguf_dump model.gguf

3. 推論の実行:

./build/bin/llama-cli -m model.gguf -p "Hello, world!" -n 32

4. 出力の比較: HuggingFace のリファレンス実装に同じプロンプトを入力し、ロジット・出力を比較します。浮動小数点の演算順序の違いにより、小さな差異(0.01 未満)は想定の範囲内です。大きな差異が出た場合は、テンソルマッピングまたはグラフトポロジーにバグがある可能性があります。

5. パープレキシティの計算: 定量的な検証として、標準ベンチマークでパープレキシティを計算しましょう。

./build/bin/llama-perplexity -m model.gguf -f wiki.test.raw

HuggingFace モデルのパープレキシティと比較します。F16 では 0.1〜0.5 ポイント以内、量子化フォーマットではそれ以上の差が許容範囲です。

flowchart LR
    CONVERT["Convert model\nHF → GGUF"] --> VERIFY["Verify tensors\ngguf_dump"]
    VERIFY --> RUN["Run inference\nllama-cli"]
    RUN --> COMPARE["Compare outputs\nvs. HF reference"]
    COMPARE --> PPL["Perplexity test\nllama-perplexity"]
    PPL --> PR["Submit PR"]

コントリビューションガイドライン: プロジェクトの CONTRIBUTING.md に、コーディング規約・PR の要件・レビュープロセスが詳しく記載されています。重要なポイントをまとめます。

  • 新しいモデルアーキテクチャを追加する PR には、そのモデルの論文またはドキュメントへのリンクを含めること
  • コンバーターはトークナイザーの変換も正しく処理すること
  • 提出前に基本的な推論テストをパスすること
  • .clang-format.clang-tidy の設定が強制する既存のコードスタイルに従うこと

ヒント: 新しいモデルを追加する最も効率的な方法は、最も近い既存のアーキテクチャを見つけて、そのコンバータークラス、テンソル読み込みブロック、グラフビルダーをコピーすることです。デコーダーのみの Transformer であれば、LLaMA の実装で作業の 80% がカバーできます。主に調整が必要なのは、正規化の配置・活性化関数・アテンションのバリアントです。

シリーズのまとめ

6 つの記事を通じて、llama.cpp における推論の完全なパスをたどってきました。GGUF ファイル形式と GGML のテンソル演算から、グラフコンテキストツールキットとアーキテクチャのディスパッチ、KV キャッシュとデコードパイプライン、そして HTTP サーバーと CLI のアプリケーション層まで。グラフコンテキストツールキット・メモリインターフェース階層・バックエンドの vtable システムという、比較的少数のよく設計された抽象化が、120 以上のモデルアーキテクチャを十数種のハードウェアバックエンドで動作させることを可能にしていることを理解できたと思います。

llama.cpp のコードベースは、丁寧に読めば読むほど多くの発見があります。このコードは抽象化よりもパフォーマンスとポータビリティを優先して設計されているため、予想より冗長に感じる部分もあるかもしれません。しかしすべての設計判断には、多様なハードウェアで効率的な推論を実現するという制約から生まれた明確な理由があります。その背景にある思想を理解することが、質の高いコントリビューションへの近道です。