Constrained decoding for LLMs in MoonBit: compile JSON Schema and regex to byte-level DFAs and mask invalid tokens.
Dependencies
| 不加约束 | 用 moonmask | |
|---|---|---|
| 做法 | 让模型自由生成,结束后做 JSON 校验,不合法就重试 | 每一步只允许“接上之后仍可能合法”的 token,其余 logit 置为 −∞ |
| 结构合法性 | 取决于模型;既有随机实验 0 / 100 | 保持合法前缀;既有随机实验完成输出 100 / 100,达到预算仍须检查 EOS |
| 成本 | 重试浪费 token 和时间 | 每个 DFA 状态的掩码算一次后缓存 |
| 适用 | 任何 API | 能拿到每一步 logits 的推理(进程内或本地推理服务) |
flowchart TB
subgraph C["① 约束 → 自动机"]
direction TB
S["JSON Schema"] -- "schema::to_regex" --> AST["语法树"]
R["Regex"] --> AST
G["GBNF"] -- "规则展开" --> AST
AST -- "Thompson 构造" --> NFA["NFA"]
NFA -- "子集构造 · 剪枝 · 距离" --> DFA[("字节 DFA")]
end
subgraph V["② 词表 → 前缀树"]
direction TB
TJ["tokenizer.json"] -- "tokenizers-moonbit" --> VO["token → 字节串"]
VO --> TR["字节前缀树"]
end
DFA --> GD{{"③ Guide:每个状态的 token 掩码(带缓存)"}}
TR --> GD
GD -- "allowed(state)" --> SP["④ 采样器:非法 token 的 logit 置 −∞"]
SP -- "advance(state, token)" --> GDsequenceDiagram
autonumber
participant M as LLM
participant G as Guide (moonmask)
participant S as 采样器
loop 直到输出 eos
M->>S: 这一步的 logits(50,257 维)
G->>S: allowed(state):当前合法的 token
S->>S: 其余 token 置 −∞,再采样
S->>G: advance(state, token)
G-->>G: state ← 下一状态
end
Note over G,S: eos 只在接受状态放行,所以结束时输出一定完整且合法| schema | 加掩码 | 不加掩码 | 平均 token 数 | DFA 状态数 |
|---|---|---|---|---|
| user.json | ██████████ 100/100 | ·········· 0/100 | 36.79 | 500 |
| order.json | ██████████ 100/100 | ·········· 0/100 | 68.29 | 1,401 |
| tool_call.json | ██████████ 100/100 | ·········· 0/100 | 34.70 | 1,016 |
./scripts/fetch-gpt2.sh # 按固定版本下载 GPT-2 tokenizer.json 并校验 SHA-256
moon run cmd/main --target native -- assets/gpt2/tokenizer.json \
examples/schemas/user.json examples/schemas/order.json examples/schemas/tool_call.json
# 可选空白模式:同样三组 schema、种子和词表
moon run cmd/main --target native -- --whitespace assets/gpt2/tokenizer.json \
examples/schemas/user.json examples/schemas/order.json examples/schemas/tool_call.jsongit clone https://github.com/FidollarinLA/moonmask.git
cd moonmask
# macOS / Linux:
export PATH="$HOME/.moon/bin:$PATH"
moon update
moon run cmd/quickstartmoon add FidollarinLA/moonmask@0.1.0import {
"FidollarinLA/moonmask/schema",
"FidollarinLA/moonmask/vocab",
"FidollarinLA/moonmask/mask",
}///|
fn main raise {
let dfa = @schema.compile({ "enum": ["ok"] })
let vocab = @vocab.Vocab::new([b"\"ok\"", b"\"no\"", b""], eos=2)
let guide = @mask.Guide::new(dfa, vocab)
// 非法 token 的分数更高,仍会被排除。
assert_eq(guide.greedy(guide.start(), [0.0, 100.0, 0.0]), Some(0))
println("PASS")
}let tok = @tokenizer.from_file("assets/gpt2/tokenizer.json")
let vocab = @vocab.from_tokenizer(tok, eos_token="<|endoftext|>")
let guide = @mask.Guide::new(@schema.compile(schema), vocab)
let mut state = guide.start()
// 每一步:只保留 guide.allowed(state),其余 logit 置为 -inf,采样得到 token,然后:
if token == vocab.eos {
assert_true(guide.is_accept(state)) // 仅在接受状态允许结束
} else {
match guide.advance(state, token) {
Some(next) => state = next
None => panic() // 非法 token;不能把 None 一律当作合法 eos
}
}| 约束 | 支持 | 详细规格 |
|---|---|---|
| JSON Schema | string(长度、pattern、UTF-8、转义)、integer / number(范围)、boolean、null、enum、const、object(required 之外的属性可省略)、array、anyOf、$defs / $ref | docs/schema-subset.md |
| Regex | 字面量、字符类、分组、选择、* + ? {n,m},按字节匹配 | regex/ |
| GBNF | 多条规则互相引用、字符类、量词、注释;不支持递归 | docs/gbnf.md |
| 词表 | GPT-2 字节级 BPE(tokenizer.json) | vocab/ |
moonmask/
├── regex/ 正则解析 → Thompson NFA → 子集构造 DFA(剪枝、距离、采样)
├── schema/ JSON Schema 子集 → 字节级正则
├── gbnf/ GBNF 文法 → 同一种 DFA
├── vocab/ GPT-2 tokenizer.json → token 字节串(基于 tokenizers-moonbit)
├── mask/ Guide:前缀树 + 每状态掩码缓存;monkey / monkey_step 采样器
├── cmd/main/ 猴子打字机实验 CLI(native 后端)
├── playground/ 浏览器 Playground(MoonBit + rabbita,独立模块,不进发布包)
├── examples/ 实验用的三个 schema
├── docs/ 规格细则、设计说明、截图
└── scripts/ fetch-gpt2.sh:下载并校验 GPT-2 词表moon test --deny-warnInstall
Download zipConstrained decoding for LLMs in MoonBit: compile JSON Schema and regex to byte-level DFAs and mask invalid tokens.
Dependencies