Claude Media
Claude CodeでvLLMの推論サーバーを詰める — max-model-lenと権限設計

Claude CodeでvLLMの推論サーバーを詰める — max-model-lenと権限設計

vLLMの--max-model-len・--gpu-memory-utilization・--tensor-parallel-sizeをClaude Codeで詰める手順です。ログの読み方、ベンチ、serveとkillの許可を書きます。

vLLMの起動引数は、Claude Codeに「起動して、ログを読んで、直して、ベンチで確かめる」ループを回させると詰めやすい領域です。調整の軸は3つあります。コンテキスト長の--max-model-len、GPUメモリの取り分を決める--gpu-memory-utilization、GPUの分け方を決める--tensor-parallel-sizeです。

任せるときに決めておくのは、判断の根拠と、触らせてよいコマンドの範囲です。根拠は起動ログとベンチ結果に置きます。範囲はpermissionsの許可リストで切ります。この記事では引数ごとの効き方、ログの読みどころ、並列数ごとのベンチ、vllm serveとkillを止める設定を、その順で書きます。

3つの引数が決めるものと、既定値

vLLMのエンジン引数のうち、最初に触る3つの役割は次のとおりです。

引数決めるもの既定値・指定方法
--max-model-len決めるもの1リクエストの最大長(プロンプト+出力)既定値・指定方法未指定ならモデル設定から自動で決まる。-1かautoでGPUに収まる最大長を自動選択。16kのように単位も書ける
--gpu-memory-utilization決めるものvLLMインスタンスが使うGPUメモリの割合(0〜1)既定値・指定方法0.92。インスタンスごとの上限で、同じGPUの別インスタンスとは無関係
--tensor-parallel-size(-tp)決めるものモデルを分割するGPU数既定値・指定方法1

--gpu-memory-utilizationは「そのインスタンスの上限」です。1枚のGPUに2つのインスタンスを載せるなら、それぞれ0.5を指定します。両方とも既定の0.92のままだと、足し算が合いません。

--max-model-lenは、メモリの節約にも並列数にも効きます。-1かautoを指定すると、モデルの最大コンテキスト長が収まればそれを使い、収まらなければ収まる最大の長さを探します。社内用途で実際に使う長さが決まっているなら、autoに任せず明示した値のほうが後で比較しやすくなります。

起動ログで見る行と、そこから決める値

GPU構成が決まったら、vLLMを起動して次のようなログ行を見ます。並列処理のドキュメントに載っている例です。

INFO 07-23 13:56:04 [kv_cache_utils.py:775] GPU KV cache size: 643,232 tokens
INFO 07-23 13:56:04 [kv_cache_utils.py:779] Maximum concurrency for 40,960 tokens per request: 15.70x

1行目は、GPUのKVキャッシュに同時に保持できるトークンの総数です。2行目は、1リクエストがmax_model_lenぶんのトークンを使う前提での同時処理数の目安です。上の例では643,232を40,960で割ると15.70になり、2行がつながっているのが分かります。

この関係から、値の決め方が出てきます。社内の用途が「プロンプトと出力を合わせて8,192トークン以内」で足りるなら、--max-model-len 8192にすると、同じKVキャッシュでの同時数の目安は約78(ログ表記なら78.5x)になり、40,960のときの約5倍です(643,232÷8,192、筆者の計算)。逆に、数値が目標の同時接続数を下回るなら、次の手があります。

  • GPUを増やすか、--tensor-parallel-sizeを上げる
  • --max-model-lenを下げる
  • --gpu-memory-utilizationを上げる

この3つのどれを動かすかが、Claude Codeに判断させる中身です。ログ2行とGPUの空きメモリを渡し、「目標同時数が出るまで、1回に1つだけ変える」と条件を付けておくと、変更の効果が分かりやすくなります。

起動失敗とメモリ不足の切り分け

起動時のメモリ不足への対処は、ドキュメントの「Conserving Memory」に並んでいます。Claude Codeに渡すときは、効く順に試す手札として並べておきます。

  1. --tensor-parallel-sizeでモデルを複数GPUに分ける
  2. 量子化したモデルを使う(静的に量子化済みのチェックポイントはそのまま使える)
  3. --max-model-lenと--max-num-seqs(最大バッチサイズ)を下げる
  4. CUDAグラフが取るメモリを減らす、または--enforce-eagerで無効にする

4つ目は注意が要ります。--enforce-eagerはCUDAグラフとtorch.compileの両方を無効にします。メモリは空きますが、速度とのトレードオフです。メモリが足りないときの最後の手として使い、ベンチで差を測ってから残すかを決めます。

起動はしても遅いときは、プリエンプション(割り込みによる再計算)を疑います。KVキャッシュが足りないと、vLLMはリクエストを一度退避して再計算します。ログには次の趣旨の警告が出ます。

WARNING ... Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space. ...

頻発する場合の対処は、gpu_memory_utilizationを上げる、max_num_seqsかmax_num_batched_tokensを下げる、tensor_parallel_sizeかpipeline_parallel_sizeを上げる、の3系統です。ただし、並列化を上げると同期のオーバーヘッドが増える点にも注意が必要です。

GPUの分け方の選び方

複数GPUでの分け方は、ドキュメントの指針が次の順です。

  • モデルが1枚に収まる: 分散推論は不要
  • 1ノードに収まるが1枚には載らない: tensor_parallel_sizeをGPU数に(4枚なら4)
  • 1ノードに収まらない: tensor_parallel_sizeをノード内GPU数に、pipeline_parallel_sizeをノード数に

GPU数がモデルサイズで割り切れない場合は、tensor_parallel_size=1にしてpipeline_parallel_sizeをGPU数にします。NVLinkのないGPU(L40Sなど)でも、スループットの面でパイプライン並列が勧められています。テンソル並列では、各プロセスがモデル全体を読んでから分割するため、ディスクの読み込み時間がTPの数に比例して延びる点も覚えておくと、起動の遅さを疑うときに役立ちます。

ベンチで確かめる — 並列数ごとのレイテンシ

引数を変えたら、数字で比べます。vLLMにはvllm bench serveがあり、起動中のサーバーに対して負荷をかけ、TTFT(最初のトークンまでの時間)・TPOT(出力1トークンあたりの時間)・ITL(トークン間の間隔)を出します。

vllm bench serve \
  --backend vllm \
  --model <モデル名> \
  --endpoint /v1/completions \
  --dataset-name custom \
  --dataset-path ./prompts.jsonl \
  --num-prompts 80 \
  --max-concurrency 16 \
  --save-result

--max-concurrencyを1、4、16、64と変えて4回走らせれば、「並列数が増えるとTTFTがどこから跳ねるか」が見えます。社内の実プロンプトで測りたいときは、--dataset-name customに{"prompt": "…"}を1行1件で書いたJSONLを渡します。

測定で踏みやすい落とし穴が1つあります。同じサーバーに同じデータセットでvllm bench serveを繰り返すと、プレフィックスキャッシュに前回のプロンプトが残り、スループットが水増しされます。対処は、--seedを変える、サーバーを再起動する、キャッシュをリセットするvllm bench sweep serveを使う、のいずれかです。Claude Codeに回させるなら、「並列数ごとの測定の間にサーバーを再起動する」と手順に書いておきます。

本番向けの負荷試験にはGuideLLMが推奨されています。vllm bench serveは引数変更の前後比較に使う、と役割を分けるのが無理のない使い方です。

Claude Codeに渡す入力と指示

渡す情報は3点です。GPU構成、モデルと用途の条件、現在の起動コマンドとログです。GPU構成は、Claude Codeに自分で取らせることもできます(許可リストは次の節)。

nvidia-smi --query-gpu=name,memory.total,memory.used --format=csv

プロジェクトのCLAUDE.mdには、調整のルールを書いておきます。次は書き方の一例です。

## vLLMの調整ルール
- 引数は1回に1つだけ変える。変更前後のログ2行(GPU KV cache size / Maximum concurrency)を並べて報告する
- 起動は`logs/vllm.log`にリダイレクトして行い、ログから読み取る
- 起動引数を変えたら、`vllm bench serve`を並列数1/4/16/64で測り、表で比較する
- 測定の間はサーバーを再起動し、プレフィックスキャッシュの影響を避ける
- 他チームのプロセスが載っているGPUでは、`pkill`・`killall`を使わない

この指示で反復が回るようになると、報告の形が一定します。「何を変えて、KVキャッシュのトークン数と同時数がどう動き、並列64でのTTFTがどうなったか」が毎回同じ表で返るので、人間は数字を見るだけで済みます。

反復の手順

手順

1サイクルの流れ

  1. 1

    現状のログを読む

    GPU KV cache sizeとMaximum concurrencyの2行を取り出し、目標の同時数との差を出します。

  2. 2

    引数を1つだけ変える

    --max-model-len、--gpu-memory-utilization、--max-num-seqsのどれか1つを動かして再起動します。

  3. 3

    起動成否とプリエンプション警告を見る

    起動に失敗したら、メモリ不足の手札(前の節)から次の1つを試します。

  4. 4

    並列数ごとにベンチを回す

    結果をJSONで保存し、TTFTとTPOTの変化を前回と並べます。

serveとkillを許可リストで切る

サーバーの起動と停止は、副作用が大きい操作です。GPUを占有し、他のジョブの邪魔をする可能性があります。そこで、読み取りと測定は許可し、起動は確認に残し、停止系は用途で分けます。

{
  "permissions": {
    "allow": [
      "Bash(nvidia-smi *)",
      "Bash(vllm bench serve *)",
      "Bash(curl -s localhost:8000/v1/models)"
    ],
    "ask": [
      "Bash(vllm serve *)",
      "Bash(kill *)"
    ],
    "deny": [
      "Bash(pkill *)",
      "Bash(killall *)"
    ]
  }
}

この設定を.claude/settings.jsonに置くと、nvidia-smiの問い合わせとベンチは確認なしで走ります。サーバーの起動とkillは、毎回承認を求められます。pkillとkillallは名前でプロセスを巻き込むため、拒否に回しています。

ルールの評価は「deny、ask、allow」の順で、最初に当たったものが結果を決めます。そのため、同じコマンドが複数の欄に当たっても、拒否が優先されます。

Bashルールの限界

3点は知っておくと安全です。

  • nohup vllm serve ...は、nohupが取り除かれてから判定されます。Bash(vllm serve *)は、nohup付きの起動にも当たります
  • &&や;でつないだ複合コマンドは、全部の部分コマンドが許可されないと確認なしでは走りません
  • /usr/bin/pkillのように絶対パスで呼んだり、sh -c 'pkill ...'に包んだりすると、Bash(pkill *)のdenyには当たりません

3つ目のとおり、Bashルールはコマンドの文字列に対する照合です。共有GPUサーバーのように「絶対に止めさせたくない」操作があるなら、拒否ルールだけに頼らず、サンドボックスやPreToolUseフックと組み合わせます。引数で絞る形のルール(特定のURLだけ許すcurlなど)も壊れやすいとされています。上の設定のcurlは、URLまで含めた完全一致にして、別のURLやオプションを付けた形には当たらないようにしています。前置きのワイルドカードで引数を絞るより抜けが少なくなります。

ログのリダイレクト(> logs/vllm.log)は、出力先がファイルの編集ルールで判定されます。ログ用ディレクトリへの書き込みが拒否されていないかは、最初の起動で確かめておきます。

落とし穴

allowしたコマンドの別の形は止まらない

Bash(vllm serve *)をaskに置いても、python -m vllm.entrypoints.openai.api_server ...のような別の入口は、このルールに当たりません。起動の入口が複数ある環境では、起動スクリプトを1本にそろえ、そのスクリプトだけを許可・確認の対象にすると、ルールの抜けが減ります。

詰めた結果を残す

最終的な引数は、起動スクリプトか設定ファイルに固定し、そのときのログ2行とベンチ表を一緒に残しておきます。モデルやGPUを入れ替えたときに、同じ手順でやり直せます。ローカルLLMをClaude Code側から使う設定については、Claude CodeをOllamaのローカルLLMに接続する手順と制約に、環境変数での接続手順があります。権限をコマンド単位で切る同じ考え方は、Claude Codeでffmpegを使うの権限設計にも出てきます。長時間動かすサーバーをバックグラウンドで扱うときの時間制限は、Claude Codeのバックグラウンドが時間制限で止まる原因と延ばし方で説明しています。

まとめ

詰める作業の核は、ログ2行とベンチ表という客観的な数字を、Claude Codeの反復に載せることです。引数の効き方はvLLMのドキュメントに書かれているので、Claude Codeには「1回に1つだけ変える」「前後を同じ形式で報告する」の2点を徹底させるだけで、再現できる調整記録が残ります。許可リストは、測定は通し、起動と停止は人の目を通す形から始めると安全です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. docs.vllm.ai/en/latest/configuration/engine_args
  3. docs.vllm.ai/en/latest/serving/parallelism_scaling
  4. docs.vllm.ai/en/latest/configuration/conserving_memory
  5. docs.vllm.ai/en/latest/configuration/optimization
  6. docs.vllm.ai/en/latest/benchmarking/cli
この記事を共有:XはてブLinkedIn