Files
cpp-httplib/docs-src/pages/ja/llm-app/ch07-code-reading.md
yhirose c95d799180 Polish the wording of the updated docs pages
Reword the Japanese sentences so they read naturally, and correct the
size of the llama.cpp server sources in the code reading chapter.
2026-10-08 20:39:13 -04:00

8.2 KiB
Raw Blame History

title, order
title order
7. llama.cpp本家のサーバー実装をコードリーディング 7

6章かけてゼロから翻訳デスクトップアプリを作りました。動くものは完成しましたが、あくまで「学習用」の実装です。では「プロダクション品質」のコードはどう違うのか? llama.cppに同梱されている公式サーバーllama-serverのソースコードを読んで、比較してみましょう。

llama-serverはllama.cpp/tools/server/にあります。同じcpp-httplibを使っているので、コードの読み方はこれまでの章と同じです。

7.1 ソースコードの場所

llama.cpp/
├── tools/server/
│   ├── server.cpp           # エントリポイント
│   ├── server-http.cpp      # HTTPサーバー(cpp-httplibを使う層)
│   ├── server-context.cpp   # 推論とスロットの管理
│   └── ...
└── vendor/cpp-httplib/
    └── httplib.h            # cpp-httplib(同梱版)

実装は役割ごとにファイルが分かれています。全体では2万行を超えますが、構造を知っていれば読むべき箇所は絞れます。

7.2 OpenAI互換API

ここまで作ってきたサーバーとllama-serverの最も大きな違いはAPIの設計です。

私たちのAPI:

POST /translate          → {"translation": "..."}
POST /translate/stream   → SSE: data: "token"

llama-serverのAPI:

POST /v1/chat/completions  → OpenAI互換のJSON
POST /v1/completions       → OpenAI互換のJSON
POST /v1/embeddings        → テキスト埋め込みベクトル

llama-serverはOpenAIのAPI仕様に合わせています。つまり、OpenAIの公式クライアントライブラリ(Pythonのopenaiパッケージなど)がそのまま動きます。

# OpenAIクライアントでllama-serverに接続する例
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="dummy")

response = client.chat.completions.create(
    model="local-model",
    messages=[{"role": "user", "content": "Hello!"}]
)

既存のツールやライブラリとの互換性を持たせるかどうかは、大きな設計判断です。私たちは翻訳専用のAPIをシンプルに設計しましたが、汎用のサーバーを作るならOpenAI互換が事実上の標準になっています。

7.3 同時リクエスト処理

私たちのサーバーはリクエストを1つずつ処理します。翻訳中に別のリクエストが来ると、前の推論が終わるまで待ちます。1人で使うデスクトップアプリなら問題ありませんが、複数人で共有するサーバーでは困ります。

llama-serverはスロットという仕組みで同時リクエストを処理します。

llama-serverのスロット管理

ポイントは、各スロットのトークンを1つずつ順番にではなく、まとめて1回のバッチで推論することです。GPUは並列処理が得意なので、2人分を同時に処理しても1人分とほとんど変わらない時間で済みます。これを「連続バッチ処理(continuous batching)」と呼びます。

私たちのサーバーではcpp-httplibのスレッドプールが各リクエストに1スレッドを割り当てますが、推論自体はllm.chat()の中でシングルスレッドです。llama-serverはこの推論部分を共有のバッチ処理ループに集約しています。

7.4 SSEフォーマットの違い

ストリーミングの仕組み自体は同じ(set_chunked_content_provider + SSE)ですが、送るデータのフォーマットが違います。

私たちの形式:

data: "去年の"
data: "春に"
data: [DONE]

llama-server(OpenAI互換):

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"去年の"}}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"春に"}}]}
data: [DONE]

私たちの形式はトークンだけを送るシンプルなものです。llama-serverはOpenAI互換のため、1つのトークンにもJSONのラッパーが付きます。冗長に見えますが、idでリクエストを識別したり、finish_reasonで停止理由を返せたりと、クライアントにとって便利な情報が含まれています。

7.5 KVキャッシュの再利用

私たちのサーバーでは、リクエストのたびにプロンプト全体をゼロから処理しています。翻訳アプリのプロンプトは短い("Translate the following text to ja..." + 入力テキスト)ので、これで問題ありません。

llama-serverは、前のリクエストと共通するプロンプトのprefixがある場合、その部分のKVキャッシュを再利用します。

KVキャッシュの再利用

長いシステムプロンプトやfew-shot例を毎回送るチャットボットでは、これだけで応答時間が大幅に短縮されます。数千トークンのシステムプロンプトを毎回処理するのと、キャッシュから一瞬で読むのとでは、体感が全く違います。

翻訳アプリではシステムプロンプトが1文だけなので効果は限定的ですが、自分のアプリに応用するときは意識したい最適化です。

7.6 構造化出力

翻訳APIはプレーンテキストを返すので、出力形式を制約する必要がありませんでした。でも、LLMにJSONで返させたい場合はどうでしょう?

プロンプト: 以下の文の感情を分析してJSONで返してください。
LLMの出力(期待): {"sentiment": "positive", "score": 0.8}
LLMの出力(現実): 感情分析の結果は以下の通りです。{"sentiment": ...

LLMは指示を無視して余計なテキストを付けることがあります。llama-serverはこの問題を**文法制約(grammar)**で解決しています。

curl http://localhost:8080/v1/chat/completions \
  -d '{
    "messages": [{"role": "user", "content": "Analyze sentiment..."}],
    "json_schema": {
      "type": "object",
      "properties": {
        "sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
        "score": {"type": "number"}
      },
      "required": ["sentiment", "score"]
    }
  }'

json_schemaを指定すると、LLMのトークン生成時に文法に合わないトークンを除外します。出力が必ず有効なJSONになるので、json::parseが失敗する心配がありません。

LLMをアプリに組み込むとき、出力を確実にパースできるかどうかは信頼性に直結します。翻訳のようなフリーテキスト出力では不要ですが、APIのレスポンスとして構造化データを返す用途では必須の機能です。

7.7 まとめ

ここまでの違いを整理します。

観点 私たちのサーバー llama-server
API設計 翻訳専用 OpenAI互換
同時リクエスト 1つずつ処理 スロット+連続バッチ
SSEフォーマット トークンのみ OpenAI互換JSON
KVキャッシュ 毎回クリア prefixを再利用
構造化出力 なし JSON Schema/文法制約
コード量 約200行 2万行以上

私たちのコードがシンプルなのは、「デスクトップアプリで1人が使う」という前提があるからです。複数人に提供するサーバーや、既存のエコシステムと連携するサーバーを作るなら、llama-serverの設計が参考になります。

逆に言えば、200行のコードでも翻訳アプリとしては十分に動きます。「必要な分だけ作る」ことの価値も、このコードリーディングから感じてもらえたら嬉しいです。

次の章へ

次の章では、ここまで作ったアプリを自分のライブラリに差し替えてカスタマイズするためのポイントをまとめます。

Next: 自分だけのアプリにカスタマイズする