セルフホストLLMが5人で遅くなる理由とは?
1人なら正常なのに5人で遅い原因は、Ollamaの並列数のデフォルトが1だからです。バッチ処理、KV cache、Prefill、キュー深度が同時接続数を決めます。
ユーザーが増えるとセルフホスト LLM の速度が低下するのはなぜですか?
セルフホスト LLM が同時接続 5 ユーザーで停止したようになるのは、サーバーが依然として 1 回に 1 つの応答しか生成せず、残りの 4 ユーザーがキューで待機するためです。Ollama のドキュメントでは、デフォルト値について明確に説明されています。OLLAMA_NUM_PARALLEL は「各モデルが同時に処理する並列リクエストの最大数で、デフォルトは 1」です。故障ではありません。5 人のうち 4 人が順番を待っています。
解決策は、より大きなサーバーを用意することとは限りません。必要なのは、同じ forward pass で多数のリクエストをモデルに処理させる serving engine と、その処理中に全員の会話を保持できる十分な余剰メモリです。どちらも重要ですが、上限を実際に決めるのは後者です。
すべてのリクエストが通る 2 つのフェーズ
Prefill はプロンプト全体を一度に読み込み、プロンプト用の attention cache を構築します。すべてのプロンプトトークンが同時にモデルを通過するため、Prefill は 1 回の大規模な行列乗算となり、演算スループットの制約を受けます。続く Decode では、回答を 1 トークンずつ生成します。各トークンの生成時に、モデルの全重みを毎回メモリから読み出す必要があります。一方、その 1 トークンに対して実行する演算量はごくわずかです。そのため、Decode はメモリ帯域幅の制約を受けます。
この非対称性が、バッチ処理が有効になる理由です。1 ユーザーの Decode では、1 トークンごとに、たとえば 5 GB の重みを読み出しますが、演算ユニットの大部分はアイドル状態になります。2 つ目のリクエストを追加すると、エンジンは同じ 5 GB を 1 回だけ読み出し、その重みを使って 2 つのトークンを計算します。2 人目のユーザーによる追加時間はほとんどありません。リクエストを厳密に 1 件ずつ処理すると、この効果が失われます。
ユーザーが体感する性能は、2 つの数値で表せます。TTFT (time to first token) は、キューでの待ち時間と Prefill の合計です。ITL (inter-token latency) は、ストリーミングされるトークン間の間隔で、Decode によって決まります。サーバーが遅い場合、通常はこのどちらかが原因です。必要な対策は原因によって異なります。
最も遅い応答を待たせる静的バッチ処理
静的バッチ処理は単純な方式です。アプリケーションコードでリクエストを自分でまとめると、この方式になります。エンジンは N 件のリクエストを集めて同時に処理し、グループ内で最も生成に時間がかかるリクエストが完了するまで、すべてのスロットを保持します。
1 人のユーザーが 1,200 トークンの要約を要求すると、1 行で済む回答 4 件もバッチに拘束されます。バッチは最も遅いリクエストが完了するまでスロットを解放しないためです。
これにより、2 つのコストが発生します。生成を終えたシーケンスは、有効な計算を何も行わないままスロットを占有し続けます。そのため、出力長が異なると実効スループットが低下します。チャットでは出力長のばらつきが特に大きくなります。バッチの形成から 1 ステップ後に到着したリクエストは、prefill を開始する前にバッチ全体の処理が完了するまで待たされます。その結果、その TTFT は別のリクエストの長文によって決まります。
連続バッチ処理は、トークンごとにリクエストを受け入れ、完了したリクエストを解放します
連続バッチ処理では、1 回のデコードステップ単位でスケジューリングします。各ステップの後、スケジューラは停止トークンを出力したシーケンスを削除し、空いたスロットに待機中のリクエストを受け入れます。step 40 で終了した応答は、バッチの終了時ではなく、step 40 の時点でスロットを解放します。
これは特殊な仕組みではありません。llama-server では、-cb, --cont-batching を「連続バッチ処理(別名 dynamic batching)を有効にするかどうか(デフォルト: enabled)」として説明しています。vLLM もこの考え方を中心に構築されています。Ollama も並列リクエストを処理します。デフォルトでは同時実行数が 1 に制限されるだけです。そのため、実際には構成で無効にしているにもかかわらず、ハードウェアが同時実行に対応していないと判断する人が多くいます。
公開されている連続バッチ処理の結果は通常、余剰の計算能力と数十 GB のキャッシュ領域を備えたデータセンター向けカードで測定されています。結果の傾向は手元のマシンにも当てはまります。ただし、結果の規模はそのまま当てはまりません。その理由は、以下のメモリのセクションで説明します。
Prefill は同じ計算資源を decode と奪い合います
4 件の応答をストリーミング配信している間に新しいリクエストが到着すると、まずそのプロンプトを prefill する必要があります。prefill は計算負荷が高い処理です。スケジューラーが prefill に専用のステップを割り当てると、その間、ストリーミング中の 4 人のユーザーにはトークンが届きません。長いプロンプトでは、開いているすべてのウィンドウで目に見える停止が発生します。誰かが送信するたびにサーバーが引っかかる、と言うときの stutter はこの状態を指します。
Chunked prefill は長いプロンプトを分割し、各部分を実行中の decode と同じステップに混在させます。vLLM のチューニングガイドは、このトレードオフを明確に説明しています。小さいチャンク予算では「decode を遅らせる prefill が少ないため、ITL が向上」します。一方、高い値では「1 回のバッチでより多くの prefill トークンを処理できるため、最初のトークンまでの時間(TTFT)が向上」します。返信が始まるのを待つユーザーを優先するか、テキストのストリーミングを見ているユーザーを優先するかを選ぶことになります。
この影響の大きさはプロンプトの長さで決まります。6,000 トークンのプロンプトに対して回答が 200 トークンの場合、200 回の decode ステップに対して 6,000 トークン分の prefill 処理が必要です。検索拡張チャットや長いシステムプロンプトでは、この状態になりやすいため、prefill は無視できる処理ではなく、ユーザーが待つ主な要因になります。長い部分が繰り返される場合は、プレフィックスキャッシュが役立ちます。vLLM は --enable-prefix-caching を提供しており、リクエストごとに再計算する代わりに、共有されたプロンプトプレフィックスのキャッシュを再利用できます。
最初に枯渇するメモリは KV キャッシュです
アクティブな会話の各トークンはすべて、モデルの各レイヤーに key ベクトルと value ベクトルを残します。これが KV キャッシュ(key/value cache)です。decode で新しいトークンを生成するたびに、プロンプト全体を再計算せずに済むのは、このキャッシュがあるためです。トークンあたりのサイズは、モデルの構造によって決まります。計算式は、2(key と value の 1 個ずつ)×レイヤー数×key/value ヘッド数×ヘッド次元×値あたりのバイト数です。これらの値はモデルの config.json から読み取ります。
一度計算すれば、上限を正確に把握できます。レイヤー数が 36、key/value ヘッド数が 8、ヘッド次元が 128 で、キャッシュを 16-bit で保持する一般的な 8B モデルでは、トークンあたり 2 36 8 128 2 bytes が必要です。これは 147,456 bytes、約 144 KiB です。8,192 トークンの会話 1 件には、約 1.2 GB のキャッシュが必要になります。5 件では、重みに加えて約 6 GB が必要です。これが、何人のユーザーを収容できるかという問いへの実際の答えです。
同時実行数が増えると、必要なコンテキストも増えます。ツールの説明にも明記されています。Ollama の FAQ には、次のようにあります。「特定のモデルで並列リクエスト処理を行うと、並列リクエスト数に応じてコンテキストサイズが増加します。たとえば、2K のコンテキストで 4 件の並列リクエストを処理すると、コンテキストは 8K になり、追加のメモリ割り当ても発生します。」必要な RAM は、OLLAMA_NUM_PARALLEL に OLLAMA_CONTEXT_LENGTH を掛けた値に比例します。llama-server では、-c で指定したコンテキストを -np スロットに分配します。そのため、スロット数だけを増やすと、各リクエストが保持できるコンテキストは小さくなります。各スロットのコンテキストは推測せず、起動ログから読み取ってください。
vLLM は代わりに、メモリを事前に確保します。--gpu-memory-utilization(デフォルトは 0.92)は「モデル実行器に使用する GPU メモリの割合」です。重みを確保した後に残ったメモリが paged KV プールになります。このプールが不足すると、vLLM はリクエストを失敗させず、スケジューラーによってリクエストを退避させます。
WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.vLLM の V1 エンジンでは、デフォルトの preemption モードは RECOMPUTE です。そのため、退避されたリクエストはキャッシュを破棄し、再び受け入れられたときに prefill をやり直します。この処理は 2 回実行されます。ドキュメントでは、「preemption と recomputation はエンドツーエンドのレイテンシーに悪影響を及ぼす可能性があります」と警告しています。平均値が正常に見える一方で、あるユーザーだけが大幅に長く待たされた理由を説明するうえで、このログ行が最も重要な手掛かりになります。累積件数を記録するには disable_log_stats=False を設定するか、vLLM が公開する Prometheus メトリクスから preemption カウンターを読み取ってください。
2、5、20人が同時利用する場合の変化
2人の場合。 余裕のあるキャッシュを備えた GPU では、ほとんど影響ありません。2本目のデコードストリームは、1本目にわずかな追加時間で並行して処理できるためです。RAM が 4~8 GB の CPU 専用 VPS では、負荷はゼロではありません。両方のストリームが同じ少数の vCPU と同じ RAM 帯域を共有するため、各ユーザーが得られる tokens per second はおよそ半分になり、キャッシュの要求量は2倍になる一方、利用できる容量は大幅に少なくなります。
5人の場合。 ここからはデフォルト設定だけでは不十分になり、まずキューの問題として現れます。OLLAMA_NUM_PARALLEL が 1 の場合、長い回答を要求したユーザーの処理が終わるまで、残りの4人が待つことになります。ただし、順番が来れば各ユーザーは通常の速度で処理されます。並列数を増やすと、問題の形が変わります。コンテキスト長がそれぞれ 8K の5スロットには、40K トークン分のキャッシュを確保する必要があります。VRAM に収まらない場合、エンジンはレイヤーをシステム RAM にオフロードします。RAM にも収まらない場合はサーバーが swap を使用し、tokens per second が急落します。
20人の場合。 チャット UI を使う20人が、通常は20件の同時リクエストを発生させるわけではありません。ハードウェアを購入する前に、この点を理解することが最も重要です。ユーザーは回答を読み、ターンの間に 20~60 秒ほど考えるため、セッションの大半はアイドル状態です。一方、20個のエージェントや20件の文書要約ジョブは、アイドル時間のない20本の実際のストリームです。必要になるサーバー構成はまったく異なります。
同時実行ユーザーですか。それともログインしているだけですか?
必要なサイズを決める前に、処理中のリクエスト数を算出します。計算は単純です。処理中のリクエスト数は、ユーザー数に、1ターンの生成時間を掛け、ターン間の秒数で割った値です。
- まず、自分の環境で単一ストリームの速度を測定します。prefill と decode の両方を確認してください。他の人のカードの数値を流用せず、自分の環境で tokens per second を測定するで得た値を使用します。
- 稼働率を見積もります。20 人のチャットユーザーがいて、1ターンあたり12秒生成し、90秒ごとに1ターン実行する場合、20 * 12 / 90 となり、処理中のリクエストは約2.7件です。
- スロット数をその値より少し大きく設定し、メモリ容量とも照合します。スロット数に1リクエストあたりのコンテキストを掛けた量が、実際に使用できるキャッシュトークン数に収まる必要があります。
- キューを短く保ち、容量超過時には速やかに、かつ明確に失敗するようにします。
使用可能なキャッシュトークン数は、重みを読み込んだ後の空きメモリを、前のセクションで示したトークンあたりのコストで割って求めます。24 GB のカードで 8B モデルを16-bitで実行すると、重みに約16 GBを使用し、デフォルトの利用率ではキャッシュとして約6 GBを使用できます。これは、8Kの会話を約5件収容できる容量です。さらに収容するには、1リクエストあたりのコンテキストを短くするか、キャッシュを8-bitで保存します(llama-server は --cache-type-k q8_0 を必要とします)。どちらも何かを犠牲にして同時実行数を増やす方法です。ハードウェアに投資する前に、このトレードオフを正直に説明した GPU VPS が API トークンに対して損益分岐点に達する条件を読む価値があります。
Ollama のデフォルト設定では不十分になる場合
並列数はサービス unit で増やしてください。シェルの export は systemd が管理するデーモンに引き継がれないためです。
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama pssystemctl show では、直前に設定した 3 つの変数が表示されるはずです。表示されない場合は drop-in が保存されていないため、その後に行う操作もすべて無効です。続いて ollama ps で、読み込まれたモデルのサイズを確認します。このサイズは重みだけの場合より大きくなります。8,192 トークンを 4 スロットで使用すると、モデルの横に 32,768 トークン分のキャッシュが確保されるためです。すべてを GPU に載せる想定だったのに、PROCESSOR 列にモデルの一部が CPU と表示される場合は、カードに残っていた容量を超えるキャッシュを要求しています。2 つの数値のいずれかを下げてください。
キューのデフォルト値も見直す必要があります。Ollama は最大 OLLAMA_MAX_QUEUE 件のリクエストをキューに入れます。「デフォルトは 512」です。この値を超えると、「サーバーが過負荷状態であることを示す 503 エラー」を返します。同時に 4 件しか処理できないサーバーで深さ 512 のキューを設定しても、守れない約束になります。300 番目のクライアントは、自分の順番が来るはるか前にタイムアウトするためです。短いキューなら、アプリケーションで再試行または報告できるエラーを返せます。いつまでも完了しないスピナーを表示するより適切です。
実際にテストしてください。2 つの端末から同じタイミングでリクエストを送信し、両方を監視します。2 番目のリクエストが 1 番目の処理完了まで何も返さない場合、並列設定は反映されていません。
本格的な推論サーバーが導入コストに見合う場合
vLLM は、余裕のある GPU があり、実際に 4 件を超えるリクエストを同時に処理している場合に、追加の構成作業に見合います。スケジューラはトークン単位で動作し、キャッシュはページングされるため、空いた断片を再利用できます。その結果、余っている VRAM をアイドル状態にせず、同時実行数へ振り向けられます。2026 年 8 月時点で、公式に記載されているインストールと起動の手順は次の 2 コマンドです。
uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instructcurl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-1.5B-Instruct",
"messages": [{"role": "user", "content": "Say hello."}]
}'choices 配列を含む応答が返れば、サーバーが起動し、モデルが読み込まれています。負荷がかかっている場合に重要な設定は 2 つです。--max-num-seqs は「1 回の反復で処理するシーケンスの最大数」、--max-num-batched-tokens は「1 回の反復で処理できるトークンの最大数」です。前者は同時実行数を制限します。後者は、前述したチャンク化された prefill の予算です。
同時に処理するリクエストが 4 件未満の場合や、対応する GPU がないサーバーでは、vLLM は複雑さが増す一方で、得られる効果は小さいです。vLLM は CUDA クラスのカードを前提とし、起動時にメモリの大部分を確保します。4〜8 GB の VPS では適切な選択ではありません。その場合は、コンテキストを短くした小さなモデルと、自分で制御するキューを使用します。推論サーバーとしての Ollama と vLLM の違い では選択基準を詳しく説明し、VPS で Qwen 3 8B を実行する では、追加ユーザーを 1 人も増やす前に中規模モデルが必要とするリソースを示します。
昔からの通説が隠しているトレードオフ
Continuous batching は総スループットを向上させ、通常は中央値のレイテンシも改善します。キューに入ったリクエストがより早く開始されるためです。
一方、テールレイテンシは悪化します。この点はほとんど語られません。
1 つのステップに追加されるシーケンスごとに少しずつ処理量が増えるため、バッチが埋まるにつれて全ユーザーの ITL が上昇します。新しく到着したリクエストの prefill は、ストリーミング中のユーザーが本来利用できたはずのステップの一部を使用します。キャッシュに負荷がかかると、スケジューラーは処理を preempt します。その結果、生成途中のリクエストが prefill の最初に戻されます。
チャット UI で現れるのは平均値ではなく、テールです。文の途中でストリームが 2 秒間停止すると、完了までの総時間が良好でも、壊れているように感じられます。想定する負荷の下で p95 TTFT と p95 ITL を測定し、平均 tokens per second は体験の説明ではなく、容量を示す値として扱ってください。
実運用での設定は、ここから導けます。メモリが許容する値を少し下回るように同時実行数を制限し、エンジンが preempt する必要をなくします。深いバッチが処理の入れ替えを繰り返すより、短く予測可能なキューの方が適しています。4 秒待った後に滑らかにストリーミングされる方が、すぐに開始しても 2 回停止するより、ユーザーの満足度が高いためです。
速度が遅い場合の確認事項
各ユーザーの処理速度は正常ですが、待ち時間が長い場合。 これは速度の問題ではなく、キューの問題です。まず並列実行の設定を確認します。モデルは正常に処理されており、1 件ずつリクエストを処理しています。
Ollama から HTTP 503 が返る場合。 キューが満杯です。サーバーが実際に処理能力の上限に達しているか、負荷を意図的に逃がすために OLLAMA_MAX_QUEUE が低く設定されています。後者は想定どおりの動作です。
CPU サーバーで負荷時に tokens per second が急落する場合。 発生中に vmstat 1 を実行します。si 列と so 列が 0 以外なら、マシンで swap が発生しています。そのため、各トークンの生成時に重みがディスクから読み込まれています。設定変更だけでは解決できません。モデルサイズまたはスロット数を減らします。
10 人に 1 人だけ、他のユーザーより大幅に長く待たされる場合。 vLLM のログで preempted を検索します。通常の原因は preemption と、その後の再計算です。許可しているコンテキスト長に対して、キャッシュの容量が不足しています。
サーバーがアイドル状態でも TTFT が悪い場合。 これは並列実行ではなく prefill の問題です。長いプロンプトでは、最初のトークンが表示されるまでに実時間がかかります。ハードウェアを確認する前に、プロンプトのサイズと prefix caching を確認します。
FAQ
自分でホストしている LLM は、2 人目が使うと遅くなるのはなぜですか?
多くの場合、実際には遅くなっていません。キューに入っています。Ollama の OLLAMA_NUM_PARALLEL はデフォルトで 1 のため、2 つ目のリクエストは 1 つ目が最後のトークンを出力するまで待機します。1 人のストリームを計測しながら、別のユーザーを待機させて 2 つのケースを区別します。処理開始後の tokens per second が通常どおりなら、原因はキューです。この場合は並列数を増やすと解消します。2 つのストリームがどちらも半分の速度で動くなら、メモリ帯域幅を実際に共有しており、ハードウェアの制限です。
小型 GPU 1 台で同時に何人のユーザーに対応できますか?
ユーザー数ではなく、メモリ量を基準にします。まず重みが必要です。次に KV cache が必要です。KV cache の容量は、アクティブな会話ごと、トークンごとに、2 × layers × key/value heads × head dimension × bytes です。一般的な 8B モデルで layers が 36、key/value heads が 8、head dimension が 128 の場合、16-bit では 1 トークンあたり約 144 KiB です。8,192 トークンの会話には、およそ 1.2 GB が必要です。16-bit でこのモデルを保持する 24 GB のカードでは、cache 用に約 6 GB が残ります。これはコンテキストを最大まで使う場合、約 5 会話分です。コンテキストを短くすれば、さらに増やせます。
continuous batching によって、各ユーザーへの返信は遅くなりますか?
全体のレイテンシは通常改善します。リクエストがバッチ全体の完了を待たなくてよくなるためです。一方、tail latency は悪化します。追加された各 sequence はすべての decoding step に処理を加えます。新しく到着したリクエストの prefill は、ストリーミング中のユーザーが使う step の一部を奪います。また、preempt されたリクエストでは prefill が 2 回必要です。平均値ではなく、p95 の inter-token latency を計測してください。平均値では隠れる一時停止も、チャット画面では明確に分かるためです。
OLLAMA_NUM_PARALLEL を増やすべきですか?それとも vLLM に移行すべきですか?
まず並列数を増やしてください。費用はかからず、drop-in file 1 つで設定できるためです。4 人が 1 つの長い回答の後ろで待機するような一般的なケースを解消できます。制限要因はメモリです。並列リクエストが増えると、保持が必要なコンテキストも増えるため、layers が CPU に spill していないか確認してください。GPU の VRAM に余裕があり、実際に 4 件を超えるリクエストが同時に処理中になっている場合は、vLLM への移行を検討します。その規模になると、paged cache と per-token scheduling による効果がコストを上回るためです。
CPU コアを増やせば、遅い LLM サーバーは改善しますか?
ユーザーが最も実感する部分には、通常効果がありません。Decode では各トークンの生成時にモデル全体をメモリから読み込むため、RAM の帯域幅が制約になります。帯域幅が飽和すると、CPU コアを増やしても改善しません。Prefill は CPU コア数に応じてスケールするため、コアを増やすと長いプロンプトの time to first token は短くなります。4 GB から 8 GB の VPS では、通常はメモリ容量が制約要因です。そのため、vCPU を増やすよりも、小さいモデルまたは短いコンテキストに変更するほうが効果的です。