この記事についてClaude(Anthropic)との共同編集により作成されました。
要約
- 負荷検証スクリプトは AI に頼めば数分で書ける。問題は書けることではなく、別々に書いたスクリプトの数字が同じ尺度に乗らないことにある
- ずれる場所は決まっている。ストリーミングを切ると TTFT / ITL が測れない、reasoning モデルでは TTFT の測定点がツールごとに違う、同じプロンプトを繰り返すと prefix cache でスループットが水増しされる、負荷発生器側が先に飽和する、負荷の掛け方(一斉送信 / 定常レート / 同時数固定)で分布が変わる
- 汎用の負荷試験(同時接続に耐えるか)と LLM の性能ベンチマーク(TTFT / ITL / tok/s)は別の問いで、NVIDIA も vLLM もそう整理している。「全部 Locust で測る」は E2E 秒数と tok/s を混同しやすい
- Python 主軸の汎用負荷検証は Locust が第一候補。ゲートウェイの高 RPS と CI ゲートには k6(スクリプトは JS、ライセンスは AGPL)を限定併用する
- vLLM の本番評価は公式が GuideLLM を推奨している。
vllm bench serveは回帰とデータセット比較、vllm bench sweepはパラメータ探索、AIPerf は NVIDIA スタックと goodput 向け- 自分の結論は、測定ツールを層ごとにチームで 1 つに統一すること。アプリ層は Locust、vLLM 層は公式の
vllm benchと GuideLLM。測定条件(ISL / OSL、負荷プロファイル、レート)はレポートに書くのが当然だが、要件ごとに違うので車輪を揃える手段にはならない。同じ TTFT という名前の数字が車輪ごとにずれるのは困るので、ツールを固定する。AI に書かせるのは測定器ではなく、決めたツールを回すシナリオとラッパー
はじめに
LLM 推論サーバーを運用していると、負荷検証は避けて通れない。「同時 50 ユーザーで p95 がどこまで伸びるか」「max_num_seqs を上げたら tok/s がどう変わるか」といった問いは、GPU を買う前にも、サーバー設定を変えるたびにも出てくる。
ここで最近の困りごとは、負荷検証ツールを作ることが簡単になりすぎたことにある。「OpenAI 互換の /v1/chat/completions に同時 N 本投げて、レイテンシとトークン数を集計するスクリプト」は、Claude Code や Cursor に頼めば数分で出てくる。動く。数字も出る。
問題はその先で、複数人が複数プロジェクトでそれぞれ AI にスクリプトを書かせると、出てきた数字が同じ尺度に乗らない。A プロジェクトは「p95 が 2.1 秒」と言い、B プロジェクトは「35 tok/s」と言い、C プロジェクトは「TTFT 400ms」と言う。どれも正しく測れているように見えて、ストリーミングの有無、プロンプト長、負荷の掛け方、prefix cache の状態が違うので、横に並べても比べられない。同じサーバーの同じ設定を測っても、スクリプトが違えば違う数字が出る。
これは車輪の再発明が非効率だという話ではない。車輪を再発明するコスト自体は、AI がほぼゼロにした。再発明された車輪同士で寸法が合わないことが問題である。
そこで、Python 主軸の汎用負荷検証ツールと、vLLM など LLM 推論サーバー向けの専用ツールを、公式ドキュメントで整理し直した。ツール比較そのものより、「どこで尺度がずれるか」と「チームで何を決めておけば比べられるようになるか」を主眼に書く。
自作スクリプトの数字がずれる場所
先に、自作スクリプト同士で数字が食い違う原因を挙げておく。ここに挙げるものは、どれも専用ツールの公式ドキュメントがわざわざ注意書きしている項目で、逆に言えば自作だと踏みやすい。
ストリーミングを切ると TTFT / ITL が測れない
LLM の体感速度は、リクエストを投げてから最初のトークンが返るまでの TTFT(Time To First Token)と、その後のトークン間隔 ITL(Inter-Token Latency)で決まる。非ストリーミングでリクエストすると、クライアントは生成完了まで待つので、この 2 つは原理的に取れない。AIPerf は TTFT / ITL を出すには --streaming が必要だと明記している1。
自作スクリプトで stream: false のまま「レスポンス時間」を測ると、出てくるのは生成完了までの E2E であって、体感速度の代理指標にはならない。汎用 HTTP ツールの「レスポンス時間」も同じで、ストリーミングにしても HTTP 完了までの時間しか記録しない。
メトリクスの名前が同じでも定義が違う
vLLM の vllm bench serve は、TTFT をリクエスト送信から最初のストリーム出力まで、TPOT を (E2E - TTFT) / (出力トークン数 - 1) としてリクエスト単位で集計し、ITL を連続するストリーム出力の間隔と定義している2。投機デコードを使うと 1 回のストリームイベントで複数トークンが返るため、ITL と TPOT が乖離しうる2。
reasoning モデルではさらにずれる。AIPerf の TTFT は reasoning を含む最初のトークンまでで、reasoning でない最初の出力トークンまでは TTFO という別の指標になっている。旧 GenAI-Perf の TTFT は後者に相当する3。つまり同じ TTFT という名前でも、ツールとバージョンで測定点が違う。vLLM 公式も、名前ではなく測定点と式で比較せよ、と書いている2。
自作スクリプトの場合、この定義は書いた人の頭の中にしかない。「TTFT 400ms」の 400ms が最初のバイトまでなのか、最初の非 reasoning トークンまでなのか、レポートを読んでも分からない。
prefix cache でスループットが水増しされる
vLLM は prefix caching を持つので、同じサーバーに同じプロンプトを繰り返し投げると、prefill が省略されてスループットが実態より良く出る。公式はこれを Warning として書いていて、意図しないなら --seed を変えるか、サーバーを再起動するか、キャッシュをリセットする vllm bench sweep serve を使え、としている2。
自作スクリプトは「Hello」を 100 回投げがちで、これは prefix cache のベンチマークをしているのに近い。
負荷発生器側が先に飽和する
Locust のデフォルト HttpUser は requests ベースで、高 RPS では負荷を掛ける側の CPU が先に飽和する。公式は FastHttpUser を用意していて、同一ハードウェアで最大 RPS が 5〜6 倍になることがあると説明している4。目安として 1 プロセスあたり FastHttpUser で約 16,000 RPS、HttpUser で約 4,000 RPS とあるが、これは被試験システムの性能ではなく、負荷発生器側の上限である4。Python はプロセスあたり 1 コアしか使い切れないので、分散時はコアあたり 1 worker が推奨されている5。
asyncio + httpx で書いた自作スクリプトが 1 プロセスで頭打ちになり、それが「サーバーの限界」として報告されている可能性は普通にある。
負荷の掛け方で分布が変わる
同じ 100 リクエストでも、一斉送信するのか、毎秒 5 本の定常レートで送るのか、Poisson 到着にするのか、同時数を 16 に固定するのかで、レイテンシ分布はまったく違う。vllm bench serve の --request-rate はデフォルトが inf(一斉送信)で、--burstiness で Poisson / Gamma、--max-concurrency で同時数上限を指定する2。GuideLLM のプロファイルは synchronous / concurrent / throughput / constant / poisson / sweep に分かれている6。vLLM の sweep 文書は、request_rate より max_concurrency のほうがエンジン負荷を直接制御できて安定しやすい、と書いている7。
自作スクリプトは asyncio.gather で一斉送信していることが多い。これは throughput プロファイルの一種であって、定常レートの結果とは比べられない。
入出力トークン長がレポートに残らない
ISL(入力トークン長)と OSL(出力トークン長)を変えれば、prefill と decode の比率が変わり、TTFT も tok/s も変わる。専用ツールはこれを prompt_tokens=512,output_tokens=128 のように明示するが、自作スクリプトでは固定文字列とデフォルトの max_tokens になりがちで、レポートに残らない。
こうして並べると分かるとおり、ずれる場所のうち ISL / OSL や負荷プロファイルは要件次第で変わってよいものだが、メトリクスの定義、ストリーミングの扱い、キャッシュへの対処、負荷発生器の上限はツールに固有である。同じツールを使っていれば、条件の違いは引数の違いとして読めるが、ツールが違えば、同じ名前の数字が違うものを指す。この観点を持ったうえで、ツールを見ていく。
先に分ける:負荷試験と性能ベンチマークは別の問い
ツールを並べる前に、測りたい対象を分ける。NVIDIA の NIM ベンチマークガイドは、Locust / k6 を負荷試験(同時接続・オートスケール・ゲートウェイ)、AIPerf を性能ベンチマーク(TTFT / ITL / トークンスループット)と分けて整理している8。vLLM 公式も、本番サーバーの評価には GuideLLM を推奨し、付属の vllm bench serve は主に機能評価と回帰用途だと位置づけている2。
| 測りたいこと | 典型メトリクス | 向いている系統 |
|---|---|---|
| API / ゲートウェイが同時接続に耐えるか | RPS、E2E レイテンシ、エラー率、CPU | Locust, k6, JMeter |
| ユーザー行動(ログイン → 複数 API)を再現するか | タスク成功率、シナリオ遅延 | Locust, Gatling |
| LLM の体感速度と容量上限 | TTFT, ITL / TPOT, tok/s, goodput | GuideLLM, vllm bench, AIPerf |
サーバー設定(max_num_seqs など)のトレードオフ | レイテンシ vs スループット曲線 | vllm bench sweep, GuideLLM sweep |
自作スクリプトがずれる根本はここで、上の表の 1 行目と 3 行目を 1 本のスクリプトで兼ねようとする。「同時接続に耐えるか」を測っているのか「体感速度」を測っているのかが混ざると、E2E 秒数と tok/s が同じレポートに並んで、どちらとも比べられなくなる。
Python 主軸の汎用負荷検証ツール
比較表
| ツール | 言語 / ランタイム | ライセンス | 強み | 弱み | Python チームでの位置づけ |
|---|---|---|---|---|---|
| Locust | Python(gevent) | MIT | シナリオが普通の Python。分散実行が OSS 標準。Web UI。任意プロトコルに拡張可能 | 単一ノードの生 RPS は k6 より低い。pass/fail は後処理が必要になりやすい | 第一候補 |
| Molotov | Python(asyncio / aiohttp) | — | コルーチンで短いシナリオを書ける。autosizing | エコシステム・UI・分散が Locust より薄い | Locust が重いときの軽量代替 |
| Taurus | Python(YAML オーケストレーション) | Apache-2.0 | JMeter / Locust 等を YAML で束ねる | 負荷生成エンジンそのものではない | 既存ツールのラッパー |
| k6 | スクリプトは JS(エンジンは Go / Sobek) | AGPL-3.0 | 単一バイナリ。高 RPS。native thresholds で CI ゲート | Python ではない。npm も使えない。AGPL | ゲートウェイ層の高負荷・CI 用 |
| Gatling | Java / Kotlin / Scala | Apache-2.0 | JVM チーム向け。HTML レポートと assertions | Python ではない | JVM サービスと揃える場合のみ |
| JMeter | Java(GUI + XML) | Apache-2.0 | JDBC / JMS など HTTP 以外のプロトコル | .jmx はレビューしにくい。VU あたり重い | DB / キュー直叩きが必要なとき |
| Vegeta | Go CLI | MIT | 定数レートの HTTP ブラスト | HTTP のみ。分散なし。シナリオ向きでない | 単発のレート確認 |
| oha | Rust CLI | MIT | wrk 系の高速 HTTP ベンチ。メンテが続いている | シナリオ・分散なし | 単一エンドポイントの上限確認 |
| wrk | C + LuaJIT | Custom | 単一マシンの生スループット | 2023 年末以降コミットが止まっているという指摘がある(一次確認はしていない) | 新規採用は見送り |
Locust — Python チームの標準候補
Locust は HTTP を中心とした OSS の負荷試験ツールで、テストは普通の Python として書く9。各仮想ユーザーは greenlet 上で動くため、async コールバックではなく同期風のコードでシナリオを書ける9。公式が挙げる特徴は、シナリオを Python で書くこと、master / worker による分散実行、リアルタイム Web UI(ヘッドレス実行も可能)、HTTP 以外もクライアントを書けば対象にできること、である9。
スループット側の注意は前述のとおりで、高 RPS が要るなら FastHttpUser を使い4、単一マシンなら locust --processes -1 で論理コア数ぶんの worker を立てる5。master 自体は User を動かさない。
LLM 向けには、実験的な locust.contrib.oai.OpenAIUser がある。OpenAI SDK を使い、response_length に出力トークン数を入れる設計で、ペイロードのバイト数より有用という位置づけである10。ただしTTFT / ITL の標準メトリクスは出ないし、公式も experimental と注記している10。Locust 2.40 以降は pytest 風の test_* 関数と session fixture でもシナリオを書けるが、こちらも experimental である10。
Locust が向くのは、Python チームがシナリオとアプリコードを同じ言語でメンテしたい場合、認証・複数ステップ・条件分岐のあるユーザー行動を再現したい場合、社内 SDK や既存クライアントを再利用したい場合、Web UI で同時ユーザー数を探索的に変えたい場合、分散負荷を OSS のまま複数マシンに広げたい場合である。向かないのは、単一ノードで数十万 RPS を出したい場合と、LLM のトークン単位 SLO を厳密に測りたい場合である。
k6 — Python ではないが、CI ゲートとしては強い
Grafana k6 は Go 製の負荷試験ツールで、テストスクリプトは埋め込み JS エンジン(Sobek)上の JavaScript である1112。Node.js ではないため npm パッケージは使えない。単一静的バイナリ、高 RPS、そして CI 向きの thresholds(満たさなければ非ゼロ終了)が強みである13。
export const options = { thresholds: { http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<200'], },};「p95 が閾値を超えたら fail」を宣言的に書けるのは、Locust だと後処理が要るところなので、CI ゲートとしては k6 のほうが素直である。
OSS の k6 は AGPL-3.0 で12、自システムへの負荷試験ツールとして社内で走らせる分と、製品に埋め込んでホストする分では法務上の扱いが違う。Python 互換を謳う xk6-python は Grafana Hackathon の PoC で、Starlark 方言であり CPython / pip は使えない。リポジトリは archived である14。
Python メインなら、k6 は「ゲートウェイの高 RPS CI」に限定して併用するのが現実的で、メインのシナリオ言語にはしない。
その他
- Molotov —
@scenario付きの asyncio コルーチン。aiohttp セッションを受け取る。gRPC 対応(2.7 以降)。--sizingで失敗率が閾値を超えるまで worker を増やす15。Locust より単純だが、UI・分散・エコシステムは薄い - JMeter — HTTP 以外(JDBC, JMS, LDAP 等)が必要なときの OSS 選択肢。テスト計画は XML(
.jmx) - Gatling — JVM DSL。assertions と HTML レポートが強い。Python チームには学習コストが大きい
- Vegeta / oha — URL を定数レートまたは高並行で叩く CLI。シナリオ維持には向かない
- Taurus — 複数エンジンの YAML ラッパー。エンジン選定を代替するものではない
LLM 推論サーバー向けの専用ツール
vLLM は OpenAI 互換 HTTP を出すので Locust でも叩けるが、本番サーバーの性能評価として公式が推しているのは GuideLLM である2。汎用ツールとは別レイヤーの専用ツールが、公式の役割分担つきで揃っている。
比較表
| ツール | 提供元 | 言語 | TTFT / ITL | 公式の位置づけ | 向く用途 |
|---|---|---|---|---|---|
| GuideLLM | vLLM project | Python 3.10–3.13 | あり(分布つき) | 本番 vLLM サーバーの推奨ベンチマーク | 容量計画、SLO、sweep |
vllm bench serve | vLLM 本体 | Python | あり(TTFT / TPOT / ITL) | 機能評価・回帰が主 | データセット比較、PR 回帰 |
vllm bench sweep | vLLM 本体 | Python | 上記を複数条件で | 設定・負荷のスイープ | max_num_seqs 等のチューニング |
| AIPerf | NVIDIA (ai-dynamo) | Python | あり(reasoning は TTFO も) | NIM / 生成 AI 推論の推奨 | goodput、GPU テレメトリ |
Locust OpenAIUser | Locust contrib | Python | なし(出力トークン数は記録) | experimental | アプリ層の同時ユーザー |
| llmperf | Ray project | Python + Ray | あり(ITL / throughput) | 古い簡易ロードテスト | 参考・レガシー |
| inference-perf | kubernetes-sigs | Python | 部分的 | GuideLLM 側の比較表に掲載 | k8s 推論負荷 |
| genai-bench | SGLang project | Python | 部分的 | 同上 | SGLang 周辺 |
GuideLLM の README にも他ツールとの比較表があるが6、プロジェクト自身の表なので割り引いて読む。たとえば vllm bench は TTFT / ITL を出すので、そこでの「フルメトリクスなし」は GuideLLM 基準の自己評価である。
GuideLLM — vLLM 本番評価の第一候補
GuideLLM は vLLM プロジェクト配下の Python パッケージで、OpenAI 互換および vLLM native サーバーに対して本番に近い負荷をかけ、TTFT / ITL / E2E の分布を取る6。vLLM のベンチマーク文書は、本番サーバー評価に GuideLLM を勧め、vllm bench serve よりデータセット読み込み・リクエスト形式・負荷パターンが柔軟だと書いている2。
pip install guidellm[recommended]最小の sweep 例(合成データ、30 秒)6:
guidellm run \ --backend kind=openai_http,target=http://localhost:8000 \ --profile kind=sweep \ --constraint kind=max_duration,seconds=30 \ --data kind=synthetic_text,prompt_tokens=256,output_tokens=128負荷プロファイルは synchronous / concurrent / throughput / constant / poisson / sweep。データは合成テキスト、HuggingFace、ローカルファイル、マルチモーダル合成、Mooncake trace の replay など6。出力は JSON / CSV / HTML。warmup / cooldown、エラー上限、過飽和(over-saturation)検出もある。Python 3.10–3.13、Linux / macOS、Apache-2.06。
注意点が 2 つ。sweep の最初のステージは同期(逐次)実行になるため、--max-requests を大きくすると極端に長くなり得るという Issue がある16。時間制約(max_duration)を先に置くか、同期ステージの件数を小さくする。もう 1 つ、CLI は kind=key,value 形式へ移行済みで、古いブログにある guidellm benchmark --rate-type=sweep はそのまま使えないことがある。AI にコマンドを書かせると古い形式が出てくることがあるので、README の現行形式で確認する。
vllm bench serve / sweep — vLLM 付属
サーバーを立ててからクライアント側で測るオンラインベンチマークである2。
vllm serve NousResearch/Hermes-3-Llama-3.1-8B
vllm bench serve \ --backend vllm \ --model NousResearch/Hermes-3-Llama-3.1-8B \ --endpoint /v1/completions \ --dataset-name sharegpt \ --dataset-path ShareGPT_V3_unfiltered_cleaned_split.json \ --num-prompts 10出力にはリクエスト / 出力トークン / 総トークンのスループットと、TTFT、TPOT、ITL が含まれる2。定義は前述のとおり。負荷パターンは --request-rate、--burstiness、--max-concurrency で、データセットは ShareGPT、random、custom JSONL、画像・音声など広い2。
vllm bench sweep は複数設定を回して可視化するスイートで7、次のサブコマンドを持つ。
serve—vllm serveのパラメータ組み合わせ(例:max_num_seqs×max_num_batched_tokens)serve_workload— 負荷レベルを探索してレイテンシ vs スループット。公式は GuideLLM の--profile sweep相当と明記。デフォルトは GuideLLM 寄せでrequest_rateだが、--workload-var max_concurrencyのほうが安定しやすい7serve_sla— 例:{"p99_e2el_ms": "<=500"}を満たす最大レート / 同時数を探索plot/plot_pareto— 曲線と tokens/s/user vs tokens/s/GPU
機能の回帰や「この PR で ShareGPT が落ちていないか」には vllm bench serve。本番の容量計画には GuideLLM。設定スイープはどちらでもできるが、vLLM 付属 sweep はサーバー起動からキャッシュリセットまで一体で回せる。
AIPerf — NVIDIA の生成 AI ベンチマーク(旧 GenAI-Perf)
AIPerf は NVIDIA の OSS(pip install aiperf)で、OpenAI 互換エンドポイントのストリーミング性能を測る17。NIM 向けガイドは Locust / k6 を負荷試験、AIPerf を性能ベンチマークと分け、後者を推奨している8。GenAI-Perf の後継で、多くのオプションは drop-in である3。
aiperf profile \ --model your-model \ --url localhost:8000 \ --endpoint-type chat \ --streaming \ --concurrency 10 \ --request-count 100--streaming を付けると TTFT / ITL が有効になる1。時間ベースなら --benchmark-duration。goodput(SLO を満たしたリクエストだけのスループット)や、disaggregated serving 向けの --prefill-concurrency もある。vLLM を OpenAI 互換で叩けるので、NVIDIA スタックや goodput / GPU テレメトリが要るときに使う。
Locust で vLLM を叩く意味
意味はあるが、GuideLLM の代替ではない。向くのは、LiteLLM や自前ゲートウェイを経由して vLLM に届く経路全体、API キー・レート制限・リトライ・マルチターンといったユーザー行動、「同時 N ユーザーがチャットする」運用試験(オートスケール、接続枯渇、タイムアウト)といったアプリ層である。
from locust import HttpUser, task, betweenimport json
class ChatUser(HttpUser): wait_time = between(1, 3)
@task def chat(self): self.client.post( "/v1/chat/completions", json={ "model": "your-model", "messages": [{"role": "user", "content": "Hello"}], "stream": True, "max_tokens": 128, }, headers={"Authorization": "Bearer dummy"}, name="/v1/chat/completions", )この書き方で Locust が記録するのは、HTTP 完了までの時間である。ストリーミングの最初のチャンク時刻(TTFT)を取るなら、自前で chunk を読んでイベントを fire する必要がある。ここまで来ると専用ツールを使うほうが早い。実験的な OpenAIUser は Responses API の例が中心で、vLLM の主経路は Chat Completions なので、アプリ試験は HttpUser のほうが現状は分かりやすい10。
使い分けは 2 本立て
サーバーのトークン性能は専用ツール、アプリ全体の同時ユーザーは Locust、という 2 本立てが基本になる。
| やりたいこと | 使うツール | 使わない理由 |
|---|---|---|
| vLLM の容量計画、SLO(TTFT p99 など) | GuideLLM | Locust はトークン指標が標準でない |
| PR 回帰、データセット比較 | vllm bench serve | GuideLLM よりセットアップが重い場合がある |
max_num_seqs 等のパラメータ探索 | vllm bench sweep または GuideLLM sweep | 手で concurrency を変えると再現しにくい |
| NIM / goodput / GPU メトリクス | AIPerf | vLLM 単体の評価なら公式推奨は GuideLLM |
| ゲートウェイ+認証+同時ユーザー | Locust | GuideLLM はユーザー行動の記述が主目的ではない |
| 単一 HTTP の生 RPS | oha / k6 | LLM 指標は出ない |
| 「全部 Locust で測る」 | しない | E2E 秒数と tok/s を混同しやすい |
実務的な順序は次のようになる。
vllm serveを固定設定で起動する- GuideLLM の sweep で、合成プロンプト(本番に近い ISL / OSL)の安全動作点を探す
- 必要なら
vllm bench sweepでエンジンパラメータを変えて Pareto を見る - 前段に LiteLLM 等があるなら、Locust で同時ユーザーとエラー率を見る
- CI では
vllm bench serveを短時間(固定 seed / キャッシュ方針を明記)で回帰する
チームで決めること:層ごとに測定ツールを 1 つにする
ここからは自分の結論である。
測定条件(ISL / OSL、負荷プロファイル、レート、seed とキャッシュ方針)をレポートに書くのは当然で、書かない数字は比べようがない。ただ、条件を書くことと、車輪を揃えることは別の話である。測定条件は要件で決まる。チャットボットのプロジェクトなら ISL は短く OSL は長め、RAG なら ISL が長く、バッチ要約なら定常レートより一斉送信のほうが実態に近い。プロジェクトごとに条件が違うのは当たり前で、そこを揃えようとしても揃わないし、揃える必要もない。
車輪を揃えるために固定するのは、条件ではなくツールである。理由は単純で、同じ TTFT という名前の数字が、車輪ごとに違うものを指していると困るからだ。上で見たとおり、TTFT の測定点、ITL と TPOT の式、ストリーミングの扱い、prefix cache への対処、負荷発生器の上限は、どれもツールに固有の話である。ツールが同じなら、この部分は自動的に揃う。残るのは ISL / OSL や負荷プロファイルといった条件の差で、これは同じツールの引数の差として読めるので、レポートを横に並べたときに「どこが違うか」がすぐ分かる。
ただし、ツールを全社で 1 つに統一することはできない。ゲートウェイの同時ユーザーを測るツールと、vLLM のトークン性能を測るツールは違う。なので、層ごとに 1 つにする。
| 層 | 測るもの | チームで決めるツール |
|---|---|---|
| アプリ層(ゲートウェイ、認証、同時ユーザー) | RPS、E2E レイテンシ、エラー率 | Locust |
| vLLM 層(推論サーバー単体) | TTFT / ITL / TPOT、tok/s | vllm bench serve と GuideLLM |
アプリ層を Locust にするのは、Python チームがシナリオをアプリコードと同じ言語で持てて、分散と Web UI が OSS 標準だからである。vLLM 層を公式の vllm bench と GuideLLM にするのは、どちらも vLLM プロジェクト配下で、メトリクスの定義が公式ドキュメントに書かれていて、prefix cache やストリーミングの注意書きが揃っているからである。回帰は vllm bench serve、容量計画と SLO は GuideLLM、という使い分けは公式の位置づけに従う。AIPerf は NVIDIA スタックに寄せるチームなら候補になるが、vLLM 中心ならまず入れない。
この 2 段の切り分けができていれば、自作スクリプトは測定器から外れる。AI に書かせるのは、決めたツールを回す側になる。Locust の locustfile.py、GuideLLM の起動コマンドと出力先を固定したラッパー、vllm bench serve を CI で短時間回すスクリプト。プロジェクトごとに変えるのは、シナリオの中身とターゲットと ISL / OSL といった要件由来の部分だけで、測定器そのものは変えない。
ここまで決めておけば、車輪の再発明は起きても、寸法は揃う。A プロジェクトの TTFT と B プロジェクトの TTFT が同じ GuideLLM の出力なら、条件の差を確認したうえで比べられる。別のツールの数字は、測定点と式を確認するまで比べない。reasoning モデルの TTFT のように、同じ名前で違うものを指す指標があるからである3。
最小コマンド例(vLLM)
サーバー:
vllm serve Qwen/Qwen3-0.6B --port 8000GuideLLM(本番寄りの評価)6:
guidellm run \ --backend kind=openai_http,target=http://localhost:8000 \ --profile kind=constant,rate=5 \ --constraint kind=max_duration,seconds=60 \ --data kind=synthetic_text,prompt_tokens=512,output_tokens=128vLLM 付属(回帰)2:
vllm bench serve \ --backend openai-chat \ --model Qwen/Qwen3-0.6B \ --endpoint /v1/chat/completions \ --dataset-name random \ --num-prompts 100 \ --random-input-len 512 \ --random-output-len 128 \ --request-rate 5 \ --max-concurrency 16AIPerf1:
aiperf profile \ --model Qwen/Qwen3-0.6B \ --url localhost:8000 \ --endpoint-type chat \ --streaming \ --concurrency 8 \ --benchmark-duration 60GuideLLM と vllm bench serve の例は ISL 512 / OSL 128、ストリーミングあり、定常レートまたは同時数固定、に揃えてある。一方 AIPerf の例は公式ドキュメントの形をそのまま載せているので、合成トークン長を指定しておらず、このままでは上の 2 本と比べられない。揃えるなら CLI リファレンス1で合成トークン長のオプションを確認して、同じ 512 / 128 にしてから当てる。
チームで決めたツールを導入する前に、この 3 本の条件を揃えて同じサーバーに順に当て、TTFT の中央値がどれくらいずれるかを一度見ておくと、「ツールをまたいだ比較にどれくらい幅を見込むべきか」の肌感が持てる。ツールを 1 つに決める理由が、数字で見える。
おわりに
負荷検証ツールを作ること自体は、AI で簡単になった。簡単になったぶん、プロジェクトごとに違う測定器が増えて、数字が比べられなくなった。
調べてみると、専用ツールの公式ドキュメントは、自作でずれる場所をほぼ全部、注意書きとして書いていた。ストリーミングを切ると TTFT が取れない、reasoning モデルでは TTFT の定義が違う、同じプロンプトの繰り返しは prefix cache で水増しされる、負荷発生器が先に飽和する、負荷の掛け方で分布が変わる。専用ツールを使うというのは、この注意書きを一式引き受けてもらうことに近い。
測定条件はレポートに書く。ただしそれは要件で変わるので、車輪を揃える手段にはならない。揃えるのはツールで、層ごとにチームで 1 つに決める。アプリ層は Locust、vLLM 層は公式の vllm bench と GuideLLM。同じツールの数字なら、条件の差は引数の差として読める。AI に書かせるのは測定器ではなく、決めたツールを回すシナリオとラッパーにする。
車輪の再発明は止められないし、止める必要もない。寸法だけ揃えればいい。
参考文献
- NVIDIA AIPerf — Command Line Options https://docs.nvidia.com/aiperf/reference/command-line-options
- vLLM — Benchmark CLI https://docs.vllm.ai/en/latest/benchmarking/cli/
- NVIDIA AIPerf — Migrating from GenAI-Perf https://docs.nvidia.com/aiperf/getting-started/migrating-from-gen-ai-perf
- Locust — Increase performance with FastHttpUser https://docs.locust.io/en/stable/increase-performance.html
- Locust — Distributed load generation https://docs.locust.io/en/stable/running-distributed.html
- vllm-project/guidellm README https://github.com/vllm-project/guidellm
- vLLM — Parameter Sweeps https://docs.vllm.ai/en/stable/benchmarking/sweeps/
- NVIDIA NIM LLMs Benchmarking — Overview https://docs.nvidia.com/nim/benchmarking/llm/2.0.0/overview.html
- Locust — What is Locust? https://docs.locust.io/en/stable/what-is-locust.html
- Locust — Testing other systems (OpenAI / pytest) https://docs.locust.io/en/stable/testing-other-systems.html
- Grafana k6 documentation https://k6.io/docs/
- grafana/k6 GitHub (AGPL-3.0) https://github.com/grafana/k6
- Grafana k6 — Thresholds https://grafana.com/docs/k6/latest/using-k6/thresholds/
- grafana-cold-storage/xk6-python https://github.com/grafana-cold-storage/xk6-python
- Molotov documentation https://molotov.readthedocs.io/en/latest/index.html
- GuideLLM Issue #588 (sweep × max-requests) https://github.com/vllm-project/guidellm/issues/588
- ai-dynamo/aiperf https://github.com/ai-dynamo/aiperf