問題の概要: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のLoRAModelとLoraManagerを活用して複数の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エンジンの初期化
EngineArgsとLLMEngineを使用して、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.」
原因: EngineArgsでenable_lora=Trueを設定していない。
解決策: エンジン初期化時に必ずenable_lora=Trueを指定する。
エラー2: 「KeyError: ‘adapter_xyz’」または「No adapter found」警告が続く
原因: generate_with_loraを呼び出す前に、そのadapter_idでengine.add_lora()を呼んでいない。
解決策: 推論の前に必ずアダプター登録を行う。登録済みアダプターIDのリストはengine.list_loras()で確認可能。
print("Registered adapters:", engine.list_loras())
エラー3: メモリ不足エラー
原因: max_lorasやmax_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.jsonとadapter_model.bin)またはvLLMがサポートする他の形式である必要があります。 - ベースモデルとLoRAアダプターの互換性(モデルアーキテクチャ、トークナイザー)はユーザーが保証する必要があります。
このカスタムアプローチにより、vLLMの高速推論性能を維持したまま、柔軟なLoRAアダプターの運用が実現できるでしょう。