self-hosted LLMが5人で遅くなる理由
1人なら快適なのに5人で遅い原因を解説します。Ollamaの並列リクエスト初期値1、batching、KV cache、prefill、queue depthが同時接続数を決めます。
ユーザーが増えると self-hosted LLM の処理が遅くなる理由
self-hosted 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 によって決まります。サーバーが遅い場合、通常はこのどちらかが原因であり、必要な対策は同じではありません。設定を変更する前に、どちらが問題なのかを確認することが重要です。prefill と decode を個別に測定することで判定できます。
最も遅い応答を待たせる静的バッチ処理
静的バッチ処理は単純な方式です。アプリケーションコードでリクエストを自分でグループ化すると、この方式になります。エンジンは N 個のリクエストを集めてまとめて実行し、グループ内で最も長い生成が完了するまで、すべてのスロットを保持します。
1,200 token の要約を求めるユーザーが 1 人いると、1 行で済む回答 4 件もバッチに拘束されます。バッチは、最も遅いリクエストが完了するまでスロットを解放しないためです。
この方式には、2 つのコストがあります。完了したシーケンスが、実質的な計算を行わないままスロットを占有し続けるため、出力長が異なるほど実効スループットが低下します。チャットでは出力長が大きく異なるため、この影響が顕著です。バッチの形成から 1 step 後に到着したリクエストは、prefill を開始する前にバッチ全体の処理が終わるまで待たされます。そのため、そのリクエストの TTFT は、別のユーザーが送った長い文章によって決まります。
Continuous batching はトークンごとにリクエストを受け入れ、完了したリクエストを解放する
Continuous batching は、1 回のデコードステップ単位でスケジュールします。各ステップの後、スケジューラは直前に停止トークンを出力したシーケンスを削除し、空いたスロットに待機中のリクエストを受け入れます。40 ステップ目で終了した応答は、バッチの終了時ではなく、40 ステップ目でスロットを解放します。
これは特殊な仕組みではありません。llama-server では、-cb, --cont-batching を「continuous batching(別名 dynamic batching)を有効にするかどうか(デフォルト: 有効)」として説明しており、vLLM もこの考え方を中心に構成されています。Ollama も並列リクエストを処理します。デフォルトでは同時実行数が 1 に制限されるだけです。そのため、実際には設定で無効にしているにもかかわらず、ハードウェアが同時実行に対応できないと判断する人が多くいます。
公開されている continuous batching の結果は通常、余剰の計算能力とキャッシュ用の数十 GB のメモリを備えたデータセンター向けカードで測定されています。結果の傾向は手元のマシンにも当てはまります。ただし、数値の規模はそのまま当てはまりません。その理由は、以下のメモリのセクションで説明します。
Prefill は decode と同じ計算資源を奪い合う
4 つの応答をストリーミング中に新しいリクエストが到着すると、最初にそのプロンプトを prefill する必要があります。prefill は計算負荷が高い処理です。スケジューラーが prefill に専用のステップを割り当てると、その間は 4 人のストリーミング利用者にトークンが届きません。プロンプトが長い場合、開いているすべてのウィンドウで明らかな停止が発生します。別の利用者が送信するたびにサーバーが引っかかると言われるのは、この遅延です。
Chunked prefill は長いプロンプトを分割し、各部分を実行中の decode と同じステップに組み込みます。vLLM のチューニングガイドは、このトレードオフを明確に説明しています。小さいチャンク予算では「decode を遅らせる prefill が少ないため ITL が向上」し、大きい値では「1 回のバッチで処理できる prefill トークンが増えるため、最初のトークンまでの時間(TTFT)が向上」します。保護する対象を選ぶ必要があります。応答の開始を待つ利用者を優先するのか、テキストのストリーミングを見ている利用者を優先するのかという選択です。
この影響の大きさはプロンプトの長さで決まります。6,000 トークンのプロンプトに対して回答が 200 トークンの場合、200 回の decode ステップに対して 6,000 トークン分の prefill 処理が必要です。検索拡張チャットと長いシステムプロンプトはいずれもこの状況を引き起こします。そのため prefill は無視できる差ではなく、利用者が待つ主な処理になります。長い部分が繰り返される場合は、Prefix caching が役立ちます。vLLM は --enable-prefix-caching を提供しており、リクエストごとに再計算する代わりに、共有されたプロンプトプレフィックスのキャッシュを再利用できます。
最初に枯渇するメモリは KV キャッシュ
アクティブな各会話の各トークンは、モデルの各レイヤーにキー ベクトルとバリュー ベクトルを残します。これが KV キャッシュ(key/value cache)です。新しいトークンごとにプロンプト全体を再計算せずにデコードできるのは、このキャッシュがあるためです。トークンあたりのサイズは、モデルの構造で決まります。2(キー 1 個とバリュー 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 token の会話 1 件には、約 1.2 GB のキャッシュが必要です。5 件なら約 6 GB です。これは weights に加えて必要になる容量であり、実際に収容できるユーザー数を決める値です。
同時実行数が増えるとコンテキストも増えます。ツールの説明にも明記されています。Ollama の FAQ には、次のようにあります。「特定のモデルで並列リクエスト処理を行うと、並列リクエスト数に応じてコンテキストサイズが増加します。たとえば、2K のコンテキストで 4 件の並列リクエストを処理すると、8K のコンテキストになり、追加のメモリ割り当てが発生します。」必要な RAM は OLLAMA_NUM_PARALLEL に OLLAMA_CONTEXT_LENGTH を掛けた値に比例します。llama-server では、-c で指定したコンテキストを -np スロットで分けて使用します。そのため、スロット数だけを増やすと、各リクエストが保持できるコンテキストは小さくなります。前提を置かず、起動ログからスロットごとのコンテキストを確認してください。
vLLM は事前に割り当てます。--gpu-memory-utilization(デフォルトは 0.92)は「モデル実行器に使用する GPU メモリの割合」です。weights の割り当て後に残った容量が paged KV pool になります。このプールが不足すると、スケジューラーはリクエストを失敗させず、リクエストを追い出します。
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 engine では、デフォルトの preemption mode は RECOMPUTE です。そのため、追い出されたリクエストはキャッシュを破棄し、再び受け入れられたときに prefill をやり直します。この処理は 2 回実行されます。ドキュメントは「preemption と recomputation はエンドツーエンドのレイテンシーに悪影響を及ぼす可能性がある」と警告しています。このログ行は、平均値が正常に見える一方で、あるユーザーだけが大幅に長く待たされた理由を説明する最も有効な手掛かりです。累積数を記録するには disable_log_stats=False を設定するか、vLLM が公開する Prometheus metrics から preemption counter を読み取ってください。
2、5、20 の同時ユーザーで何が変わるか
2 ユーザー。 余裕のあるキャッシュを備えた GPU では、ほとんど影響ありません。2 本目のデコードストリームが 1 本目に重なり、追加時間がわずかだからです。4 to 8 GB の RAM を搭載した CPU-only VPS では、負荷は無視できません。2 本のストリームが同じ少数の vCPU と同じ RAM 帯域を共有するため、各ユーザーの tokens per second はおよそ半分になります。キャッシュ要求量は倍増しますが、利用できる容量ははるかに小さいためです。
5 ユーザー。 ここからはデフォルト設定だけでは足りず、まずキューの問題になります。OLLAMA_NUM_PARALLEL が 1 の場合、長い回答を要求したユーザーを 4 人が待つことになります。ただし、自分の順番になれば、各ユーザーは通常の速度で処理されます。並列数を増やすと、問題の形が変わります。コンテキスト長が 8K のスロットを 5 つ使うと、40K トークン分のキャッシュが必要です。VRAM に収まらなければ、エンジンはレイヤーをシステム RAM へオフロードします。RAM にも収まらなければ、ホストは swap を使用し、tokens per second は急落します。
20 ユーザー。 チャット UI を使う 20 人が、通常 20 件の同時リクエストを送るわけではありません。ハードウェアを購入する前に、これを理解することが最も重要です。人は回答を読んでから次の入力を送るまでに 20 to 60 秒ほど考えるため、セッションの大半はアイドル状態です。20 個のエージェント、または 20 件の文書要約ジョブは、アイドル時間のない実ストリームが 20 本ある状態です。これは別のマシンが必要になる負荷です。ある開発者が自分の Ollama server に コーディングエージェントを接続した場合、前者よりも後者に近い負荷になります。エージェントはタスクが実行されている間、リクエストを送り続け、人間のような読解時の待ち時間を挟まないためです。
ユーザーは同時実行していますか、それともログインしているだけですか?
何かをサイジングする前に、処理中のリクエスト数を見積もります。計算は単純です。処理中のリクエスト数は、ユーザー数に、1 ターンの生成にかかる秒数を掛け、ターン間の秒数で割った値です。
- まず、自分の環境で単一ストリームの速度を測定します。prefill と decode の両方を測定してください。他人のカードの数値を流用せず、自分の環境で tokens per second を測定する で得た値を使います。
- 稼働率を見積もります。チャットユーザーが 20 人で、1 ターンあたり 12 秒生成し、90 秒ごとに 1 ターン実行する場合、20 * 12 / 90 となり、処理中のリクエストは約 2.7 件です。
- slot 数をその値より少し多く設定し、メモリ使用量と照合します。slot 数にリクエストごとの context を掛けた量が、実際に利用できる cache token 数に収まる必要があります。
- キューは短く保ち、容量超過時には速やかに、かつ明確に失敗するようにします。
利用可能な cache token 数は、重みを読み込んだ後の空きメモリを、前のセクションで示した token あたりのコストで割って求めます。24 GB のカードで 8B model を 16-bit で実行すると、重みに約 16 GB を使い、デフォルトの利用率では約 6 GB の cache を利用できます。これは 8K の会話約 5 件分です。さらに収容するには、リクエストごとの context を短くするか、cache を 8-bit で保存します(llama-server は --cache-type-k q8_0 を使用します)。どちらも何かを犠牲にして同時実行数を増やす方法です。そのトレードオフを正確に理解してから、ハードウェアへの投資を決めてください。GPU VPS が API token に対して損益分岐する条件。
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 つの数値のどちらかを下げてください。通常はコンテキスト長を下げるほうが安全です。ただし、ウィンドウが小さすぎるとエラーにならず、長いプロンプトが静かに切り詰められます。そのため、モデルが収まるまで値を下げ続けるのではなく、num_ctx を意図的に決めることを推奨します。
キューのデフォルト値も再確認してください。Ollama は最大 OLLAMA_MAX_QUEUE 件のリクエストをキューに入れます。「デフォルトは 512」です。これを超えると「サーバーが過負荷であることを示す 503 エラー」を返します。1 回に 4 件を処理するサーバーで深さ 512 のキューを設定しても、その約束は守れません。300 番目の位置にいるクライアントは、自分の番が来るはるか前にタイムアウトするためです。短いキューなら、アプリケーションで再試行または報告できるエラーを返せます。いつまでも終わらない待機表示より適切です。
実際にテストしてください。2 つのターミナルから同時に 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つです。1つは --max-num-seqs で、「1回の反復で処理するシーケンスの最大数」です。もう1つは --max-num-batched-tokens で、「1回の反復で処理できるトークンの最大数」です。前者は同時実行数を制限します。後者は前述したチャンク化された prefill の予算です。
同時に処理中のリクエストが4件未満の場合、または対応する GPU がない環境では、vLLM は構成を複雑にする一方で、得られる効果は小さくなります。vLLM は CUDA クラスの GPU を前提とし、起動時にメモリの大部分を確保します。そのため、4 から 8 GB の VPS では適切な選択ではありません。そのような環境では、コンテキストを短くした小規模なモデルと、自分で制御するキューを使うのが適切です。Ollama と vLLM の推論サーバーとしての違いではこの選択を詳しく説明し、VPS で Qwen 3 8B を実行するでは、追加のユーザーを1人も増やす前に中規模モデルが要求する条件を示します。
フォークロアが隠しているトレードオフ
Continuous batching は総スループットを向上させます。また、キューに入ったリクエストが早く処理を開始できるため、通常は中央値のレイテンシも改善します。一方、テールレイテンシは悪化します。この点はほとんど語られません。
1 ステップ内のシーケンスが増えるたびに処理量が少し増えるため、バッチが埋まるにつれて全体の ITL が上昇します。新しく到着したリクエストの prefill は、ストリーミング中のユーザーが本来利用できたはずのステップの一部を占有します。キャッシュに余裕がなくなると、スケジューラはプリエンプトを実行します。その結果、生成途中のリクエストは prefill の開始位置に戻されます。
チャット UI で目立つのは平均値ではなくテールです。文の途中でストリームが 2 秒間停止すると、完了までの総時間が良好でも、壊れているように見えます。想定する負荷で p95 TTFT と p95 ITL を測定してください。平均 tokens per second は容量を表す数値であり、ユーザー体験そのものを表す指標ではないと考えてください。
実際の設定はここから導けます。メモリが許容する上限より同時実行数を少し低く制限し、エンジンがプリエンプトを実行する必要がないようにします。スラッシングする深いバッチより、短く予測可能なキューのほうが優れています。4 秒待ってから滑らかにストリーミングされるほうが、すぐに開始して 2 回停止するより、ユーザーの満足度が高いからです。
遅い場合に確認すること
各ユーザーの処理速度は正常だが、待ち時間が長い。 これは速度の問題ではなく、キューの問題です。最初に並列設定を確認します。モデルは正常に処理しており、1 回に 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 を確認します。静止期間の後に戻ってきた最初のユーザーだけが長く待ち、その後のユーザーは問題ない場合、prefill ではありません。Ollama がモデルをアンロードし、重みを再びディスクから読み込んでいる可能性があります。リクエスト間でモデルを常駐させることで、この可能性を切り分けます。
FAQ
自己ホストした LLM は、2 人目のユーザーが使うと遅くなるのはなぜですか?
多くの場合、実際には遅くなっていません。キューに入っています。Ollama は OLLAMA_NUM_PARALLEL の既定値が 1 のため、2 つ目のリクエストは 1 つ目のリクエストが最後のトークンを生成するまで待機します。1 人目のストリームを計測しながら、別のユーザーが待機する状況を確認すると、2 つのケースを区別できます。処理開始後の tokens per second が通常どおりならキューが原因で、parallel count を増やすと解消します。2 つのストリームがどちらも半分の速度で動作するなら、メモリ帯域幅を実際に共有しており、ハードウェア上の制限です。
1 台の小型 GPU で、同時に何人のユーザーへ対応できますか?
ユーザー数ではなく、メモリ容量を基準に計算します。まず weights、その次に KV cache です。KV cache の容量は、アクティブな会話ごとに、トークンあたり 2 times layers times key/value heads times head dimension times bytes で決まります。一般的な 8B model は、36 layers、8 key/value heads、head dimension 128 の場合、16-bit で 1 トークンあたり約 144 KiB を消費します。そのため、8,192 token の会話には約 1.2 GB が必要です。その model を 16-bit で保持する 24 GB のカードでは、cache 用に約 6 GB が残ります。これは full context で約 5 会話分に相当します。context を短くすれば、さらに増やせます。
continuous batching を有効にすると、各ユーザーへの返信は遅くなりますか?
中央値の latency は通常改善します。リクエストがバッチ全体の完了を待たなくてよくなるためです。一方、tail latency は悪化します。sequence が 1 つ増えるたびに、各 decoding step の処理量が増えます。新しいリクエストが到着すると、streaming 中のユーザーに割り当てられていた step の一部が prefill に使われます。また、preempt されたリクエストでは prefill が 2 回必要です。平均値ではなく p95 inter-token latency を計測してください。平均値では隠れる一時停止も、チャット画面では明確に分かるためです。
OLLAMA_NUM_PARALLEL を増やすべきですか。それとも vLLM に移行すべきですか?
まず parallel count を増やしてください。追加コストがなく、drop-in file 1 つで設定できるためです。4 人が 1 つの長い回答の後ろでキューに入るという、一般的な状況を解消できます。制限になるのはメモリです。parallel requests を増やすと保持すべき context も増えるため、layers が CPU に spill していないか確認してください。GPU の VRAM に余裕があり、4 件を超えるリクエストが実際に同時実行される場合は、vLLM への移行を検討します。この規模になると、paged cache と per-token scheduling による効果が、導入コストを上回るためです。
CPU コアを増やせば、遅い LLM server は改善しますか?
ユーザーが最も強く感じる部分には、通常効果がありません。Decode では各トークンの生成時に model 全体をメモリから読み込むため、RAM bandwidth がボトルネックになります。bandwidth が飽和すると、CPU コアを増やしても改善しません。一方、prefill は CPU コア数に応じてスケールするため、コアを増やすと長い prompt の time to first token は短縮できます。4 から 8 GB の VPS では、通常はメモリ容量が制約になります。そのため、vCPU を増やすよりも、小さい model を使うか context を短くする方が効果的です。