状態: ドラフト (2026-10-27)、実測値。 8GBクラスのノートPC用GPU(Ada世代)1基、vLLM 0.31.0(xgrammar 0.2.7, llguidance 1.7.6)、Qwen3-Reranker-0.6B を使用して 2026-10-27 に測定しました。公式ドキュメントは 0.32.0 基軸で確認し、構造化出力のドキュメントは 0.31.0 と同様でした。 2026-10-27 追加。 対話用の Qwen3-0.6B・Qwen3-4B(AWQ)と vLLM 0.32.0(xgrammar 0.2.8)を用いて正解のある抽出タスクを測定した結果を 3-1 節に追加しました。0.31.0 の数値はそのまま残しています。 作成規則: 実測データを基本とし、測定できなかったものは「未測定」、確認できなかったものは「未確認」と表記します。
目次
- 1. 原理: 次のトークン候補から文法外のものを除外する
- 2. 使い方と名称の変遷
- 3. 実際に測定してみる
- 3-1. 2026-10-27 再測定: 正解のある抽出タスク
- 4. 運用上の留意点
- 5. 確認できなかった点
- 参考
1. 原理: 次のトークン候補から文法外のものを除外する
モデルはステップごとにボキャブラリ全体(このモデルでは 151,669 個)に対するスコアを算出し、その中から 1 つを選択します。構造化出力は、選択の直前に 1 つのステップを挟み込みます。これまでの生成テキストを文法エンジン(有限オートマトンやプッシュダウン・オートマトン)に入力して「今出現可能なトークン」の集合を求め、それ以外のトークンのスコアを負の無限大でマスクします。この集合をビットマスクに変換し、GPU のスコアテンソルに一度に適用します。
지금까지: {"priority": "
문법상 다음에 올 수 있는 것: l(ow), m(edium), h(igh) 로 시작하는 토큰만
나머지 151,000여 토큰 → 점수 -inf
そのため、モデルがどれほど的外れであっても、結果は文法に違反しません。ただし、2 つのコストが生じます。
- 文法を生成するコスト: JSON スキーマや正規表現をトークン単位のステートマシンに変換する処理です。ボキャブラリが 15 万件あるため、トークンごとに「このトークンはこの状態で許可されるか」を事前に計算しておく処理に負荷がかかります。同じスキーマはキャッシュされます。
- ステップごとにマスクを生成するコスト: リクエストごとに状態を 1 つ進め、マスクを生成して GPU に送信します。
XGrammar の論文(Dong et al., MLSys 2025)によると、コンテキストに依存しない判定トークンを事前に計算しておき、文法計算を GPU 実行と並行して処理することで、従来の手法よりも最大 100 倍高速であり、サービングエンジンと組み合わせた場合のオーバーヘッドはほとんどないと報告されています(要約に基づく。条件は要約に記載なし)。vLLM は xgrammar と llguidance (guidance バックエンド) の両方を使用し、デフォルト値の auto はリクエスト内容を確認して選択します。
2. 使い方と名称の変遷
OpenAI 形式の response_format と vLLM 固有の structured_outputs の 2 つの方法があります。
| 目的 | リクエストに含める内容 |
|---|---|
| JSON スキーマ | response_format: {"type": "json_schema", "json_schema": {...}} または structured_outputs: {"json": 스키마} |
| 任意の JSON オブジェクト | response_format: {"type": "json_object"} |
| 選択肢のいずれか | structured_outputs: {"choice": ["low", "medium", "high"]} |
| 正規表現 | structured_outputs: {"regex": "[A-Z]{3}-[0-9]{4}"} |
| 文脈自由文法 (EBNF) | structured_outputs: {"grammar": "..."} |
| タグ内のみ JSON | structured_outputs: {"structural_tag": ...}(パート 5 の strict ツール呼び出しはこの方式) |
バージョンアップに伴い名称が変更されました。従来の guided_json, guided_regex, guided_choice, guided_grammar やリクエストごとの guided_decoding_backend は 0.12.0 で削除され、現在は structured_outputs に統合されました。バックエンドはサーバー側で --structured-outputs-config.backend によって決定され、リクエストごとにバックエンドを選択する機能は V1 から削除されました。正規表現の文法はバックエンドごとに異なり、xgrammar と guidance は Rust 形式、lm-format-enforcer は Python の re 形式です。0.32.0 以降では、64 階層より深くネストされた JSON スキーマは 400 エラーとして拒否されます(リリースノートに基づく、未測定)。
3. 実際に測定してみる
方法
- 環境: 8GB クラスのノートPC用GPU(Ada 世代)1基、vLLM 0.31.0、Qwen3-Reranker-0.6B (bf16)。このモデルはチャット形式で質問するとほぼ常に「No.」と返答します。そのため、 制約なしの場合とありの場合の差が非常に明確に現れます。
- リクエスト: チャット API を使用し、「name, priority(low/medium/high)、tokens(整数)、tags(文字列のリスト) のキーを持つ JSON のみで 1 つのタスクについて回答せよ」と要求し、
temperature 0.7,max_tokens 160、リクエストごとに異なるシードを指定しました。 - バリエーション: 制約なし、
json_schema(4 つのキーが必須、追加のキーは禁止、文字列長の上限あり)、json_object,choice,regex. - サーバー: バックエンドを
auto(デフォルト)、guidance,xgrammarに切り替えて 3 回起動しました。 - 反復: 同時リクエスト数 1 は 5 バッチ(1 バッチ目はウォームアップ)、同時リクエスト数 16 は 4 バッチ(1 バッチ目はウォームアップ)。「正確性」は、レスポンスを JSON としてパースし、4 つのキーが正確に存在し、かつ値の型が正しいかどうかで判定しました。
結果 1. 強制した場合の間違いは途切れたものだけであった
同時リクエスト数 16、測定 3 バッチ分(48 リクエスト)の合計です。
| バリエーション | auto | guidance | xgrammar | 応答例 |
|---|---|---|---|---|
| 制約なし | 0/48 | 0/48 | 0/48 | No. |
| json_object | 0/48 | 0/48 | 0/48 | {"status": "no", "data": []} |
| json_schema | 47/48 | 46/48 | 46/48 | {"name": "", "priority": "high", "tokens": 0, "tags": []} |
| choice | 48/48 | 48/48 | 48/48 | low |
| regex | 48/48 | 48/48 | 48/48 | IOW-1139 |
- json_schema において誤っていた応答は、すべて
finish_reasonがlengthであったケースでした。 guidance と xgrammar においてそれぞれ 2 回ずつ 160 トークン制限に達し、JSON が閉じられる前に終了しました(auto サーバーはこの項目を記録する前に実行してしまったため原因を記録できませんでした)。文法は「次のトークン」を制限するだけであり、「適切なタイミングでの終了」を保証するわけではありません。max_tokensをスキーマの最大長に対して余裕を持たせて指定し、クライアント側でfinish_reasonを必ず確認する必要があります。 - json_object は JSON 文法のみを強制します。 サンプルは文法的に正しい JSON でしたが、要求されたキーが 1 つも含まれていませんでした。キーが重要な場合は json_schema を使用します。
- 正しいことと有用であることは異なります。 スキーマに準拠した応答の大部分は、空の名前、0、空のリストでした。モデルが実行できないタスクを文法が肩代わりしてくれるわけではありません。
- auto サーバーの json_schema 応答における出力トークン数は、xgrammar 固定サーバーのバッチと同一でした(同時 1 において 28, 35, 32, 27)。このリクエストでは auto が xgrammar を選択したと推測されます(ログに選択の記録がないため未確認)。
結果 2. コスト: 初回リクエスト、およびトークンごとのわずかなオーバーヘッド
| バリエーション | バックエンド | 新スキーマ初回リクエスト TTFT(同時 1、ウォームアップバッチ) | 以降の TTFT 中央値 | トークン間隔中央値(同時 1) | トークン間隔中央値(同時 16) |
|---|---|---|---|---|---|
| 制約なし | xgrammar サーバー | 43.6ms | 14.1ms | 5.9ms | 6.3ms |
| json_schema | xgrammar | 445.7ms | 14.2ms | 6.3ms | 7.4ms |
| json_schema | guidance | 974.6ms | 14.3ms | 6.0ms | 7.3ms |
| json_schema | auto | 508.0ms | 14.3ms | 6.2ms | 7.5ms |
| regex | xgrammar | 15.0ms | 13.7ms | 6.5ms | 7.8ms |
| choice | xgrammar | 22.3ms | 14.2ms | 6.4ms | 7.7ms |
- 新しいスキーマの初回リクエストが最もコストがかかります。 同じスキーマの 2 回目以降のリクエストでは、TTFT は制約なしの場合と同等になりました(約 14ms)。初回リクエストの数値には、文法生成だけでなくサーバー側で最初に実行されるカーネルの準備時間も含まれており、両者を分離することはできませんでした。
- トークン間隔は、単一リクエスト時で約 7%(5.9 → 6.3ms)、同時 16 で約 17%(6.3 → 7.4ms)増加しました。 ただし、制約なしの応答は「No.」の 3 トークンのみであるため、比較の基準となる長さが短いです。同時 16 での増加幅が大きいのは、16 件のリクエストに対するマスクをステップごとに生成するためだと考えられます。
- regex と choice は文法が小さいため、初回リクエストのコストはほとんどありませんでした。
遭遇した問題: 初回サンプリングバッチにおける数秒の遅延
測定中、サーバーのログに次のような警告が出力されました。
WARNING [jit_monitor.py:140] Triton kernel JIT compilation during inference: _topk_topp_kernel.
This causes a latency spike; consider extending warmup to cover this shape/config.
新しいサーバーで初めて温度サンプリング(top-k・top-p)リクエストのバッチが入力されると、その場でカーネルがコンパイルされ、該当バッチの TTFT が 3.9 秒(guidance サーバー、同時 1)から 11 秒(guidance サーバー、同時 16)まで増加しました。貪欲デコーディングのみを実行している間はこの警告は発生しませんでした。コンパイルは 1 回で完了するわけでもありませんでした。別のサーバーで温度 1.0 のリクエストを同時 1, 8, 32 の順で送信したところ、並行数を変更するたびにそのバッチの処理が再び低速化しました(256 トークン生成において、同時 1 は 5.4 秒台から 1.5 秒、同時 8 は 7.4 秒台から 1.9 秒、同時 32 は 8.4 秒台から 3.2 秒)。バッチサイズごとにカーネルを新しく生成しているものとみられます。モデルの generation_config.json がデフォルトのサンプリング値(temperature 0.6, top_k 20, top_p 0.95)を上書きしているという警告も同時に出力されました。運用環境では、サーバーを起動した後に本番と同等のサンプリング設定を持つリクエストで一度ウォームアップを行ってから、トラフィックを受け付ける必要があります。0.32.0 のリリースノートには、サンプラーのウォームアップコンパイルを削減したという項目があります(未測定)。
3-1. 2026-10-27 再測定: 正解のある抽出タスク
前回のテストでは「正しい JSON であるか」のみを確認しました。今回は値が正しいかどうかも確認しました。
- 環境: vLLM 0.32.0 公式コンテナ。モデルは対話用 Qwen3-0.6B (bf16)、その FP8 オンライン量子化版、GPTQ 8bit 版、Qwen3-4B AWQ 4bit 版。思考プロセスはオフにしました。
- タスク: 「The job called backup-db has high priority, needs 1200 tokens and should be tagged nightly and storage. Reply with JSON only.」のような文 6 つ(タスク名・優先度・トークン数・タグが異なる)を同時に送信し、3 バッチ(18 リクエスト)を測定しました。最初のバッチはウォームアップとして除外しました。
- 集計: 「即座に読み込み可能」とは応答がそのまま
json.loadsになるかどうか、「値が正しい」とはコードブロックを除外した上で 4 つの値がすべて正しいかどうかです。
| モデル | バリエーション | 即座に読み込み可能 | スキーマ準拠 | 値が正しい | 応答時間中央値 |
|---|---|---|---|---|---|
| Qwen3-0.6B | 制約なし | 0/18 | 0/18 | 18/18 | 254.9ms |
| Qwen3-0.6B | json_object | 18/18 | 18/18 | 18/18 | 248.0ms |
| Qwen3-0.6B | json_schema | 18/18 | 18/18 | 18/18 | 273.5ms |
| Qwen3-0.6B FP8 | 制約なし | 6/18 | 6/18 | 18/18 | 187.0ms |
| Qwen3-0.6B GPTQ 8-bit | なし(強制なし) | 0/18 | 0/18 | 18/18 | 185.6ms |
| 上記3モデル | json_object・json_schema | 54/54 | 54/54 | 54/54 | |
| Qwen3-4B AWQ 4-bit | なし(強制なし) | 18/18 | 18/18 | 18/18 | 476.0ms |
| Qwen3-4B AWQ 4-bit | json_schema | 18/18 | 18/18 | 18/18 | 507.5ms |
- 小さい対話モデルは値は合っているものの、フォーマットが間違っていました。 Qwen3-0.6B は強制がないと常に
json …のようにマークダウンのコードブロックで囲んでいました。人間が読む分には正しい答えですが、プログラムからはそのまま読み込めません。 json_object を使うことでこの問題が解消されました。 - 同じモデルでも量子化によって挙動が変わりました。 FP8版は18回中6回、コードブロックなしで回答しました。出力の癖に依存するパーサーは、モデルや量子化を変更した際に破綻する可能性があるということです。
- 大きいモデルはコードブロックなしで回答しました。 4B は強制がなくても18回すべてそのまま読み込めました。
- 構造化出力によって値が変更されることはありませんでした。 すべてのバリエーションで値の正答率は 18/18 でした。今回のタスクが簡単だったためでもあります。
- コストの傾向は 0.31.0 のときと同様でした。 0.6B において json_schema は強制なしよりも応答が約 19ms (7%) 長く、新しいスキーマに対する初回リクエストはウォームアップバッチにおいて TTFT 195ms でした。
- リランクモデルを用いて前回のテストを 0.32.0 で再実行した結果(同時実行数 16、 json_schema 47/48、不正解の1つは長さ超過)も 0.31.0 と同様でした。初回サンプリングバッチにおけるカーネルコンパイルの遅延も 0.32.0 にそのまま残っていました(リランクモデル 5.3秒、 Qwen3-0.6B 6.2秒)。
4. 運用上のポイント
- スキーマは事前にウォームアップしておきます。 サービスで使用するスキーマの数が決まっている場合は、サーバー起動直後に一度ずつ送信しておくことで、初回のユーザーが数百年…ではなく数百 ms 余分に待たされることがなくなります。リクエストごとにスキーマが異なるサービス(ユーザーがスキーマを送信する API)では、このコストを継続的に支払うことになります。
- ユーザーが送信したスキーマは信頼できない入力です。 文法を生成するコストはスキーマのサイズや構造に応じて増大するため、外部からスキーマを受け取る場合はサイズや深さに上限を設けます。 0.32.0 が深さ 64 を超えるスキーマを拒否するようになったのも、同じ理由によるものと考えられます。
finish_reasonを確認します。 構造化出力の「常に正しい JSON」は、最後まで生成しきった場合の話です。- キーが必要な場合は json_object ではなく json_schema を使用します。
- バックエンドは基本 auto に設定しつつ、初回リクエストの遅延が問題になる場合は今回の測定のように両方のバックエンドで同じスキーマを再測定して選択します。今回のスキーマでは xgrammar の初回リクエストは guidance の半分であり、それ以降の値は同等でした。
5. 検証できなかった事項
- 大規模なスキーマ(ネストが深いオブジェクト、長い enum)、文脈自由文法(EBNF)、構造タグのコストは未計測です。
- 初回リクエストのコストのうち、文法生成とカーネル準備を切り分けることができませんでした。 auto および guidance サーバーで同時実行数 16 の最初の json_schema バッチに 1.5秒以上かかり、 xgrammar サーバーでは 30ms であった理由も未確認です。
- 推論モデル(思考プロセスを先に生成するモデル)と組み合わせて使用した際の動作(
--structured-outputs-config.enable_in_reasoning)は未計測です。
参考
- vLLM ドキュメント、Structured Outputs(0.31.0・0.32.0 共通): https://docs.vllm.ai/en/latest/features/structured_outputs.html
- Dong 他、「XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models」、MLSys 2025. https://arxiv.org/abs/2411.15100
- llguidance: https://github.com/guidance-ai/llguidance
- vLLM 0.32.0 リリースノート(スキーマの深さ制限、サンプラーのウォームアップ): https://github.com/vllm-project/vllm/releases/tag/v0.32.0
- 本シリーズ第5弾の ツール呼び出し