llama.cppサーバでオンデマンドにモデルをロードできるプロキシサーバ「llama-swap」を試す
ここで知った。
I finally got rid of Ollama というスレなのだが、スレ主は、Ollamaに以下の不満があったらしい。
- 独自のディレクトリ構造やファイル名ルールのせいで、好きな場所にモデルを置けず、「変な場所/名前」のモデルが増えて、管理が煩雑
- モデルの切り替え(アンロード・ロード)が面倒
- 機能が過剰
でこれを以下のようにして、シンプルで柔軟な環境に移行できたとのこと。
- 推論エンジン: llama.cpp または ik_llama.cpp
- モデル管理: llama-swap でモデルのロード・アンロードや自動アンロード
- config.yaml に各モデルのパラメータ(think/no_think など)をまとめて設定 可能
- フロントエンド: Open WebUI
- 「ワークスペース」またはドロップダウンリストからモデルを選択すると、自動で現在のモデルをアンロードして新しいモデルをロード可能
- モデルの取得方法: Hugging Face から wget で好きな場所にダウンロード
- ディレクトリ構造や名称を気にせず、他の推論エンジンでも利用可能
この中で「llama-swap」というのを聞いたことがなかったので、少し調べてみる。
GitHubレポジトリ
llama-swap
llama-swap は、軽量かつ透明性の高いプロキシサーバーで、llama.cpp のサーバーへの自動モデル切り替え機能を提供します。
golang で実装されており、インストールは非常に簡単(依存関係なしの単一バイナリ)、設定もシンプル(単一の yaml ファイル)です。開始するには、事前構築済みバイナリをダウンロードするか、提供されている Docker イメージを利用してください。
機能:
- ✅ 簡単デプロイ:依存関係なしの単一バイナリ
- ✅ 簡単設定:単一の yaml ファイル
- ✅ オンデマンドでのモデル切り替え
- ✅ OpenAI API 対応エンドポイント:
- ✅ llama-swap 独自 API エンドポイント
- ✅
Groups
を使って複数モデルを同時実行 (#107)- ✅
ttl
を設定してタイムアウト後に自動アンロード- ✅ 任意のローカル OpenAI 互換サーバーを利用可能(llama.cpp, vllm, tabbyAPI など)
- ✅ Docker および Podman サポート
- ✅ モデルごとのサーバー設定を完全制御
llama-swap の仕組み
OpenAI 互換エンドポイントへのリクエスト時に、llama-swap は
model
の値を抽出し、適切なサーバー設定を読み込んでサービスします。もし既存の上流サーバーが異なるモデルを実行していれば、自動的に正しいサーバーへ差し替え(swap)られます。基本設定では単一モデルのみを扱いますが、
groups
機能を使えば複数モデルを同時にロードできます。システムリソースの使い方は完全にコントロール可能です。config.yaml
llama-swap の設定は意図的にシンプルです:
models: "qwen2.5": cmd: | /app/llama-server -hf bartowski/Qwen2.5-0.5B-Instruct-GGUF:Q4_K_M --port ${PORT} "smollm2": cmd: | /app/llama-server -hf bartowski/SmolLM2-135M-Instruct-GGUF:Q4_K_M --port ${PORT}
.. さらに多くの高度な機能もサポート:
groups
で複数モデルを同時実行macros
で再利用可能なスニペット定義ttl
で自動アンロード設定aliases
で馴染みのあるモデル名を使用(例: "gpt-4o-mini")env
で推論サーバーへカスタム環境変数を渡すuseModelName
で上流サーバーに送信するモデル名を上書きhealthCheckTimeout
でモデル起動待機時間を制御${PORT}
で動的ポート割り当てcmdStop
で Docker/Podman コンテナを優雅に停止詳細は 設定ドキュメント を参照してください。
同様のものには以下があった
機能的にもGithub Star的にもllama-swapのほうが無難な感はある
Ubuntu-22.04サーバ(RTX4090)で試す。
説明の簡単のため、以後の作業は/data
ディレクトリで行うものとする。適宜読み替えていただければ。
事前準備
とりあえずllama.cppをビルドしておく。
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
cmake -B build -DGGML_CUDA=ON -DLLAMA_CURL=ON
cmake --build build --config Release -j 8
これで llama-server が /data/llama.cpp/build/bin/llama-server
として作成される。
find /data/llama.cpp -type f -name "llama-server" -ls
105385566 4908 -rwxrwxr-x 1 kun432 kun432 5024072 6月 13 16:39 /data/llama.cpp/build/bin/llama-server
次にモデルのGGUFファイルを用意。モデルは /data/ggufs
に配置するものとする。今回は以下を使用。
DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf
Phi-4-mini-instruct-Q8_0.gguf
gemma-3-4b-it-Q8_0.gguf
mmproj-model-f16.gguf
すでにダウンロード済みのものがあればそれを使えば良い。あと、Gemmaはマルチモーダルなのでmmprojファイルもダウンロードした。
cd /data
mkdir ggufs && cd ggufs
wget --content-disposition https://huggingface.co/unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF/resolve/main/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf?download=true
wget --content-disposition https://huggingface.co/mmnga/Phi-4-mini-instruct-gguf/resolve/main/Phi-4-mini-instruct-Q8_0.gguf?download=true
wget --content-disposition https://huggingface.co/ggml-org/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it-Q8_0.gguf?download=true
wget https://huggingface.co/ggml-org/gemma-3-4b-it-GGUF/resolve/main/mmproj-model-f16.gguf?download=true -O gemma-3-4b-it-mmproj-f16.gguf
ls -lt
合計 13856308
-rw-rw-r-- 1 kun432 kun432 5122746880 6月 13 17:24 DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf
-rw-rw-r-- 1 kun432 kun432 4130226336 6月 13 17:21 gemma-3-4b-it-Q8_0.gguf
-rw-rw-r-- 1 kun432 kun432 851251104 6月 13 17:06 gemma-3-4b-it-mmproj-f16.gguf
-rw-rw-r-- 1 kun432 kun432 4084611392 3月 6 05:00 Phi-4-mini-instruct-Q8_0.gguf
llama-swapのインストール
インストール方法は以下の3種類
- Docker
- ビルド済みバイナリ
- ソースからビルド
今回はビルド済みバイナリを使うことにする。
まず作業ディレクトリを作成
cd /data
mkdir llama-swap && cd llama-swap
リリースのページから、実行環境のアーキテクチャに合わせたバイナリをダウンロード。
以下のアーキテクチャに対応している様子。
- Windows・amd64
- Linux・amd64/arm64
- FreeBSD・amd64
- Mac・amd64/arm64
今回はLinux・amd64で、2025/06/13時点の最新であるv125を使用。
wget https://github.com/mostlygeek/llama-swap/releases/download/v125/llama-swap_125_linux_amd64.tar.gz
tar zxvf llama-swap_125_linux_amd64.tar.gz
以下が展開された。
LICENSE.md
README.md
llama-swap
https://github.com/mostlygeek/llama-swap/wiki/Configuration を参考に、設定ファイルを作成。色々細かく設定できるようだけどまずはシンプルに。
healthCheckTimeout: 120
logLevel: info
startPort: 10001
macros:
"latest-llama": >
/data/llama.cpp/build/bin/llama-server
--port ${PORT}
models:
"deepseek-r1-qwen3-8b":
cmd: |
${latest-llama}
--model /data/ggufs/DeepSeek-R1-0528-Qwen3-8B-UD-Q4_K_XL.gguf
--ctx-size 65536
--min_p 0.01
--n-gpu-layers 99
--n-predict 16384
--repeat-penalty 1
--temp 0.6
--top-p 0.95
aliases:
- deepseek-r1
checkEndpoint: /health
ttl: 60
"phi4-mini-instruct":
cmd: |
${latest-llama}
--model /data/ggufs/Phi-4-mini-instruct-Q8_0.gguf
aliases:
- phi4-mini
ttl: 60
"gemma-3-4b-it":
cmd: |
${latest-llama}
--model /data/ggufs/gemma-3-4b-it-Q8_0.gguf
--mmproj /data/ggufs/gemma-3-4b-it-mmproj-f16.gguf
aliases:
- gemma-3
ttl: 60
llama-swapを起動
./llama-swap --config config.yaml
デフォルトは8080番ポートで起動する
llama-swap listening on :8080
ではリクエストを送ってみる。まずDeepSeek-R1-0528-Qwen3-8B。
curl -s http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer no-key" \
-d '{
"model":"deepseek-r1-qwen3-8b",
"messages": [
{
"role": "user",
"content": "こんにちは!"
}
]
}' | \
jq -r '.choices[0].message.content'
<think>
(今、私は優雅で知的な日本の女性の教師です。私は和やかでありながらもプロフェッショナルな態度で、学生と接しています。今回の対話は日本語で行われる可能性が高いです。ユーザーが私に「こんにちは」と挨拶しているので、私は礼儀正しく、熱意を込めて応答し、教師としての親しみやすさを示すべきです。)
(私は学生の受動的側面に注目するのではなく、話題を私の専門分野である日本語や日本文化に導くべきです。結局のところ、私の役割は教育を提供することであり、単なる挨拶ではありません。したがって、まず相手の「こんにちは」と返事をしつつ、話題を自然に転換し、私の専門分野を示す必要があります。)
(うん、こうすることで私は教師の役割を明確にしつつ、対話を活性化させることができます。よし、では今からこうやって学生との対話を始めましょう。)
</think>
こんにちは!私はあなたの日本語学習を助けるためにここにいます。あなたの日本語レベルはどの段階ですか?また、何か特定の話題について話し合いたいことはありますか?
ログを見ているとリクエストがあればllama-serverを起動してモデルをロード、今回はTTLを設定しているのでリクエストがなくなったらモデルをアンロードしているのがわかる。
(snip)
main: server is listening on http://127.0.0.1:10001 - starting the main loop
srv update_slots: all slots are idle
srv log_server_r: request: GET /health 127.0.0.1 200
[INFO] <deepseek-r1-qwen3-8b> Health check passed on http://localhost:10001/health
srv params_from_: Chat format: Content-only
slot launch_slot_: id 0 | task 0 | processing task
slot update_slots: id 0 | task 0 | new prompt, n_ctx_slot = 65536, n_keep = 0, n_prompt_tokens = 4
slot update_slots: id 0 | task 0 | kv cache rm [0, end)
slot update_slots: id 0 | task 0 | prompt processing progress, n_past = 4, n_tokens = 4, progress = 1.000000
slot update_slots: id 0 | task 0 | prompt done, n_past = 4, n_tokens = 4
slot release: id 0 | task 0 | stop processing: n_past = 302, truncated = 0
slot print_timing: id 0 | task 0 |
prompt eval time = 11.88 ms / 4 tokens ( 2.97 ms per token, 336.73 tokens per second)
eval time = 2341.62 ms / 299 tokens ( 7.83 ms per token, 127.69 tokens per second)
total time = 2353.50 ms / 303 tokens
srv update_slots: all slots are idle
srv log_server_r: request: POST /v1/chat/completions 127.0.0.1 200
[INFO] Request 127.0.0.1 "POST /v1/chat/completions HTTP/1.1" 200 1984 "curl/7.81.0" 7.616123293s
(snip)
[INFO] Request 127.0.0.1 "POST /v1/chat/completions HTTP/1.1" 200 1984 "curl/7.81.0" 7.616123293s
[INFO] <deepseek-r1-qwen3-8b> Unloading model, TTL of 60s reached
srv operator(): operator(): cleaning up before exit...
では他のモデルも。
curl -s http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer no-key" \
-d '{
"model":"phi4-mini",
"messages": [
{
"role": "system",
"content": "あなたは親切な日本語のアシスタントです。"
},
{
"role": "user",
"content": "こんにちは!"
}
]
}' | \
jq -r '.choices[0].message.content'
こんにちは!お元気ですか?今日はどのようにお手伝いできますか?
Gemma3
curl -s http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer no-key" \
-d '{
"model": "gemma-3",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "この画像について詳しく説明して。"
},
{
"type": "image_url",
"image_url": {
"url": "https://storage.googleapis.com/zenn-user-upload/82968d23b6c5-20250228.jpg"
}
}
]
}
]
}' | \
jq -r '.choices[0].message.content'
この画像は、日本、岡山県の岡山港の風景を捉えています。以下に詳細な説明をします。
**全体的な印象:**
* **晴天:** 青空の下、穏やかな海面が広がっており、晴れた日中の風景です。
* **水景:** 波紋が刻まれた海面が、奥行きと動きを出しています。
**主なランドマーク:**
* **岡山港:** 左右に伸びる桟橋、船、埠頭などが、港の賑わいを伝えています。
* **岡山タワー:** 鮮やかな赤色のタワーが、写真の中心に位置し、岡山を象徴する建造物です。その独特なデザインは、多くの観光客を魅了します。
* **岡山駅ビル(岡山ステーションビル):** タワーの左側に位置する、現代的なデザインの建物です。
* **その他高層ビル:** タワーの右側には、いくつかの高層ビルが並んでいます。
**背景:**
* **山:** 遠くに緑豊かな山々が見え、岡山港の背中を支えています。
* **船:** 桟橋に停泊しているクルーズ船や客船が見えます。
**その他:**
* **水面反射:** 海面の波紋が、建物のシルエットを反射し、幻想的な雰囲気を醸し出しています。
**総評:**
この画像は、岡山港の美しい景観と、岡山タワーという象徴的な建造物を組み合わせた、魅力的な風景写真です。岡山を訪れる人々にとって、大切な観光名所となっていることがわかります。
もし、特定の要素についてより詳しく知りたい場合は、お気軽にお尋ねください。
設定ファイルについては以下に記載がある。
groupsを使うと非常に細かく制御できて良さそう。
以下はo4-miniによる日本語訳
# llama-swap YAML 設定例 # ------------------------------------- # # - 以下は llama-swap の利用可能な全設定オプションです。 # - デフォルト値がある設定、またはオプションと記載されているものは省略可能です。 # - 必須と記載されている設定は必ず設定ファイルに含めてください。 # healthCheckTimeout: モデルがリクエストに応答可能になるまで待機する秒数 # - オプション、デフォルト: 120 # - 最小値は 15 秒。これ未満は 15 秒に設定されます healthCheckTimeout: 500 # logLevel: ログ出力レベル # - オプション、デフォルト: info # - 有効なレベル: debug, info, warn, error logLevel: info # startPort: `${PORT}` マクロの開始ポート番号 # - オプション、デフォルト: 5800 # - `${PORT}` マクロは model.cmd や model.proxy 設定で使用可能 # - 使用ごとに自動的にインクリメントされます startPort: 10001 # macros: 文字列:文字列 の辞書 # - オプション、デフォルト: 空辞書 # - 再利用可能なスニペット # - model.cmd, cmdStop, proxy, checkEndpoint 内で使用 # - 共通設定を簡潔に macros: "latest-llama": > /path/to/llama-server/llama-server-ec9e0301 --port ${PORT} # models: モデル設定の辞書 # - 必須 # - 各キーは API リクエストで使用するモデル ID # - 設定未定義時はデフォルト値を使用 # - 利用可能な設定例: env, cmd, cmdStop, proxy, aliases, checkEndpoint, ttl, unlisted models: "llama": # cmd: 推論サーバー起動コマンド # - 必須 # - CLI で実行する文字列と同様 # - `|` でコメント付きコマンドが可能、解析時に除外 # - マクロ使用可 cmd: | # ${latest-llama} は上記マクロ ${latest-llama} --model path/to/Qwen2.5-1.5B-Instruct-Q4_K_M.gguf # env: cmd 実行時に注入する環境変数の配列 # - オプション、デフォルト: 空配列 # - 各要素は文字列: ENV_NAME=value env: - "CUDA_VISIBLE_DEVICES=0,1,2" # proxy: llama-swap が API リクエストをルーティングする URL # - オプション、デフォルト: http://localhost:${PORT} # - cmd で `${PORT}` 使用時は省略可 # - カスタムポート使用時は必須 proxy: http://127.0.0.1:8999 # aliases: この設定を適用する別名モデル # - オプション、デフォルト: 空配列 # - グローバルに一意 # - 特定モデルを偽装する際に有用 aliases: - "gpt-4o-mini" - "gpt-3.5-turbo" # checkEndpoint: サーバー準備確認用 URL パス # - オプション、デフォルト: /health # - "none" でチェックをスキップ # - HTTP 200 を期待 # - 全リクエストは準備完了まで待機 checkEndpoint: /custom-endpoint # ttl: 自動アンロードまでの秒数 # - オプション、デフォルト: 0 # - 0 で自動アンロード無効 ttl: 60 # useModelName: 上流サーバーへ送信するモデル名を上書き # - オプション、デフォルト: "" # - 特定形式を要求される場合に有用 useModelName: "qwen:qwq" # 非公開モデル例: "qwen-unlisted": # unlisted: true/false # - オプション、デフォルト: false # - /v1/models や /upstream に非表示 # - API からは通常リクエスト可能 unlisted: true cmd: llama-server --port ${PORT} -m Llama-3.2-1B-Instruct-Q4_K_M.gguf -ngl 0 # Docker 実行例: # Docker/Podman は cmd と cmdStop の組み合わせで利用可 "docker-llama": proxy: "http://127.0.0.1:${PORT}" cmd: | docker run --name dockertest --init --rm -p ${PORT}:8080 -v /mnt/nvme/models:/models ghcr.io/ggml-org/llama.cpp:server --model '/models/Qwen2.5-Coder-0.5B-Instruct-Q4_K_M.gguf' # cmdStop: モデルを優雅に停止するコマンド # - オプション、デフォルト: "" # - 他システム管理の停止に有用 # - POSIX: SIGTERM、Windows: taskkill # - 停止猶予 5 秒後に強制終了 # - 上流プロセス ID は ${PID} マクロで利用可 cmdStop: docker stop dockertest # groups: グループ設定の辞書 # - オプション、デフォルト: 空辞書 # - モデル切り替え挙動の高度制御 # - 一部モデルを常駐し、他を切り替え # - モデル ID は models セクションで定義済みであること # - モデルは一つのグループにのみ所属可 # - 挙動は swap, exclusive, persistent で制御 # - 詳細は issue #109 groups: # group1: デフォルト挙動 (インスタンス全体で同時実行モデルを1つに制限) "group1": # swap: グループ内モデル切り替え制御 # - オプション、デフォルト: true # - true: 同時に1モデル実行 # - false: 全モデル同時実行、切り替えなし swap: true # exclusive: 他グループへの影響 # - オプション、デフォルト: true # - true: このグループ実行時に他グループをアンロード # - false: 影響なし exclusive: true # members: モデル参照 (必須) members: - "llama" - "qwen-unlisted" # 例: # - このグループ内は全モデル同時実行 # - 他グループ起動時にアンロード "group2": swap: false exclusive: false members: - "docker-llama" - "modelA" - "modelB" # 例: # - 永続グループ: 他グループからのアンロードを防止 "forever": # persistent: 他グループからのアンロード防止 # - オプション、デフォルト: false # - 個別モデル挙動には影響なし persistent: true # グループ内切り替え・他グループアンロード防止 swap: false exclusive: false members: - "forever-modelA" - "forever-modelB" - "forever-modelc"
簡易なGUIもついている。llama-swapが起動しているポート(デフォルトは8080)にブラウザでアクセスするとこんな感じ。
"view logs"で、llama-swapとアップストリーム(今回の場合はllama.cppのサーバモード)のログが見れる。なお、ログはcurlでも取得可能。
"configured models"で、設定されているモデルの一覧が表示され、モデルのアンロードが可能。
あとモデル名をクリックすると、モデルがロードされ、llama-serverのGUIが表示された。
あぁ、手元で簡単に試すならこれで十分だなぁ・・・
まとめ
Ollamaは便利なんだけど、確かにGGUFの管理は複雑すぎるという気はするし(Ollamaのインタフェースだけ見るなら気にしなくてもよいのだけど)、あとモデルのアンロードするためにOllama自体の再起動とか確かにやってたな・・・
llama-swapならそのあたりを自分で細かく管理できそう。アップストリームはllama.cppじゃなくてもOpenAI互換サーバなら動くみたい(ただしllama.cppに最適化されていて、それ以外ならDockerが推奨とある)なので、llama-swapでまとめて管理みたいなのもできそうだね。