HTTPリクエストからトークンへ:サーバーとCLIツールの仕組み
前提知識
- ›第1〜4回の記事
- ›HTTP APIとRESTの基本的な理解
HTTPリクエストからトークンへ:サーバーとCLIツールの仕組み
ここまで取り上げてきたGGMLテンソル、計算グラフ、KVキャッシュ、デコードループといった要素はすべて、libllama(ユーザーとのやり取り方法に関して特定の意見を持たないC/C++ライブラリ)の中に収まっています。tools/ にあるアプリケーション層こそが、llama.cppと実際のユーザーが接する場所です。OpenAI互換エンドポイントを持つHTTPサーバー、インタラクティブなCLI、量子化ツールなど、様々なツールがここに揃っています。
本記事では、サーバーのスロットベースの並行モデル、タスクキューのアーキテクチャ、APIの全体像、そしてllama.cppの設計の中でも特に興味深い決断の1つ(CLIが独自の推論ループを実装せず、サーバーのインフラをまるごと再利用している点)について解説します。
サーバーアーキテクチャの概要
サーバーのアーキテクチャは、tools/server/ 以下の複数のヘッダーファイルにまたがる3つのコンポーネントを中心に構成されています。
server_context は全体を統括するオーケストレーターです。tools/server/server-context.h で定義されており、pimpl(ポインタ・トゥ・実装)パターンを用いて内部実装を server_context_impl の背後に隠蔽しています。モデル、コンテキスト、スロット、メインイベントループを管理します。
server_queue はタスクの受付とルーティングを担います。tools/server/server-queue.h で定義されており、スレッドセーフなタスクキューとして機能します。すべてのスロットが使用中のときにタスクを保留する遅延実行、優先度のサポート、そしてアイドル時にモデルをアンロードするスリープモードを備えています。
server_task は作業の最小単位を表します。tools/server/server-task.h で定義されており、タスクのタイプ(補完、埋め込み、キャンセルなど)と入力パラメータを保持します。多段階処理のための子タスクを持つこともできます。
flowchart TD
HTTP["HTTP Request\n/v1/chat/completions"] --> ROUTES["Route Handler"]
ROUTES --> TASK["Create server_task"]
TASK --> QUEUE["server_queue.post()"]
QUEUE --> LOOP["Main Event Loop"]
LOOP --> SLOT["Assign to Slot"]
SLOT --> DECODE["llama_decode()"]
DECODE --> SAMPLE["Sample token"]
SAMPLE --> RESULT["server_task_result"]
RESULT --> SSE["SSE Stream / JSON Response"]
メインイベントループはシングルスレッド(「メインループ」スレッド)で動作し、キューからタスクを一つずつ処理します。これは意図的な設計です——llama_decode() を並行して呼び出すには複数のコンテキストや慎重なロック制御が必要になるため、その複雑さを避けています。代わりに、並行性はスロットの仕組みで実現しています。
スロットシステム
サーバーは「スロット」という抽象化を通じて、単一のロード済みモデルに対する複数の並行推論リクエストを処理できます。各スロットは独立した推論ストリームを表し、それぞれが以下を持ちます。
- プロンプトの状態と生成済みトークン
- サンプリングパラメータとサンプラーチェーン
- KVキャッシュ内の位置
- ストリーミング状態(SSEレスポンス用)
新しいリクエストが来ると、サーバーはそれをアイドル状態のスロットに割り当てます。メインループはアクティブな全スロットを巡回し、それぞれのバッチを使って llama_decode() を呼び出すことで推論を進めます。これは協調的マルチタスキングの一形態です——すべてのスロットが同じモデルの重みと llama_context を共有しつつ、KVキャッシュ内ではそれぞれ独自のシーケンスIDを持ちます。
sequenceDiagram
participant R1 as Request 1
participant R2 as Request 2
participant Q as server_queue
participant L as Main Loop
participant S1 as Slot 0
participant S2 as Slot 1
participant CTX as llama_context
R1->>Q: Completion task
R2->>Q: Completion task
Q->>L: Dequeue tasks
L->>S1: Assign Request 1
L->>S2: Assign Request 2
loop Update cycle
L->>S1: Prepare batch
L->>S2: Prepare batch
L->>CTX: llama_decode(combined batch)
CTX-->>S1: Logits for seq 0
CTX-->>S2: Logits for seq 1
S1->>R1: Stream token (SSE)
S2->>R2: Stream token (SSE)
end
スロット数は起動時に --parallel N で設定できます。スロットを追加するたびに、コンテキストウィンドウの一部をそのスロットが占有します。たとえば4スロット・n_ctx=8192 の場合、各スロットが使えるのは実質〜2048ポジションです(KVキャッシュのパーティショニングの実装はより細かいですが)。
ヒント: 一人のユーザー専用にサーバーを動かす場合は
--parallel 1が最適です。スロットを増やすほど、1リクエストあたりのコンテキスト長が減り、メモリ使用量も増加します。複数ユーザーが同時接続する環境では、想定される同時接続数に--parallelを合わせ、それに応じて--ctx-sizeもスケールさせましょう。
APIの全体像とルート登録
サーバーは tools/server/server.cpp でセットアップされる3系統のAPIを公開しています。
OpenAI互換エンドポイント:
POST /v1/chat/completions— ストリーミング対応のチャット補完POST /v1/completions— テキスト補完POST /v1/embeddings— 埋め込みの生成GET /v1/models— 利用可能なモデルの一覧
ネイティブエンドポイント:
POST /completion— 全パラメータを制御できる生の補完POST /tokenize— テキストのトークン化POST /detokenize— トークンの逆変換GET /health— サーバーのヘルスチェックGET /props— サーバーのプロパティ
Anthropic互換エンドポイント:
POST /v1/messages— Anthropic Messages API形式
各ルートハンドラーは共通のパターンに従っています。JSONリクエストをパースし、適切なパラメータで server_task を生成し、キューに投入して、server_response_reader で結果を待ちます。ストリーミングの場合、結果はServer-Sent Events(SSE)として送信されます。
ルートハンドラーは ex_wrapper() でラップされており、例外をキャッチして適切なHTTPエラーレスポンスに変換します——引数が不正な場合は 400、内部エラーの場合は 500 が返ります。
CLIによるサーバー内部の再利用
llama.cppの設計の中でも特に驚きをもたらすのが、tools/cli/cli.cpp に見られる次のコードです。
#include "server-context.h"
#include "server-task.h"
struct cli_context {
server_context ctx_server; // <-- the same server_context!
json messages = json::array();
// ...
};
インタラクティブなCLIは独自の推論ループを持っていません。代わりに cli_context が server_context をラップし、HTTPハンドラーと同じタスク/キューインターフェース経由でやり取りします。CLIでメッセージを入力すると、server_task が生成されてサーバーのキューに投入され、server_response_reader で結果が読み取られます。
classDiagram
class server_context {
+load_model()
+start_loop()
+get_response_reader()
}
class server_http_context {
Routes: /v1/chat/completions
Routes: /v1/completions
Creates server_tasks
}
class cli_context {
+ctx_server: server_context
+messages: json
Creates server_tasks
}
server_context <-- server_http_context : "routes to"
server_context <-- cli_context : "wraps"
この設計にはいくつかの利点があります。
-
機能の一致 — サーバーで利用できる機能(ツール呼び出し、マルチモーダル入力、文法制約、投機的デコードなど)はすべて、自動的にCLIでも使えます。
-
単一のコードパス — 推論パイプラインのバグ修正は、サーバーとCLIの両方に同時に適用されます。CLIが独自に乖離するリスクがありません。
-
テストの容易さ — CLIはHTTPスタックを必要とせず、サーバーのコアロジックを実質的にインテグレーションテストする役割を果たします。
トレードオフとして、CLIは実際には1スロットしか使わないにもかかわらず、サーバーの依存関係(タスクキュー、スロット管理など)をすべて引き継ぐことになります。
共通ユーティリティ層
common/ ディレクトリには、すべてのツールが利用する共有インフラが含まれています。
引数パース — common/arg.h の common_params_parse() は、各ツールが受け付ける数百ものCLIフラグ(モデルパス、コンテキストサイズ、GPUレイヤー数、サンプリングパラメータなど)を処理します。すべてのツールが同じ common_params 構造体を共有しています。
サンプリングラッパー — common/sampling.h は、libllama の低レベルな llama_sampler チェーンを、設定・リセット・パラメータ管理を扱う高レベルなインターフェースでラップしています。
チャットテンプレートエンジン — common/jinja/ ディレクトリには、チャットテンプレートを適用するためのJinja2互換テンプレートエンジンが含まれています。モデルのGGUFメタデータにチャットテンプレート(tokenizer.chat_template キー)が含まれている場合、このエンジンがユーザー/アシスタント/システムのメッセージをモデルが期待するフォーマットにレンダリングします。
flowchart TD
subgraph "common/ utilities"
ARG["arg.h / arg.cpp\nArgument parsing"]
SAMP["sampling.h\nSampling wrappers"]
CHAT["chat.h\nChat template application"]
JINJA["jinja/\nJinja2 template engine"]
LOG["log.h\nLogging utilities"]
end
subgraph "Tools"
CLI["tools/cli"]
SRV["tools/server"]
QNT["tools/quantize"]
BENCH["tools/bench"]
end
CLI --> ARG
CLI --> CHAT
SRV --> ARG
SRV --> SAMP
SRV --> JINJA
QNT --> ARG
BENCH --> ARG
ツールエコシステムとモデル変換
サーバーとCLI以外にも、llama.cppにはいくつかの専用ツールが含まれています。
tools/quantize/ — モデルを量子化フォーマット間で変換します(例:F16 → Q4_K_M)。GGUFファイルを読み込み、各テンソルを対象フォーマットに再量子化して、新しいGGUFファイルとして書き出します。データ依存の量子化を行うための重要度行列(--imatrix)にも対応しています。
tools/bench/ — プロンプト処理とトークン生成スループットのパフォーマンスベンチマークを行います。
tools/perplexity/ — テストコーパスに対してパープレキシティを計算し、モデルの品質を評価します。
tools/imatrix/ — キャリブレーションデータから重要度行列を計算します。量子化品質の向上に使用されます。
tools/tts/ — 音声合成モデルを使ったテキスト読み上げ(Text-to-Speech)機能です。
tools/mtmd/ — テキストに加えて画像や音声を処理するモデル向けのマルチモーダルツールです。
モデル変換 — convert_hf_to_gguf.py スクリプトは、新しいモデルをllama.cppに取り込む主要な方法です。HuggingFaceのモデルチェックポイント(PyTorchの .bin またはSafeTensorsの .safetensors)を読み込み、テンソル名をGGUFの命名規則にマッピングし、ハイパーパラメータをGGUFメタデータとして書き込み、.gguf ファイルを出力します。変換ツールが使用するPython製のGGUFリーダー/ライターは gguf-py/ ライブラリで提供されています。
ヒント: モデルを量子化する際、ほとんどのユースケースで品質とサイズのバランスが最も優れているのはQ4_K_Mです。積極的な量子化レベル(Q2_K以下)では、キャリブレーションデータセットと
--imatrixを組み合わせることで最良の結果が得られます。
次のステップ
これでHTTPリクエストからトークン出力に至るまでのフルスタックを一通り見てきました。シリーズ最終回は実践的な内容に移ります。llama.cppに新しいモデルアーキテクチャを追加する手順をステップバイステップで解説し、GGUF変換からグラフ構築、テストに至るまで、触れるべきすべてのファイルを網羅します。