llama-serverの連続バッチング設定|並列リクエストの確認方法

CHATGPT / AIChatGPTなどの生成AIを活用して運営・記事制作・更新を行っています。制作方針

UbuntuとNVIDIA GPUでllama-serverを運用する人向けに、--parallelと--cont-batchingの関係、コンテキスト配分、同時リクエストの確認手順と注意点を公式仕様に基づいて解説します。

複数リクエストを並列枠と連続バッチへ流し、KVキャッシュを共有する待ち行列図解

先に結論

  • --parallel同時に処理できるサーバースロット数--cont-batching実行中の共有バッチへ新しいリクエストを継続投入する方式です。どちらもGPUスレッド数ではありません。
  • 2026年7月28日に確認したllama.cppのmaster HEADでは、continuous batchingは既定で有効です。再現可能な運用では既定値に頼らず、--parallel 2 --kv-unified --cont-batchingのように明示します。
  • --ctx-sizeはKVキャッシュ方式と一緒に決めます。unified KVでは全スロットが容量を共有し、非unifiedではスロットごとに分割されます。
  • 最初は1スロットで起動確認し、2スロット、必要なら4スロットへ段階的に増やします。/slots/metrics、同時curlで実際の状態を確認します。
  • 本記事に速度、スループット、VRAM使用量の実測値は掲載しません。最適な並列数はGPU、モデル、量子化、入力長、出力長で変わるためです。

確認日: 2026年7月28日
固定した版: llama.cpp master HEAD 81616410050cb5d8b733b39863807f4852591d8a
想定環境: Ubuntu 24.04 LTS、NVIDIA GPU、導入済みのCUDA Toolkit、単一のGGUFモデル、localhostでのAPI確認
検証区分: 固定SHAの公式README、開発資料、ビルド資料、実装コードを確認した手順です。Ubuntu+NVIDIA GPU実機でのビルド、推論、性能測定は行っていません。
画像: アイキャッチは4件のリクエスト、2スロット、共有バッチを表現した当サイトの要点図解です。llama-serverの実画面ではありません。

この記事で扱う範囲

K-FutureBlogには、nginx、TLS、認証、ログまで扱うllama-server本番運用ガイドと、全体のオプションを引くためのllama.cpp主要パラメータ一覧があります。

本記事はその内容を繰り返さず、Ubuntu+NVIDIA GPUで動かすllama-serverについて、continuous batching、parallel slots、KVキャッシュの関係を理解し、同時リクエストで設定を確認することに範囲を限定します。DockerでのCUDA起動から始めたい場合は、先にllama.cpp Dockerガイドを参照してください。

前提環境と固定したコミット

項目本記事の前提確認すること
OSUbuntu 24.04 LTSuname -alsb_release -a
GPUNVIDIA GPU 1基nvidia-smi
CUDACUDA Toolkit導入済みnvcc --version
モデル読み取り可能なGGUF 1ファイルモデル名、量子化、ファイルハッシュ
llama.cpp81616410050cb5d8b733b39863807f4852591d8agit rev-parse HEAD
公開範囲127.0.0.1:8080外部公開は別途TLS・認証を設計

確認時点の既定ブランチとHEADは、次のコマンドで取得しました。masterは更新が速いため、後日同じコマンドを実行した結果が本記事のSHAと異なるのは正常です。

git ls-remote --symref https://github.com/ggml-org/llama.cpp.git HEAD

固定SHAのllama-server公式READMEでは、--parallelの既定値は-1、continuous batchingは既定で有効です。さらに現行実装では、autoの並列数は4スロットかつunified KVとして解決されます。明示的な--parallel 4だけでは同じ構成にならないため、本記事で扱うparallel、KV、continuous batching、context、batchの引数を明示します。

連続バッチングと並列リクエストの違い

リクエストA ─┐
リクエストB ─┼→ 利用可能なserver slot → 共有batch → llama_decode → GPU
リクエストC ─┘       空きがなければ待機

公式のサーバー開発資料によると、サーバーコンテキストは全スロットで1つのバッチを共有します。更新処理では、各アクティブスロットから生成中の次トークン、または未処理のプロンプトトークンをバッチへ加え、容量へ達した時点または全スロットを確認した時点でllama_decodeを呼びます。

設定役割増やしたときに変わるもの
--parallel Nサーバースロット数同時に推論処理へ入れるシーケンス上限
--cont-batching連続・動的バッチング既存生成の途中でも新しいプロンプトを共有バッチへ投入できる
--batch-size N論理最大バッチサイズまとめて扱うトークンの論理上限
--ubatch-size N物理最大バッチサイズバックエンドへ渡す物理バッチの上限
--threads-http NHTTP処理スレッド数HTTP層の処理。推論スロット数ではない

通常の1出力リクエストを想定すると、--parallel 2では最大2シーケンスが推論スロットを利用し、それを超えるリクエストは受理された後に空きを待つ場合があります。continuous batchingを有効にしてもスロット数自体は増えません。反対にスロットを複数用意しても、--no-cont-batchingでは新しいプロンプトを生成中のバッチへ継続投入しません。

また、LoRAアダプターなど互換性のない設定を使うスロット同士は同じバッチへまとめられません。「同時リクエスト数」と「1回のGPU処理へ効率よくまとまる数」は分けて考えます。

CUDA版llama-serverの導入手順

次の例は新しい作業ディレクトリへ固定SHAを取得し、CUDA対応のllama-serverだけをビルドします。CUDA ToolkitとNVIDIAドライバーの導入は済んでいる前提です。

set -euo pipefail

sudo apt-get update
sudo apt-get install -y build-essential cmake git curl jq

source_dir="llama.cpp-8161641"
if [[ -e "$source_dir" ]]; then
  printf '作業先が既に存在します: %s\n' "$source_dir" >&2
  exit 1
fi

git clone --filter=blob:none \
  https://github.com/ggml-org/llama.cpp.git \
  "$source_dir"
cd "$source_dir"

LLAMA_CPP_SHA="81616410050cb5d8b733b39863807f4852591d8a"
git checkout --detach "$LLAMA_CPP_SHA"
test "$(git rev-parse HEAD)" = "$LLAMA_CPP_SHA"

cmake -S . -B build \
  -DGGML_CUDA=ON \
  -DCMAKE_BUILD_TYPE=Release

cmake --build build \
  --config Release \
  --target llama-server \
  -j"$(nproc)"

ビルド後は、バイナリの版と利用できる引数を確認します。将来の版では既定値や引数名が変わる可能性があるため、固定したソースと実バイナリの両方を記録します。

./build/bin/llama-server --version

./build/bin/llama-server --help |
  grep -E -- '--ctx-size|--batch-size|--ubatch-size|--kv-unified|--parallel|--cont-batching|--slots|--metrics'

CUDAの公式ビルド手順は、同じSHAのllama.cpp Build guideで確認できます。ドライバーとCUDAの組み合わせに問題がある場合は、並列設定より先に単一リクエストの起動を通してください。

設定例1:1スロットで基準を作る

最初は1スロットでモデル、CUDA、APIの基本動作を確認します。8192は説明用の開始値であり、すべてのモデルや用途に対する推奨値ではありません。

MODEL_PATH="/srv/models/your-model.gguf"

if [[ ! -r "$MODEL_PATH" ]]; then
  printf 'GGUFを読み取れません: %s\n' "$MODEL_PATH" >&2
else
  ./build/bin/llama-server \
    --model "$MODEL_PATH" \
    --alias local \
    --host 127.0.0.1 \
    --port 8080 \
    --n-gpu-layers auto \
    --ctx-size 8192 \
    --parallel 1 \
    --kv-unified \
    --cont-batching \
    --batch-size 2048 \
    --ubatch-size 512 \
    --slots \
    --metrics
fi

--batch-size 2048--ubatch-size 512は固定SHA時点の既定値を明示したものです。まず既定値で基準を作り、処理時の一時メモリ不足や実測上の変更理由がある場合だけ調整します。KVコンテキスト容量そのものの不足は、batch sizeを下げても解消しません。--ctx-size、並列数、入力・履歴・出力の長さを見直します。

設定例2:2スロットとunified KVで共有する

次は2スロットへ増やし、unified KVを明示します。--ctx-size 16384は全スロットが使う共有容量です。2件のリクエストがそれぞれ独立した16Kを予約する設定ではありません。

MODEL_PATH="/srv/models/your-model.gguf"

if [[ ! -r "$MODEL_PATH" ]]; then
  printf 'GGUFを読み取れません: %s\n' "$MODEL_PATH" >&2
else
  ./build/bin/llama-server \
    --model "$MODEL_PATH" \
    --alias local \
    --host 127.0.0.1 \
    --port 8080 \
    --n-gpu-layers auto \
    --ctx-size 16384 \
    --parallel 2 \
    --kv-unified \
    --cont-batching \
    --batch-size 2048 \
    --ubatch-size 512 \
    --slots \
    --metrics
fi

unified KVでは、短いリクエストが使わない容量を別の長いリクエストが利用できます。そのため容量利用の観点では入力長が不均一なAPIの候補になりますが、適否は実際の入力長分布と負荷試験で判断します。全スロットが最大長へ同時到達できる保証はありません。入力、会話履歴、生成予定トークンの合計を見積もり、同時利用時の共有容量に余裕を残します。

スロットごとに固定配分したい場合

同じ--ctx-size 16384 --parallel 2--no-kv-unifiedを選ぶと、固定SHAの実装では各スロットのコンテキストは約8192へ分割されます。割り切れない値は256トークン単位の調整が入るため、起動ログと/slotsで実値を確認します。

MODEL_PATH="/srv/models/your-model.gguf"

if [[ ! -r "$MODEL_PATH" ]]; then
  printf 'GGUFを読み取れません: %s\n' "$MODEL_PATH" >&2
else
  ./build/bin/llama-server \
    --model "$MODEL_PATH" \
    --alias local \
    --host 127.0.0.1 \
    --port 8080 \
    --n-gpu-layers auto \
    --ctx-size 16384 \
    --parallel 2 \
    --no-kv-unified \
    --cont-batching \
    --slots \
    --metrics
fi

固定配分は1リクエストが他のスロットの空き容量を使わないため予測しやすい反面、短いリクエストが多いと未使用領域が残ります。共有と固定のどちらがよいかは、実際の入力長分布で判断します。

確認方法1:起動状態とスロットを確認する

最初にヘルスチェックと適用されたプロパティを確認します。モデルが利用可能なら/healthはHTTP 200を返し、ロード中などは503になる場合があります。

curl -fsS http://127.0.0.1:8080/health | jq

curl -fsS http://127.0.0.1:8080/props |
  jq '{
    build_info,
    total_slots,
    slot_context: .default_generation_settings.n_ctx,
    endpoint_slots,
    endpoint_metrics
  }'

続いて各スロットのID、処理中かどうか、スロット側から見えるコンテキスト上限を確認します。

curl -fsS http://127.0.0.1:8080/slots |
  jq '[.[] | {id, is_processing, n_ctx}]'

unified KVでは、各スロットのn_ctxに共有プール全体と同じ論理上限が表示されても、その容量がスロットごとに独立確保されているわけではありません。起動引数、KV方式、同時リクエストの合計トークンを一緒に記録します。

確認方法2:同時リクエストを送る

次のスクリプトは、他のクライアントを止めたテスト用サーバーへ4件のAPIリクエストを同時に開始し、レスポンス本文とHTTPステータスを一時ファイルへ分けて保存します。各バックグラウンド処理をPIDごとに待つため、失敗したリクエストも確認できます。性能試験ではなく、parallel slotsの機能テストです。

(
response_dir="$(mktemp -d /tmp/llama-parallel-check.XXXXXX)"
pids=()

for request_id in 1 2 3 4; do
  (
    payload="$(
      jq -nc \
        --arg prompt "並列リクエスト ${request_id} への短い回答を作成してください。" \
        '{
          model: "local",
          messages: [{role: "user", content: $prompt}],
          max_tokens: 128,
          stream: false
        }'
    )"

    if http_status="$(
      curl --silent --show-error \
        --output "${response_dir}/response-${request_id}.json" \
        --write-out '%{http_code}' \
        http://127.0.0.1:8080/v1/chat/completions \
        -H 'Content-Type: application/json' \
        --data-binary "$payload"
    )"; then
      printf '%s\n' "$http_status" \
        >"${response_dir}/status-${request_id}.txt"
      test "$http_status" = "200"
    else
      printf 'curl-error\n' \
        >"${response_dir}/status-${request_id}.txt"
      exit 1
    fi
  ) &
  pids+=("$!")
done

failed=0
for pid in "${pids[@]}"; do
  if ! wait "$pid"; then
    failed=1
  fi
done

for request_id in 1 2 3 4; do
  http_status="$(
    cat "${response_dir}/status-${request_id}.txt" 2>/dev/null ||
      printf 'missing'
  )"
  printf 'request %s: HTTP %s\n' "$request_id" "$http_status"

  if [[ -s "${response_dir}/response-${request_id}.json" ]]; then
    jq -r \
      '.choices[0].message.content // .error.message // "unexpected response"' \
      "${response_dir}/response-${request_id}.json"
  else
    printf '(response bodyなし)\n'
  fi
done

if [[ "$failed" -eq 0 ]]; then
  printf '4件すべてHTTP 200です。\n'
  exit 0
else
  printf '失敗したリクエストがあります。\n' >&2
  exit 1
fi
)

リクエストの実行中に、別のターミナルからスロットを監視します。回答が短いと観測前に完了することがあるため、1回の表示だけで「並列化されていない」と判断しません。

watch -n 0.5 \
  'curl -fsS http://127.0.0.1:8080/slots | jq "[.[] | {id, is_processing, n_ctx}]"'

--parallel 2で4件を送った場合、同時に推論処理へ入れるのは最大2スロットで、残りは受理後に空きを待つ場合があります。4件すべてがHTTP 200であること、レスポンス本文にエラーがないこと、監視中に複数スロットのis_processingが変化することを確認します。この同時投入テストだけでは、continuous batching単独の挙動は切り分けられません。

確認方法3:後着リクエストを観測する

continuous batchingは、すでに生成中のシーケンスがある状態で、新しく届いたプロンプトを共有バッチへ加えられる仕組みです。他のクライアントを止めたテスト用サーバーで、次のテストはAを開始し、/slotsでAの生成開始を確認してからBを送ります。Aが処理中のまま、別スロットのBでプロンプト処理または生成が進んだことを観測します。

(
set -u -o pipefail

staged_dir="$(mktemp -d /tmp/llama-staged-check.XXXXXX)"

send_request() {
  local request_id="$1"
  local max_tokens="$2"
  local prompt="$3"
  local payload
  local http_status

  payload="$(
    jq -nc \
      --arg prompt "$prompt" \
      --argjson max_tokens "$max_tokens" \
      '{
        model: "local",
        messages: [{role: "user", content: $prompt}],
        max_tokens: $max_tokens,
        stream: false
      }'
  )"

  http_status="$(
    curl --silent --show-error \
      --output "${staged_dir}/${request_id}.json" \
      --write-out '%{http_code}' \
      http://127.0.0.1:8080/v1/chat/completions \
      -H 'Content-Type: application/json' \
      --data-binary "$payload"
  )" || return 1

  printf '%s: HTTP %s\n' "$request_id" "$http_status"
  [[ "$http_status" = "200" ]]
}

send_request \
  "request-a" \
  1024 \
  "連続バッチングについて章立てを付けた長い技術解説を作成してください。" &
pid_a="$!"

slot_a=""
for _ in $(seq 1 60); do
  slot_a="$(
    curl -fsS http://127.0.0.1:8080/slots |
      jq -r \
        '[.[] |
          select(
            .is_processing and
            ((.next_token.n_decoded // 0) > 0)
          )
        ][0].id // empty'
  )" || true

  if [[ -n "$slot_a" ]]; then
    printf 'Aの生成開始をslot %sで確認しました。\n' "$slot_a"
    break
  fi

  sleep 0.5
done

if [[ -z "$slot_a" ]]; then
  printf 'Aの生成開始を確認できませんでした。\n' >&2
  wait "$pid_a" || true
  exit 1
fi

send_request \
  "request-b" \
  128 \
  "連続バッチングを一文で説明してください。" &
pid_b="$!"

observed=0
for _ in $(seq 1 100); do
  slots_json="$(
    curl -fsS http://127.0.0.1:8080/slots
  )" || break

  if jq -e \
    --argjson slot_a "$slot_a" \
    '
      any(.[];
        .id == $slot_a and .is_processing
      )
      and
      any(.[];
        .id != $slot_a and
        .is_processing and
        (
          ((.n_prompt_tokens_processed // 0) > 0) or
          ((.next_token.n_decoded // 0) > 0)
        )
      )
    ' <<<"$slots_json" >/dev/null; then
    observed=1
    jq \
      '[.[] | {
        id,
        is_processing,
        n_prompt_tokens_processed,
        n_decoded: (.next_token.n_decoded // 0)
      }]' <<<"$slots_json"
    break
  fi

  if ! jq -e \
    --argjson slot_a "$slot_a" \
    'any(.[]; .id == $slot_a and .is_processing)' \
    <<<"$slots_json" >/dev/null; then
    break
  fi

  sleep 0.2
done

failed=0
wait "$pid_a" || failed=1
wait "$pid_b" || failed=1

if [[ "$failed" -ne 0 ]]; then
  printf '失敗したリクエストがあります。\n' >&2
  exit 1
elif [[ "$observed" -ne 1 ]]; then
  printf 'Aの処理中にBの進行を観測できませんでした。\n' >&2
  exit 1
else
  printf 'Aの処理中にBのプロンプト処理または生成が進みました。\n'
  exit 0
fi
)

このテストは、Aのnext_token.n_decodedが1以上になった後でBを送り、Aが処理中の間に別スロットのn_prompt_tokens_processedまたはnext_token.n_decodedが増えたことを条件にします。モデルがAを短く終了した場合や、監視間隔の間にBが完了した場合は観測できないため、テスト環境でAの生成上限やプロンプトを調整します。これは後着リクエストが生成中のシーケンスと並行して進んだことの機能確認であり、バッチング効率や性能向上を証明するベンチマークではありません。

確認方法4:metricsとGPU状態を見る

--metricsを付けるとPrometheus互換の/metricsが有効になります。次の3項目は、処理中、待機中、1回のdecodeで使われたスロット数の変化を見るための入口です。

curl -fsS http://127.0.0.1:8080/metrics |
  grep -E '^llamacpp:(requests_processing|requests_deferred|n_busy_slots_per_decode)'

別のターミナルでGPUメモリと使用状況も確認します。

watch -n 1 nvidia-smi

記録する項目は、llama.cppのSHA、GGUF名とハッシュ、起動引数、同時リクエスト数、入力・出力トークン数、HTTPステータス、処理中・待機中リクエスト、GPUメモリ、エラーです。速度を比較する場合は、同じモデル、プロンプト、生成上限で1スロットと2スロットを測り、平均値だけでなく各リクエストの待ち時間も残します。

並列数を増やす順序

  1. --parallel 1でモデル読み込み、単発API、出力品質を確認する
  2. 必要な入力、履歴、出力を含むコンテキスト容量を決める
  3. --parallel 2へ増やし、unified KVまたは固定配分を明示する
  4. 2件と4件の同時リクエストで、スロット、待機、エラー、GPUメモリを確認する
  5. 実際のピーク同時数が必要な場合だけ、コンテキスト総量を見直して4スロットを試す
  6. 変更前後を同じ条件で比較し、問題があれば直前の構成へ戻す

スロットを増やすと同時に保持するシーケンスも増えますが、GPUの計算量やメモリ帯域が増えるわけではありません。全体の処理量が改善しても、1件当たりの応答開始や生成が遅くなる可能性があります。実測なしで「Nスロットが最速」と決めないことが重要です。

注意点

  • autoと明示値は同じではない: 固定SHAでは--parallel未指定のautoが4スロット+unified KVになります。--parallel 4だけを指定した構成とは異なります。
  • コンテキストを二重計上しない: unified KVの--ctx-sizeは共有容量です。各スロットへ同じ容量が個別予約されるわけではありません。
  • 共有KV不足は他スロットへ波及し得る: 固定SHAの実装には、KVの空き確保を再試行しても不足した場合、Context size has been exceededを処理中の全スロットへ返して解放する経路があります。実機での発生条件は未検証ですが、1件だけの失敗と決めつけず同時処理中の全レスポンスを確認します。
  • batch sizeは利用者数ではない: --batch-size--ubatch-sizeはトークン処理のバッチ上限です。同時に推論処理へ入れるシーケンス数は--parallel、HTTP層の処理スレッド数は--threads-httpが扱います。空きスロットを超えるHTTPリクエストは、受理後にdeferredとなる場合があります。
  • APIのparallel_tool_callsとは別: ツール呼び出しを並行生成するAPIフィールドは、サーバースロットの--parallelと無関係です。
  • 監視エンドポイントも保護する: 固定SHAでは/slotsは既定で有効、/metricsは既定で無効です。外部へそのまま公開せず、認証とアクセス制御の対象にします。
  • localhostから始める: --host 0.0.0.0へ変えるだけでは安全な公開になりません。TLS、APIキー、リバースプロキシ、ファイアウォールは本番運用ガイドに分けて確認してください。
  • masterを追従し続けない: 動作確認したSHAを固定し、更新時は別プロセスまたは別ホストで同じ機能テストを実行してから切り替えます。

未検証範囲

  • Ubuntu 24.04 LTSとNVIDIA GPU実機でのCUDAビルド、モデル読み込み、推論
  • 特定のGPU、CUDA、ドライバー、GGUF、量子化における最適スロット数
  • TTFT、生成速度、合計スループット、VRAM使用量の性能値
  • 複数GPU、LoRA、マルチモーダル、speculative decodingとの併用
  • unified KVの容量が不足する実機上の発生条件、モデル別の影響、長時間安定性
  • systemd、コンテナ、TLS、nginx、認証、外部監視基盤への組み込み

これらは環境依存性が高いため、本記事のコマンドは機能確認の出発点として扱ってください。GPUバックエンドごとの比較方法はllama.cpp GPUバックエンド比較も参考になります。

まとめ

llama-serverの並列設定では、--parallelでスロット数、--cont-batchingで新しいシーケンスを共有バッチへ投入する方式、--ctx-size--kv-unifiedでコンテキスト容量の持ち方を決めます。4つの値を別々に理解すると、設定ミスを切り分けやすくなります。

1スロットで基準を作り、2スロットへ増やし、同時curl/slots/metricsnvidia-smiで確認するのが安全な進め方です。最大値を先に決めるのではなく、実際の入力長と同時利用数に必要な容量だけを割り当てます。

参考資料