benchmark-vllm-model について
Chalk 上で vLLM により提供するモデルのトークン単価を測るスキルです。継続負荷テスト、GPU 飽和度の確認、クラウド SKU の価格を組み合わせ、再現可能なコストを算出します。
主な機能
- 01GPU 上で vLLM を起動
- 02プロンプト長と並列数をスイープ
- 03サーバー側のトークン処理量を測定
- 04DCGM で GPU 飽和度を確認
- 05Cloud Billing SKU からコストを算出
- 06入力トークンとリクエストの費用を報告
活用シーン
- 01モデル提供設定の比較
- 02100 万入力トークンあたりの費用推定
- 03GPU が律速する場合の処理量評価
- 04再現可能なデプロイ費用レポートの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ chalk-ai/ chalk-ai-plugins --skill benchmark-vllm-modelインストール後、Claude Code で/benchmark-vllm-modelを入力して読み込まれたか確認します。
同梱ファイルscripts/bench.py
詳細
Chalk 上 vLLM でモデルの $/token をベンチマークする
根拠のあるトークンあたりのコストを算出します。GPU でモデルを提供し、飽和状態まで負荷をかけ、GPU が実際にボトルネックであることを確認してから、信頼できるクラウド料金を測定スループットで割ります。
順序が重要です。飽和状態の確認がないスループット値には反証可能性がなく、ブログ記事から引用した $/hr の値はたいてい誤っています(手順 5 を参照)。
1. モデルを提供する
chalk container run --dir <chalk-project> --json --no-spinners \
-i vllm/vllm-openai:latest -n bench-server \
--gpu nvidia-l4:1 --cpu 4 --memory 16Gi --port 8000 --lifetime 2h \
--env "HF_TOKEN=$HF_TOKEN" --env HF_HOME=/root/.cache/huggingface \
--await-running --await-timeout 20m \
-e vllm -- serve <MODEL> --port 8000 \
--dtype bfloat16 --max-model-len 16384 --gpu-memory-utilization 0.90
chalk container get --name bench-server は webUrl(パブリックにアクセス可能)と podName を返します。GET $webUrl/health をポーリングしてください。ウェイトの読み込みが終わるまでは 503 を返し、その後 200 を返します。7B モデルのダウンロードには数分かかります。
GPU の選択。 コンテナー/サンドボックスのワークロードを実行できるのは、chalk.ai/workload-type=compute のラベルが付いた GPU プールだけです。ステージングクラスターでは l4-gpus(L4、24GB、ネイティブ bf16)が該当し、最大 2 ノード x 1 GPU に制限されています。このラベルのないプール(例:t4-gpus)では Pod が Unschedulable のままになり、コントローラーは 300s 後に処理を断念します。これは GPU ノードのコールドオートスケールより短いため、リクエスト自体が有効でも、単に容量不足で失敗することがあります。GPU を空けてから再試行してください。
Turing(T4)はネイティブ bf16 に対応していないため、そこで実行する場合は --dtype float16 を使用してください。
2. 結果を左右するサービングフラグ
実際に実行する予定の設定でベンチマークし、設定したフラグを明記してください。これらのフラグは結果を大きく左右します。
| Flag | 重要な理由 |
|---|---|
--max-num-seqs | 同時処理するシーケンス数の上限です。大きくしすぎると短いリクエストでスケジューラーの時間を浪費し、値を上げてもスループットが低下することがあります。値を変えて測定してください。 |
--max-num-batched-tokens | プリフィルのチャンク予算です。プロンプトが長く出力が短いワークロードでは、制約要因になります。 |
--enable-prefix-caching | vLLM V1 ではデフォルトで有効です。見かけ上のプロンプトスループットを押し上げます(落とし穴を参照)。 |
--limit-mm-per-prompt '{"image":1}' | マルチモーダルモデルは、プロンプトあたりの最悪ケースの画像数を基準にピークアクティベーションメモリを見積もるため、KV キャッシュが圧迫されます。この値を固定してください。 |
--dtype | Ada/Hopper では bf16、Turing では fp16 を使用します。 |
--enforce-eager | CUDA グラフのキャプチャをスキップします。起動は速くなりますが、デコードは遅くなります。出力が 1 トークンの場合は問題ありませんが、一般的なサービングには適しません。一般化を意図したベンチマークでは有効のままにしないでください。 |
--api-key | コンテナーの webUrl はパブリックに名前解決できます。認証なしの LLM エンドポイントが公開されます。chalk container run --authenticated と組み合わせてください。 |
3. 負荷をかける
このスキルに同梱されている scripts/bench.py を使用してください。プロンプト長と同時実行数を変えて測定し、一定した状態の測定ウィンドウにおけるスループットを、vLLM 独自の Prometheus カウンター(vllm:prompt_tokens_total、vllm:generation_tokens_total、vllm:request_success_total)から読み取ります。
./scripts/bench.py --base-url "$URL" --model "$MODEL" \
--input-lens 128,512 --concurrency 8,32,64 --duration 30
実際の失敗要因をそれぞれ解消する、測定方法に関するルールは次のとおりです。
- 固定リクエスト数ではなく、継続的な測定ウィンドウを使います。 10 秒未満のウィンドウではジッターの影響が大きくなります。測定ウィンドウの前にウォームアップのランプアップを行い、20 秒以上のウィンドウを使ってください。
- クライアント側の計測ではなく、サーバー側のカウンターを使います。 クライアントのオーバーヘッドと WAN のレイテンシを測定から除外できます。
- スループットが上昇しなくなるまで同時実行数を変えて測定します。 同時実行数を 1 段階だけ測定しても、どこで飽和するかは分かりません。
- 一意のプロンプトプレフィックスを使います。 同一のプロンプトはプレフィックスキャッシュにヒットし、結果を押し上げます(同梱スクリプトでは各リクエストにシードを設定しています)。
- クライアント側がボトルネックになっていないか確認します。 同時実行数が高いときにスループットが低下する場合は、まずサーバーよりも負荷生成側の CPU を疑ってください。より高性能なマシン、またはクラスター内から再実行してください。
負荷生成ツールを実行する場所。 まずはノート PC からの実行で問題ありません。ネットワーク経路を原因から除外するには、クラスター内でコンテナーとして実行し(--cpu 6、vLLM イメージには Python と httpx がすでに含まれています)、サーバーのPod IP(kubectl get pod <podName> -o jsonpath='{.status.podIP}')を指定してから、パブリックな webUrl を使って再度実行してください。2 つの結果が一致すれば、イングレスはボトルネックではありません。推測せず、実際に測定してください。
4. GPU がボトルネックか確認する
スループットの数値がハードウェアの数値と言えるのは、ハードウェアが飽和していた場合だけです。 モデルを実行しているノードで DCGM を確認します。
NODE=$(kubectl -n ns-<env> get pod <podName> -o jsonpath='{.spec.nodeName}')
DCGM=$(kubectl -n chalk-telemetry get pods -o wide | grep "dcgm.*$NODE" | awk '{print $1}')
kubectl -n chalk-telemetry port-forward "$DCGM" 19400:9400 &
curl -s localhost:19400/metrics | grep -E '^DCGM_FI_DEV_(GPU_UTIL|POWER_USAGE|FB_USED)'
実行中にサンプルを取得してください。GPU_UTIL が 100 で、POWER_USAGE がカードの TDP
(L4 は 72W、A100 40GB は 300W)なら、GPU がボトルネックであり、その数値は実際の性能を示しています。
使用率が 100% を大きく下回っている場合、測定したのはクライアント、ネットワーク、または
スケジューラーの上限であり、GPU ではありません。エクスポーターは一定間隔でスクレイプするため、
連続するサンプルが同じになるのは想定どおりです。
5. 実際の $/hr を調べる
サードパーティの料金ページやウェブ検索の要約を信用しないでください。 GPU マシンファミリーでは、アクセラレーターが vCPU や RAM とは別の SKUであることが多く、 集約サイトではこれが省かれるため、ノードの料金が 3 分の 1 に過小評価されることがよくあります。
GCP の Cloud Billing Catalog(Compute Engine サービス
6F81-5844-456A)に直接問い合わせ、ノードを構成する SKU の料金を合計します。
# paginate skus?pageSize=5000¤cyCode=USD with an Authorization: Bearer token
# from `gcloud auth print-access-token`, filter serviceRegions for your region,
# then match description against your machine family and accelerator.
計算例:us-central1 の g2-standard-8(8 vCPU、32 GiB、L4 1 基):
| SKU | 料金 | 数量 | 小計 |
|---|---|---|---|
| アメリカで稼働する G2 インスタンスコア | $0.024988 /hr | 8 | $0.199904 |
| アメリカで稼働する G2 インスタンス RAM | $0.002927 /GiB-hr | 32 | $0.093664 |
| アメリカで稼働する Nvidia L4 GPU | $0.560040 /hr | 1 | $0.560040 |
| $0.853608 /hr |
GPU SKU を省くと $0.2936/hr となります。これは公開料金の集約サイトに掲載されていることがある誤った数値です。 同じ方法で Spot 料金も計算し、想定どおりの割引になっていることを確認して、妥当性をチェックしてください。
Spot と 1yr/3yr CUD の料金も取得してください。これらは別の SKU であり、本番環境へのデプロイで実際に支払う料金です。
6. コストを計算する
$/sec = node_hourly / 3600
$/1M in-tok = $/sec / prompt_tok_per_s * 1e6
$/1M req = $/sec / rps * 1e6
**入力トークン 100 万件あたりの $ と、リクエスト 100 万件あたりの $**に加えて、オンデマンド/Spot/CUD の各料金を報告してください。
適切な単位を選んでください。 1 つのトークンだけを出力する分類器やガードレールでは、出力トークン数とリクエスト数は同じであり、「出力トークンあたりの $」は実質的に意味のない指標です。**分類 1 件あたりの $**を先頭に示してください。チャットモデルやコーディングモデルではデコードがコストの大部分を占めるため、出力トークンあたりの $ が意味のある指標になります。--output-tokens には 1 ではなく、現実的な生成完了長を設定してください。
落とし穴
- プレフィックスキャッシュにより、入力トークンあたりの $ が実際より高く見えます。 共有システムプロンプトはキャッシュされるため、
prompt_tokens_totalには再計算されていないトークンも含まれます。同梱のスクリプトはヒット率を報告します。入力トークンすべてに対して課金するベンダーとの比較としては公平ですが、実際に追加で処理できる量を示すものではありません。どちらの数値を提示しているのか明記してください。 - 飽和時の数値は下限であり、予測値ではありません。 これらは稼働率 100% の場合の数値です。実際の稼働率が 20% なら、5 倍してください。ほとんどの本番環境では、GPU のアイドル時間がコストの大部分を占めます。
- ツール呼び出しモデルには、適切なパーサーとチェックポイントが必要です。
--enable-auto-tool-choice --tool-call-parser hermesと instruct モデルを組み合わせてください。 Coder 向けに調整されたチェックポイントの中には、ツール呼び出しをフェンス付き JSON テキストとして出力するものがあり、パーサーがこれを無視します。そのため、tool_choice: autoではtool_callsが null を返す一方、tool_choice: requiredは動作します(ガイド付きデコーディングが出力形式を強制するためです)。 - 完了したコンテナーポッドはガベージコレクションされ、ログも失われます。 1 回限りのドライバースクリプトの末尾に
sleepを入れると、引き続きkubectl logsでログを取得できます。 - k8s コンテナーでは
chalk container execは実装されていません (only implemented for host containers)。エントリーポイント経由で処理を実行するか、--compute-class host/サンドボックス API を使用してください。 bashにスクリプトをパイプすると、標準入力を読み取る処理がある場合に気づかないまま失敗します。 子プロセスがスクリプトの残りを読み込んでしまいます。スクリプトをファイルに書き出し、exec </dev/nullを追加してください。- サンドボックスにはネットワークの外向き通信がなく、許可するフラグもありません。 パブリック IP への通信もブロックされます。ワークロードから何らかの宛先へ接続する必要がある場合は、
chalk container run --route <CIDR>を使用してください。
レポート
モデル、GPU、デフォルト以外のすべてのサービングフラグ、測定したスループット、DCGM の飽和度を示す証拠、および $/hr の内訳となる SKU の構成を記載してください。これらがないコスト・パー・トークンの数値は再現できないため、引用しないでください。