この記事の対象読者
- 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には、いくつかの明確な設計原則があります。
- ランタイム時のメモリ割り当てゼロ: 必要なメモリは事前に確保。推論中の動的割り当てを排除
- 静的計算グラフ: グラフは事前に構築。実行時のオーバーヘッドを最小化
- 量子化テンソルのネイティブサポート: 低ビット表現を第一級市民として扱う
- ハードウェア最適化: 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ファイル完結
- mmap対応: 高速なモデルロード
- 汎用アーキテクチャサポート: 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. 学習ロードマップ
この記事を読んだ後、次のステップとして以下をおすすめします。
初級者向け(まずはここから)
-
GGMLのサンプルを動かす
- ggml/examples
- GPT-2の小さいモデルで推論を試す
-
llama.cppを体験する
- llama.cpp Getting Started
- 量子化モデルをダウンロードして推論
中級者向け(実践に進む)
-
Pythonバインディングを使う
- ggml-python
- Pythonから低レベルAPI操作
-
独自モデルを変換する
- GGUF量子化記事
- Hugging Faceモデル → GGUF変換
上級者向け(さらに深く)
-
GGMLのソースコードを読む
- ggml.c
- テンソル演算の実装詳細
-
新しい演算子を追加する
- HOWTO-add-op
- カスタム演算子の実装
-
バックエンドを実装する
- ggml-backend-impl.h
- 新しいハードウェアへの対応
9. まとめ
この記事では、GGMLライブラリについて以下を解説しました。
- GGMLとは: Georgi Gerganov氏が開発したC言語のテンソルライブラリ。llama.cppの心臓部
- 設計哲学: ミニマリズム、ランタイム時メモリ割り当てゼロ、量子化テンソルのネイティブサポート
- 基本概念: ggml_context、ggml_tensor、ggml_cgraph、バックエンド
- GGMLからGGUFへ: ファイル形式は進化したが、ライブラリは引き続き活発に開発中
私の所感
GGMLを学んで感じたのは、「制約が創造性を生む」ということです。
「Pythonで書けば楽なのに、なぜC言語?」と最初は思いました。でも、C言語で書くからこそ、依存関係ゼロでどこでも動く。メモリを手動管理するからこそ、推論中の動的割り当てをゼロにできる。
PyTorchは「何でもできる」強力なツールですが、GGMLは「LLM推論を極限まで効率化する」という一点に集中しています。その結果、スマートフォンでも70Bモデルが動く世界が実現しました。
「大きなフレームワークを使う」だけでなく、「小さなライブラリがどう動いているか」を理解することは、エンジニアとしての視野を広げてくれます。
皆さんも、ぜひGGMLのソースコードを覗いてみてください。きっと新しい発見があるはずです。
参考文献
- GGML GitHub Repository - 公式リポジトリ
- Introduction to ggml - Hugging Face - 公式チュートリアル
- ggml.ai - 公式サイト
- llama.cpp - Wikipedia - 歴史的背景
- GGUF Specification - GGUFフォーマット仕様
- ggml-python Documentation - Pythonバインディング
- GGUF量子化ってなんだ? - 関連記事(GGUF実践)