7月18日、Netflix Technology Blogが「In-House LLM Serving at Netflix」と題した記事を公開した。LLM推論を外部APIに依存せず自社インフラで完結させるという判断は一見シンプルに聞こえるが、制約付きデコーディングのスケーリング問題やデプロイ時の協調問題など、本番環境が初めて露わにした落とし穴が随所に待ち受けていた。その設計思想と具体的な解法を以下で紐解く。
「LLMだからといって特別扱いしない」という設計思想
Netflixの既存ML推論基盤は、JVM製の統合サービングシステムを中心に構成されている。XGBoost・TensorFlow・PyTorchの各モデルを束ねるバックエンド Model Scoring Service(MSS) が存在し、その下層にはNVIDIA Triton Inference Serverが動いている。LLMもこのMSSに統合することで、既存のクライアントライブラリ・ヘルスチェック・デプロイパイプラインをそのまま流用できる設計を採った。
「LLMをスペシャルスノーフレーク(他とは異なる特別扱い)にしない」というのが記事の核心的な方針だ。gRPCによる既存の呼び出しパスを維持しつつ、LLMエコシステムのデファクト標準となったOpenAI互換APIを追加フロントエンドとして並置することで、ホスト型APIから自社ファインチューニングモデルへの移行をほぼシームレスにした。
エンジン選定:TensorRT-LLMからvLLMへ
当初はTensorRT-LLMを採用していたが、2025年夏時点でvLLMをメインエンジン(paved-path)として再選定した。判断の軸は純粋な性能よりも「運用適合性」だ。
- カスタムモデルアーキテクチャを多段コンパイルなしでロードできる(非標準モデルの反復速度が上がる)
- カスタムデコードロジック用の拡張フックが存在する(後述の制約付きデコーディングに必須)
- デバッグのしやすさ(コンパイル済みエンジンに比べて障害調査が容易)
- ML実践者の習熟度(研究用途でvLLMを使い慣れた人材が多く、研究→本番の引き継ぎコストが下がる)
オープンソースエンジンが専用スタックとの性能差を概ね埋めたことも再選定の背景にある。
Tritonとの統合で踏んだ地雷
vLLMをTritonに組み込む際、vLLMバックエンドとPythonバックエンドの2択がある。vLLMバックエンドはモデルアーティファクトをJSONコンフィグだけで済ませられ、I/Oテンソル仕様をデプロイ時に動的生成するため、モデルとフロントエンドを独立して進化させられる。原則としてこちらが正解だ。
ただし本番で2つの問題が露出した。
Triton/vLLMのバージョン不整合:TritonのvLLMバックエンドは特定のvLLM APIに対してコンパイルされている。例えばTriton 25.09がvllm.engine.metricsをインポートしようとしても、vLLM 0.11.2でそのモジュールは削除済みのためバックエンドが起動しない。互換バージョンをサービスイメージに固定し、モデル作者がパッケージング時にvLLMバージョンを上書きできないよう制限する対応を取った。
カスタムモデルロジック:標準的なHuggingFace互換モデル以外(カスタム前処理・後処理、アンサンブルパイプライン等)はPythonバックエンドのexecute()を直接制御する必要があり、脱出口として残し続けることになる。
本番で一番面白い話:制約付きデコーディングをスケールさせるまで
記事で最も密度が高いのが、制約付きデコーディング(Constrained Decoding) のスケーリング問題だ。制約付きデコーディングとは、LLMがトークンを1つ生成するたびにビジネスルールや文法制約を適用し、「構造的に正しい出力しか生成しない」よう推論ループそのものに制約を組み込む手法を指す。
Netflixの一部ワークロードでは、生成トークンに対してビジネスルールを事後適用するのではなく、デコードループ内に制約を埋め込む設計を採っている。vLLMのカスタムlogits processorインタフェースを利用し、制約をステートマシンとしてモデル化、各ステップでトークン選択可否のマスクを生成する仕組みだ。
vLLM V0での壁
当初のPure Python実装は機能面では動いたが、スケールでボトルネックに直面した。V0ではカスタムlogits processorがリクエストごとに逐次実行される。GPUがバッチ全体のlogitsを生成→CPUへコピー→各リクエストの制約ロジックをPythonで順番に処理、という流れだ。GILにより並列化できないため、CPU処理時間はバッチサイズに比例して増加する。モデルのforward passはGPU上で効率よくバッチ処理されているのに、end-to-endレイテンシがCPUにボトルネックされる。単一リクエストのベンチマークでは見えず、現実の並行負荷下で初めて顕在化するタイプの問題だ。
vLLM V1での解決
V1ではlogits processorがバッチレベルに移行した。Netflixはカスタムプロセッサをバッチ全体を一括処理する設計に書き直し、ホットパスをC++でマルチスレッド実装してGILを回避した。元記事のFigure 3が示すとおり、バッチサイズが増えてもlogits処理時間はフラットになった。
ただしV1移行で新たな問題が2つ出た。
- Partial prefill:V1はチャンク単位でprefillを行うため、1リクエストが複数エンジンステップにまたがってprefillされる。BatchUpdateにはprefillが完了したか否かを判断する粒度がなく、内部追跡を追加した。
- Preemption:メモリ逼迫時にvLLMはKVキャッシュを退避して後でリスケジュールする。出力トークン列が単調増加するという前提でステートマシンが設計されていたため、トークン履歴が巻き戻ったことを検出してステートマシンをリセット・再初期化する処理を加えた。
デプロイ戦略:Red-BlackとVersioned
デプロイには2戦略を用意している。
Red-Blackは新バージョンを既存版と並列起動し、ヘルスチェック通過後に段階的にトラフィックを切り替える。I/Oスキーマが変わらない場合の標準パスだ。スキーマ変更が伴う場合、上流コンシューマーは新バージョン完全稼働前に設定を変えられないため、移行ウィンドウ中に「旧形式リクエストが新バージョンへ届いてエラーになる」協調問題が発生する。
Versionedは(modelId, modelVersion)ペアごとに独立デプロイを維持し、複数バージョンを同時稼働させる。コンシューマーは新バージョンの準備完了を確認してから切り替えられるため協調問題を回避できる。ただし移行期間中のGPUコスト増が伴う。
Netflixが推奨するのは、テンソルの形状などの可変設定をモデル内部に埋め込んでバージョン非依存にし、コストの安いRed-Blackを標準パスにすること。Versionedは破壊的インタフェース変更が避けられないレアケースに限定する。
その他の運用メモ
モデルキャッシュ:起動時にS3やHugging Faceから直接ダウンロードするとコールドスタートが長すぎる。モデルアナウンス時点でAmazon FSx(AWS が提供する高スループット・低レイテンシのマネージドファイルシステムサービス)にマテリアライズしておき、ウォームスタートでは高性能ファイルシステムを参照する設計にした。
統合metricsエンドポイント:vLLMはPrometheus(オープンソースの監視・メトリクス収集システム)メトリクスを.dbファイルとして書き出し、TritonはTriton独自のPrometheusエンドポイントを持つ。TritonのブリッジはvLLMの40以上のメトリクスのうち9つしか公開せず、トークンスループットやKVキャッシュ使用率などが欠落していた。軽量HTTPプロキシを追加して両方を単一の/metricsエンドポイントにマージし、既存のダッシュボードをそのまま利用できるようにした。
詳細はIn-House LLM Serving at Netflixを参照していただきたい。