Closed7

llama.cppサーバでオンデマンドにモデルをロードできるプロキシサーバ「llama-swap」を試す

kun432kun432

ここで知った。

https://www.reddit.com/r/LocalLLaMA/comments/1l8pem0/i_finally_got_rid_of_ollama/

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」というのを聞いたことがなかったので、少し調べてみる。

kun432kun432

GitHubレポジトリ

https://github.com/mostlygeek/llama-swap

llama-swap

llama-swap は、軽量かつ透明性の高いプロキシサーバーで、llama.cpp のサーバーへの自動モデル切り替え機能を提供します。

golang で実装されており、インストールは非常に簡単(依存関係なしの単一バイナリ)、設定もシンプル(単一の yaml ファイル)です。開始するには、事前構築済みバイナリをダウンロードするか、提供されている Docker イメージを利用してください。

機能:

  • ✅ 簡単デプロイ:依存関係なしの単一バイナリ
  • ✅ 簡単設定:単一の yaml ファイル
  • ✅ オンデマンドでのモデル切り替え
  • ✅ OpenAI API 対応エンドポイント:
    • v1/completions
    • v1/chat/completions
    • v1/embeddings
    • v1/rerank
    • v1/audio/speech (#36)
    • v1/audio/transcriptions (docs)
  • ✅ llama-swap 独自 API エンドポイント
    • /log - リモートログ監視
    • /upstream/:model_id - 上流 HTTP サーバーへの直接アクセス (デモ)
    • /unload - 実行中モデルの手動アンロード (#58)
    • /running - 現在実行中モデルの一覧 (#61)
  • 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 コンテナを優雅に停止

詳細は 設定ドキュメント を参照してください。

kun432kun432

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
https://huggingface.co/unsloth/DeepSeek-R1-0528-GGUF

Phi-4-mini-instruct-Q8_0.gguf
https://huggingface.co/mmnga/Phi-4-mini-instruct-gguf

gemma-3-4b-it-Q8_0.gguf
mmproj-model-f16.gguf
https://huggingface.co/ggml-org/gemma-3-4b-it-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

リリースのページから、実行環境のアーキテクチャに合わせたバイナリをダウンロード。

https://github.com/mostlygeek/llama-swap/releases

以下のアーキテクチャに対応している様子。

  • 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 を参考に、設定ファイルを作成。色々細かく設定できるようだけどまずはシンプルに。

config.yaml
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'
出力
この画像は、日本、岡山県の岡山港の風景を捉えています。以下に詳細な説明をします。

**全体的な印象:**

*   **晴天:** 青空の下、穏やかな海面が広がっており、晴れた日中の風景です。
*   **水景:** 波紋が刻まれた海面が、奥行きと動きを出しています。

**主なランドマーク:**

*   **岡山港:** 左右に伸びる桟橋、船、埠頭などが、港の賑わいを伝えています。
*   **岡山タワー:** 鮮やかな赤色のタワーが、写真の中心に位置し、岡山を象徴する建造物です。その独特なデザインは、多くの観光客を魅了します。
*   **岡山駅ビル(岡山ステーションビル):** タワーの左側に位置する、現代的なデザインの建物です。
*   **その他高層ビル:** タワーの右側には、いくつかの高層ビルが並んでいます。

**背景:**

*   **山:** 遠くに緑豊かな山々が見え、岡山港の背中を支えています。
*   **船:** 桟橋に停泊しているクルーズ船や客船が見えます。

**その他:**

*   **水面反射:** 海面の波紋が、建物のシルエットを反射し、幻想的な雰囲気を醸し出しています。

**総評:**

この画像は、岡山港の美しい景観と、岡山タワーという象徴的な建造物を組み合わせた、魅力的な風景写真です。岡山を訪れる人々にとって、大切な観光名所となっていることがわかります。

もし、特定の要素についてより詳しく知りたい場合は、お気軽にお尋ねください。
kun432kun432

設定ファイルについては以下に記載がある。

https://github.com/mostlygeek/llama-swap/wiki/Configuration

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"
kun432kun432

簡易なGUIもついている。llama-swapが起動しているポート(デフォルトは8080)にブラウザでアクセスするとこんな感じ。

"view logs"で、llama-swapとアップストリーム(今回の場合はllama.cppのサーバモード)のログが見れる。なお、ログはcurlでも取得可能。

"configured models"で、設定されているモデルの一覧が表示され、モデルのアンロードが可能。

あとモデル名をクリックすると、モデルがロードされ、llama-serverのGUIが表示された。

あぁ、手元で簡単に試すならこれで十分だなぁ・・・

kun432kun432

まとめ

Ollamaは便利なんだけど、確かにGGUFの管理は複雑すぎるという気はするし(Ollamaのインタフェースだけ見るなら気にしなくてもよいのだけど)、あとモデルのアンロードするためにOllama自体の再起動とか確かにやってたな・・・

llama-swapならそのあたりを自分で細かく管理できそう。アップストリームはllama.cppじゃなくてもOpenAI互換サーバなら動くみたい(ただしllama.cppに最適化されていて、それ以外ならDockerが推奨とある)なので、llama-swapでまとめて管理みたいなのもできそうだね。

このスクラップは3ヶ月前にクローズされました