vLLMを用いたオープンソースLLMの推論スループットを最大化する実装手法

GPU追加は最終手段。vLLMとPagedAttentionで挑むメモリ管理の物理的最適化

この記事は急速に進化する技術について解説しています。最新情報は公式ドキュメントをご確認ください。

約17分で読めます
文字サイズ:
GPU追加は最終手段。vLLMとPagedAttentionで挑むメモリ管理の物理的最適化
目次

この記事の要点

  • GPUメモリの断片化問題の根本的解決
  • PagedAttentionによるKVキャッシュの効率的な管理
  • 既存GPUリソースの物理的な利用効率を最大化

なぜ高性能GPUでもLLMは遅くなるのか:ボトルネックの物理的構造

「最新のH100や、VRAMが32GBに達するようなウルトラハイエンドGPUを導入したのに、思ったほどスループットが出ない」

このような課題は、AIシステム開発の現場で頻繁に耳にします。推論速度の課題を「計算能力(Compute)」の問題だと捉えるケースは多いですが、LLM(大規模言語モデル)の推論において、真のボトルネックが計算機のリソース不足であることは稀です。実態は、ほとんどのケースで「メモリ帯域(Memory Bandwidth)」と「メモリ容量の管理」に起因しています。

もし推論速度を上げるために安易にGPUの枚数を増やそうとしているなら、少し待ってください。それは、穴の空いたバケツに水を注ぐために、蛇口を増やそうとしているのと同じかもしれません。まずはバケツの穴、つまり「メモリ管理の非効率」を塞ぐことが先決です。

計算能力ではなく「メモリ帯域」が限界を決める

LLMの推論プロセス、特にトークンを一つずつ生成するデコーディング段階は、典型的な「メモリ律速(Memory Bound)」の処理です。

GPUの演算コア(Tensor Coreなど)は非常に高速ですが、そのコアにデータを供給するメモリ(VRAM)からの転送速度が追いついていません。これは、超高速な料理人(GPUコア)に対して、食材(重みパラメータとKVキャッシュ)を運ぶウェイター(メモリバス)が遅すぎるレストランのようなものです。料理人は常に手持ち無沙汰で、次の食材が届くのを待っています。

最新世代のGPUではVRAM 16GB以上が標準化され、FP8などの低精度フォーマットの活用でVRAM消費を大幅に抑制する技術も進化しています。しかし、この状況で単にGPUをより上位のモデルに変えても、メモリ帯域幅の利用効率が根本的に改善されない限り、期待するほどの速度向上は得られません。ここで取り組むべきは、この限られた帯域をいかに効率よく使い、一度メモリに載せたデータをどれだけ有効活用するかという点です。

KVキャッシュの断片化が生むリソースの浪費

さらに深刻なのが、KVキャッシュ(Key-Value Cache)の取り扱いです。KVキャッシュは、過去のトークンの計算結果を保存しておくことで、再計算を防ぐ重要な仕組みです。しかし、Hugging Face Transformersなどの従来の実装では、このKVキャッシュのために「連続したメモリ領域」を事前に確保する必要がありました。

想像してみてください。4人掛けのテーブル席しかないレストランに、1人で来店した客を次々と案内している状況を。しかも、「後で友人が来るかもしれない(生成トークンが増えるかもしれない)」という理由で、常に最大人数分の席を予約ブロックし続けているのです。

実際の推論では、プロンプトの長さも生成される応答の長さも事前には分かりません。そのため、システムは安全を見て「最大シーケンス長」分のメモリを確保します。結果として、GPUメモリの大部分は「予約されているが使われていない空席(断片化)」で埋め尽くされます。

なお、最新のTransformersではアーキテクチャが大刷新され、TensorFlowやFlaxのサポートを終了してPyTorchエコシステムへフォーカスを絞りました。その過程で、推論時のページング注意機構が導入されたり、外部の専門エンジンとの統合が強化されたりしています。つまり、従来の「連続したメモリ確保」という非効率な前提自体が、業界全体で見直されるフェーズに入っているのです。

vLLMが解決する「メモリのテトリス」問題

ここで登場するのがvLLMであり、その核心技術であるPagedAttentionです。先述したTransformersのエコシステム再構築においても、vLLMは単なる競合ツールではなく、推論に特化した強力なエンジンとして統合される前提の設計になっています。

vLLMのアプローチは、オペレーティングシステム(OS)における「仮想メモリ」と「ページング」の仕組みをLLMに応用したものです。OSがメモリを固定長の「ページ」に分割し、物理メモリ上のバラバラな場所に配置しても論理的には連続しているように扱うのと同様に、vLLMはKVキャッシュをブロック単位で管理します。

つまり、先ほどのレストランの例で言えば、4人掛けテーブルに拘るのをやめ、1人用の椅子を必要な数だけ、空いている場所にどこでも配置できるようにしたのです。客(トークン)が増えれば、空いている隙間に椅子を追加するだけです。連続したスペースを確保する必要はありません。

これにより、メモリの断片化(フラグメンテーション)はほぼゼロになります。隙間なくデータを詰め込めるようになるため、同じGPUメモリ容量で、より多くのリクエスト(バッチサイズ)を同時に処理できるようになります。これが、vLLMが「物理的に」スループットを向上させるメカニズムです。魔法ではなく、メモリ空間という物理リソースに対する、極めて論理的な「テトリス」の勝利なのです。

導入前の現在地確認:推論パフォーマンスのベースライン測定

なぜ高性能GPUでもLLMは遅くなるのか:ボトルネックの物理的構造 - Section Image

最適化の旅に出る前に、必ず行わなければならないことがあります。それは「現状(ベースライン)の測定」です。システム開発において、数値に基づかない改善は単なる「変更」に過ぎません。

vLLMの効果を正当に評価し、その価値を明確に示すためにも、まずは標準的な実装でのパフォーマンスを正確に記録しておく必要があります。

スループット(トークン/秒)とレイテンシの正しい計測法

LLMのパフォーマンス指標として、単に「レスポンスタイム」を見るだけでは不十分です。以下の2つの指標を明確に区別して計測することが重要です。

  1. Time to First Token (TTFT):
    リクエストを送ってから、最初のトークンが生成されるまでの時間。これはユーザーが感じる「反応の速さ」に直結します。プロンプト処理(Prefill)の性能がここに現れます。

  2. Inter-token Latency (ITL) / Generation Throughput:
    2つ目のトークン以降、次のトークンが生成されるまでの平均時間、または1秒あたりに生成されるトークン数。これは文章生成の滑らかさと、システム全体の処理能力(スループット)を示します。

最適化によって、TTFTはそれほど変わらなくても、スループット(同時処理能力)が劇的に向上するケースが多々あります。この重要な変化を見逃さないように注意を払う必要があります。

Hugging Face Transformersでのベンチマーク実施

まずは、現在多くの現場で標準的に使われている transformers ライブラリを用いた推論スクリプトで計測します。最新のTransformersでは内部設計が大きく刷新され、TensorFlowなどのサポートが終了し、PyTorchを主要フレームワークとしてバックエンドの最適化が進められています。また、vLLMなどの外部ツールとの統合も強化されていますが、まずは素の状態でのベースラインを知る必要があります。モデルはLlamaなどの一般的なサイズで構いません。

# ベースライン計測用スクリプトのイメージ
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_id = "meta-llama/Meta-Llama-3-8B"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.float16, device_map="auto")

prompt = "AIエンジニアとして成功するためのロードマップを教えてください。"
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")

# 計測開始
start_time = time.time()
# first tokenまでの時間を計測するロジックが必要だが、ここでは簡易的にgenerate全体を計測
outputs = model.generate(inputs, max_new_tokens=200)
end_time = time.time()

print(f"Total time: {end_time - start_time:.4f} sec")
# 実際にはここで生成トークン数を割り、スループットを算出する

この単純な実行を行い、同時に複数のリクエストを投げた場合(バッチ処理)の挙動も確認します。おそらく、バッチサイズを上げるとすぐに OutOfMemoryError (OOM) に遭遇するか、レイテンシが許容できないほど悪化することに気づくはずです。最新バージョンで推論の改善や量子化の第一級サポートが追加されていても、メモリ管理の物理的な限界は依然として立ちはだかります。

GPUメモリ使用率のプロファイリング手法

数値だけでなく、GPUの中で何が起きているかを視覚化します。ターミナルで watch -n 0.5 nvidia-smi を実行し、推論中のメモリ使用率の推移を観察してください。

注目すべきは、推論が走っていないアイドル時と、負荷がかかったピーク時のメモリ使用量の差です。従来のTransformers実装では、モデルをロードした時点で大量のVRAMを消費し、推論時にはさらに急激にメモリを食いつぶします。しかし、その中身の多くは「予約済みだが未使用」の領域です。

より詳細な分析には、PyTorchのプロファイラや nvitop などのツールを使うと良いでしょう。メモリの割り当て(Allocation)と予約(Reservation)の乖離が大きい場合、そこには巨大な最適化の余地(=vLLMの出番)が眠っています。

vLLM実装のコアステップ:PagedAttentionを活かす構成術

現状の非効率さを目の当たりにしたところで、いよいよvLLMの導入に移ります。ここでは単なるインストール手順だけでなく、PagedAttentionの物理的な挙動を意識した設定のポイントを解説します。

基本インストールと互換性の確認

vLLMはPythonのパッケージとして提供されていますが、GPUのCUDAバージョンとの相性が非常に重要です。古いドライバのまま進めると、エラーに悩まされる可能性があります。

# CUDA 12.1環境の場合の推奨インストール
pip install vllm

導入前に nvcc --version でCUDAコンパイラのバージョンを確認し、PyTorchが認識しているCUDAバージョンと一致しているか確認することをお勧めします。不整合がある場合は、Dockerコンテナ(NVIDIA公式のPyTorchイメージなど)を利用するのが最も安全で確実な近道です。

オフライン推論とAPIサーバーモードの使い分け

vLLMには大きく分けて2つの利用形態があります。

  1. LLMクラス(オフライン推論):
    バッチ処理で大量のデータを一括処理したい場合や、独自のPythonアプリケーション内部に組み込む場合に使用します。

  2. API Server(オンライン推論):
    OpenAI互換のAPIエンドポイントとして立ち上げる方法です。マイクロサービスアーキテクチャを採用している場合や、既存のOpenAI APIクライアントをそのまま流用したい場合はこちらが適しています。

システム構築の観点からは、まずはAPIサーバーモードでの立ち上げをマスターすることが推奨されます。これにより、推論エンジンをアプリケーションロジックから切り離し、独立してスケーリングさせることが可能になります。

# LlamaモデルをAPIサーバーとして起動する例
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Meta-Llama-3-8B-Instruct \
  --dtype auto \
  --api-key your_secret_key

メモリブロックサイズ設定のベストプラクティス

PagedAttentionの効率を左右する隠れたパラメータに block_size があります。これは、メモリを「何トークン分の塊(ページ)」で管理するかを決める設定です。デフォルトは16ですが、GPUアーキテクチャによってはこれを調整することで数%の性能向上が見込めます。

  • block_size=16 (デフォルト): 多くのケースでバランスが良い。
  • block_size=32/64: メモリの管理オーバーヘッド(メタデータ)を減らせるが、最後のブロックに生じる未使用領域(内部断片化)がわずかに増える。

物理的なイメージで言えば、ノートの罫線の幅を決めるようなものです。罫線が広すぎると1行に数文字しか書かない場合に行が無駄になりますし、狭すぎると行数の管理が大変になります。基本はデフォルトで問題ありませんが、極限までチューニングしたい場合は、ベンチマークを取りながら調整する価値があります。

スループットを最大化する「連続バッチ処理」のチューニング

vLLM実装のコアステップ:PagedAttentionを活かす構成術 - Section Image

vLLMの真価は、PagedAttentionを基盤とした「連続バッチ処理(Continuous Batching)」にあります。これを理解し、適切にチューニングできるかどうかが重要です。

Continuous Batching(連続バッチ)の仕組みとメリット

従来の「静的バッチ処理」は、バスの運行に似ています。定員(バッチサイズ)がいっぱいになるか、全員の処理が終わるまで、バスは動き続け、誰も降りられません。早く処理が終わったリクエスト(短い文章)も、一番遅いリクエスト(長い文章)が終わるまで待たされます。これはGPUリソースの無駄です。

一方、vLLMの「連続バッチ処理」は、ベルトコンベア、あるいは回転寿司のようなイメージです。リクエストごとに処理が終わり次第、そのリクエストはメモリから退出し、即座に空いたスペースに新しいリクエストが入ってきます。全ての処理サイクル(イテレーション)で、新しいリクエストを受け入れるチャンスがあるのです。

これにより、GPUの使用率(Utilization)を常に高く保つことができます。待ち時間が減り、システム全体のスループットが飛躍的に向上します。

gpu_memory_utilizationの安全な調整ライン

vLLMの設定で最も重要なのが --gpu-memory-utilization です。これは、vLLMがGPUメモリのどれくらいを「自分の領域」として確保するかを指定します。デフォルトは0.9(90%)です。

「100%使い切ったほうが効率が良いのでは?」と考えるのは危険です。残りの10%は、PyTorchのランタイムオーバーヘッドや、予期せぬアクティベーションの一時的な増大のために残しておく「安全マージン(Safety Margin)」です。

もし、他のプロセスが同じGPUを使っていない専用環境であれば、0.95程度まで攻めることができます。しかし、0.98などを設定すると、OOMでプロセスごとクラッシュするリスクが高まります。本番環境では0.9、開発環境でのストレステスト時のみ0.95を推奨します。物理的な限界ギリギリを攻めるのは良いですが、システムダウンは避けるべきです。

同時リクエスト数とレイテンシのトレードオフ管理

--max-num-seqs(最大同時処理シーケンス数)も重要なパラメータです。これを増やせばスループット(単位時間あたりの処理量)は上がりますが、1つ1つのリクエストに割り当てられる計算時間が減るため、レイテンシ(応答速度)は悪化します。

ここで「ビジネス要件」とのすり合わせが必要です。

  • チャットボット: ユーザーの待ち時間が重要。レイテンシ優先で、同時接続数を制限する。
  • バッチ分析・要約: ユーザーは待っていない。スループット優先で、同時処理数を限界まで上げる。

負荷テストツール(例えば Locust や vLLM付属のベンチマークツール)を使い、横軸に同時リクエスト数、縦軸にレイテンシとスループットをプロットしたグラフを作成してください。両者の曲線が交差するポイント、あるいはレイテンシが許容限界(例: 200ms/token)を超える手前が、そのシステムの「スイートスポット」です。

本番運用を見据えた安定化とモニタリング戦略

PoC(概念実証)で動いたからといって、そのまま本番環境に投入するのは無謀です。推論サーバーは24時間365日、安定して稼働し続けなければなりません。ここでは、vLLM特有の運用監視ポイントを解説します。

Prometheus/Grafanaでのメトリクス可視化

vLLMは標準で /metrics エンドポイントを提供しており、Prometheus形式でメトリクスを出力します。これを活用しましょう。以下の指標は必ずダッシュボードに表示させましょう。

  • vllm:num_requests_running: 現在処理中のリクエスト数。
  • vllm:num_requests_waiting: GPUの空きを待っているキュー内のリクエスト数。これが常に0より大きい場合、リソース不足のサインです。
  • vllm:gpu_cache_usage_perc: KVキャッシュの使用率。これが100%に張り付くと、新しいリクエストが処理できず待ち行列に入ります。

特に gpu_cache_usage_perc は、PagedAttentionの空き状況そのものです。これが常に高い値(90%以上)を示しているなら、トラフィックに対してメモリ容量が限界に達しています。この時初めて、「GPUの追加」や「モデルの量子化(Quantization)」を検討すべきタイミングです。

リクエストキューの詰まりを検知するアラート設定

システムが健全かどうかを判断する最もシンプルな指標は「待ち行列(Queue)」です。vllm:num_requests_waiting が一定時間(例えば1分間)継続して閾値を超えた場合、アラートを発報するように設定します。

また、OOMエラーはログ監視で検知します。vLLMはメモリ管理が優秀ですが、入力プロンプトが極端に長い(モデルのコンテキストウィンドウを超える)リクエストが来た場合などにはエラーを吐く可能性があります。これらを早期に検知し、自動再起動やロードバランサからの切り離しを行う仕組みを整えておくことが重要です。

モデル並列化(Tensor Parallelism)によるスケーリング

単一のGPUでメモリが足りない場合、vLLMはTensor Parallelism (TP) をサポートしています。これはモデルの重みと計算を複数のGPUに分割する技術です。

例えば、70Bクラスのモデルを動かすには、通常24GBや40GBのVRAMを持つGPU 1枚では足りません。この場合、2枚または4枚のGPUを束ねて1つの巨大なGPUとして扱います。

# 4枚のGPUを使って推論する場合
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Meta-Llama-3-70B-Instruct \
  --tensor-parallel-size 4

ここで注意すべきは、TPは「GPU間の通信」を頻繁に行うため、NVLinkなどの高速なインターコネクトがない環境(PCIeのみでの接続など)では、通信がボトルネックになり速度が出ないことです。物理的な配線とトポロジーを理解した上で構成を選択する必要があります。

投資対効果の試算:推論コスト半減へのロードマップ

最後に、技術的な最適化がビジネスにどのようなインパクトを与えるかを整理します。これは、次のプロジェクト予算を検討する際の重要な判断材料となります。

同じGPUリソースで処理量を2倍にする意味

vLLMを適切に導入することで、スループットが2倍〜4倍になることは珍しくありません。これは経営視点で見れば、「ハードウェアコストを1/2〜1/4に圧縮した」ことと同義です。

例えば、月額20万円のGPUインスタンスを4台使っていたサービスが、最適化によって2台で同じリクエスト量を捌けるようになれば、年間でコスト削減になります。エンジニアの工数を数日投入するだけで、これだけのROI(投資対効果)が出る施策はそう多くありません。

クラウドGPUコストの削減シミュレーション

API利用(トークン課金)と自社ホスティング(時間課金)の損益分岐点も、vLLMによって大きく変わります。推論効率が上がれば上がるほど、自社ホスティングの単価(トークンあたりのコスト)は下がります。

もし現在、OpenAIやAnthropicのAPIコストが増大しているなら、vLLMを用いたオープンソースモデルへの切り替えを試算してみてください。「1分間に生成できるトークン数」をベースに計算すると、一定規模以上のトラフィックがあるサービスでは、自社ホスティングの方が圧倒的に安くなるポイントが必ず存在します。

次に目指すべき最適化(量子化、投機的デコーディング)

vLLMとPagedAttentionは、メモリ管理の最適化でした。これに加え、モデル自体のサイズを小さくする量子化(Quantization: AWQ, GPTQ)や、小さなモデルで当たりをつけて大きなモデルで検証する投機的デコーディング(Speculative Decoding)**を組み合わせることで、さらなる高速化が可能です。

技術の進化は止まりません。しかし、どの技術も「物理的なリソースをどう効率的に使うか」という原理原則は同じです。まずはvLLMで足元を固め、堅牢で高速なAIインフラを構築してください。それができた時、提供するサービスは競合他社には真似できない「速さ」と「安さ」という強力な武器を手に入れているはずです。

まとめ

LlamaをAPIサーバーとして起動する例 - Section Image 3

GPUリソースの追加は、最適化をやり尽くした後の「最終手段」です。まずは今あるメモリ空間を見直し、PagedAttentionという「整理整頓の技術」を導入することで、驚くほどのパフォーマンス改善が得られます。

実証データに基づいた確信を持って、実装の一歩を踏み出してみてください。

GPU追加は最終手段。vLLMとPagedAttentionで挑むメモリ管理の物理的最適化 - Conclusion Image

コメント

コメントは1週間で消えます
コメントを読み込み中...