Open9

llama.cppのコードを読む

TsujinoTsujino

example/main/main.cpp(llama-cliの元となるファイル)を読んでみる

以下、ChatGPT

LLaMA テキスト生成・チャットプログラムの概要

このコードは、LLaMA (Large Language Model Meta AI) を利用した テキスト生成チャット のプログラムです。以下の主要な処理を行っています。


処理の流れ

1. モデルの初期化と設定

  • コマンドライン引数を解析 し、モデルの設定を適用。
  • LLaMAモデルのロードNUMA (Non-Uniform Memory Access) の設定
  • ロジット計算やエンベディング処理のための初期化

2. 入力の準備

  • ユーザーが指定したプロンプトをトークン化
  • セッションのキャッシュをロード し、再利用可能なデータを取り出す。
  • 文脈サイズを超えないように調整

3. 対話モードの処理

  • インタラクティブモードの有効化
  • Ctrl+C による中断処理
  • チャットテンプレートを利用したメッセージのフォーマット

4. テキスト生成の処理

  • 事前トークンを処理しながら、モデルが次の単語を予測
  • コンテキストがいっぱいになった場合、古いトークンを削除する (コンテキスト管理)
  • Reverse Prompt (特定の単語で生成を停止) の検出

5. 結果の表示

  • 生成されたトークンをコンソールに出力
  • 必要に応じて対話モードを続行
  • 生成のパフォーマンス情報を記録

6. 終了処理

  • セッションデータを保存
  • モデルとスレッドプールの解放

ポイント

🔹 モデル・パラメータ関連

  • common_params_parse

    • コマンドライン引数を処理し、適切なモード (テキスト生成 or チャット) を選択。
  • llama_backend_init

    • LLaMAのバックエンドを初期化。
  • common_init_from_params

    • モデルを読み込み、LoRAアダプター (軽量ファインチューニング) を適用。

🔹 インタラクション

  • sigint_handler

    • Ctrl+C をキャッチし、安全に終了。
  • common_sampler_sample

    • 次のトークンを選択し、生成を進める。
  • console::readline

    • インタラクティブモードでユーザー入力を取得。

🔹 セッション管理

  • llama_state_save_file
    • セッションのキャッシュを保存し、次回の起動を高速化。

用途

このコードは、LLaMAを用いた インタラクティブなAIチャットボットテキスト生成アプリケーション の実装に役立つ設計になっています。

TsujinoTsujino

ggmlについて

https://huggingface.co/blog/introduction-to-ggml

ggml は、Transformer 推論に重点を置いた C および C++ で書かれた機械学習 (ML) ライブラリです。

概念

用語 説明
ggml_context テンソル、グラフ、(オプションで)データなどのオブジェクトを保持する「コンテナ」
ggml_cgraph 計算グラフを表す。バックエンドに転送される「計算の順序」。
ggml_backend 計算グラフを実行するためのインターフェースを表す。バックエンドには、CPU (デフォルト)、CUDA、Metal (Apple Silicon)、Vulkan、RPC など、さまざまな種類がある。
ggml_backend_buffer_type バッファタイプを表す。各ggml_backendに接続された「メモリアロケータ」。たとえば、GPU で計算を実行する場合は、buffer_type(通常はbuftと略記される) を介してGPU にメモリを割り当てる必要がある。
ggml_backend_buffer buffer_typeによって割り当てられたバッファを表す。バッファには複数のテンソルのデータを保持できることに留意。
ggml_gallocr 計算グラフで使用されるテンソルを効率的に割り当てるために使用されるグラフメモリ アロケータを表す。
ggml_backend_sched 複数のバックエンドの同時使用を可能にするスケジューラ。大規模なモデルや複数の GPU を扱うときに、異なるハードウェア (GPU と CPU など) に計算を分散できる。また、スケジューラは GPU でサポートされていない操作を CPU に自動的に割り当て、最適なリソース使用率と互換性を確保する。
TsujinoTsujino

計算実行方法(シンプル版)

#include "ggml.h"
#include "ggml-cpu.h"
#include <string.h>
#include <stdio.h>

int main(void) {
    // 行列積を実行するために行列のデータを初期化する
    const int rows_A = 4, cols_A = 2;
    float matrix_A[rows_A * cols_A] = {
        2, 8,
        5, 1,
        4, 2,
        8, 6
    };
    const int rows_B = 3, cols_B = 2;
    float matrix_B[rows_B * cols_B] = {
        10, 5,
        9, 9,
        5, 4
    };

    // 1. テンソルデータを格納するために`ggml_context`を割り当てる
    // 割り当てに必要なサイズを計算
    size_t ctx_size = 0;
    ctx_size += rows_A * cols_A * ggml_type_size(GGML_TYPE_F32); // tensor a
    ctx_size += rows_B * cols_B * ggml_type_size(GGML_TYPE_F32); // tensor b
    ctx_size += rows_A * rows_B * ggml_type_size(GGML_TYPE_F32); // result
    ctx_size += 3 * ggml_tensor_overhead(); // metadata for 3 tensors
    ctx_size += ggml_graph_overhead(); // compute graph
    ctx_size += 1024; // some overhead (exact calculation omitted for simplicity)

    // テンソルデータを格納するため`ggml_context`を作成
    struct ggml_init_params params = {
        /*.mem_size   =*/ ctx_size,
        /*.mem_buffer =*/ NULL,
        /*.no_alloc   =*/ false,
    };
    struct ggml_context * ctx = ggml_init(params);

    // 2. テンソルを作成しデータを格納
    struct ggml_tensor * tensor_a = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols_A, rows_A);
    struct ggml_tensor * tensor_b = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols_B, rows_B);
    memcpy(tensor_a->data, matrix_A, ggml_nbytes(tensor_a));
    memcpy(tensor_b->data, matrix_B, ggml_nbytes(tensor_b));


    // 3. 行列積操作のために`ggml_cgraph`を作成する
    struct ggml_cgraph * gf = ggml_new_graph(ctx);

    // result = a*b^T
    // 注意: ggml_mul_mat(A, B) を実行すると、B は内部で転置される
    // そのため、結果も転置される
    struct ggml_tensor * result = ggml_mul_mat(ctx, tensor_a, tensor_b);

    // result テンソルを計算対象としてマークする
    ggml_build_forward_expand(gf, result);

    // 4. 計算を実行
    int n_threads = 1; // Optional: number of threads to perform some operations with multi-threading
    ggml_graph_compute_with_ctx(ctx, gf, n_threads);

    // 5. 結果を取得する(出力テンソル)
    float * result_data = (float *) result->data;
    printf("mul mat (%d x %d) (transposed result):\n[", (int) result->ne[0], (int) result->ne[1]);
    for (int j = 0; j < result->ne[1] /* rows */; j++) {
        if (j > 0) {
            printf("\n");
        }

        for (int i = 0; i < result->ne[0] /* cols */; i++) {
            printf(" %.2f", result_data[j * result->ne[0] + i]);
        }
    }
    printf(" ]\n");

    // 6. メモリを解放して終了
    ggml_free(ctx);
    return 0;
}

TsujinoTsujino

計算実行方法(backendを指定)

#include "ggml.h"
#include "ggml-alloc.h"
#include "ggml-backend.h"
#ifdef GGML_USE_CUDA
#include "ggml-cuda.h"
#endif

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main(void) {
    // 行列積を実行するための行列データを初期化
    const int rows_A = 4, cols_A = 2;
    float matrix_A[rows_A * cols_A] = {
        2, 8,
        5, 1,
        4, 2,
        8, 6
    };
    const int rows_B = 3, cols_B = 2;
    float matrix_B[rows_B * cols_B] = {
        10, 5,
        9, 9,
        5, 4
    };

    // 1. バックエンドを初期化
    ggml_backend_t backend = NULL;
#ifdef GGML_USE_CUDA
    fprintf(stderr, "%s: CUDA バックエンドを使用\n", __func__);
    backend = ggml_backend_cuda_init(0); // デバイス 0 を初期化
    if (!backend) {
        fprintf(stderr, "%s: ggml_backend_cuda_init() に失敗しました\n", __func__);
    }
#endif
    // GPU バックエンドがない場合は、CPU バックエンドにフォールバック
    if (!backend) {
        backend = ggml_backend_cpu_init();
    }

    // 必要なメモリサイズを計算
    size_t ctx_size = 0;
    ctx_size += 2 * ggml_tensor_overhead(); // テンソルのオーバーヘッドを考慮
    // それ以外の割り当ては不要

    // 2. `ggml_context` を割り当ててテンソルデータを保存
    struct ggml_init_params params = {
        /*.mem_size   =*/ ctx_size,
        /*.mem_buffer =*/ NULL,
        /*.no_alloc   =*/ true, // テンソルは後で ggml_backend_alloc_ctx_tensors() により割り当てられる
    };
    struct ggml_context * ctx = ggml_init(params);

    // テンソルのメタデータ(形状とデータ型のみ)を作成
    struct ggml_tensor * tensor_a = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols_A, rows_A);
    struct ggml_tensor * tensor_b = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols_B, rows_B);

    // 4. すべてのテンソルを格納するための `ggml_backend_buffer` を割り当て
    ggml_backend_buffer_t buffer = ggml_backend_alloc_ctx_tensors(ctx, backend);

    // 5. メインメモリ(RAM)からバックエンドバッファへテンソルデータをコピー
    ggml_backend_tensor_set(tensor_a, matrix_A, 0, ggml_nbytes(tensor_a));
    ggml_backend_tensor_set(tensor_b, matrix_B, 0, ggml_nbytes(tensor_b));

    // 6. 行列積のための `ggml_cgraph` を作成
    struct ggml_cgraph * gf = NULL;
    struct ggml_context * ctx_cgraph = NULL;
    {
        // 一時的なコンテキストを作成し、グラフを構築
        struct ggml_init_params params0 = {
            /*.mem_size   =*/ ggml_tensor_overhead() * GGML_DEFAULT_GRAPH_SIZE + ggml_graph_overhead(),
            /*.mem_buffer =*/ NULL,
            /*.no_alloc   =*/ true, // テンソルは後で ggml_gallocr_alloc_graph() により割り当てられる
        };
        ctx_cgraph = ggml_init(params0);
        gf = ggml_new_graph(ctx_cgraph);

        // result = A * B^T
        // 注意: ggml_mul_mat(A, B) は B を内部的に転置する
        // 結果も転置された形となる
        struct ggml_tensor * result0 = ggml_mul_mat(ctx_cgraph, tensor_a, tensor_b);

        // "result" テンソルとその依存関係をすべて計算グラフに追加
        ggml_build_forward_expand(gf, result0);
    }

    // 7. `ggml_gallocr` を作成して計算グラフ用のメモリを確保
    ggml_gallocr_t allocr = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend));
    ggml_gallocr_alloc_graph(allocr, gf);

    // (8. オプション: `ggml_backend_sched` を使用したスケジューリングは省略)

    // 9. 計算の実行
    int n_threads = 1; // オプション: 一部の処理をマルチスレッドで実行するスレッド数
    if (ggml_backend_is_cpu(backend)) {
        ggml_backend_cpu_set_n_threads(backend, n_threads);
    }
    ggml_backend_graph_compute(backend, gf);

    // 10. 計算結果(出力テンソル)を取得
    // この例では、出力テンソルは常にグラフの最後のノードとなる
    struct ggml_tensor * result = gf->nodes[gf->n_nodes - 1];
    float * result_data = malloc(ggml_nbytes(result));
    // テンソルデータはデバイスバッファに格納されているため、RAM にコピーする必要がある
    ggml_backend_tensor_get(result, result_data, 0, ggml_nbytes(result));
    printf("行列積 (%d x %d)(転置された結果):\n[", (int) result->ne[0], (int) result->ne[1]);
    for (int j = 0; j < result->ne[1] /* 行数 */; j++) {
        if (j > 0) {
            printf("\n");
        }

        for (int i = 0; i < result->ne[0] /* 列数 */; i++) {
            printf(" %.2f", result_data[j * result->ne[0] + i]);
        }
    }
    printf(" ]\n");
    free(result_data);

    // 11. メモリを解放して終了
    ggml_free(ctx_cgraph);
    ggml_gallocr_free(allocr);
    ggml_free(ctx);
    ggml_backend_buffer_free(buffer);
    ggml_backend_free(backend);
    return 0;
}
TsujinoTsujino

2つのサンプルの差分

  1. バックエンドの初期化
    // 1. バックエンドを初期化
    ggml_backend_t backend = NULL;
#ifdef GGML_USE_CUDA
    fprintf(stderr, "%s: CUDA バックエンドを使用\n", __func__);
    backend = ggml_backend_cuda_init(0); // デバイス 0 を初期化
    if (!backend) {
        fprintf(stderr, "%s: ggml_backend_cuda_init() に失敗しました\n", __func__);
    }
#endif
    // GPU バックエンドがない場合は、CPU バックエンドにフォールバック
    if (!backend) {
        backend = ggml_backend_cpu_init();
    }
  1. ctxの作成

計算グラフ用のバッファは後段のggml_cgraphに格納するため、ctx用に確保されたサイズが小さい(?)

    // 必要なメモリサイズを計算
    size_t ctx_size = 0;
    ctx_size += 2 * ggml_tensor_overhead(); // テンソルのオーバーヘッドを考慮
    // それ以外の割り当ては不要

    // 2. `ggml_context` を割り当ててテンソルデータを保存
    struct ggml_init_params params = {
        /*.mem_size   =*/ ctx_size,
        /*.mem_buffer =*/ NULL,
        /*.no_alloc   =*/ true, // テンソルは後で ggml_backend_alloc_ctx_tensors() により割り当てられる
    };
    struct ggml_context * ctx = ggml_init(params);
シンプル版のコード
    // 1. テンソルデータを格納するために`ggml_context`を割り当てる
    // 割り当てに必要なサイズを計算
    size_t ctx_size = 0;
    ctx_size += rows_A * cols_A * ggml_type_size(GGML_TYPE_F32); // tensor a
    ctx_size += rows_B * cols_B * ggml_type_size(GGML_TYPE_F32); // tensor b
    ctx_size += rows_A * rows_B * ggml_type_size(GGML_TYPE_F32); // result
    ctx_size += 3 * ggml_tensor_overhead(); // metadata for 3 tensors
    ctx_size += ggml_graph_overhead(); // compute graph
    ctx_size += 1024; // some overhead (exact calculation omitted for simplicity)

    // テンソルデータを格納するため`ggml_context`を作成
    struct ggml_init_params params = {
        /*.mem_size   =*/ ctx_size,
        /*.mem_buffer =*/ NULL,
        /*.no_alloc   =*/ false,
    };
    struct ggml_context * ctx = ggml_init(params);
  1. バッファの割り当て
    // 4. すべてのテンソルを格納するための `ggml_backend_buffer` を割り当て
    ggml_backend_buffer_t buffer = ggml_backend_alloc_ctx_tensors(ctx, backend);
  1. バッファへテンソルデータをコピー
    ggml_backend_tensor_set(tensor_a, matrix_A, 0, ggml_nbytes(tensor_a));
    ggml_backend_tensor_set(tensor_b, matrix_B, 0, ggml_nbytes(tensor_b));
  1. ggml_cgraphを作成する
    // 6. 行列積のための `ggml_cgraph` を作成
    struct ggml_cgraph * gf = NULL;
    struct ggml_context * ctx_cgraph = NULL;
    {
        // 一時的なコンテキストを作成し、グラフを構築
        struct ggml_init_params params0 = {
            /*.mem_size   =*/ ggml_tensor_overhead() * GGML_DEFAULT_GRAPH_SIZE + ggml_graph_overhead(),
            /*.mem_buffer =*/ NULL,
            /*.no_alloc   =*/ true, // テンソルは後で ggml_gallocr_alloc_graph() により割り当てられる
        };
        ctx_cgraph = ggml_init(params0);
        gf = ggml_new_graph(ctx_cgraph);
         ...
  1. ggml_gallocrを作成
    // 7. `ggml_gallocr` を作成して計算グラフ用のメモリを確保
    ggml_gallocr_t allocr = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend));
    ggml_gallocr_alloc_graph(allocr, gf);
  1. データの取得

バックエンドから持ってくる必要がある(?)

    struct ggml_tensor * result = gf->nodes[gf->n_nodes - 1];
    float * result_data = malloc(ggml_nbytes(result));
    // テンソルデータはデバイスバッファに格納されているため、RAM にコピーする必要がある
    ggml_backend_tensor_get(result, result_data, 0, ggml_nbytes(result));
    printf("行列積 (%d x %d)(転置された結果):\n[", (int) result->ne[0], (int) result->ne[1]);
    for (int j = 0; j < result->ne[1] /* 行数 */; j++) {
        if (j > 0) {
            printf("\n");
        }

        for (int i = 0; i < result->ne[0] /* 列数 */; i++) {
            printf(" %.2f", result_data[j * result->ne[0] + i]);
        }
    }
    printf(" ]\n");
    free(result_data);
TsujinoTsujino

各関数等の深掘り

ggml_backend_cpu_init()
  • ggml_cpu_init() でCPUバックエンドを初期化
  • ctx(ggml_backend_cpu_context型)を作成
    • スレッド数
    • スレッドプール
    • work_data(作業用メモリ?)
    • work_size(作業用メモリサイズ?)
    • abort_callback(中断時のコールバック関数?)
    • abort_callback_data(コールバック関数の引数?)
  • cpu_backendを作成
    • ggml_backend_cpu_guid(バックエンドの一意な識別子)
    • ggml_backend_cpu_i(インターフェース、cpuでメモリを扱う際の関数の集まり)
    • ggml_backend_reg_dev_get(...) () (デバイス情報)
    • ctx
  • cpu_backendが返される
ggml_init()
  • クリティカルセクションに入るためのロックを取得
  • 最初の実行の場合、FP16とFP32のルックアップテーブルを作成
  • クリティカルセクション終了
  • ctx作成
  • mem_bufferが設定されていないとエラー
  • mem_bufferがメモリアライメントに揃っていなかったらエラー
  • ctx返却
ggml_backend_alloc_ctx_tensors()

GGML の計算コンテキスト (ctx) に対して、指定したバックエンド (backend) に適したバッファを割り当てる処理を行う。

  • ...
TsujinoTsujino

ggml_new_tensor_*d

  • ggml tensorを作成する関数
  • ggml.cに定義されている
  • *に次元数が入る。1~4次元がサポートされている
  • ggml_new_tensor_*d -> ggml_new_tensor -> ggml_new_tensor_impl

ggml_new_tensor_impl

コード
static struct ggml_tensor * ggml_new_tensor_impl(
        struct ggml_context * ctx,
        enum   ggml_type      type,
        int                   n_dims,
        const int64_t       * ne,
        struct ggml_tensor  * view_src,
        size_t                view_offs) {

    GGML_ASSERT(type >= 0 && type < GGML_TYPE_COUNT);
    GGML_ASSERT(n_dims >= 1 && n_dims <= GGML_MAX_DIMS);

    // find the base tensor and absolute offset
    if (view_src != NULL && view_src->view_src != NULL) {
        view_offs += view_src->view_offs;
        view_src   = view_src->view_src;
    }

    size_t data_size = ggml_row_size(type, ne[0]);
    for (int i = 1; i < n_dims; i++) {
        data_size *= ne[i];
    }

    GGML_ASSERT(view_src == NULL || data_size == 0 || data_size + view_offs <= ggml_nbytes(view_src));

    void * data = view_src != NULL ? view_src->data : NULL;
    if (data != NULL) {
        data = (char *) data + view_offs;
    }

    size_t obj_alloc_size = 0;

    if (view_src == NULL && !ctx->no_alloc) {
        // allocate tensor data in the context's memory pool
        obj_alloc_size = data_size;
    }

    struct ggml_object * const obj_new = ggml_new_object(ctx, GGML_OBJECT_TYPE_TENSOR, GGML_TENSOR_SIZE + obj_alloc_size);
    GGML_ASSERT(obj_new);

    struct ggml_tensor * const result = (struct ggml_tensor *)((char *)ctx->mem_buffer + obj_new->offs);

    *result = (struct ggml_tensor) {
        /*.type         =*/ type,
        /*.buffer       =*/ NULL,
        /*.ne           =*/ { 1, 1, 1, 1 },
        /*.nb           =*/ { 0, 0, 0, 0 },
        /*.op           =*/ GGML_OP_NONE,
        /*.op_params    =*/ { 0 },
        /*.flags        =*/ 0,
        /*.src          =*/ { NULL },
        /*.view_src     =*/ view_src,
        /*.view_offs    =*/ view_offs,
        /*.data         =*/ obj_alloc_size > 0 ? (void *)(result + 1) : data,
        /*.name         =*/ { 0 },
        /*.extra        =*/ NULL,
        /*.padding      =*/ { 0 },
    };

    // TODO: this should not be needed as long as we don't rely on aligned SIMD loads
    //GGML_ASSERT_ALIGNED(result->data);

    for (int i = 0; i < n_dims; i++) {
        result->ne[i] = ne[i];
    }

    result->nb[0] = ggml_type_size(type);
    result->nb[1] = result->nb[0]*(result->ne[0]/ggml_blck_size(type));
    for (int i = 2; i < GGML_MAX_DIMS; i++) {
        result->nb[i] = result->nb[i - 1]*result->ne[i - 1];
    }

    ctx->n_objects++;

    return result;
}
  • Assert
    • type(GGML_TYPE_F32とか)がggml_type(Enum)に定義された要素に当てはまっているか(有効なテンソルのデータ型であるか)をチェック
    • 次元数が1~4に収まっているかをチェック
  • テンソルのviewが別のテンソルを指している場合、view_offsview_srtをセット(?)
  • テンソルのデータサイズを計算
    • 1行分のデータサイズを計算(1行の要素数*データタイプ/ブロック)(ブロックは量子化関連の用語、後ほど調べる)
    • 1行分のデータサイズ*残りの次元の数を計算
  • ビューで参照しているメモリが、元テンソルの範囲内に収まっているかAssert
  • テンソルの実際のデータポインタを、ビューのオフセット分だけずらす
  • ggml_objectを作成
TsujinoTsujino

build_graph

サイズ計算で素数が登場する理由

計算グラフのサイズを計算する際、素数になるようにしている。

コード
size_t ggml_hash_size(size_t min_sz) {
    // next primes after powers of two
    static const size_t primes[] = {
        2, 3, 5, 11, 17, 37, 67, 131, 257, 521, 1031,
        2053, 4099, 8209, 16411, 32771, 65537, 131101,
        262147, 524309, 1048583, 2097169, 4194319, 8388617,
        16777259, 33554467, 67108879, 134217757, 268435459,
        536870923, 1073741827, 2147483659
    };
    static const size_t n_primes = sizeof(primes)/sizeof(primes[0]);

    // find the smallest prime that is larger or equal than min_sz
    size_t l = 0;
    size_t r = n_primes;
    while (l < r) {
        size_t m = (l + r)/2;
        if (primes[m] < min_sz) {
            l = m + 1;
        } else {
            r = m;
        }
    }
    size_t sz = l < n_primes ? primes[l] : min_sz | 1;
    return sz;
}

理由としては、ハッシュテーブルのkeyの衝突を防ぐため。
まず、ハッシュテーブルはテーブルのキーの登録有無をすばやく確認できるようにするために作成している(?)
素数にすることで、算出したインデックスが特定の値に偏ることを防ぐことができる。
疑似コードとしては以下。

index = key % hash_size;
<!-- たとえばkeyはテンソルのアドレス -->

ここで、hash_sizeが8の時を考えると、因数が1, 2, 4, 8。
テンソルのアドレスがもし因数のうちのどれかの周期になってしまうとindexの値が偏ってしまう。
そのため素数が安心。

処理

  • ggml_cgraphでグラフを作成(実体はまだ。ポインタ情報等をもっているだけ)
  • ggml_mul_mutで計算結果の次元を計算、必要情報(入力テンソル、オペレーション等)を格納したresultを作成(メモリ確保)してアドレスを返却
  • ggml_build_forward_expandで計算グラフにテンソル情報が埋められる
    • ggml_visit_parentsで再起的にノードを見ていき(srcを遡っていき)、入力ノードまでたどり着いたら、nodeのアドレスを順番にcgraph->leafscgraph->nodes)に登録していく(登録するごとにn_nodes,n_leafsを増やす)。ハッシュテーブルを見てすでに登録していればスキップする。