TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

结构化输出:为每个 Token 屏蔽「非法字符」的代价

状态:草稿 (2026-10-27),实测。 测试于 2026-10-27,环境为单张 8GB 级笔记本 GPU(Ada 架构),vLLM 0.31.0(xgrammar 0.2.7,llguidance 1.7.6),Qwen3-Reranker-0.6B。官方文档基于 0.32.0 版本确认,结构化输出文档与 0.31.0 相同。 2026-10-27 新增。 在第 3.1 节中补充了使用对话版 Qwen3-0.6B·Qwen3-4B(AWQ) 和 vLLM 0.32.0(xgrammar 0.2.8) 对带标准答案的提取任务进行测试的结果。0.31.0 的数值保持不变。 编写规则:以实测数据为基础,未能测出的标记为「未实测」,未确认的内容标记为「未确认」。

目录

1. 原理:从下一个 Token 候选集中清除语法之外的内容

模型在每一步都会对整个词表(该模型为 151,669 个)中的每个词进行打分,并挑选其中一个。结构化输出会在挑选前插入一个步骤:将目前已生成的文本输入语法引擎(有限状态自动机或下推自动机)以获取「当前可接受的 Token」集合,并将其余 Token 的分数设为负无穷。该集合会被转换为位图(Bitmask),并一次性应用到 GPU 的分数张量上。

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

因此,无论模型多么不靠谱,结果都会遵守语法。但这也带来了两项代价。

  1. 生成语法的代价:即把 JSON Schema 或正则表达式转换为 Token 级引擎的工作。由于词表大小达 15 万,预先计算「此 Token 在此状态下是否被允许」的开销很大。相同的 Schema 会被缓存。
  2. 每步构建掩码的代价:每次请求都需要推进一步状态、构建掩码并发送给 GPU。

XGrammar 论文(Dong 等, MLSys 2025)指出,通过预先计算与上下文无关的判定 Token,并使语法计算与 GPU 执行重叠,其速度比传统方法快最多 100 倍,且与推理引擎配合使用时几乎没有额外开销(基于摘要,具体条件摘要中未提及)。vLLM 同时使用了 xgrammar 和 llguidance(guidance 后端),默认值 auto 会根据请求进行选择。

2. 使用方法与名称变更

这里有两种写法:OpenAI 格式的 response_format 和 vLLM 专用的 structured_outputs 。

所需功能 请求中的参数
JSON Schema 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 Schema 将会返回 400 错误(基于发布说明,未实测)。

3.亲自测一测

方法

结果 1:强制约束下出错的仅有被截断的情况

并发 16 下,三次测量批次(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

结果 2:代价:首次请求,以及每个 Token 的微小开销

变体 后端 新 Schema 首次请求 TTFT(并发 1,预热批次) 后续 TTFT 中位数 Token 间隔中位数(并发 1) Token 间隔中位数(并发 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

遇到的问题:首个采样批次的几秒钟延迟

测试过程中,服务器日志中打印了这样的警告:

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、8、32 发送温度 1.0 的请求时,每次首次改变并发度都会导致该批次再次变慢(生成 256 个 Token 时,并发 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」。这次我们同时验证了数值是否正确。

模型 变体 直接读取 符合 Schema 数值正确 响应时间中位数
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
上述三个模型 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

4. 运维经验总结

5. 未能确认的事项

参考资料

登录后即可点赞

评论

还没有评论。

登录后即可发表评论