【vLLM】LoRAアダプターを動的ロードする方法と「No adapter found」エラー解決

問題の概要:vLLMでLoRAアダプターを動的に切り替えられない

vLLMは、大規模言語モデル(LLM)を高速推論するための強力なライブラリです。Fine-tuningされたモデル、特にLoRA(Low-Rank Adaptation)アダプターを活用する際、推論サーバーを再起動することなく、異なるタスクやユーザーに応じて複数のLoRAアダプターを動的にロード・切り替えたいという要求は多くあります。

しかし、vLLMの基本的な使い方では、サーバー起動時に--model--enable-loraを指定するだけでは、この動的な切り替えは実現できません。具体的には、以下のようなエラーに遭遇することがあります。

# エラーメッセージ例
vllm.lora.request_manager: WARNING: No adapter found for adapter_id: my_lora_adapter_1. The request will be handled with the base model.

この警告は、リクエストで指定したアダプターIDがサーバーに認識されていないことを意味し、結果としてベースモデル(LoRA適応なし)での推論が行われてしまいます。本記事では、この問題の原因を解説し、vLLMのLoRAModelLoraManagerを活用して複数のLoRAアダプターを動的にロード・管理する実践的な解決方法をステップバイステップで紹介します。

原因の解説:静的ロードと動的ロードの違い

問題の根本原因は、vLLMサーバーのLoRAアダプターのロード方法にあります。

1. 静的ロード(従来の方法)

一般的なvLLMサーバー起動コマンドは以下の通りです。

python -m vllm.entrypoints.api_server 
    --model meta-llama/Llama-2-7b-hf 
    --enable-lora

この方法では、サーバー起動時にLoRAサポートを有効化しますが、使用するアダプターのパスは指定していません。アダプターは推論リクエストが来た時に初めて指定することになりますが、サーバーはそのアダプターファイルを事前に認識していないため、上記の「No adapter found」警告が発生します。これは、アダプターのメタデータや重みがメモリにロードされていない状態です。

2. 必要な「動的ロード」の概念

真の意味での「動的ロード」とは、サーバー起動後、稼働中に新しいLoRAアダプターを登録(ロード)し、その後そのアダプターIDを使って推論リクエストを送信できる機能を指します。vLLMはこの機能を内部でサポートしていますが、標準のAPIサーバー(api_server)はこのための専用エンドポイントを提供していません。そのため、カスタムスクリプトを作成してvllm.engine.llm_engine.LoRAModelクラスを直接操作する必要があります。

解決方法:カスタムスクリプトによる動的LoRAロード

ここでは、vLLMをPythonスクリプトから直接呼び出し、LoRAアダプターを動的に管理する方法を説明します。

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

まず、vLLMがインストールされていることを確認します。必要なクラスをインポートします。

# lora_dynamic_manager.py
from vllm import SamplingParams
from vllm.engine.llm_engine import LLMEngine
from vllm.engine.arg_utils import EngineArgs
from vllm.lora.request import LoRARequest
import argparse

ステップ2: LLMエンジンの初期化

EngineArgsLLMEngineを使用して、LoRAを有効化したエンジンインスタンスを作成します。これはAPIサーバーの内部で行われている処理に相当します。

def init_engine(model_path: str):
    engine_args = EngineArgs(
        model=model_path,
        enable_lora=True,
        max_loras=8,          # メモリに保持する最大LoRA数
        max_lora_rank=16,     # LoRAの最大ランク
        lora_extra_vocab_size=256, # トークナイザー拡張サイズ
        max_num_seqs=256,
        max_model_len=2048
    )
    engine = LLMEngine.from_engine_args(engine_args)
    return engine

max_lorasは、エンジンが同時にキャッシュできるLoRAアダプターの最大数です。使用頻度の低いアダプターはLRU(Least Recently Used)ポリシーでメモリから削除されます。

ステップ3: LoRAアダプターの動的登録

エンジンが初期化された後、engine.add_lora(...)メソッドを使って新しいアダプターを登録します。

def register_lora_adapter(engine, adapter_id: str, adapter_path: str):
    """
    動的にLoRAアダプターをエンジンに登録する。
    """
    try:
        # アダプターをロードしてエンジンに登録
        engine.add_lora(adapter_id, adapter_path)
        print(f"Successfully registered LoRA adapter: {adapter_id}")
    except Exception as e:
        print(f"Failed to register LoRA adapter {adapter_id}: {e}")
        # 具体的なエラー例
        # FileNotFoundError: [Errno 2] No such file or directory: '/path/to/adapter'
        # ValueError: The adapter may be incompatible with the base model.

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

登録済みのアダプターを使って推論を行うには、LoRARequestオブジェクトを作成し、engine.generateの引数に渡します。

def generate_with_lora(engine, prompt: str, adapter_id: str, sampling_params: SamplingParams):
    """
    特定のLoRAアダプターを適用してテキスト生成を行う。
    """
    # LoRAリクエストを作成
    lora_request = LoRARequest(adapter_id, 1) # 第2引数はLoRAのバージョン(通常は1)

    request_id = "req_001" # 一意のリクエストID

    # エンジンに推論リクエストを追加
    engine.add_request(
        request_id,
        prompt,
        sampling_params,
        lora_request=lora_request # ここでLoRAアダプターを指定
    )

    # 結果の取得(簡略化した例。実際は非同期処理が必要)
    outputs = []
    while engine.has_unfinished_requests():
        request_outputs = engine.step()
        for output in request_outputs:
            if output.finished:
                outputs.append(output)
    # 最初のリクエストの結果を返す
    if outputs:
        return outputs[0].outputs[0].text
    return ""

ステップ5: メイン処理の実装例

上記の関数を組み合わせた、実際の動作するスクリプトの例です。

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--base-model", type=str, required=True)
    parser.add_argument("--lora-path-1", type=str)
    parser.add_argument("--lora-path-2", type=str)
    args = parser.parse_args()

    # 1. エンジン初期化
    print("Initializing LLM Engine...")
    engine = init_engine(args.base_model)

    # 2. LoRAアダプターの動的登録
    if args.lora_path_1:
        register_lora_adapter(engine, "adapter_math", args.lora_path_1)
    if args.lora_path_2:
        register_lora_adapter(engine, "adapter_code", args.lora_path_2)

    # 3. サンプリングパラメータ設定
    sampling_params = SamplingParams(temperature=0.7, top_p=0.9, max_tokens=100)

    # 4. アダプターを使い分けて推論
    test_prompt = "What is the derivative of x^2?"
    print(f"nPrompt: {test_prompt}")

    # ベースモデルで推論
    print("n--- Base Model Output ---")
    result_base = generate_with_lora(engine, test_prompt, None, sampling_params)
    print(result_base)

    # 数学用LoRAアダプターで推論
    print("n--- Math Adapter Output ---")
    result_math = generate_with_lora(engine, test_prompt, "adapter_math", sampling_params)
    print(result_math)

    # コード用LoRAアダプターで推論(登録済みの場合)
    code_prompt = "Write a Python function to calculate factorial."
    print(f"nPrompt: {code_prompt}")
    print("n--- Code Adapter Output ---")
    result_code = generate_with_lora(engine, code_prompt, "adapter_code", sampling_params)
    print(result_code)

if __name__ == "__main__":
    main()

このスクリプトは以下のように実行します。

python lora_dynamic_manager.py 
  --base-model meta-llama/Llama-2-7b-hf 
  --lora-path-1 ./my_math_lora_adapter 
  --lora-path-2 ./my_code_lora_adapter

コード例・コマンド例:よくあるエラーと対処法

エラー1: 「RuntimeError: The engine has not been initialized with LoRA support.」

原因: EngineArgsenable_lora=Trueを設定していない。

解決策: エンジン初期化時に必ずenable_lora=Trueを指定する。

エラー2: 「KeyError: ‘adapter_xyz’」または「No adapter found」警告が続く

原因: generate_with_loraを呼び出す前に、そのadapter_idengine.add_lora()を呼んでいない。

解決策: 推論の前に必ずアダプター登録を行う。登録済みアダプターIDのリストはengine.list_loras()で確認可能。

print("Registered adapters:", engine.list_loras())

エラー3: メモリ不足エラー

原因: max_lorasmax_lora_rankが大きすぎる、または物理メモリが不足している。

解決策: max_lorasを必要最小限に減らす。不要なアダプターはengine.remove_lora(adapter_id)で明示的に削除する。

# アダプターの削除
engine.remove_lora("adapter_math")
print("Adapter removed. Current:", engine.list_loras())

まとめ・補足情報

vLLMでLoRAアダプターを動的にロード・切り替えるには、標準のapi_serverではなく、LLMEngineを直接操作するカスタムスクリプトを作成する必要があります。核心となるステップは、(1) enable_lora=Trueでエンジンを初期化、(2) engine.add_lora(adapter_id, adapter_path)でアダプターを登録、(3) LoRARequestオブジェクトを生成リクエストに付与する、の3点です。

この方法を応用すれば、Web API(FastAPI等)を構築し、エンドポイントを通じてクライアントが新しいLoRAアダプターをアップロード・登録し、即座に推論に利用するといった、本格的なマルチテナント型LLMサービスを開発することも可能になります。

重要な注意点:

  • 動的ロードは便利ですが、アダプターのロード自体には計算オーバーヘッド(数秒程度)がかかります。ホットなアダプターはメモリにキャッシュされるため、2回目以降の呼び出しは高速です。
  • アダプターパスは、Hugging Face形式のアダプター(adapter_config.jsonadapter_model.bin)またはvLLMがサポートする他の形式である必要があります。
  • ベースモデルとLoRAアダプターの互換性(モデルアーキテクチャ、トークナイザー)はユーザーが保証する必要があります。

このカスタムアプローチにより、vLLMの高速推論性能を維持したまま、柔軟なLoRAアダプターの運用が実現できるでしょう。

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