TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Structured Outputs: The Cost of Masking "Illegal Characters" Per Token

Status: Draft (2026-10-27), empirically measured. Measured on 2026-10-27 using a single 8GB-class laptop GPU (Ada generation), vLLM 0.31.0 (xgrammar 0.2.7, llguidance 1.7.6), and Qwen3-Reranker-0.6B. Official documentation was checked for 0.32.0, and the structured output documentation was identical to 0.31.0. Added 2026-10-27. Results from measuring an extraction task with correct answers using conversational Qwen3-0.6B and Qwen3-4B (AWQ) with vLLM 0.32.0 (xgrammar 0.2.8) have been added to Section 3-1. The 0.31.0 figures remain unchanged. Writing conventions: Empirical data is the default; unmeasured items are marked as "Unmeasured", and unverified items are marked as "Unverified".

Table of Contents

1. Principle: Filtering Out-of-Grammar Candidates from Next Token Predictions

At each step, the model computes scores for the entire vocabulary (151,669 tokens for this model) and selects one. Structured outputs insert a step just before selection: the text generated so far is fed into a grammar machine (a finite state machine or pushdown automaton) to obtain the set of "tokens allowed to appear now," and the scores of all other tokens are overwritten with negative infinity. This set is converted into a bitmask and applied to the GPU score tensor all at once.

지금까지: {"priority": "
문법상 다음에 올 수 있는 것: l(ow), m(edium), h(igh) 로 시작하는 토큰만
나머지 151,000여 토큰 → 점수 -inf

Consequently, no matter how erratic the model is, the output adheres to the grammar. However, this introduces two types of costs.

  1. Grammar compilation cost: The process of converting a JSON schema or regular expression into a token-level machine. Because the vocabulary has 150,000 tokens, pre-calculating whether "this token is allowed in this state" for every token is heavy. Identical schemas are cached.
  2. Per-step mask generation cost: For each request, the state advances by one step, a mask is generated, and it is sent to the GPU.

The XGrammar paper (Dong et al., MLSys 2025) reports that by pre-computing tokens determined independently of the context and overlapping grammar computation with GPU execution, it is up to 100x faster than traditional methods, and when used with a serving engine, the overhead is nearly negligible (based on the abstract; conditions are not specified in the abstract). vLLM uses both xgrammar and llguidance (guidance backend), and the default value auto chooses based on the request.

2. How to Use and Evolution of Names

There are two formats: the OpenAI-style response_format and the vLLM-exclusive structured_outputs .

Desired Output Request Parameter
JSON Schema response_format: {"type": "json_schema", "json_schema": {...}} or structured_outputs: {"json": 스키마}
Any JSON Object response_format: {"type": "json_object"}
One of the options structured_outputs: {"choice": ["low", "medium", "high"]}
Regular Expression structured_outputs: {"regex": "[A-Z]{3}-[0-9]{4}"}
Context-Free Grammar (EBNF) structured_outputs: {"grammar": "..."}
JSON Only Within Tags structured_outputs: {"structural_tag": ...}(Strict tool calling in Part 5 uses this method)

The naming changed as the API evolved. The old guided_json, guided_regex, guided_choice, guided_grammar and per-request guided_decoding_backend were removed in 0.12.0, and they are now consolidated into a single structured_outputs . The backend is determined server-side via --structured-outputs-config.backend , and the ability to choose a backend per request was removed in V1. Regular expression grammars differ by backend: xgrammar and guidance use Rust-style syntax, while lm-format-enforcer uses Python re syntax. Starting from 0.32.0, JSON schemas nested deeper than 64 levels are rejected with a 400 error (based on release notes, unmeasured).

3. Direct Measurement

Method

Result 1. When Enforced, the Only Failures Were Truncations

Totals for concurrency 16 across three measurement batches (48 requests).

Variant auto guidance xgrammar Example Response
No enforcement 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

Result 2. Costs: The First Request and a Small Per-Token Overhead

Variant Backend First Request TTFT for New Schema (Concurrency 1, Warmup Batch) Median TTFT Thereafter Median Token Interval (Concurrency 1) Median Token Interval (Concurrency 16)
No enforcement xgrammar server 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

Incident: A Few Seconds on the First Sampling Batch

During measurements, the following warning appeared in the server logs.

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.

When a batch of temperature sampling (top-k, top-p) requests arrived for the first time on a new server, the kernel was compiled on the fly, increasing the TTFT for that batch from 3.9 seconds (guidance server, concurrency 1) to up to 11 seconds (guidance server, concurrency 16). This warning did not appear while running greedy decoding exclusively. Furthermore, compilation did not happen just once. When sending requests with temperature 1.0 sequentially at concurrency levels 1, 8, and 32 to another server, that batch slowed down again every time the concurrency level changed for the first time (generating 256 tokens took 5.4s then 1.5s for concurrency 1; 7.4s then 1.9s for concurrency 8; 8.4s then 3.2s for concurrency 32). It appears new kernels are generated for each batch size. A warning was also logged stating that the model's generation_config.json overwrote the default sampling values (temperature 0.6, top_k 20, top_p 0.95). In production, servers should be warmed up with requests matching actual sampling configurations after startup before accepting traffic. The 0.32.0 release notes mention a reduction in sampler warmup compilation overhead (unmeasured).

3-1. Re-measurement on 2026-10-27: Extraction Task with Correct Answers

The previous test only checked for "valid JSON." This time, we also checked whether the values were correct.

Model Variant Immediate Parseability Schema Compliance Correct Values Median Response Time
Qwen3-0.6B No enforcement 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 No enforcement 6/18 6/18 18/18 187.0ms
Qwen3-0.6B GPTQ 8-bit None (forced) 0/18 0/18 18/18 185.6ms
Above three models json_object / json_schema 54/54 54/54 54/54
Qwen3-4B AWQ 4-bit None (forced) 18/18 18/18 18/18 476.0ms
Qwen3-4B AWQ 4-bit json_schema 18/18 18/18 18/18 507.5ms

4. Key Takeaways for Operations

5. Unverified Items

References

Sign in to like

Comments

No comments yet.

Sign in to leave a comment