【vLLM】LoRAアダプターを動的にロード・切り替える方法と「No adapter found」エラー解決

問題の概要:vLLMでLoRAアダプターを動的に扱う際の課題

大規模言語モデル(LLM)の推論エンジンであるvLLMは、その高速な推論速度から広く利用されています。ここに、特定タスク用に軽量にチューニングされたLoRA (Low-Rank Adaptation) アダプターを組み合わせることで、一つの基盤モデルで複数の専門タスクを効率的に処理したいというニーズがあります。

しかし、vLLMで複数のLoRAアダプターを動的にロード・アンロードしたり、推論リクエストごとに切り替えようとすると、以下のようなエラーや課題に直面することがあります。

  • エラーメッセージ例1: RuntimeError: No adapter found for id: 1. Please load the adapter first.
  • エラーメッセージ例2: ValueError: The model does not have an adapter named 'my_lora'.
  • 課題: サーバーを再起動せずに、新しいアダプターを追加したい。
  • 課題: リクエストAにはアダプターAを、リクエストBにはアダプターBを適用したい。

本記事では、vLLMのLoRA機能を用いてこれらの課題を解決し、複数のLoRAアダプターを柔軟に管理・運用する方法を解説します。

原因の解説:vLLMのLoRAサポートと動的管理の仕組み

vLLMはバージョン0.2.0以降、公式にLoRAをサポートしています。しかし、その初期の実装では、推論エンジン(LLMEngine)の起動時にすべてのLoRAアダプターを指定してロードする方式が主でした。これでは、サーバー実行中に新しいアダプターを追加したり、使用しないアダプターをメモリから解放する「動的な管理」が困難でした。

この問題の根本的な原因は以下の2点にあります。

1. アダプターマッピングの不足

vLLMエンジンは、内部で「アダプターID」と実際のLoRA重みを紐付けるマッピングテーブルを保持しています。動的にアダプターをロードするプロセスが適切に実行されないと、リクエストで指定したアダプターIDがこのマッピングテーブルに見つからず、No adapter foundエラーが発生します。

2. メモリ管理の制約

LoRAアダプターはモデル重みに追加されるため、GPUメモリを消費します。多数のアダプターを一度にロードするとメモリ不足を引き起こす可能性があり、必要なアダプターのみを必要なタイミングでロードする仕組みが求められます。

幸い、vLLMは後続のアップデートでAsyncLLMEngineとその関連メソッドを通じた動的なLoRA管理APIを提供しており、これらを正しく使用することで問題を解決できます。

解決方法:動的ロード・切り替えのステップバイステップガイド

ここでは、vLLMのAsyncLLMEngineを使用し、サーバー起動後も柔軟にLoRAアダプターを管理する方法を説明します。OpenAI互換のAPIサーバーとして構築することを想定します。

ステップ1: 環境構築と必要なインポート

まず、vLLMをLoRAサポート付きでインストールし、必要なモジュールをインポートします。

# vLLMのインストール (LoRAサポート付き)
pip install "vllm[lora]"

# サンプルコードの主要インポート
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.engine.async_llm_engine import AsyncLLMEngine
from vllm import SamplingParams
import asyncio

ステップ2: AsyncLLMEngineの初期化(LoRA有効化)

エンジン初期化時に、LoRA機能を有効にします。ここでは基盤モデルだけをロードし、アダプターは後から動的に追加します。

# エンジン引数の設定
engine_args = AsyncEngineArgs(
    model="meta-llama/Llama-2-7b-hf", # 基盤モデル
    enable_lora=True, # LoRAサポートを有効化
    max_loras=4,       # 同時に保持できるLoRAアダプターの最大数
    max_lora_rank=16,  # LoRAランクの最大値
    dtype="half",      # メモリ節約のため半精度を使用
    gpu_memory_utilization=0.9
)

# 非同期エンジンの作成
engine = AsyncLLMEngine.from_engine_args(engine_args)

ステップ3: LoRAアダプターの動的ロード

エンジン起動後、add_loraメソッドを使用してLoRAアダプターをロードします。アダプターはローカルパスまたはHugging Face HubのIDで指定できます。

async def load_lora_adapters():
    # アダプター1をロード (感情分析用)
    await engine.add_lora(
        lora_name="sentiment-analysis", # アダプター識別名
        lora_path="./adapters/sentiment_lora/", # ローカルのアダプターディレクトリ
    )
    # アダプター2をロード (コード生成用、Hubから)
    await engine.add_lora(
        lora_name="code-generation",
        lora_path="username/code-llama-lora", # Hugging Face HubのモデルID
    )
    print("LoRA adapters loaded successfully.")

# 非同期関数の実行
asyncio.run(load_lora_adapters())

ステップ4: ロード済みアダプターを使用した推論の実行

推論リクエスト時に、lora_requestパラメータで使用するアダプター名を指定します。

async def generate_with_lora(prompt, adapter_name):
    sampling_params = SamplingParams(temperature=0.7, max_tokens=100)
    
    # リクエストの作成。lora_requestでアダプターを指定。
    request_id = "req_001"
    results_generator = engine.generate(
        prompt=prompt,
        sampling_params=sampling_params,
        request_id=request_id,
        lora_request=adapter_name # ここで動的にアダプターを選択
    )
    
    async for request_output in results_generator:
        return request_output.outputs[0].text

# 使用例
prompt = "この製品のレビューは素晴らしいです。"
result = asyncio.run(generate_with_lora(prompt, "sentiment-analysis"))
print(f"Result with sentiment adapter: {result}")

prompt2 = "def fibonacci(n):"
result2 = asyncio.run(generate_with_lora(prompt2, "code-generation"))
print(f"Result with code adapter: {result2}")

ステップ5: アダプターの動的アンロード

メモリを解放するため、不要になったアダプターは削除できます。

async def remove_lora_adapter(adapter_name):
    try:
        await engine.remove_lora(adapter_name)
        print(f"Adapter '{adapter_name}' has been removed.")
    except ValueError as e:
        print(f"Error removing adapter: {e}")

# 使用例
asyncio.run(remove_lora_adapter("sentiment-analysis"))

コード例・コマンド例:OpenAI API互換サーバーとして運用

実運用では、vLLMが提供するOpenAI互換APIサーバーを使用することが多いでしょう。--enable-loraオプションを付けてサーバーを起動し、リクエスト時にextra_bodyでアダプターを指定します。

サーバー起動コマンド

# APIサーバーの起動 (LoRA有効化)
python -m vllm.entrypoints.openai.api_server 
    --model meta-llama/Llama-2-7b-hf 
    --enable-lora 
    --max-loras 4 
    --served-model-name llama-2-lora

クライアントからのリクエスト例 (Python)

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="token-abc123"
)

# アダプターを指定してチャット補完をリクエスト
completion = client.chat.completions.create(
    model="llama-2-lora", # サーバー起動時のモデル名
    messages=[{"role": "user", "content": "How are you?"}],
    extra_body={  # ここでLoRAアダプターを指定
        "lora_name": "sentiment-analysis"
    }
)
print(completion.choices[0].message.content)

cURLを使用したリクエスト例

curl http://localhost:8000/v1/chat/completions 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer token-abc123" 
  -d '{
    "model": "llama-2-lora",
    "messages": [{"role": "user", "content": "Write a Python function to sort a list."}],
    "extra_body": {"lora_name": "code-generation"}
  }'

まとめ・補足情報

vLLMのAsyncLLMEngineとその動的LoRA管理APIを使用することで、サーバーの再起動なしに複数のLoRAアダプターをロード・切り替え・アンロードすることが可能になります。これにより、リソースを効率的に利用しつつ、単一の基盤モデルで多様なタスクに対応する柔軟な推論システムを構築できます。

重要なポイントと注意点

  • メモリ計画: max_lorasmax_lora_rankはGPUメモリに直結するパラメータです。利用可能なメモリとアダプターのサイズに基づいて適切に設定してください。
  • アダプターの互換性: ロードするLoRAアダプターは、エンジンにロードした基盤モデルと完全に互換性がある(同じベースモデル向けに学習された)ことを確認してください。互換性がない場合、推論結果が意味をなさないか、エラーが発生します。
  • パフォーマンス: 動的にアダプターを切り替える場合、最初のリクエスト時にアダプター重みをGPUメモリにロードするためのわずかなレイテンシが発生する可能性があります。ホットリクエストに対応するため、よく使うアダプターはあらかじめロードしておくなどの最適化を検討しましょう。
  • エラーハンドリング: 本番環境では、add_loragenerate呼び出し時のエラー(例: アダプターパスが存在しない、メモリ不足など)を適切に捕捉・処理するロジックを実装することをお勧めします。

この動的LoRA機能を活用すれば、マルチテナントなLLMサービスや、ユーザーごとにカスタマイズされたモデルを提供するアプリケーションの開発が格段に容易になります。

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