0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

vLLM + LiteLLMをWSL/Docker Composeで動かし、Qwen Codeから接続するまで

0
Posted at

はじめに

ローカルLLMをQwen Codeから使う環境として、最初はLM Studioを使っていました。GUIでモデルを読み込み、OpenAI互換APIのURLを指定するだけで接続できます。個人利用なら、この構成はかなり扱いやすいです。

一方で、次のような要件が出てくると、推論サーバーとAPIゲートウェイを分けたくなります。

  • vLLMの推論設定を細かく変更したい
  • APIキーをLiteLLMに集約したい
  • 将来、複数モデルや複数バックエンドを扱いたい
  • クライアントから推論サーバーを直接見せたくない
  • PostgreSQLを使った認証・利用管理へ拡張したい

そこで、LLM用PCのWSL2上でvLLMを動かし、その前段にLiteLLMを置く構成を再検証しました。クライアントPCのQwen Codeから、LAN経由でLiteLLMへ接続するところまで確認します。

ただし、先に結論を書くと、構築はLM Studioよりかなり難しくなります。動けば便利ですが、動くまでに確認する層が多いからです。

構成

今回の構成は次のとおりです。

クライアントPC
└─ Qwen Code
      │ http://<LLM用PCのWindows IP>:4000/v1
      ▼
LiteLLM :4000
      │ Docker内部ネットワーク
      ▼
vLLM :8000
      ▼
Qwen/Qwen3.5-9B

PostgreSQLはLiteLLMの認証・管理用DBとして同じComposeプロジェクトに配置します。

端末・サービス 役割
LLM用PCのWSL2 Docker Compose、vLLM、LiteLLM、PostgreSQLを実行
vLLM モデル推論とOpenAI互換API
LiteLLM APIゲートウェイ、認証、モデル名の抽象化
PostgreSQL LiteLLMのDB
クライアントPC Qwen Codeからのクライアント接続

検証時の主なバージョンは以下です。

  • vLLM: 0.24.0
  • vLLMイメージ: vllm/vllm-openai:v0.24.0
  • モデル: Qwen/Qwen3.5-9B
  • 最大Context Length: 49152
  • 最大同時系列数: 64
  • LiteLLM: docker.litellm.ai/berriai/litellm:main-latest
  • GPU: RTX 5090

この記事のコマンドは、秘密情報を直接書かない形にしています。APIキーやDBパスワードは環境変数で渡し、ログ・Git・チャットへ貼り付けないようにします。

先に結論:いきなりCompose化しない

最初からvLLM、LiteLLM、PostgreSQLを同時に起動すると、問題が起きたときに原因が分かりません。

今回は次の順で進めました。

  1. Docker DesktopとWSL2からGPUが見えることを確認
  2. ネイティブ環境でvLLM単体を起動
  3. vLLMのAPIを確認
  4. vLLMをDocker Composeへ移行
  5. Compose内のvLLM APIを確認
  6. LiteLLMとPostgreSQLを追加
  7. LiteLLM経由のAPIを確認
  8. LAN上の別PCから接続
  9. Qwen Codeから接続

この順番にしたことで、vLLM本体の問題と、Docker・LiteLLM・ネットワーク・Qwen Codeの問題を分離できました。

1. Docker DesktopとGPUを確認する

LLM用PCのWSLで確認します。

cat /etc/os-release
docker --version
docker compose version
docker context show
docker info | grep -E 'Server Version|Operating System|Security Options'
nvidia-smi

Docker DesktopのWSL2バックエンドを使う場合、WSL内にnvidia-ctkがインストールされていなくても、コンテナからGPUが見えれば問題ありません。

念のため、CUDAサンプルも実行しました。

docker run --rm --gpus=all \
  nvcr.io/nvidia/k8s/cuda-sample:nbody \
  nbody -gpu -benchmark

ここでGPU名とベンチマーク結果が表示されない状態で、vLLMの切り分けを始めないほうが安全です。

2. モデルキャッシュを確認する

今回はネットワークから毎回モデルを取得するのではなく、ホスト側にあるHugging Faceのsnapshotをコンテナから参照しました。

find "$HOME/.cache/huggingface/hub/models--Qwen--Qwen3.5-9B" \
  -maxdepth 3 -name config.json -print

snapshots/<revision>/config.jsonが見つかることを確認します。

Docker起動時に次のようなエラーが出た場合、モデルそのものが壊れているとは限りません。

Temporary failure in name resolution

ローカルキャッシュを使う構成なのに、コンテナがHugging Faceへ接続しようとしている可能性があります。snapshotのマウント先と、オフライン起動用の環境変数を確認します。

3. vLLMをネイティブ環境で起動する

まずuvで専用の仮想環境を作成します。

uv venv /home/<user>/.venv-vllm-cu128 --python 3.12
source /home/<user>/.venv-vllm-cu128/bin/activate

検証時はvLLM 0.24.0を使用しました。CUDAバックエンドやwheelの組み合わせは、使用するGPUとvLLMの公式配布状況に合わせて固定します。

export UV_HTTP_TIMEOUT=600
uv pip install \
  vllm==0.24.0 \
  --torch-backend=cu129 \
  --extra-index-url https://wheels.vllm.ai/0.24.0/cu129/ \
  --extra-index-url https://download.pytorch.org/whl/cu129 \
  --index-strategy unsafe-best-match

インストール後にバージョンとGPU認識を確認します。

vllm --version
python -c 'import torch, vllm; print(torch.__version__); print(torch.version.cuda); print(torch.cuda.is_available()); print(vllm.__version__)'

vLLMはLANへ直接公開せず、まず127.0.0.1:8000だけで起動します。

unset CUDA_HOME
unset VLLM_VERSION
export VLLM_USE_FLASHINFER_SAMPLER=0

vllm serve Qwen/Qwen3.5-9B \
  --host 127.0.0.1 \
  --port 8000 \
  --tensor-parallel-size 1 \
  --max-model-len 49152 \
  --max-num-seqs 64 \
  --reasoning-parser qwen3 \
  --enable-auto-tool-choice \
  --tool-call-parser qwen3_coder \
  --api-key "$VLLM_API_KEY" \
  > "$HOME/tmpvllm-serve.log" 2>&1 &
echo $! > "$HOME/vllm-native.pid"

起動ログと待受状態を確認します。

tail -n 100 "$HOME/tmpvllm-serve.log"
ss -lntp | grep ':8000'

APIキーを環境変数から渡して、モデル一覧を確認します。

curl -i http://127.0.0.1:8000/v1/models \
  -H "Authorization: Bearer $VLLM_API_KEY"

4. 思考出力によるfinish_reason=length

初回の通常応答試験では、応答本文が空になりました。HTTPエラーではありません。

原因は、Qwen3.5の思考出力を含む要求に対して、max_tokens=32では生成上限が足りなかったことでした。最終回答へ到達する前に上限へ達し、finish_reason=lengthになっていました。

検証スクリプトでは、思考出力を抑制して再試験しました。

{
  "chat_template_kwargs": {
    "enable_thinking": false
  },
  "max_tokens": 64
}

その結果、最終回答あり、finish_reason=stopを確認できました。

この経験から、HTTPステータスだけでなく、次の値も確認するようにしました。

  • finish_reason
  • 最終回答の有無
  • ストリーミングの[DONE]
  • Tool callの関数名と引数

5. API検証をスクリプト化する

長いPythonコードをターミナルへ貼り付けると、引用符や改行の崩れで別の問題が起きます。そこで、リポジトリの検証スクリプトを使いました。

VLLM_BASE_URL=http://127.0.0.1:8000/v1 \
VLLM_MODEL=Qwen/Qwen3.5-9B \
VLLM_API_KEY="$VLLM_API_KEY" \
python3 vllm_compose_api_check.py

期待する確認項目は次の4つです。

models: HTTP 200
chat: HTTP 200, finish_reason=stop, content_present=True
stream: HTTP 200, chunks=True, done=True, finish_reason=stop
tool: HTTP 200, tool_calls=True, function=get_weather, finish_reason=stop

vLLM単体でここまで通ったら、ネイティブvLLMを停止します。

kill "$(cat "$HOME/vllm-native.pid")" 2>/dev/null || true
rm -f "$HOME/vllm-native.pid"
ss -lntp | grep ':8000' || true

6. Docker Composeへ移行する

Composeでは、vLLMにホスト側のportsを設定しません。外部からアクセスする入口はLiteLLMだけにします。

概念的には次のようなネットワーク構成です。

services:
  vllm:
    networks:
      - llm-internal

  litellm:
    ports:
      - "0.0.0.0:4000:4000"
    networks:
      - llm-internal
      - llm-public

  postgres:
    networks:
      - llm-internal

まず設定ファイルを検証し、vLLMだけを起動します。

docker compose config --quiet
docker pull vllm/vllm-openai:v0.24.0
docker compose up -d vllm
docker compose ps -a
docker compose logs --tail=100 vllm

Compose内のvLLMに検証スクリプトをコピーして実行します。

docker compose cp vllm_compose_api_check.py vllm:/tmp/vllm_compose_api_check.py
docker compose exec -T vllm python3 /tmp/vllm_compose_api_check.py

7. 苦戦した点その1:KV CacheのGPUメモリ不足

ComposeでvLLMを起動したとき、次のエラーが発生しました。

ValueError: No available memory for the cache blocks

モデルが壊れたのかと思いましたが、原因はKV Cache用のGPUメモリ不足でした。ネイティブvLLMを停止したつもりでも、プロセスやDocker Desktop/WSL側にGPUメモリが残っていると、ComposeのvLLMが起動できません。

確認したことは次のとおりです。

nvidia-smi
docker compose logs --tail=100 vllm

通常のdocker compose downでは解放されない状態が残った場合、Windows側でWSLを停止してから再起動しました。

wsl --shutdown

それでも起動しない場合は、max-num-seqs8または16へ下げ、max-model-len32768へ下げて起動を確認します。

なお、通常の停止でdocker compose down -vを使うのは避けます。名前付きボリュームまで削除され、LiteLLMのDBやモデルキャッシュの再構築が必要になるためです。

8. LiteLLMとPostgreSQLを追加する

vLLM単体のAPI確認後に、PostgreSQLとLiteLLMを起動します。

docker compose up -d postgres litellm
docker compose config --services
docker compose ps -a
docker compose logs --tail=100 postgres litellm

LiteLLMの設定では、次の関係になります。

項目 設定
外部から見えるモデル名 qwen-vllm
LiteLLMからvLLMへのURL http://vllm:8000/v1
vLLMの認証キー VLLM_API_KEY
LiteLLMの認証キー LITELLM_MASTER_KEY
DB DATABASE_URL

ここで、vLLM用のキーとLiteLLM用のキーを混同しないことが重要です。クライアントPCへ渡すのはLITELLM_MASTER_KEYです。

9. 苦戦した点その2:LiteLLMのポートが公開されない

LiteLLMにportsを設定したのに、ホスト側から接続できませんでした。

docker port vllm-litellm-compose-litellm-1 4000
docker compose ps

4000/tcpしか表示されず、0.0.0.0:4000になっていませんでした。

原因は、LiteLLMをinternal: trueのネットワークだけへ接続していたことでした。Docker Desktop環境では、内部ネットワークだけに接続したコンテナのホストポート公開が期待どおりにならない場合があります。

LiteLLMを内部ネットワークと公開用ネットワークの両方へ接続し、コンテナを再作成しました。

docker compose up -d --force-recreate litellm
docker port vllm-litellm-compose-litellm-1 4000

最終的に0.0.0.0:4000を確認できました。

10. LiteLLM経由のAPIを確認する

モデル一覧を確認します。

curl -sS -w '\nHTTP %{http_code}\n' \
  http://127.0.0.1:4000/v1/models \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

レスポンスにqwen-vllmが含まれ、HTTP 200になれば、LiteLLMからvLLMへのモデル接続は確認できています。

同じ検証スクリプトをLiteLLM向けに実行します。

VLLM_BASE_URL=http://127.0.0.1:4000/v1 \
VLLM_MODEL=qwen-vllm \
VLLM_API_KEY="$LITELLM_MASTER_KEY" \
python3 vllm_compose_api_check.py

通常応答、ストリーミング、Tool callのすべてでHTTP 200を確認しました。Tool callでfinish_reason=tool_callsになるのは正常です。

11. 苦戦した点その3:DB未設定では認証テストが成立しない

最初はPostgreSQLを用意せずにLiteLLMを起動していました。その状態で誤ったAPIキーを送ると、期待していたHTTP 401ではなく、次のようなHTTP 400が返りました。

no_db_connection

これは、APIキーが正しいかどうかを判定する前に、LiteLLMのDB接続に失敗していたためでした。

PostgreSQLをComposeへ追加し、LiteLLMのマイグレーション完了後に再試験しました。

curl -sS -w '\nHTTP %{http_code}\n' \
  http://127.0.0.1:4000/v1/models \
  -H 'Authorization: Bearer invalid-test-token'

今度はHTTP 401 Unauthorizedとなり、認証拒否を確認できました。

エラーコードだけを見ると、認証の問題に見えてしまいます。LiteLLMでは、DBのhealth、DATABASE_URL、起動ログも一緒に確認する必要があります。

12. LAN上の別PCから接続する

WSL内の127.0.0.1は、クライアントPCからは使えません。クライアントからは、LLM用PCのWindows側LAN IPv4アドレスを指定します。

LLM用PCのWindows PowerShellでIPアドレスを確認します。

ipconfig

クライアントPCからポートを確認します。

Test-NetConnection <LLMPCIP> -Port 4000

次の表示になれば、ネットワークとポート公開は確認できています。

TcpTestSucceeded : True

PowerShellでは、curlが別名として解釈されることがあるため、HTTP確認ではcurl.exeを使います。

curl.exe -sS -w "\nHTTP %{http_code}\n" `
  http://<LLMPCIP>:4000/v1/models `
  -H "Authorization: Bearer <LITELLM_MASTER_KEY>"

13. Qwen CodeからLiteLLMへ接続する

検証時のQwen Codeは0.21.6でした。このバージョンでは、次のCLIオプションは使用しませんでした。

  • --auth-type
  • --openai-api-key
  • --openai-base-url

これらを指定してInvalid Valuesになる場合、オプションの使い方ではなく、インストール済みバージョンが対応していない可能性があります。

Qwen Codeを起動します。

qwen

Qwen Code内で/authを実行し、以下を設定します。

  1. Custom Provider
  2. プロトコル: OpenAI
  3. Base URL: http://<LLM用PCのIP>:4000/v1
  4. API key: LiteLLMのLITELLM_MASTER_KEY
  5. Model: qwen-vllm

その後、/modelqwen-vllmを選択し、/doctorまたは短い質問で応答を確認します。

LM Studioの古いprovider設定が残っている場合、Qwen CodeがLM Studioへ接続し続けることがあります。その場合は、/authでOpenAI providerを選び直し、/modelでモデルを選択し直します。

14. 苦戦した点その4:ContextWindowExceededError

Qwen Codeから接続できた後、長い入力でContextWindowExceededErrorが発生しました。

vLLM全体の最大Context Lengthを49152にしていても、クライアントが出力用に32768トークンを予約すると、入力に使える領域は約16384トークンしか残りません。

Qwen Code側の設定では、generationConfigをトップレベルへ置くのではなく、modelProviders.openai[]内のid: qwen-vllmへ配置しました。

{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen-vllm",
        "baseUrl": "http://<LLM用PCのIP>:4000/v1",
        "envKey": "OPENAI_API_KEY",
        "generationConfig": {
          "contextWindowSize": 49152,
          "samplingParams": {
            "max_tokens": 8192
          }
        }
      }
    ]
  }
}

設定ファイルの場所は、次のいずれかです。

  • %USERPROFILE%\\.qwen\\settings.json
  • プロジェクト内の.qwen\\settings.json

既存ファイルを全体上書きせず、既存のqwen-vllmエントリへ追加します。保存後、Qwen Codeを完全に終了して再起動します。

LiteLLMのログにrequested 32768 output tokensが残る場合、設定が反映されていません。max_tokens4096へ下げると入力領域は増えますが、長いコード生成では出力が途中で切れる可能性があります。

検証結果

最終的に、次の項目を確認できました。

試験 結果
Docker DesktopからGPU認識 合格
vLLM単体のモデルAPI HTTP 200
vLLM単体の通常応答 HTTP 200、finish_reason=stop
vLLM単体のストリーミング チャンク受信、[DONE]まで完了
vLLM単体のTool call tool_callsを確認
Compose上のvLLM 起動・API確認ともに合格
LiteLLMのモデルAPI qwen-vllmをHTTP 200で確認
LiteLLM経由の通常応答 合格
LiteLLM経由のストリーミング 合格
LiteLLM経由のTool call 合格
不正LiteLLMキー HTTP 401
クライアントPCからの接続 合格
Qwen Codeからの接続 合格

LM Studioと比べてどうだったか

機能面では、LM StudioとvLLM + LiteLLMの両方で、通常応答、ストリーミング、Tool call、Qwen Code接続を確認できました。

ただし、同じ条件で性能を測ったわけではありません。量子化方式、Context Length、出力上限、思考モード、同時実行設定が揃っていないため、速度やGPUメモリ使用量の優劣は今回の検証からは断定していません。

個人利用の現在の構成は、次の理由でLM Studioを採用しました。

  • 導入が簡単
  • GUIでモデルを切り替えやすい
  • 障害の切り分けが容易
  • ロールバックしやすい

一方、vLLM + LiteLLMは、将来の共有基盤候補として残します。

  • APIキーをLiteLLMへ集約できる
  • 複数モデル・複数バックエンドへ拡張しやすい
  • vLLMのContext Lengthやparserを細かく制御できる
  • 利用者管理や監査へ発展させやすい

便利さと引き換えに、WSL、Docker、GPU、ネットワーク、DB、認証をまとめて運用する必要があります。今回一番時間がかかったのは、モデルを動かすことよりも、その周辺の状態を一つずつ揃えることでした。

まとめ

vLLM + LiteLLMを使うと、vLLMをLANへ直接公開せず、LiteLLMを唯一のAPI入口にできます。認証や複数バックエンドを考えるなら、LM Studioより拡張しやすい構成です。

ただし、切り分けの順番を間違えると、GPUメモリ不足、モデルキャッシュ、Dockerネットワーク、DB未設定、クライアントの出力予約が一つの問題に見えてしまいます。

今回のポイントは、次の4つです。

  1. vLLM単体を先に合格させる
  2. APIはHTTPステータスだけでなく生成終了理由まで確認する
  3. vLLMをLANへ公開せず、LiteLLMだけを公開する
  4. Qwen Codeのバージョンに合わせて/authとprovider設定を使う

個人利用ならLM Studioのままでも十分です。構成管理や認証を重視する段階になったら、今回のようにvLLM + LiteLLMへ段階的に移行するのが現実的だと思います。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?