ステータス:ドラフト(2026-10-07)、実測記事です。 アダプターは性能テスト用に作成した ランダムな重みなので出力品質には意味がなく、速度とリソースの挙動のみを測定しました。計算したものは「計算」、測定できなかったものは「未実測」と表記しました。 作成ルール:実測データを基本とし、測定できなかったものは「未実測」と表記します。
目次
- 1. なぜLoRAをサービングするのか
- 2. 環境と方法
- 3. スループットのコスト
- 4. アダプタースロット:アダプター数が--max-lorasを超えた場合
- 4-1. 同じテストをSGLangで行うと
- 5. 実行中にアダプターをロードするのにかかる時間
- 6. 運用への示唆
- 7. 面接で想定される質問
- 外部の根拠:論文と公式資料で確認したこと
- 確認できなかったこと
- 参考文献
- 確認したバージョンと日付
1. なぜLoRAをサービングするのか
用途ごとにモデルを微調整(ファインチューニング)すると、その分モデル全体をメモリにロードする必要があります。LoRAはベースの重みWをそのままにし、低次元の行列2つ(B、A)を学習して W + (α/r)·B·A に変更する方式であるため、 1つのベースモデルに小さなアダプターを複数アタッチし 、リクエストごとに異なるアダプターを使用できます。顧客別・業務別のモデルを1枚のGPUでサービス提供したい理由がここにあります。vLLMのドキュメントでは、アダプターをリクエスト単位で少ないオーバーヘッドでサービングできると説明されています。この記事では、その「少ない」がこの環境において何パーセントなのかを計測します。
2. 環境と方法
| 項目 | 値 |
|---|---|
| GPU | 8GBクラスのノートPC用GPU 1枚、他の作業なし |
| サーバー | vLLM 0.31.0、Qwen3-1.7B(bf16)、 --max-model-len 4096, --max-num-seqs 64, --max-lora-rank 16 |
| アダプター | PEFTで作成したLoRA 3つ(a、b、c)。ランク8、対象はアテンションのq、k、v、oプロジェクション、 lora_B をシード値が異なるランダムな値(標準偏差0.02)で初期化 |
| アダプターのサイズ | 学習可能なパラメータ3,211,264個で、ベースモデル1,720,574,976個の 0.19%(測定)。bf16であれば約6.4MB(計算) |
| リクエスト | 入力 Write a long story about a robot.、出力128トークン、温度0、 ignore_eos、リクエストごとにサーバーのモデル名でアダプターを選択 |
| 繰り返し | 同じリクエストの束をまとめて送信し、総スループット(出力トークン数 ÷ かかった時間)を計算 |
同時リクエスト数にご注意ください。 この記事の表には「一斉に送信したリクエスト数」を記載しています(3個、18個、48個)。テストツールの引数名のせいで当初は「同時1、16」と呼んでいましたが、実際には3倍のリクエストを送信していることを確認し、修正しました。
アダプターがランダムなため、出力には意味がありません。アダプター行列のサイズと演算量は学習の有無とは無関係であるため、 速度とメモリの挙動は学習済みアダプターと同じであるとみなしますが、学習済みアダプターで確認はしていません(未実測)。
3. スループットのコスト
ベースサーバー(LoRAオフ)とLoRAを有効にしたサーバーに同じリクエストを送信しました。
| 一斉に送信したリクエスト数 | サーバー | リクエストが使用するモデル | 総スループット | ベース比 | ITL中央値 |
|---|---|---|---|---|---|
| 3 | LoRAオフ | ベース | 199 tok/s | 基準 | 15ms |
| 3 | LoRAオン | ベース(アダプターなし) | 200 tok/s | +0.5% | 15ms |
| 3 | LoRAオン | アダプター a | 182 tok/s | −8.5% | 16ms |
| 48 | LoRAオフ | ベース | 2,667 tok/s | 基準 | 17ms |
| 48 | LoRAオン | ベース(アダプターなし) | 2,664 tok/s | −0.1% | 17ms |
| 48 | LoRAオン | アダプター a | 2,190 tok/s | −17.9% | 19ms |
| 48 | LoRAオン | a と b を交互に | 2,323 tok/s | −12.9% | 20ms |
| 48 | LoRAオン | a、b、アダプターなしを交互に | 2,346 tok/s | −12.0% | 20ms |
- LoRA機能を有効にしておくこと自体はタダでした。 アダプターを使用しないリクエストのスループットは、LoRAをオフにしたサーバーと同等でした(200対199、2,664対2,667)。
- アダプターを使用するリクエストはスループットが低下しました。 上記の1回ずつ計測した値は、3個のとき8.5%、48個のとき17.9%でしたが、後述の「繰り返しテスト」において48個の値は最初の測定値が低かったことが原因で膨らんでいたことが判明しました(繰り返すと約10%)。ドキュメントの「少ないオーバーヘッド」は、この環境においては 小さくない値でした。アダプターによる追加の演算は、1.7Bモデルにおいて重みの読み込みコスト(第21編)に比べて小さくないという意味として解釈しますが、どの演算がどれくらいの割合を占めているのかについては分解していません(推定)。
- 当初は「アダプターを混ぜても悪化しなかった」と記載していましたが、繰り返しの検証により覆りました。 1回ずつ計測した値では、アダプターaのみ(2,190)よりもaとbを混ぜたとき(2,323)の方に高い値が出ましたが、5回繰り返したところ、aのみは中央値2,390、混ぜた場合は2,330でした。1回だけ計測した2,190は、サーバーを立ち上げて最初に呼び出したアダプターリクエストであったために遅かった値です(後述)。
- ITLの中央値は15msから20msに増加しました。 遅延が増加した幅は、スループットの低下と同じ傾向を示しています。
- モデルが大きくなっても割合は減少しませんでした。 このモデルは1トークンあたり約15msと小さいため、アダプターの固定コストが相対的に大きいと予想していました。そこでQwen3-4B-FP8(1トークンあたり約19ms、同じ方法で作成したランダムなアダプター、スロット2)でサーバーを2回立ち上げて測定したところ、アダプターaのスループットの変化は、同時1で両方とも−11%(155から138 tok/s)、同時16で−18.3%と−10.7%でした。−18.3%側はKVが6,320トークンだったサーバー、−10.7%側は10,896トークンだったサーバーです(第14編)。アダプターa、b、cを混ぜた同時6は397、396 tok/sであり、スロット不足による急落もそのまま見られました。精度(fp8)が異なるため、1.7Bとの厳密な比較ではありません。
繰り返しテスト(各5回、同一サーバー)。 リクエスト48個(同時16の3倍)を、アダプターなし、アダプターa、アダプターaとbを交互に、の3つの条件で順序をシャッフルしながら5回ずつ送信しました(--max-loras 2).
| 条件 | スループット(tok/s)、5回 | 中央値 | ベース比 |
|---|---|---|---|
| アダプターなし | 2,647, 2,658, 2,669, 2,663, 2,667 | 2,663 | 基準 |
| アダプター a | 2,183, 2,398, 2,390, 2,391, 2,390 | 2,390 | -10.3% |
| アダプター a と b を交互に | 2,329, 2,330, 2,332, 2,329, 2,332 | 2,330 | -12.5% |
- 最初の測定値のみが低くなりました。 アダプターaの最初の値2,183は、サーバーを立ち上げた後に初めてアダプターを呼び出したときの測定値であり、それに続く4つの値(2,390〜2,398)は安定していました。最初のアダプター使用時にロードと初期化のコストが混ざったものとみられます(分解はしていません)。先ほどの1回ずつ計測した値が17.9%と大きく見えた理由はここにあります。ベンチマークの 最初の値はウォームアップとして捉え、繰り返し測定して中央値を使用すべきだという事例です。
- アダプターのコストは約10〜12% です。アダプターが1つのときの方が、2つを混ぜたときよりもわずかに安価でした(-10.3%対-12.5%)。繰り返し間でのばらつきは、アダプターなしが±0.4%、アダプターa(最初の値を除く)が±0.2%、aとbが±0.1%と小さく、この差には意味があります。
- 上記の3リクエストテストでの-8.5%(182対199)も1回ずつ計測した値であるため、ウォームアップが混ざっていた可能性があります(繰り返しなし)。
4. アダプタースロット:アダプター数が --max-loras を超えた場合
--max-loras は、 1つのバッチ内に含めることができる異なるLoRAの数です(デフォルトは1)。当初は「GPU上に同時にロードしておけるアダプターの数」と記載していましたが、この記事で使用したvLLM 0.31.0イメージのソース(vllm/config/lora.py)を直接確認したところ、説明文は「Max number of LoRAs in a single batch」となっており、CPU側に配置するアダプター数は別の --max-cpu-loras(デフォルトは max_loras と同じ値)でした。アダプター3個でリクエスト18個を同時に送信しました(各設定2回繰り返し。2回の値がほぼ同じであることが多かったですが、スロット2つのアダプターa、bは886と984で異なっていました)。
| サーバー設定 | アダプターなし | アダプター a、b | アダプター a、b、c |
|---|---|---|---|
--max-loras 2 |
1,120 tok/s | 886〜984 tok/s | 529 tok/s |
--max-loras 4 |
1,131 tok/s | 986〜987 tok/s | 950 tok/s |
- スロットが不足するとスループットがほぼ半分になります。 スロット2つでアダプター3個を交互に使用した場合、529 tok/sとなり、スロット4つの場合の950 tok/sの56%、アダプターなしの基準(1,120)の47%となりました。 原因はソースコードで確認しました。 同じイメージのスケジューラーのソース(
vllm/v1/core/sched/scheduler.py)では、待機中のリクエストを選択する際、すでにバッチに含まれている異なるLoRAの数がmax_lorasに達しており、そのリクエストのLoRAがバッチに含まれていない場合、そのリクエストをスキップしてキューに留置します(コメントは「Scheduling would exceed max_loras, skip」)。つまり、スロット2つでアダプター3個が交互に出現する場合、3つ目のアダプターのリクエストが継続して後回しにされ、バッチが小さくなり、その分総スループットが低下します。アダプターを切り替えて待機する時間が原因だと記述していた当初の推測は、このソースコードの事実に基づいて修正しましたが、後回しにされたリクエストの待機時間の分布や実際のバッチサイズはサーバーのメトリクスからは読み取っていません(未確認)。 - スロットが十分であれば、アダプターの数が増えてもコストはほぼ変わりません。 スロット4つにおいて、アダプター2個(987)と3個(950)の差は4%でした。
- したがって、
--max-lorasは「ある時点の同じバッチで使用されるアダプターの数」に合わせて設定します。 アダプターが数百個あっても、同時にリクエストに登場するアダプターの数がスロット数の範囲内であれば問題ありませんが、そうでなければ上記のように半分程度の水準まで低下する可能性があります。スロットはアダプターの重みと演算スペースを事前に確保するためGPUメモリを使用しますが、このサーバーでは、スロットを有効にしたサーバーの総使用メモリは6,144MiBであり、有効にしていないサーバー(6,168MiB)と同等でした(KVキャッシュがその分縮小するかたちで吸収されたとみられますが、KVの容量は確認していません)。
4-1. 同じテストをSGLangで行うと
同じモデルと、同じランダムなアダプター3個(rank 16、パラメータの0.19%)を使用して、SGLang 0.5.21のLoRA(--enable-lora --max-loras-per-batch N)を測定しました。
| テスト | SGLang | vLLM(上記第3節、第4節) |
|---|---|---|
| 同時16(リクエスト48個)、アダプターなし | 2,544 tok/s(サーバー3回の中心値、2,533〜2,552) | 2,664(LoRA有効サーバーのアダプターなしリクエスト) |
| 同負荷、アダプター a | 2,515 tok/s(-1.1%、サーバー3回の中心値、2,509〜2,518) | 2,390 tok/s(-10.3%、5回の中央値) |
| リクエスト18個、アダプター2個を混合、スロット2 | 1,045 tok/s(アダプターなし 1,027) | 886〜984 tok/s(アダプターなし 1,120) |
| リクエスト18個、アダプター3個を混合、スロット2 | 558 tok/s | 529 tok/s |
| 同テスト、スロット4 | 1,034 tok/s(サーバー3回の中心値、1,030〜1,038、初回テスト 1,045) | 950 tok/s |
- アダプターあたりのコストはSGLangの方が小さかった(-1.1%対-10.3%。当初はSGLangの値が1回計測しただけなのでvLLMのように最初の値が低い可能性があると書きましたが、サーバーを3回再起動して計測したところ、アダプターaの減少は-0.9%、-1.3%、-1.1%とほぼ同じで、最初の値が低くなる現象はありませんでした。同時実行1では199から194 tok/s(-2.0〜-2.5%)でした。両エンジンのLoRAカーネルとバッチ処理方式が異なるという手がかりですが、原因は分解していません(推測)。この結果はこのモデルとランク16のアダプターでの値であるため、一般化はしません。
- スロットが不足する崖っぷちは両エンジンで同じでした(これらの値もそれぞれ1回ずつ計測したものです)。1つのバッチに互いに異なるアダプターがスロットより多い場合、スループットが半分近くに低下し(SGLang 558対1,045、vLLM 529対950)、スロットをアダプター数以上に増やすと回復しました。スロット数がアダプターの種類数より少いってはならないという第4節の結論は、エンジンに関わらず有効でした。
- 限界:同じランダムアダプターであるため、実際に学習されたアダプターとは異なる可能性があります。SGLangのアダプター1つとスロット4の値はサーバー3回で繰り返しましたが、スロット2の崖っぷち(558、529)は1回ずつ計測した値です。
5. 実行中にアダプターを適用するのにかかる時間
VLLM_ALLOW_RUNTIME_LORA_UPDATING=True でサーバーを立ち上げた後、 /v1/load_lora_adapter でアダプターcを追加しました。
| 項目 | 結果 |
|---|---|
| ロードリクエストの応答 | LoRA adapter 'c' added successfully、呼び出し全体0.18秒(リモートコマンド実行オーバーヘッドを含む) |
| ロード直後の最初のリクエスト | 3つのリクエスト 182 tok/s、アダプターaと同じ速度 |
- アダプター1つ(6.4MB)の適用は、サーバーの再起動なしで0.2秒以内に完了しました。 サーバーの再起動が35〜55秒(第14話)であるのとは対照的です。アダプターのデプロイがモデルのデプロイよりもはるかに軽量であることが、LoRAサービングの運用上の利点です。
- ただし、この機能は本番環境で使用しないようにという警告がドキュメントに記載されています。 vLLMのドキュメントには、実行時のアダプター変更が隔離され完全に信頼できる環境でない限り、本番環境で使用しないようにと記載されています。リクエストによって任意のパスのアダプターがロードされる可能性があるため、セキュリティ上のリスクであると理解しています(直接確認していない解釈です)。本番環境では開始時に
--lora-modulesで指定するか、アダプターのデプロイをパイプラインで制御する方が安全です。 - サーバーの準備時間は、LoRAを有効にすると70秒となり、有効にしていないサーバー(35〜55秒で再起動)よりも長くなりました。LoRAを有効にしたサーバーは、CUDAグラフをより多くの形態でキャプチャしていました(ログで38個対19個)。
6. 本番環境への持ち帰り
- アダプターを使用するリクエストのスループットコストを事前に測定します。 この環境では8〜18%であり、モデルと同時実行性によって異なります。
--max-lorasを同時に登場するアダプターの数に合わせます。 スロットが不足すると半分程度の水準に低下します。- アダプターのデプロイは軽量ですが、無制限のロードは危険です。 パイプラインで制御し、検証済みのものだけを適用します。
- LoRAを有効にしたままにしておくことのコストはありませんでした。 アダプターを使用しないリクエストには影響がありませんでした。
- 学習されたアダプターやより大きなモデルでは再度測定する必要があります。 この記事の数値は、小型モデル(1.7B)と4B-FP8の1点、ランダムアダプターから得られたものです。
7. 面接で聞かれそうな質問
- LoRAアダプターを複数サービングする理由は? ベースモデルを1つ共有し、アダプター(ベースモデルの0.19%水準)のみを切り替えて使用するため、用途別に全体モデルをロードするよりもGPUメモリがはるかに少なくて済む。
- アダプターを使用するとどれくらい遅くなるのか? この環境では1.7Bモデルはリクエスト48個基準で約10〜12%減少した(5回繰り返しの中央値、最初の1回ずつ計測した17.9%はウォームアップが混ざった値)。LoRAを有効にしておくことだけによるコストはなかった。
--max-lorasは何を決定するものか、不足した場合は? 1つのバッチ内に収めることができる互いに異なるLoRAの数(デフォルト1)である。アダプターがこの数を超えると、スケジューラーがそのアダプターのリクエストをスキップし、バッチが小さくなる。アダプター3つをスロット2つでサービングすると、スループットが529 tok/sになり、スロット4つの950の56%になった。- アダプターのデプロイがモデルのデプロイよりも簡単な理由は? アダプター6.4MBをサーバー再起動なしで0.2秒でロードした。モデルは再起動35〜55秒に重みのロードが加わる。
- ランタイムでのLoRAロードを本番環境で使用しない理由は? ドキュメントでは、隔離された信頼できる環境でない限り使用しないよう警告している。
外部の根拠:論文と公式資料で確認した内容
外部の数値はモデル、GPU、アダプターの構成が異なるため直接比較せず、傾向と桁数の一致のみを判定しました。
| トピック | 資料と確認した場所 | 確認した内容と条件 | この記事との関係 |
|---|---|---|---|
| LoRAのサイズ | Hu他, 「LoRA: Low-Rank Adaptation of Large Language Models」, arXiv 2106.09685(ICLR 2022), 概要および4.2節 | GPT-3 175Bで学習パラメータを最大10,000倍、チェックポイントを350GBから35MBに削減(ランク4、クエリとバリューの投影のみ適用)。推論遅延が増加しないということは W0 + BA で統合された単一のアダプターに対してのみ成立し、異なるアダプターを1つのバッチに混在させることは統合された状態では困難であると論文の限界に記載されている |
支持。 我々のアダプターはベースモデルの0.19%(3,211,264個、モデル設定から計算して一致)。第3節の8.5〜17.9%の減少は、統合せずにリクエストごとに選択する方式のコスト |
| マルチアダプターサービングのスループット | Chen他, Punica, MLSys 2024, arXiv 2310.18547, 第7節 図11。Sheng他, S-LoRA, MLSys 2024, arXiv 2311.03285, 第7.2節 表3 | Punica: A100 80GB 1枚、Llama-2 7B・13B、ランク16。同じアダプターのみを使用する場合、バックボーン専用のvLLMよりも7Bが約8.4%、13Bが約12.2%低い(論文の数値から計算)かつトークンあたり約2msの追加遅延。S-LoRA: アダプターをホストメモリに置き、実行中のリクエストの分だけGPUに持ち込むことで、アダプターが5個から100個に増えてもスループットを維持、わずかな減少 | 限定的な支持。 アダプターを使用するリクエストのスループット減少と桁数が同じ。「スロット超過時の半分」に対応する外部数値はなく、S-LoRAはアクティブなアダプター数がコストを決定すると説明しており、この現象がvLLMの max_loras 構成に特有のものである可能性を示唆 |
--max-loras の意味 |
vLLM 0.31.0 ソース config/lora.py, v1/core/sched/scheduler.py(この記事で使用した画像から直接読み取り) |
1つのバッチ内の異なるLoRAの数(デフォルト1)。制限に達すると他のアダプターのリクエストをスキップする。 max_cpu_loras は別個(デフォルトは max_lorasと同じ) |
最初の記述を訂正。 上記第4節 |
| オーバーヘッドとランタイムロード | vLLMドキュメント 「LoRA Adapters」(2026-09-28表記) | アダプターをリクエスト単位で少ないオーバーヘッドでサービングすると記載されており、定量的数値はない。 VLLM_ALLOW_RUNTIME_LORA_UPDATING は隔離された信頼できる環境でない限り本番環境で使用しないよう警告があり、具体的な理由は記載されていない |
限定的な支持。 「少ないオーバーヘッド」はこの環境では8.5〜17.9%と小さくなかった |
確認できなかったこと
- 学習されたアダプターでの速度と出力品質。 ランダムアダプターで速度のみを測定しました。
- 大きなモデル、より高いランク、アダプターがすべての線形層を対象とする場合ののコスト。
- 繰り返し誤差。 スロットテストのみ2回繰り返し、第3節は1回です。aとbを混ぜた値がa単体よりも高く出た違いは解釈していません。
- 延期されたリクエストの待機時間とバッチサイズ。 サーバー指標(Prometheus)を読み取っていません。
--max-cpu-lorasなどのCPU側キャッシュ設定の効果と数百個規模のアダプター。- アダプターが存在する場合のプレフィックスキャッシュとの相互作用(アダプターごとにキャッシュが分離されるかどうか)。
参考資料
- vLLMドキュメント, LoRA Adapters: https://docs.vllm.ai/en/latest/features/lora/
- Hugging Face PEFT, LoRA: https://huggingface.co/docs/peft/package_reference/lora
- このシリーズの第14話(vLLMの負荷とサーバー準備時間)、第21話(トークン速度とメモリ帯域幅)
確認したバージョンと日付
2026-10-07基準。vLLM 0.31.0、PEFT 0.21.2、accelerate 1.15.0、Qwen3-1.7B(bf16)。