Dockerで実践するHugging Face TGI構築ガイド:本番環境LLMのスループットとレイテンシを徹底最適化

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

深夜2時の修羅場:リクエスト集中でLLMサーバーがダウンした瞬間

午前2時ちょうど、PagerDutyのアラートがけたたましく鳴り響きました。AIバックエンドクラスタ全体が真っ赤に染まっています。Datadogのダッシュボードを見ると、p99レイテンシは420msから一気に45秒へと急上昇。数百件のリクエストがHTTP 504 Gateway Timeoutを返していました。

NVIDIA A10Gの24GB VRAM使用率は99%に達して天井に張り付いていました。当時のシステムは、素のTransformersライブラリを内製のFastAPIラッパーで包んだだけの構成でした。トラフィックが毎秒15リクエストから120リクエストへ跳ね上がった際、サーバーはキューに入ったリクエストを1件ずつ順次処理していたのです。KVキャッシュの深刻な断片化が発生し、クラスタ全体が完全にフリーズしてクラッシュしました。

あの夜に痛感した教訓は一つです。「開発用アーキテクチャをそのまま本番環境の負荷に耐えさせようとしてはならない」。1台のLLMインスタンスで数百の同時接続をさばくには、専用の推論エンジンや本格的なLLMのプロダクション環境へのデプロイ基盤が不可欠です。

本番LLM運用の3つのアプローチを比較

その夜、GPUクラスタを復旧させるべく、以下の3つの選択肢を比較検討しました:

  • 従来のFastAPI + Transformers:Hugging FaceのパイプラインをAPIでラップする構成。15分ほどですぐに構築できコードの修正も容易なため、プロンプト検証や社内デモには最適です。
  • Dockerコンテナ経由のvLLM:PagedAttentionで知られるオープンソース推論エンジン。KVキャッシュの扱いが非常に優れており、大規模バッチ実行時のスループットが高く、コミュニティのサポートも活発です。
  • Docker上のHugging Face Text Generation Inference (TGI):Hugging Face公式の本番向けソリューションで、RustとC++/CUDAを組み合わせて設計されています。Continuous Batching、PagedAttention、FlashAttention-2、Tensor Parallelismがあらかじめ統合されており、OpenAI互換エンドポイントや高速なgRPCインターフェースも備えています。

各アプローチのメリット・デメリット分析

1. FastAPI + Transformers

最大のメリットは実装とデバッグの手軽さです。しかし、致命的な弱点はContinuous Batchingに対応していない点にあります。10人のユーザーが同時にリクエストを送った場合、2人目は1人目の最後のトークン生成が完了するまで待たされます。GPU使用率は100%に張り付いたかと思えば0%に急降下し、VRAMは無制限に肥大化して頻繁にCUDA Out-Of-Memory (OOM)エラーを引き起こします。LangServeのように手軽にAPI化できるツールも検証用途には優れていますが、大規模負荷には別の対策が必要です。

2. vLLM

vLLMはPagedAttentionによるページング機構によってメモリの無駄を解消しています。トークン生成速度も現行トップクラスです。一方で、エンタープライズ統合の面でやや課題があります。Hugging Face Hubのプライベートリポジトリから重みをダウンロードしたり、厳密なトークン認証を行ったり、トークン単位のストリーミングメトリクスを計測したりする場合、vLLMでは追加の手動ランタイム設定が必要になります。

3. Hugging Face TGI

TGIは非常に実用的なハイブリッドアーキテクチャを採用しています。Rustで記述されたルーターとスケジューラーにより、スケジューリングのオーバーヘッドがほぼゼロに抑えられています。一方、推論カーネルは高度に最適化されたC++/CUDAで動作します。DockerイメージにはFlashAttention-2やAWQ、GPTQ、BitsAndBytesなどの量子化形式が標準でサポートされており、チェックポイントの変換も不要です。デメリットとしては、Dockerイメージがかなり巨大(約15〜20GB)である点と、TuringまたはAmpere以降のGPUアーキテクチャが必要な点が挙げられます。小規模な社内ツールやローカル開発環境であればOllamaなどの選択肢もありますが、本番の高スループット運用ではTGIの堅牢性が際立ちます。

本番クラスタ復旧にTGIを選定した決定打

サーバーを今すぐ復旧させなければならない危機的状況の中、TGIは以下の3つの緊急要件を過不足なく満たしていました:

  1. 公式Dockerイメージの完備:CUDAドライバ、PyTorch、wheelの依存関係トラブルで時間を浪費することがありません。ホストにNVIDIA Container Toolkitさえ導入されていれば、コマンド一発ですぐに起動できます。
  2. 高精度なContinuous Batching:TGIはリクエストを固定的な静的バッチにまとめません。リクエストAの最後のトークン生成が完了した瞬間に、その空きスロットへ待機中のリクエストBが動的に割り込まれます。これにより、従来のFastAPIスクリプトと比較して4〜5倍のスループットを実現できました。
  3. プロアクティブなVRAM保護機構:入出力トークン数やキューの同時実行数を厳格に制限可能です。GPUメモリが枯渇する前にタイムアウトしたリクエストを自動的に遮断してくれます。

実践:DockerによるTGIデプロイ手順

ステップ1:ホスト環境とNVIDIA Container Toolkitの確認

まず、ホスト側にドライバが正しくインストールされ、DockerからGPUが認識されていることを確認します。以下のコマンドを実行してテストします:

docker run --rm --gpus all nvidia/cuda:12.1.1-base-ubuntu22.04 nvidia-smi

GPU情報(A10G、A100、RTX 4090など)が正常に出力されれば、TGIイメージをプルする準備は完了です。

ステップ2:キャッシュディレクトリの作成とコンテナの起動

コンテナの再起動ごとに何十ギガバイトものモデル重みを再ダウンロードさせないよう、ホスト側のキャッシュディレクトリをコンテナにマウントします。あの深夜、チームを救ったQwen/Qwen2.5-7B-Instructモデルのデプロイコマンドは以下の通りです:

mkdir -p /data/hf_cache

docker run -d \
  --name tgi-server \
  --gpus all \
  --shm-size 1g \
  -p 8080:80 \
  -v /data/hf_cache:/data \
  -e MODEL_ID=Qwen/Qwen2.5-7B-Instruct \
  -e MAX_CONCURRENT_REQUESTS=128 \
  -e MAX_INPUT_TOKENS=2048 \
  -e MAX_TOTAL_TOKENS=4096 \
  -e MAX_BATCH_PREFILL_TOKENS=4096 \
  ghcr.io/huggingface/text-generation-inference:2.4.0

ステップ3:サーバーダウンを防ぐパラメータのチューニング

実際のVRAM容量に応じて、以下の環境変数を適切に調整する必要があります:

  • --shm-size 1g:必須の設定です。RustとPythonのワーカープロセスが高速なプロセス間通信(IPC)を行うためにこの共有メモリ領域を使用します。これが不足すると、POSIX共有メモリの枯渇によりコンテナがクラッシュします。
  • MAX_INPUT_TOKENS & MAX_TOTAL_TOKENS:数千トークンにおよぶ長大なプロンプトが送信されてKVキャッシュがあふれるのを防ぎます。
  • MAX_BATCH_PREFILL_TOKENS:プロンプトのprefillフェーズで同時に計算するトークン数を制限します。A100 80GBなどのGPUであれば、8192まで引き上げることでレスポンス速度をさらに向上できます。
  • MAX_CONCURRENT_REQUESTS:キューの上限値です。この閾値を超えた場合、TGIは直ちにHTTP 429を返してGPUの負荷爆発を防止します。

ステップ4:起動ログの確認

コンテナがVRAMにモデルを展開し、カーネルのウォームアップを完了するまでに数十秒かかります。以下のコマンドで進捗ログを確認します:

docker logs -f tgi-server

ログにConnectedと表示され、続いてRouter started at 0.0.0.0:80が出力されれば、リクエスト受付の準備は完了です。

ステップ5:cURLによるエンドポイント検証

TGIには標準でOpenAI互換の/v1/chat/completionsエンドポイントが用意されています。シンプルなcURLコマンドでTime-To-First-Token(TTFT)を計測できます:

curl -X POST http://localhost:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "tgi",
    "messages": [
      {"role": "system", "content": "あなたはトラブルシューティングを支援するDevOpsエキスパートです。"},
      {"role": "user", "content": "GPUのメモリリークを確認するにはどうすればよいですか?"}
    ],
    "max_tokens": 256,
    "temperature": 0.7
  }'

ステップ6:Pythonクライアントによるストリーミングトークン受信

ユーザーインターフェース上で文字をリアルタイムに滑らかに出力させるには、/generate_streamエンドポイントへ接続します:

import requests
import json

url = "http://localhost:8080/generate_stream"
payload = {
    "inputs": "Continuous Batchingの仕組みを簡潔に説明してください:",
    "parameters": {
        "max_new_tokens": 200,
        "temperature": 0.2
    }
}

response = requests.post(url, json=payload, stream=True)

for line in response.iter_lines():
    if line:
        decoded_line = line.decode('utf-8')
        if decoded_line.startswith("data:"):
            data = json.loads(decoded_line[5:])
            print(data.get("token", {}).get("text", ""), end="", flush=True)
print()

本番運用における実践的トラブルシューティング

  • 起動直後のOOM(メモリ不足):7BモデルがFP16精度で15GBを超えるVRAMを消費してしまう場合は、-e QUANTIZE=awqまたは-e QUANTIZE=bitsandbytes-nf4フラグを追加してください。必要なVRAM容量を約5.5GB〜6GB程度まで圧縮できます。
  • p99レイテンシの急激な悪化:多くの場合、クライアントが規定値を超える長大なプロンプトを送信していることが原因です。手前のNginxやTraefikのレイヤーでリクエストボディのサイズを厳格に制限し、早期にドロップするように設定します。また、リクエストの性質に応じてモデルやバックエンドを振り分けるSemantic Routerによるインテリジェントなリクエスト・ルーティングを導入することも有効です。
  • 複数GPUでの70Bモデルの実行:-e NUM_SHARD=4(4基のGPUを使用する場合)を追加します。TGIはtorchrunなどの手動設定を必要とせず、NCCLを介して自動的にTensor Parallelismを適用してくれます。

TGIへの全面移行を完了してから40分後、午前3時に再度計測を行ったところ、p99レイテンシは45秒から1.15秒へと劇的に改善しました。VRAM使用率は120 req/sの負荷下でも81%で安定。翌朝9時のトラフィックピーク時も、1件のOOMエラーを出すこともなく無事に乗り切ることができました。

Share: