【vLLM】Dockerデプロイ時のトラブルシューティングと本番環境向け最適構成ガイド

問題の概要:vLLMのDockerデプロイで遭遇する典型的なエラーと課題

vLLMは大規模言語モデル(LLM)の高速推論エンジンとして注目されていますが、Dockerコンテナを用いて本番環境にデプロイする際、いくつかの障壁に直面することがあります。特に、GPUリソースの不足、依存ライブラリのバージョン不一致、ネットワーク設定の不備、メモリ管理の問題が頻発します。具体的には、以下のようなエラーメッセージが表示され、サービスが正常に起動しないケースが多く報告されています。

# エラー例1: GPU関連のエラー
RuntimeError: No CUDA GPUs are available
# または
pynvml.nvml.NVMLError_LibraryNotFound: NVML Shared Library Not Found

# エラー例2: メモリ不足エラー
torch.cuda.OutOfMemoryError: CUDA out of memory.
# または、vLLM固有のエラー
vllm.utils.CPUOffloadingRequiredError: Not enough GPU memory to allocate ...

# エラー例3: ポート競合や接続エラー
Address already in use
Connection refused to OpenAI-compatible API server

これらのエラーは、開発環境では問題なく動作していたvLLMが、Dockerを介した本番環境デプロイの段階で顕在化するため、開発者を悩ませます。本記事では、これらの根本原因を解説し、ステップバイステップの解決方法と、安定した本番環境を構築するためのベストプラクティスを提供します。

原因の解説:なぜDockerデプロイで問題が起こるのか

vLLMのDockerデプロイが失敗する主な原因は、環境の隔離とリソース抽象化にあります。

1. GPUリソースの認識とアクセス問題

DockerコンテナはデフォルトではホストマシンのGPUを認識しません。NVIDIA Docker Runtime(nvidia-docker2)が正しくインストール・設定されていない、またはDocker runコマンドに--gpusオプションが指定されていない場合、「No CUDA GPUs are available」エラーが発生します。また、ホストのCUDAドライバとコンテナ内のCUDA Toolkitのバージョン不一致も原因となります。

2. メモリ管理とオフローディングの設定ミス

vLLMは推論速度を最大化するため、モデルパラメータとKVキャッシュを可能な限りGPUメモリに保持しようとします。利用可能なGPUメモリを超えるモデルをロードしようとすると、OutOfMemoryエラーが発生します。この場合、gpu_memory_utilizationの調整や、--deviceオプションの指定、あるいはCPUオフローディングの設定が必要です。

3. ネットワークとサービスバインディング

vLLMはデフォルトでポート8000でOpenAI互換のAPIサーバーを起動します。Dockerコンテナ内で起動したサービスを外部からアクセス可能にするには、ポートのマッピング(-pオプション)が必要です。また、コンテナ内のサービスが0.0.0.0ではなく127.0.0.1にバインドされていると、ホストマシンから接続できません。

4. 永続化データとモデルファイルの管理

巨大なモデルファイル(数十GB)を毎回コンテナ内にダウンロードするのは非現実的です。ホストのストレージをコンテナにマウントする必要がありますが、パーミッションエラーやパス指定の誤りが発生しがちです。

解決方法:ステップバイステップのトラブルシューティングと本番構成

ステップ1: 基礎環境の確認とNVIDIA Dockerのセットアップ

まず、ホスト環境がDockerとNVIDIA GPUを正しく認識しているか確認します。

# ホストのNVIDIAドライバとCUDAバージョンを確認
nvidia-smi

# Dockerがインストールされているか確認
docker --version

# NVIDIA Container Toolkitがインストールされているか確認
docker run --rm --gpus all nvidia/cuda:12.1.1-base-ubuntu22.04 nvidia-smi

上記の最後のコマンドでGPU情報が表示されなければ、NVIDIA Container Toolkitのインストールが必要です。Ubuntuの場合のインストールコマンド例は以下の通りです。

# NVIDIA Container Toolkitのインストール(Ubuntu例)
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

ステップ2: 適切なvLLM Dockerイメージの選択と実行

vLLMは公式に複数のDockerイメージを提供しています。CUDAバージョンや機能に応じて選択します。本番環境では特定のバージョンをタグ指定することを推奨します。

# 最新のvLLMイメージをプル
docker pull vllm/vllm-openai:latest

# より軽量なイメージを利用する場合(例)
# docker pull vllm/vllm-openai:cuda12.1.0

# 基本的な実行コマンド(モデルは例として"facebook/opt-125m"を使用)
docker run --rm --gpus all 
  -p 8000:8000 
  -v /path/to/model/cache:/root/.cache/huggingface 
  vllm/vllm-openai:latest 
  --model facebook/opt-125m

上記コマンドで「Address already in use」エラーが出る場合は、ホストのポート8000が他のプロセスで使用されています。-p 8080:8000のようにポートを変更してください。

ステップ3: メモリ不足エラーの解決とパラメータチューニング

GPUメモリ不足が発生した場合、以下のいずれかの方法、またはその組み合わせで対応します。

方法A: GPUメモリ使用率の制限
--gpu-memory-utilizationパラメータで使用率を調整します。0.9(90%)がデフォルトです。

docker run --rm --gpus all 
  -p 8000:8000 
  vllm/vllm-openai:latest 
  --model meta-llama/Llama-2-7b-chat-hf 
  --gpu-memory-utilization 0.8 
  --max-model-len 2048 # コンテキスト長を制限してメモリ使用を削減

方法B: 複数GPUの利用
複数GPUがある環境では、--tensor-parallel-sizeを指定してモデルを分割します。

docker run --rm --gpus 2  # 2つのGPUを指定
  -p 8000:8000 
  vllm/vllm-openai:latest 
  --model meta-llama/Llama-2-13b-chat-hf 
  --tensor-parallel-size 2 # 2GPU間でモデルを分散

方法C: CPUオフローディングの有効化(最終手段)
GPUメモリがどうしても足りない場合、一部のレイヤーをCPUにオフロードします。性能は低下します。

docker run --rm --gpus all 
  -p 8000:8000 
  vllm/vllm-openai:latest 
  --model TheBloke/Llama-2-13B-AWQ 
  --device cpu # CPUオフローディングを有効化

ステップ4: 本番環境向けの堅牢なDocker Compose構成例

単一のdocker runコマンドよりも、Docker Composeを使用することで、設定の管理とサービスのオーケストレーションが容易になります。以下は本番環境を想定したdocker-compose.ymlの例です。

version: '3.8'

services:
  vllm-api:
    image: vllm/vllm-openai:latest
    container_name: vllm-server
    runtime: nvidia # NVIDIA Runtimeを指定
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    ports:
      - "8000:8000"
    volumes:
      # 1. モデルキャッシュの永続化
      - ./hf_cache:/root/.cache/huggingface
      # 2. 設定ファイルのマウント(任意)
      - ./config:/app/config
      # 3. ログの永続化
      - ./logs:/app/logs
    environment:
      # 環境変数でモデルを指定可能
      - MODEL=meta-llama/Llama-2-7b-chat-hf
      - HUGGING_FACE_HUB_TOKEN=your_token_here # 非公開モデル用
    command: >
      --model $${MODEL}
      --served-model-name llama-2-7b
      --api-key your_api_key_here # APIキーによる保護
      --gpu-memory-utilization 0.85
      --max-num-batched-tokens 4096
      --host 0.0.0.0 # コンテナ外からの接続を許可
    restart: unless-failure # 異常終了時に自動再起動
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

この構成ファイルを使用してサービスを起動・停止するコマンドは以下の通りです。

# バックグラウンドで起動
docker-compose up -d

# ログを確認
docker-compose logs -f vllm-api

# サービスを停止
docker-compose down

コード例・コマンド例:クライアントからの接続テスト

vLLMサーバーが正常に起動したら、OpenAI SDK互換のエンドポイントにリクエストを送信してテストします。

# Pythonクライアントの例
from openai import OpenAI

# ベースURLをDockerコンテナのマッピングポートに設定
client = OpenAI(
    api_key="your_api_key_here", # --api-keyで設定した値
    base_url="http://localhost:8000/v1" # ホストから見たアドレス
)

# チャット補完のリクエスト
response = client.chat.completions.create(
    model="llama-2-7b", # --served-model-nameで指定した名前
    messages=[
        {"role": "user", "content": "こんにちは、自己紹介をしてください。"}
    ],
    max_tokens=100
)

print(response.choices[0].message.content)

# ストリーミングレスポンスの例
stream = client.chat.completions.create(
    model="llama-2-7b",
    messages=[{"role": "user", "content": "日本で有名な観光地を3つ教えて"}],
    max_tokens=150,
    stream=True
)

for chunk in stream:
    if chunk.choices[0].delta.content is not None:
        print(chunk.choices[0].delta.content, end="")

また、curlコマンドを使った簡単なテストも可能です。

curl http://localhost:8000/v1/models
# レスポンス例: {"object":"list","data":[{"id":"llama-2-7b","object":"model"}]}

curl http://localhost:8000/v1/chat/completions 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer your_api_key_here" 
  -d '{
    "model": "llama-2-7b",
    "messages": [{"role": "user", "content": "Hello!"}],
    "max_tokens": 50
  }'

まとめ・補足情報

vLLMをDockerで本番環境にデプロイする際の課題は、主に「GPUリソース」「メモリ」「ネットワーク」「永続化」の4つの観点から整理できます。本記事で紹介したトラブルシューティングのステップとDocker Composeの構成例を参考にすることで、これらの課題を系統的に解決し、安定した推論サービスを構築できるでしょう。

追加のベストプラクティス:

  1. 監視の実装: docker statsやcAdvisor、Prometheus + Grafanaを用いて、コンテナのGPUメモリ使用率、推論レイテンシー、リクエスト率を監視します。
  2. ヘルスチェック: Docker Composeのhealthcheckオプションや、KubernetesのLiveness Probeを設定し、サービスが生きているか定期的に確認します。
  3. モデルの最適化: 本番環境では、AWQやGPTQなどの量子化技術を適用したモデルを使用することで、メモリ使用量を削減し、推論速度を向上させることができます。vLLMはこれらのフォーマットをネイティブサポートしています。
  4. セキュリティ: --api-keyオプションの利用は必須です。さらに、本番環境ではDockerネットワークを分離し、リバースプロキシ(Nginx等)を通してTLS/SSLによる暗号化を施すことを強く推奨します。

vLLMは急速に進化しているプロジェクトです。最新の情報や最適な設定は、公式GitHubリポジトリやドキュメントを常に参照することを心がけてください。Dockerデプロイの成功は、LLMアプリケーションをプロダクションに移行するための重要な第一歩となります。

この記事は役に立ちましたか?