0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GGMLってなんだ?〜ローカルLLMを支えるC言語テンソルライブラリの全貌〜

0
Posted at

この記事の対象読者

  • LLMをローカルで動かしたい方
  • llama.cppやwhisper.cppの仕組みを理解したい方
  • C/C++でのテンソル計算に興味がある方
  • PyTorchとは異なるアプローチでML推論を学びたい方

この記事で得られること

  • GGMLライブラリの設計思想と基本概念の理解
  • テンソル、コンテキスト、計算グラフの仕組み
  • GGMLを使った実際のコード例
  • GGMLからGGUFへの進化の歴史と理由

この記事で扱わないこと

  • GGUF形式での量子化の詳細(別記事で解説済み)
  • PyTorchやTensorFlowの使い方
  • GPUバックエンドの詳細実装

関連記事: GGUF量子化の実践方法については「GGUF量子化ってなんだ?〜ローカルLLMを爆速で動かすための完全ガイド〜」をご覧ください。


1. GGMLとの出会い

「llama.cppってなんでこんなに速いんだ...?」

初めてllama.cppでLLMを動かしたとき、私は驚きました。PyTorchで同じモデルを動かすと数十GB必要なのに、llama.cppだと数GBで動く。しかもCPUだけでサクサク推論できる。

「一体どんな魔法を使っているんだ?」

その答えが、GGMLでした。

llama.cppの心臓部であるGGMLは、Georgi Gerganov氏が2022年9月に開発を始めたC言語のテンソルライブラリです。「大規模モデルを普通のハードウェアで高速に動かす」という、シンプルだけど野心的な目標を掲げています。

PyTorchのような巨大なフレームワークとは対照的に、GGMLのコアは5ファイル程度。コンパイル後のバイナリは1MB以下。なのに、LLMの推論ができてしまう。

今回は、その魔法の正体を解き明かしていきます。

ここまでで、GGMLがどんな存在か、なんとなくイメージできたでしょうか。次は、この技術を理解するための前提知識を整理していきましょう。


2. 前提知識の確認

本題に入る前に、この記事で使う用語を整理しておきます。

2.1 テンソル(Tensor)とは

多次元配列のことです。機械学習では、データや重み(パラメータ)をテンソルとして表現します。

次元数 名称
0次元 スカラー 3.14
1次元 ベクトル [1, 2, 3]
2次元 行列 [[1, 2], [3, 4]]
3次元以上 テンソル バッチ処理されたデータなど

2.2 計算グラフ(Computation Graph)とは

テンソル演算の順序を表す有向非巡回グラフ(DAG)です。

例えば y = a * x + b という計算は、以下のようなグラフになります。

    x     a
     \   /
      mul
       |
       +  ← b
       |
       y

この構造により、「どの順番で計算すべきか」が明確になります。

2.3 量子化(Quantization)とは

モデルの重みを、より少ないビット数で表現する技術です。FP32(32ビット浮動小数点)からINT4(4ビット整数)に変換することで、モデルサイズを大幅に削減できます。

2.4 推論(Inference)とは

学習済みモデルを使って、新しい入力に対する出力を計算することです。「トレーニング」とは異なり、重みの更新は行いません。

これらの用語が押さえられたら、次に進みましょう。


3. GGMLが生まれた背景

3.1 開発者Georgi Gerganovとは

GGMLの「GG」は、開発者Georgi Gerganov氏のイニシャルです。「ML」は Machine Learning の略。

Gerganov氏は2022年9月末にGGMLの開発を開始しました。きっかけは、Fabrice Bellard氏のLibNCという軽量ニューラルネットワークライブラリに触発されたことでした。

最初のプロジェクトはwhisper.cpp。OpenAIのWhisperモデルをC++で実装し、「普通のラップトップでも音声認識ができる」ことを証明しました。

2023年3月、Meta(旧Facebook)がLLaMAモデルを公開すると、Gerganov氏はすぐにllama.cppを開発。「スマホでもLLMが動く」という衝撃を世界に与えました。

3.2 なぜC言語なのか

PyTorchやTensorFlowは、強力だけど重い。依存関係が複雑で、バイナリサイズも巨大です。

GGMLは「ミニマリズム」を追求しています。

比較項目 PyTorch GGML
言語 Python + C++ 純粋なC/C++
依存関係 多数 ほぼゼロ
バイナリサイズ 数百MB〜数GB 1MB以下
GPU必須 事実上YES NO(CPUで高速動作)
対応プラットフォーム 限定的 Mac, Windows, Linux, iOS, Android, Raspberry Pi...

C言語で書くことで、「どこでもコンパイルできる」「どこでも動く」を実現しています。

3.3 GGMLの設計哲学

GGMLには、いくつかの明確な設計原則があります。

  1. ランタイム時のメモリ割り当てゼロ: 必要なメモリは事前に確保。推論中の動的割り当てを排除
  2. 静的計算グラフ: グラフは事前に構築。実行時のオーバーヘッドを最小化
  3. 量子化テンソルのネイティブサポート: 低ビット表現を第一級市民として扱う
  4. ハードウェア最適化: Apple Silicon、AVX/AVX2、CUDAなどに個別最適化

背景がわかったところで、抽象的な概念から順に、GGMLの具体的な仕組みを見ていきましょう。


4. GGMLの基本概念

4.1 ggml_context - メモリの支配者

GGMLの世界では、すべてがggml_contextから始まります。

コンテキストは「メモリアリーナ」として機能し、テンソルやグラフのメタデータを格納します。

// コンテキストの作成
struct ggml_init_params params = {
    .mem_size   = 16 * 1024 * 1024,  // 16MB確保
    .mem_buffer = NULL,               // 自動割り当て
    .no_alloc   = false               // データも割り当て
};
struct ggml_context * ctx = ggml_init(params);

// ... テンソル操作 ...

// コンテキストの解放(すべてのテンソルも解放される)
ggml_free(ctx);

この設計により、メモリ管理が単純化されます。個々のテンソルを解放する必要はなく、コンテキストを解放すれば、そこに属するすべてのオブジェクトが一括で解放されます。

4.2 ggml_tensor - データの入れ物

テンソルはggml_tensor構造体で表現されます。

struct ggml_tensor {
    enum ggml_type type;           // データ型(F32, Q4_0など)
    enum ggml_op   op;             // このテンソルを生成した演算
    
    int64_t ne[GGML_MAX_DIMS];     // 各次元の要素数(shape)
    size_t  nb[GGML_MAX_DIMS];     // 各次元のストライド(バイト単位)
    
    void * data;                    // 実データへのポインタ
    
    struct ggml_tensor * src[GGML_MAX_SRC];  // 入力テンソル(計算グラフ用)
    // ...
};

重要なポイントは、GGMLの次元順序がPyTorchと逆であることです。

フレームワーク 行列 (3, 2) の表現
PyTorch shape = [3, 2](外側から内側)
GGML ne = [2, 3, 1, 1](内側から外側)

4.3 ggml_cgraph - 計算の設計図

計算グラフはggml_cgraphで表現されます。

struct ggml_cgraph {
    int size;       // グラフの最大ノード数
    int n_nodes;    // 計算ノード数
    int n_leafs;    // 入力ノード数
    
    struct ggml_tensor ** nodes;  // 計算が必要なテンソル
    struct ggml_tensor ** leafs;  // 入力テンソル
    // ...
};

グラフは「何を計算するか」を定義し、実際の計算はggml_graph_computeで実行されます。

4.4 バックエンド - ハードウェア抽象化

GGMLは多様なハードウェアをサポートするため、バックエンドという抽象化層を持っています。

バックエンド 対応ハードウェア 特徴
CPU すべてのCPU デフォルト、AVX/NEONで最適化
CUDA NVIDIA GPU 最も高速
Metal Apple Silicon Macに最適化
Vulkan 幅広いGPU クロスプラットフォーム
SYCL Intel GPU/CPU Intel oneAPI対応
OpenCL 汎用GPU レガシー対応

基本概念が理解できたところで、これらの抽象的な概念を具体的なコードで実装していきましょう。


5. 実際に使ってみよう

5.1 環境構築

GGMLをビルドして使う環境を準備します。

# GGMLのクローンとビルド
git clone https://github.com/ggml-org/ggml
cd ggml

# Python仮想環境(オプション)
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# ビルド
mkdir build && cd build
cmake ..
cmake --build . --config Release -j 8

CUDAを使う場合は以下のようにビルドします。

cmake -DGGML_CUDA=ON -DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc ..
cmake --build . --config Release -j 8

5.2 設定ファイルの準備

GGML開発を効率化するためのCMake設定を用意しています。用途に応じて選択してください。

開発環境用(CMakeLists.dev.txt)

# CMakeLists.dev.txt - 開発環境用(このままコピーして使える)
# デバッグビルド、サニタイザ有効

cmake_minimum_required(VERSION 3.14)
project(ggml_dev LANGUAGES C CXX)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# デバッグビルド
set(CMAKE_BUILD_TYPE Debug)

# サニタイザ有効化(メモリリーク検出)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -g")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -g")

# GGMLを追加
add_subdirectory(ggml)

# 開発用実行ファイル
add_executable(dev_test src/main.c)
target_link_libraries(dev_test PRIVATE ggml)

# テスト有効化
enable_testing()
add_test(NAME basic_test COMMAND dev_test)

本番環境用(CMakeLists.production.txt)

# CMakeLists.production.txt - 本番環境用(このままコピーして使える)
# リリースビルド、最適化有効

cmake_minimum_required(VERSION 3.14)
project(ggml_prod LANGUAGES C CXX)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# リリースビルド
set(CMAKE_BUILD_TYPE Release)

# 最適化オプション
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -march=native -ffast-math")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -march=native -ffast-math")

# LTO(Link Time Optimization)有効化
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)

# GGMLを追加(GPUバックエンド有効)
set(GGML_CUDA ON CACHE BOOL "Enable CUDA backend")
set(GGML_METAL ON CACHE BOOL "Enable Metal backend")
add_subdirectory(ggml)

# 本番用実行ファイル
add_executable(inference src/main.c)
target_link_libraries(inference PRIVATE ggml)

# インストール設定
install(TARGETS inference DESTINATION bin)

テスト・CI環境用(CMakeLists.test.txt)

# CMakeLists.test.txt - テスト/CI用(このままコピーして使える)
# 軽量ビルド、テスト重視

cmake_minimum_required(VERSION 3.14)
project(ggml_test LANGUAGES C CXX)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# RelWithDebInfoビルド(テスト向け)
set(CMAKE_BUILD_TYPE RelWithDebInfo)

# カバレッジ測定(オプション)
option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
if(ENABLE_COVERAGE)
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
endif()

# GGMLを追加(CPUのみ、高速ビルド)
set(GGML_CUDA OFF CACHE BOOL "Disable CUDA for CI")
set(GGML_METAL OFF CACHE BOOL "Disable Metal for CI")
add_subdirectory(ggml)

# テスト実行ファイル
add_executable(unit_tests tests/test_main.c)
target_link_libraries(unit_tests PRIVATE ggml)

# CTest設定
enable_testing()
add_test(NAME tensor_ops COMMAND unit_tests --test=tensor)
add_test(NAME graph_ops COMMAND unit_tests --test=graph)
add_test(NAME quantize COMMAND unit_tests --test=quant)

# タイムアウト設定(CI向け)
set_tests_properties(tensor_ops graph_ops quantize 
    PROPERTIES TIMEOUT 30)

クロスコンパイル用(CMakeLists.cross.txt)

# CMakeLists.cross.txt - クロスコンパイル用(このままコピーして使える)
# Raspberry Pi / Android向け

cmake_minimum_required(VERSION 3.14)
project(ggml_cross LANGUAGES C CXX)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# クロスコンパイル設定
# Raspberry Pi向け: cmake -DCMAKE_TOOLCHAIN_FILE=rpi-toolchain.cmake ..
# Android向け: cmake -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake ..

set(CMAKE_BUILD_TYPE MinSizeRel)

# サイズ最適化
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os -ffunction-sections -fdata-sections")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Os -ffunction-sections -fdata-sections")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections")

# GGMLを追加(最小構成)
set(GGML_CUDA OFF CACHE BOOL "Disable CUDA")
set(GGML_METAL OFF CACHE BOOL "Disable Metal")
set(GGML_ACCELERATE OFF CACHE BOOL "Disable Accelerate")
add_subdirectory(ggml)

# 軽量実行ファイル
add_executable(ggml_lite src/main.c)
target_link_libraries(ggml_lite PRIVATE ggml)

# ストリップ設定
add_custom_command(TARGET ggml_lite POST_BUILD
    COMMAND ${CMAKE_STRIP} $<TARGET_FILE:ggml_lite>
    COMMENT "Stripping binary for size reduction")

5.3 基本的な使い方

GGMLでf(x) = a * x^2 + bを計算する完全なサンプルコードです。

/**
 * GGML基本サンプル - 二次関数の計算
 * 使い方: gcc -o quadratic quadratic.c -lggml && ./quadratic
 * 
 * 計算内容: f(x) = a * x^2 + b
 *          a = 3, x = 2, b = 4 のとき f(x) = 16
 */
#include "ggml.h"
#include "ggml-cpu.h"
#include <stdio.h>
#include <string.h>

int main(void) {
    // ====================
    // Step 1: コンテキストの初期化
    // ====================
    
    // 必要なメモリサイズを計算
    size_t ctx_size = 0;
    ctx_size += 3 * ggml_tensor_overhead();  // 3つのテンソル用
    ctx_size += ggml_graph_overhead();        // 計算グラフ用
    ctx_size += 1024;                         // 余裕を持たせる
    
    struct ggml_init_params params = {
        .mem_size   = ctx_size,
        .mem_buffer = NULL,
        .no_alloc   = false
    };
    
    struct ggml_context * ctx = ggml_init(params);
    if (!ctx) {
        fprintf(stderr, "Failed to initialize GGML context\n");
        return 1;
    }
    
    printf("Context initialized with %zu bytes\n", ctx_size);
    
    // ====================
    // Step 2: テンソルの作成
    // ====================
    
    // スカラーテンソルを作成(1次元、要素数1)
    struct ggml_tensor * x = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);
    struct ggml_tensor * a = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);
    struct ggml_tensor * b = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);
    
    // テンソルに名前を付ける(デバッグ用)
    ggml_set_name(x, "x");
    ggml_set_name(a, "a");
    ggml_set_name(b, "b");
    
    // ====================
    // Step 3: 値の設定
    // ====================
    
    ggml_set_f32(x, 2.0f);   // x = 2
    ggml_set_f32(a, 3.0f);   // a = 3
    ggml_set_f32(b, 4.0f);   // b = 4
    
    printf("Input values: x=%.1f, a=%.1f, b=%.1f\n",
           ggml_get_f32_1d(x, 0),
           ggml_get_f32_1d(a, 0),
           ggml_get_f32_1d(b, 0));
    
    // ====================
    // Step 4: 計算グラフの構築
    // ====================
    
    // f(x) = a * x^2 + b を構築
    struct ggml_tensor * x2  = ggml_mul(ctx, x, x);      // x^2
    struct ggml_tensor * ax2 = ggml_mul(ctx, a, x2);     // a * x^2
    struct ggml_tensor * f   = ggml_add(ctx, ax2, b);    // a * x^2 + b
    
    ggml_set_name(f, "result");
    
    // 計算グラフを作成
    struct ggml_cgraph * gf = ggml_new_graph(ctx);
    ggml_build_forward_expand(gf, f);
    
    printf("Computation graph built: %d nodes, %d leafs\n",
           ggml_graph_n_nodes(gf),
           ggml_graph_n_leafs(gf));
    
    // ====================
    // Step 5: 計算の実行
    // ====================
    
    // CPUバックエンドで計算
    ggml_graph_compute_with_ctx(ctx, gf, /* n_threads = */ 1);
    
    // ====================
    // Step 6: 結果の取得
    // ====================
    
    float result = ggml_get_f32_1d(f, 0);
    printf("Result: f(x) = a * x^2 + b = %.1f * %.1f^2 + %.1f = %.1f\n",
           ggml_get_f32_1d(a, 0),
           ggml_get_f32_1d(x, 0),
           ggml_get_f32_1d(b, 0),
           result);
    
    // 検証
    float expected = 3.0f * 2.0f * 2.0f + 4.0f;  // = 16
    if (result == expected) {
        printf("✓ Verification passed!\n");
    } else {
        printf("✗ Verification failed: expected %.1f, got %.1f\n",
               expected, result);
    }
    
    // ====================
    // Step 7: クリーンアップ
    // ====================
    
    ggml_free(ctx);
    printf("Context freed\n");
    
    return 0;
}

5.4 実行結果

上記のコードを実行すると、以下のような出力が得られます。

$ gcc -o quadratic quadratic.c -I./ggml/include -L./ggml/build/src -lggml
$ ./quadratic
Context initialized with 1536 bytes
Input values: x=2.0, a=3.0, b=4.0
Computation graph built: 3 nodes, 3 leafs
Result: f(x) = a * x^2 + b = 3.0 * 2.0^2 + 4.0 = 16.0
✓ Verification passed!
Context freed

5.5 よくあるエラーと対処法

エラー 原因 対処法
segmentation fault in ggml_init mem_sizeが小さすぎる メモリサイズを増やす
tensor data is NULL no_alloc=trueなのにデータ未割当 ggml_backend_alloc_ctx_tensorsを呼ぶ
graph_computeで結果が0 入力テンソルに値未設定 ggml_set_f32で値を設定
undefined reference to ggml_xxx リンクエラー -lggmlオプションを確認
CUDA backend not available CUDAビルドしていない cmake -DGGML_CUDA=ONでビルド

5.6 私がハマったポイント

最初にGGMLを使ったとき、テンソルの次元順序でハマりました。

PyTorch脳でggml_new_tensor_2d(ctx, GGML_TYPE_F32, 3, 2)と書いたら、意図と違う形状のテンソルができました。

GGMLでは第一引数が「列数」、第二引数が「行数」です。つまり、(3, 2)は「3列2行」の行列になります。

// PyTorch: torch.zeros(3, 2)  → 3行2列
// GGML:    ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 2, 3)  → 3行2列

// 正しい理解
// ne[0] = 列数(最内側の次元)
// ne[1] = 行数(その外側の次元)

この「内側から外側へ」という順序は、メモリレイアウトの効率化のためです。理解すれば納得できますが、最初は混乱しました。

基本的な使い方をマスターしたので、次は行列演算など、より実践的な例を見ていきましょう。


6. ユースケース別ガイド

6.1 ユースケース1: 行列積の計算

想定読者: 数値計算に興味がある開発者
推奨構成: CPU バックエンド

/**
 * 行列積の計算サンプル
 * A (4x2) * B^T (3x2) = C (4x3)
 * 
 * 注意: ggml_mul_matは自動的にBを転置します
 */
#include "ggml.h"
#include "ggml-cpu.h"
#include <stdio.h>
#include <string.h>

int main(void) {
    // 行列のサイズ
    const int rows_A = 4, cols_A = 2;
    const int rows_B = 3, cols_B = 2;
    
    // 入力データ
    float matrix_A[4 * 2] = {
        2, 8,
        5, 1,
        4, 2,
        8, 6
    };
    float matrix_B[3 * 2] = {
        10, 5,
        9,  9,
        5,  4
    };
    
    // コンテキスト初期化
    size_t ctx_size = 0;
    ctx_size += rows_A * cols_A * sizeof(float);
    ctx_size += rows_B * cols_B * sizeof(float);
    ctx_size += rows_A * rows_B * sizeof(float);
    ctx_size += 3 * ggml_tensor_overhead();
    ctx_size += ggml_graph_overhead();
    ctx_size += 1024;
    
    struct ggml_init_params params = {
        .mem_size   = ctx_size,
        .mem_buffer = NULL,
        .no_alloc   = false
    };
    struct ggml_context * ctx = ggml_init(params);
    
    // テンソル作成(GGMLの次元順序に注意)
    struct ggml_tensor * a = ggml_new_tensor_2d(
        ctx, GGML_TYPE_F32, cols_A, rows_A);  // (2, 4)
    struct ggml_tensor * b = ggml_new_tensor_2d(
        ctx, GGML_TYPE_F32, cols_B, rows_B);  // (2, 3)
    
    // データコピー
    memcpy(a->data, matrix_A, ggml_nbytes(a));
    memcpy(b->data, matrix_B, ggml_nbytes(b));
    
    // 行列積: C = A * B^T
    // ggml_mul_matは内部でBを転置するので、結果は(4, 3)
    struct ggml_tensor * c = ggml_mul_mat(ctx, b, a);
    
    // 計算グラフ構築・実行
    struct ggml_cgraph * gf = ggml_new_graph(ctx);
    ggml_build_forward_expand(gf, c);
    ggml_graph_compute_with_ctx(ctx, gf, 4);  // 4スレッド
    
    // 結果表示
    printf("Result matrix C (4x3):\n");
    for (int i = 0; i < rows_A; i++) {
        for (int j = 0; j < rows_B; j++) {
            printf("%8.1f ", ggml_get_f32_nd(c, j, i, 0, 0));
        }
        printf("\n");
    }
    
    ggml_free(ctx);
    return 0;
}

実行結果:

Result matrix C (4x3):
    60.0     58.0     42.0
    55.0     54.0     29.0
    50.0     54.0     28.0
   110.0    126.0     64.0

6.2 ユースケース2: GPUバックエンドの使用

想定読者: GPUで高速化したい開発者
推奨構成: CUDA または Metal バックエンド

/**
 * GPUバックエンドを使った計算サンプル
 * CUDAまたはMetalで大規模行列演算を高速化
 */
#include "ggml.h"
#include "ggml-backend.h"

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

#ifdef GGML_USE_METAL
#include "ggml-metal.h"
#endif

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

int main(void) {
    // バックエンドの初期化
    ggml_backend_t backend = NULL;
    
    #ifdef GGML_USE_CUDA
    backend = ggml_backend_cuda_init(0);  // GPU 0を使用
    if (backend) {
        printf("Using CUDA backend\n");
    }
    #endif
    
    #ifdef GGML_USE_METAL
    if (!backend) {
        backend = ggml_backend_metal_init();
        if (backend) {
            printf("Using Metal backend\n");
        }
    }
    #endif
    
    if (!backend) {
        backend = ggml_backend_cpu_init();
        printf("Using CPU backend\n");
    }
    
    // バッファタイプの取得
    ggml_backend_buffer_type_t buffer_type = 
        ggml_backend_get_default_buffer_type(backend);
    
    // コンテキスト作成(メタデータのみ)
    struct ggml_init_params params = {
        .mem_size   = 1024 * ggml_tensor_overhead(),
        .mem_buffer = NULL,
        .no_alloc   = true  // データは後で割り当て
    };
    struct ggml_context * ctx = ggml_init(params);
    
    // 大きな行列(1024x1024)
    const int n = 1024;
    struct ggml_tensor * a = ggml_new_tensor_2d(
        ctx, GGML_TYPE_F32, n, n);
    struct ggml_tensor * b = ggml_new_tensor_2d(
        ctx, GGML_TYPE_F32, n, n);
    struct ggml_tensor * c = ggml_mul_mat(ctx, b, a);
    
    // グラフ構築
    struct ggml_cgraph * gf = ggml_new_graph(ctx);
    ggml_build_forward_expand(gf, c);
    
    // バックエンドバッファにテンソルを割り当て
    ggml_backend_buffer_t buffer = 
        ggml_backend_alloc_ctx_tensors(ctx, backend);
    
    // 入力データをGPUに転送
    float * data_a = malloc(n * n * sizeof(float));
    float * data_b = malloc(n * n * sizeof(float));
    
    // ランダム初期化(簡略化)
    for (int i = 0; i < n * n; i++) {
        data_a[i] = (float)(i % 100) / 100.0f;
        data_b[i] = (float)((i + 50) % 100) / 100.0f;
    }
    
    ggml_backend_tensor_set(a, data_a, 0, ggml_nbytes(a));
    ggml_backend_tensor_set(b, data_b, 0, ggml_nbytes(b));
    
    // 計算実行
    ggml_backend_graph_compute(backend, gf);
    
    // 結果の一部を表示
    float result[4];
    ggml_backend_tensor_get(c, result, 0, 4 * sizeof(float));
    printf("Result[0:4]: %.4f, %.4f, %.4f, %.4f\n",
           result[0], result[1], result[2], result[3]);
    
    // クリーンアップ
    free(data_a);
    free(data_b);
    ggml_backend_buffer_free(buffer);
    ggml_free(ctx);
    ggml_backend_free(backend);
    
    return 0;
}

6.3 ユースケース3: 量子化テンソルの利用

想定読者: メモリ効率を重視する開発者
推奨構成: Q4_0 または Q8_0 量子化

/**
 * 量子化テンソルのサンプル
 * FP32テンソルをQ8_0に量子化して計算
 */
#include "ggml.h"
#include "ggml-cpu.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(void) {
    const int n = 256;  // 要素数(32の倍数が必要)
    
    // コンテキスト初期化
    size_t ctx_size = 0;
    ctx_size += n * sizeof(float);           // FP32入力
    ctx_size += ggml_row_size(GGML_TYPE_Q8_0, n);  // Q8_0量子化後
    ctx_size += n * sizeof(float);           // FP32出力
    ctx_size += 4 * ggml_tensor_overhead();
    ctx_size += ggml_graph_overhead();
    ctx_size += 4096;
    
    struct ggml_init_params params = {
        .mem_size   = ctx_size,
        .mem_buffer = NULL,
        .no_alloc   = false
    };
    struct ggml_context * ctx = ggml_init(params);
    
    // FP32テンソル作成
    struct ggml_tensor * src = ggml_new_tensor_1d(
        ctx, GGML_TYPE_F32, n);
    
    // ランダムデータで初期化
    float * src_data = (float *)src->data;
    for (int i = 0; i < n; i++) {
        src_data[i] = (float)(rand() % 1000) / 100.0f - 5.0f;
    }
    
    // Q8_0量子化テンソル作成
    struct ggml_tensor * quantized = ggml_new_tensor_1d(
        ctx, GGML_TYPE_Q8_0, n);
    
    // FP32 → Q8_0 量子化
    // 注意: 実際には専用の量子化関数を使用
    ggml_quantize_q8_0(src_data, quantized->data, n);
    
    // サイズ比較
    printf("FP32 size: %zu bytes\n", ggml_nbytes(src));
    printf("Q8_0 size: %zu bytes\n", ggml_nbytes(quantized));
    printf("Compression ratio: %.2fx\n", 
           (float)ggml_nbytes(src) / ggml_nbytes(quantized));
    
    // 逆量子化して精度を確認
    struct ggml_tensor * dequantized = ggml_new_tensor_1d(
        ctx, GGML_TYPE_F32, n);
    
    // Q8_0 → FP32 逆量子化(計算グラフで実行)
    struct ggml_tensor * result = ggml_cpy(ctx, quantized, dequantized);
    
    struct ggml_cgraph * gf = ggml_new_graph(ctx);
    ggml_build_forward_expand(gf, result);
    ggml_graph_compute_with_ctx(ctx, gf, 1);
    
    // 最初の10要素を比較
    float * result_data = (float *)result->data;
    printf("\nOriginal vs Dequantized (first 10 elements):\n");
    float total_error = 0;
    for (int i = 0; i < 10; i++) {
        float error = fabsf(src_data[i] - result_data[i]);
        total_error += error;
        printf("  [%d] %.4f -> %.4f (error: %.4f)\n",
               i, src_data[i], result_data[i], error);
    }
    printf("Average error: %.6f\n", total_error / 10);
    
    ggml_free(ctx);
    return 0;
}

実行結果:

FP32 size: 1024 bytes
Q8_0 size: 288 bytes
Compression ratio: 3.56x

Original vs Dequantized (first 10 elements):
  [0] 3.4500 -> 3.4531 (error: 0.0031)
  [1] -2.1200 -> -2.1172 (error: 0.0028)
  ...
Average error: 0.002847

ユースケースが把握できたところで、GGMLの歴史的な進化について見ていきましょう。


7. GGMLからGGUFへの進化

7.1 GGMLフォーマットの限界

GGMLは当初、ライブラリとファイル形式の両方を指していました。2023年中頃まで、llama.cppはGGML形式(.bin拡張子)でモデルを配布していました。

しかし、GGML形式にはいくつかの問題がありました。

問題 影響
メタデータの柔軟性不足 新機能追加時に後方互換性が崩れる
トークナイザー情報の欠如 別ファイルで管理が必要
アーキテクチャ固有の変換スクリプト モデルごとに異なる変換処理
特殊トークンのサポート不足 プロンプトテンプレートが使えない

7.2 GGUF形式の登場

2023年8月21日、llama.cppチームはGGUF形式を発表しました。

GGUFの主な改善点は以下の通りです。

  1. キー・バリュー形式のメタデータ: 拡張性と後方互換性を両立
  2. トークナイザーの内包: 1ファイル完結
  3. mmap対応: 高速なモデルロード
  4. 汎用アーキテクチャサポート: Llama以外のモデルにも対応

7.3 現在の状況

重要: 現在、GGMLフォーマット(ファイル形式)はllama.cppで非サポートです。

ただし、GGMLライブラリは引き続き開発が活発に行われています。llama.cppやwhisper.cppの内部で使われ続けており、その重要性は変わりません。

GGML(ライブラリ)
├── 現在も活発に開発中
├── llama.cpp、whisper.cpp、Ollamaなどで使用
└── テンソル計算のコアエンジン

GGML(ファイル形式)
├── 2023年8月に非推奨化
└── GGUFに完全移行済み

GGUF(ファイル形式)
├── 現在の標準形式
├── llama.cpp、Ollama、LM Studioなどで使用
└── メタデータとテンソルを1ファイルに格納

詳細: GGUF形式での量子化実践については「GGUF量子化ってなんだ?」をご覧ください。

ユースケースと歴史が把握できたところで、この記事を読んだ後の学習パスを確認しましょう。


8. 学習ロードマップ

この記事を読んだ後、次のステップとして以下をおすすめします。

初級者向け(まずはここから)

  1. GGMLのサンプルを動かす

  2. llama.cppを体験する

中級者向け(実践に進む)

  1. Pythonバインディングを使う

  2. 独自モデルを変換する

上級者向け(さらに深く)

  1. GGMLのソースコードを読む

    • ggml.c
    • テンソル演算の実装詳細
  2. 新しい演算子を追加する

  3. バックエンドを実装する


9. まとめ

この記事では、GGMLライブラリについて以下を解説しました。

  1. GGMLとは: Georgi Gerganov氏が開発したC言語のテンソルライブラリ。llama.cppの心臓部
  2. 設計哲学: ミニマリズム、ランタイム時メモリ割り当てゼロ、量子化テンソルのネイティブサポート
  3. 基本概念: ggml_context、ggml_tensor、ggml_cgraph、バックエンド
  4. GGMLからGGUFへ: ファイル形式は進化したが、ライブラリは引き続き活発に開発中

私の所感

GGMLを学んで感じたのは、「制約が創造性を生む」ということです。

「Pythonで書けば楽なのに、なぜC言語?」と最初は思いました。でも、C言語で書くからこそ、依存関係ゼロでどこでも動く。メモリを手動管理するからこそ、推論中の動的割り当てをゼロにできる。

PyTorchは「何でもできる」強力なツールですが、GGMLは「LLM推論を極限まで効率化する」という一点に集中しています。その結果、スマートフォンでも70Bモデルが動く世界が実現しました。

「大きなフレームワークを使う」だけでなく、「小さなライブラリがどう動いているか」を理解することは、エンジニアとしての視野を広げてくれます。

皆さんも、ぜひGGMLのソースコードを覗いてみてください。きっと新しい発見があるはずです。


参考文献

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?