Skip to content

Fuse byte strings and inline grammar rules in place - #729

Merged
Ubospica merged 9 commits into
mlc-ai:mainfrom
Ubospica:perf/in-place-optimizer-passes
Jul 28, 2026
Merged

Ubospica merged 9 commits into
mlc-ai:mainfrom
Ubospica:perf/in-place-optimizer-passes

Conversation

@Ubospica

@Ubospica Ubospica commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

改动

语法优化器中的字节串合并会把相邻字面字符串合成一段,规则内联会用规则内容替换引用。旧实现每次执行都重建整份语法树,包括未改变的子树。本请求把两个阶段改为单次遍历的原地重写:

  • 未变子树保留原编号,只为发生变化的容器追加新记录。
  • 字节串合并先检查序列是否真的需要改写,规则内联只重建受影响的选择。
  • 规则名称查找表改为首次按名称操作时再构造,只使用数字编号的重写不再支付字符串摘要成本。
  • 不使用的旧表达式记录由紧随其后的无用代码删除阶段统一清理。

同时修正两个正确性问题:从序列中删除空字节串,保持空规则分析正确;规则内联时递归访问新复制的嵌套表达式与序列剩余元素。

性能

使用发布构建,每个版本交替运行两组,每组 15 个样本,下列结果为 30 个样本的中位数。

单独优化阶段:

  • 字节串合并,无需改写:216.45 -> 35.24 微秒,快 6.14 倍。
  • 字节串合并,需要改写:273.26 -> 186.05 微秒,快 1.47 倍。
  • 规则内联,无需改写:219.50 -> 48.20 微秒,快 4.55 倍。
  • 规则内联,需要改写:456.73 -> 193.93 微秒,快 2.36 倍。

完整流程:

  • 在同时触发两个阶段的 501 条规则语法上,完整语法优化从 15.581 毫秒降到 15.414 毫秒,减少 1.1%;从扩展巴科斯范式(Extended Backus-Naur Form,EBNF)文本开始的全流程从 17.882 毫秒降到 17.819 毫秒,减少 0.4%。
  • 包含 200 个必需字符串属性的 JSON Schema(用于描述 JSON 数据结构的规格)首次编译从 7.221 毫秒降到 6.732 毫秒,降低 6.8%,快 1.07 倍。

验证

  • 新增字节串合并、规则内联、无需改写、前瞻断言和输入语法不可变的专项测试。
  • 新增空字节串影响空规则分析,以及外层内联后遗漏嵌套表达式的回归测试。
  • 语法解析 67 项、Lark 语法转换 338 项、生成温度 34 项、空规则编译 4 项和结构化标签 1,024 项 Python 语言测试全部通过。Lark 在这里指项目支持的一种语法文本格式。
  • 发布构建、格式检查和提交前检查通过。

Copilot AI review requested due to automatic review settings July 23, 2026 09:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the ByteStringFuser and RuleInliner grammar-optimization passes to avoid rebuilding the full grammar on every invocation, switching them to in-place rewrites (static void Apply(Grammar* grammar)) with copy-on-first-rewrite behavior to preserve external holders of the original grammar.

Changes:

  • Convert ByteStringFuser and RuleInliner from “return-new-grammar” passes to in-place passes, and update GrammarOptimizer to call the new APIs.
  • Add in-place mutation helpers on Grammar::Impl to append new expr records and shrink existing records’ payloads.
  • Update TVM FFI testing bindings to the new in-place pass signatures.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
cpp/tvm_ffi/tvm_ffi.cc Updates test bindings to call the new in-place optimizer passes.
cpp/grammar_impl.h Adds AddGrammarExpr and ShrinkGrammarExprData to support in-place record updates.
cpp/grammar_functor.h Updates pass API signatures and clarifies in-place/copy-on-write behavior in docs.
cpp/grammar_functor.cc Implements in-place byte-string fusing and rule inlining, and updates optimizer flow.
Comments suppressed due to low confidence (1)

cpp/grammar_functor.cc:2140

  • Same as in RuleInlinerImpl: this deep-copy step clones optimized and precomputed FSM caches (complete_fsm, per_rule_fsms, etc.) from the source Grammar::Impl (via GrammarBuilder(const Grammar&)). Because the pass then mutates expr records in-place, those caches become stale; if ByteStringFuser::Apply is called on an optimized grammar, the resulting grammar may still appear optimized while its FSMs no longer match the grammar.
      if (!owned) {
        // Copy before the first rewrite so that other holders of this grammar are unaffected.
        grammar_ref = GrammarBuilder(grammar_ref).Get(grammar_ref->GetRootRuleId());
        owned = true;
      }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cpp/grammar_functor.cc Outdated
Comment on lines 606 to 610
if (!owned) {
// Copy before the first rewrite so that other holders of this grammar are unaffected.
grammar_ref = GrammarBuilder(grammar_ref).Get(grammar_ref->GetRootRuleId());
owned = true;
}
Ubospica added 9 commits July 28, 2026 08:47
Cover fusing shapes (full run, split runs, no-op), non-inlinable rules
(empty-string choice, rule-ref body, mid-sequence ref), lookahead
assertion rewriting/preservation, and input grammar immutability.
FromMutableGrammar no longer builds the rule name map (name-based
operations are not needed by in-place passes), the deep copies at the
optimizer entry and in the FFI wrappers copy the impl directly instead
of going through GrammarBuilder, the expr memo and the inlinability
cache use plain arrays indexed by dense ids instead of hash maps, and
unchanged sequences/choices are probed read-only without materializing
child id vectors.
Builders bound to an existing grammar (copy constructor and
FromMutableGrammar) defer building the rule name map to the first
name-based operation (AddRule, GetNewRuleName, GetRuleId), so id-only
rewriting pays no name hashing cost while name-based operations keep
working on any builder.
Remove empty byte strings before nullable analysis and recursively visit nested expressions during rule inlining.
@Ubospica
Ubospica force-pushed the perf/in-place-optimizer-passes branch from 647c4d0 to bc65e27 Compare July 28, 2026 12:50
@Ubospica
Ubospica merged commit dda4b4d into mlc-ai:main Jul 28, 2026
9 checks passed
@Ubospica
Ubospica deleted the perf/in-place-optimizer-passes branch July 28, 2026 12:53
@Ubospica Ubospica changed the title perf: apply byte string fusing and rule inlining in place Fuse byte strings and inline grammar rules in place Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants