metrale_grammar/compiler/compiled_grammar.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// CompiledGrammar — port of `class CompiledGrammar` / `CompiledGrammar
4// ::Impl` from `cpp/compiled_grammar.cc` + `cpp/compiled_grammar_impl.h`.
5//
6// A grammar compiled against a specific tokenizer: it bundles the
7// optimized, FSM-accelerated grammar, the tokenizer info, and a lazy
8// per-parser-state adaptive-token-mask cache. The matcher (W6) consults
9// `get_or_compute_mask` to fill the logit bitmask fast.
10//
11// XGrammar-2 JIT (lazy) MASK COMPILATION
12// --------------------------------------
13// The eager port precomputed an `AdaptiveTokenMask` for *every*
14// reachable scanable state up front. For tool-call JSON-schema grammars
15// that is hundreds of masks per `compile_*` call — most never used by a
16// single generation. This port computes each state's mask LAZILY, on
17// first lookup by the matcher, and caches it in `mask_cache`. Only the
18// states a generation actually visits get a mask computed, once each.
19
20use std::sync::{Arc, Mutex};
21
22use ahash::AHashMap;
23
24use crate::earley::ParserState;
25use crate::grammar::GrammarData;
26use crate::grammar::functor::hash_sequence;
27use crate::support::hash::hash_combine;
28use crate::tokenizer::TokenizerInfo;
29
30use super::decompose::GrammarDecomposition;
31use super::mask::AdaptiveTokenMask;
32use super::mask_gen::MaskGenerator;
33use super::rule_cache::{RuleLevelCache, RuleMaskKey};
34
35/// The inner, shared state of a [`CompiledGrammar`]. Port of
36/// `CompiledGrammar::Impl`.
37#[derive(Debug)]
38pub struct CompiledGrammarImpl {
39 /// Maximum synchronous prewarm workers, inherited from the compiler.
40 pub prewarm_max_threads: usize,
41 /// The optimized, FSM-accelerated grammar (shared — the lazy
42 /// `MaskGenerator` needs an `Arc<GrammarData>`).
43 pub grammar: Arc<GrammarData>,
44 /// The tokenizer this grammar was compiled against.
45 pub tokenizer_info: TokenizerInfo,
46 /// Lazy per-parser-state adaptive-token-mask cache. Equivalent to
47 /// the C++ `adaptive_token_mask_cache` — a plain hash map, populated
48 /// on demand: empty after compilation, filled by [`CompiledGrammar::
49 /// get_or_compute_mask`] as the matcher reaches each state.
50 ///
51 /// The matcher drives one `CompiledGrammar` single-threaded per
52 /// request, so the C++ uses a plain `unordered_map` with no locking.
53 /// We cannot drop the lock entirely, however: [`super::super::
54 /// matcher::BatchGrammarMatcher`]'s rayon `par_iter_mut` fill path
55 /// runs many matchers that were all cloned from one `CompiledGrammar`
56 /// — they share this `Arc<CompiledGrammarImpl>` and may call
57 /// `get_or_compute_mask` concurrently. A single uncontended `Mutex`
58 /// lock is far cheaper than `DashMap`'s shard-hash + shard-select
59 /// machinery on every per-token lookup, while still keeping
60 /// `CompiledGrammarImpl: Sync`.
61 pub mask_cache: Mutex<AHashMap<ParserState, Arc<AdaptiveTokenMask>>>,
62 /// TagDispatch second-slice precomputation, keyed by rule id. Built
63 /// once at compile time and retained here so on-demand mask
64 /// computation can feed it to the `MaskGenerator`. `Arc`-wrapped
65 /// so `MaskGenerator::new` clones a pointer, not the map.
66 pub tag_slice: Arc<AHashMap<i32, Vec<bool>>>,
67 /// The cross-grammar [`RuleLevelCache`], shared from the owning
68 /// [`super::GrammarCompiler`] (`None` when the compiler's cache is
69 /// disabled, or for the `vocab_size == 0` degenerate path). The lazy
70 /// mask path consults it on a per-state cache miss before falling
71 /// back to a full `MaskGenerator` recomputation — a rule
72 /// structurally identical to one seen in any previous request
73 /// reuses its computed masks.
74 pub rule_cache: Option<RuleLevelCache>,
75 /// WGRAMMAR static/dynamic decomposition (Tier 3c), computed ONCE
76 /// at compile time by [`super::decompose::decompose_static_regions`].
77 /// Classifies every rule as fixed scaffolding (with its literal
78 /// bytes precomputed) or a dynamic value slot — the compile-time
79 /// index of the grammar's static structure. See the module docs of
80 /// `decompose.rs` for how this composes with Tiers 2 and 3b.
81 pub decomposition: GrammarDecomposition,
82}
83
84impl CompiledGrammarImpl {
85 /// Approximate heap memory usage in bytes. Port of
86 /// `MemorySize(const CompiledGrammar::Impl&)`. The mask term sums
87 /// only the masks computed so far (the lazy cache).
88 pub fn memory_size(&self) -> usize {
89 let grammar_bytes = self.grammar.complete_fsm.memory_size()
90 + self.grammar.num_exprs() as usize * 4
91 + self.grammar.num_rules() as usize * 32;
92 let mask_bytes: usize = self
93 .mask_cache
94 .lock()
95 .expect("mask_cache mutex poisoned")
96 .values()
97 .map(|m| m.memory_size())
98 .sum();
99 grammar_bytes + mask_bytes
100 }
101
102 /// Build the cross-grammar [`RuleMaskKey`] for a canonical parser
103 /// state, returning it together with the shared [`RuleLevelCache`].
104 ///
105 /// `None` — the rule-level cache is unavailable for this state — when
106 /// any of the following hold (faithful to the upstream
107 /// `rule_level_cache_is_available` guard in `grammar_compiler.cc`):
108 /// * the compiler has no `rule_cache`;
109 /// * the rule has no structural FSM hash (the hasher could not
110 /// hash it — e.g. an unbroken complex cycle);
111 /// * the canonical FSM node id has no entry in the rule's
112 /// `per_rule_fsm_new_state_ids` renumbering.
113 ///
114 /// KEY DERIVATION. The structural part is `(fsm_hash,
115 /// fsm_new_node_id, state_cnt, edge_cnt)` — exactly the upstream
116 /// `RuleLevelCache::Impl::NodeKey`. Upstream caches the
117 /// *no-lookahead* mask under the bare `fsm_hash` and the
118 /// lookahead-aware mask under `HashCombine(fsm_hash, lookahead_hash,
119 /// is_exact_lookahead)`. This port's `MaskGenerator` yields the
120 /// final lookahead-applied mask in one shot, so the key folds in the
121 /// lookahead hash (and `is_exact_lookahead`) whenever the rule has a
122 /// lookahead assertion, and folds in `is_root` — guaranteeing a
123 /// cache hit is byte-identical to a fresh recompute. A plain
124 /// no-lookahead non-root rule therefore keys on the bare structural
125 /// hash and reuses freely across grammars.
126 fn rule_mask_key(
127 &self,
128 canonical: &ParserState,
129 is_root: bool,
130 ) -> Option<(&RuleLevelCache, RuleMaskKey)> {
131 let cache = self.rule_cache.as_ref()?;
132 let rule_id = canonical.rule_id;
133 let base_hash = (*self.grammar.per_rule_fsm_hashes.get(rule_id as usize)?)?;
134
135 // Map the FSM node id (`element_id`) to its canonical renumbered
136 // id via `per_rule_fsm_new_state_ids` (a list of original->new
137 // pairs, exactly as the C++ `original_to_new_id` scan).
138 let renumber = self
139 .grammar
140 .per_rule_fsm_new_state_ids
141 .get(rule_id as usize)?;
142 let fsm_new_node_id = renumber
143 .iter()
144 .find(|(orig, _)| *orig == canonical.element_id)
145 .map(|(_, new)| *new)?;
146
147 let fsm = self.grammar.per_rule_fsms[rule_id as usize]
148 .as_ref()
149 .expect("optimized grammar must have a per-rule FSM");
150 let state_cnt = fsm.num_states() as i32;
151 let edge_cnt = fsm.num_edges() as i32;
152
153 // Fold lookahead + root into the hash so a hit is byte-exact.
154 let rule = self.grammar.rule(rule_id);
155 let mut fsm_hash = base_hash;
156 if rule.lookahead_assertion_id != -1 {
157 // `hash_sequence` may return `None` for a non-hashable
158 // lookahead sequence; treat that as "cache unavailable" so
159 // we never key two semantically different masks alike.
160 let la_hash = hash_sequence(&self.grammar, rule.lookahead_assertion_id)?;
161 fsm_hash = hash_combine(&[fsm_hash, la_hash, rule.is_exact_lookahead as u64]);
162 }
163 if is_root {
164 fsm_hash = hash_combine(&[fsm_hash, u64::MAX]);
165 }
166
167 Some((
168 cache,
169 RuleMaskKey {
170 fsm_hash,
171 fsm_new_node_id,
172 state_cnt,
173 edge_cnt,
174 },
175 ))
176 }
177}
178
179/// A grammar compiled against a tokenizer — the result of
180/// preprocessing performed by [`super::GrammarCompiler`].
181///
182/// Cheap to clone: the inner state is shared via [`Arc`], matching the
183/// C++ pimpl `shared_ptr` semantics.
184#[derive(Debug, Clone)]
185pub struct CompiledGrammar {
186 pimpl: Arc<CompiledGrammarImpl>,
187}
188
189impl CompiledGrammar {
190 /// Wrap an already-built [`CompiledGrammarImpl`].
191 pub fn from_impl(pimpl: Arc<CompiledGrammarImpl>) -> Self {
192 Self { pimpl }
193 }
194
195 /// The associated optimized grammar.
196 pub fn grammar(&self) -> &GrammarData {
197 &self.pimpl.grammar
198 }
199
200 /// Shared-pointer access to the optimized grammar — used to seed a
201 /// lazily-constructed `MaskGenerator`.
202 pub fn grammar_arc(&self) -> Arc<GrammarData> {
203 Arc::clone(&self.pimpl.grammar)
204 }
205
206 /// The associated tokenizer info.
207 pub fn tokenizer_info(&self) -> &TokenizerInfo {
208 &self.pimpl.tokenizer_info
209 }
210
211 /// The WGRAMMAR static/dynamic decomposition (Tier 3c) — the
212 /// compile-time split of every rule body into fixed scaffolding
213 /// spans (with precomputed literal bytes) and dynamic value-slot
214 /// spans.
215 ///
216 /// Computed once during compilation. A scheduler / matcher can
217 /// consult it — before the first decode step — to learn the
218 /// grammar's static/dynamic structure without any per-token work.
219 /// Each [`Segment::Static`](super::Segment::Static) span is exactly
220 /// a forced-byte run Tier 3b would otherwise rediscover lazily; the
221 /// genuine WGRAMMAR delta is doing this at compile time.
222 pub fn decomposition(&self) -> &GrammarDecomposition {
223 &self.pimpl.decomposition
224 }
225
226 /// Get the adaptive token mask for a canonical parser state,
227 /// computing and caching it on first access (the XGrammar-2 JIT
228 /// lazy path).
229 ///
230 /// `canonical` MUST be the canonical cache key — `rule_start_pos =
231 /// -1`, `sub_element_id`/`repeat_count`/`partial_codepoint = 0`,
232 /// `sequence_id = body_expr_id` — exactly the tuple the eager port
233 /// used. `is_root` is whether `canonical.rule_id` is the grammar
234 /// root (the root rule has no uncertain tokens).
235 ///
236 /// The returned `Arc` is byte-identical to what the old eager
237 /// compile produced: same `MaskGenerator`, same canonical key.
238 pub fn get_or_compute_mask(
239 &self,
240 canonical: ParserState,
241 is_root: bool,
242 ) -> Arc<AdaptiveTokenMask> {
243 // Fast path: a cache hit takes one uncontended `Mutex` lock and
244 // an `Arc` clone — nothing else.
245 if let Some(hit) = self
246 .pimpl
247 .mask_cache
248 .lock()
249 .expect("mask_cache mutex poisoned")
250 .get(&canonical)
251 {
252 return Arc::clone(hit);
253 }
254 // Per-grammar miss: compute the mask WITHOUT holding the lock —
255 // the `MaskGenerator` scan is expensive, and serializing it
256 // under the lock would defeat the parallel `BatchGrammarMatcher`
257 // fill. A concurrent computer of the same state simply does
258 // duplicate work; the `entry` double-check below keeps a single
259 // canonical `Arc`.
260 //
261 // Before recomputing, consult the cross-grammar `RuleLevelCache`
262 // (Tier 2): a rule structurally identical to one seen in a
263 // previous request — even from a different grammar — already has
264 // its masks computed. The cross-grammar key is structural
265 // (FSM hash + canonical node + node/edge counts), so a hit is
266 // byte-identical to a fresh `MaskGenerator` run.
267 let computed = match self.pimpl.rule_mask_key(&canonical, is_root) {
268 Some((cache, key)) => {
269 cache.get_or_compute(key, || Self::compute_mask(&self.pimpl, canonical, is_root))
270 }
271 None => Arc::new(Self::compute_mask(&self.pimpl, canonical, is_root)),
272 };
273 let mut cache = self
274 .pimpl
275 .mask_cache
276 .lock()
277 .expect("mask_cache mutex poisoned");
278 Arc::clone(cache.entry(canonical).or_insert(computed))
279 }
280
281 /// Run a full `MaskGenerator` scan for `canonical` — the
282 /// authoritative (uncached) mask computation. `&Arc<AHashMap>`
283 /// deref-coerces to the `&AHashMap` argument.
284 fn compute_mask(
285 pimpl: &Arc<CompiledGrammarImpl>,
286 canonical: ParserState,
287 is_root: bool,
288 ) -> AdaptiveTokenMask {
289 let mut generator = MaskGenerator::new(
290 Arc::clone(&pimpl.grammar),
291 canonical,
292 &pimpl.tokenizer_info,
293 &pimpl.tag_slice,
294 );
295 generator.get_adaptive_token_mask(is_root)
296 }
297
298 /// Approximate memory usage in bytes. Port of
299 /// `CompiledGrammar::MemorySizeBytes`.
300 pub fn memory_size_bytes(&self) -> usize {
301 self.pimpl.memory_size()
302 }
303
304 /// OVERLAPPED MASK GENERATION (Tier 2, "configurable JIT").
305 ///
306 /// Eagerly compute the `k` most-expensive adaptive-token masks,
307 /// populating the lazy JIT cache before the matcher needs them. Uses
308 /// at most the compiler's `max_threads`, including the caller, and
309 /// joins all workers before returning. Metrale Engine calls this synchronously
310 /// before prompt prefill; no GPU overlap is implied.
311 ///
312 /// COST RANKING. A state's mask cost is dominated by the breadth of
313 /// its first-character scan: every distinct first byte the state can
314 /// accept seeds an `EarleyParser` walk over the matching slice of
315 /// the sorted vocabulary. We therefore rank reachable scanable
316 /// states by their char-range edge count (a cheap, monotonic proxy
317 /// for that breadth) and warm the top `k`.
318 ///
319 /// Returns the number of masks actually computed (`<= k`, and `<=`
320 /// the number of reachable scanable states). `k == 0`, or an empty
321 /// vocabulary, computes nothing. Idempotent: a state already in the
322 /// cache is recomputed cheaply via `get_or_compute_mask`'s fast path
323 /// and still counts toward the return value.
324 pub fn compile_top_k_masks(&self, k: usize) -> usize {
325 use crate::earley::NO_PREV_INPUT_POS;
326
327 if k == 0 || self.pimpl.tokenizer_info.vocab_size() == 0 {
328 return 0;
329 }
330 let grammar = &self.pimpl.grammar;
331 let root_rule_id = grammar.root_rule_id();
332
333 // Collect (cost, canonical_state, is_root) for every reachable
334 // scanable state, where cost is the char-range edge count.
335 let mut ranked: Vec<(usize, ParserState, bool)> = Vec::new();
336 for rule_id in 0..grammar.num_rules() {
337 let rule = grammar.rule(rule_id);
338 let fsm = grammar.per_rule_fsms[rule_id as usize]
339 .as_ref()
340 .expect("optimized grammar must have a per-rule FSM");
341 let mut reachable = ahash::AHashSet::new();
342 fsm.reachable_states(&mut reachable);
343 let is_root = rule_id == root_rule_id;
344 for state_id in reachable {
345 let char_edges = fsm
346 .fsm()
347 .edges(state_id as usize)
348 .iter()
349 .filter(|e| e.is_char_range())
350 .count();
351 if char_edges == 0 {
352 continue; // not scanable — no mask to compute
353 }
354 let canonical =
355 ParserState::new(rule_id, rule.body_expr_id, state_id, NO_PREV_INPUT_POS, 0);
356 ranked.push((char_edges, canonical, is_root));
357 }
358 }
359
360 // Most expensive first. `select_nth_unstable` would suffice, but
361 // a full sort keeps the warm order deterministic for tests and
362 // the list is small (one entry per scanable FSM state).
363 ranked.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.0));
364 let take = k.min(ranked.len());
365 let pending = self.uncached_masks(&ranked[..take]);
366 super::prewarm::for_each_bounded(&pending, self.pimpl.prewarm_max_threads, &|&(
367 _,
368 canonical,
369 is_root,
370 )| {
371 self.get_or_compute_mask(canonical, is_root);
372 });
373 take
374 }
375
376 pub(super) fn uncached_masks(
377 &self,
378 selected: &[(usize, ParserState, bool)],
379 ) -> Vec<(usize, ParserState, bool)> {
380 let cache = self
381 .pimpl
382 .mask_cache
383 .lock()
384 .expect("mask_cache mutex poisoned");
385 selected
386 .iter()
387 .copied()
388 .filter(|(_, state, _)| !cache.contains_key(state))
389 .collect()
390 }
391
392 /// Shared-pointer access to the inner state — used by the matcher.
393 pub fn inner(&self) -> &Arc<CompiledGrammarImpl> {
394 &self.pimpl
395 }
396
397 /// Enumerate every reachable scanable canonical [`ParserState`] and
398 /// its (lazily-computed) [`AdaptiveTokenMask`].
399 ///
400 /// This is exactly the state set the *eager* compiler precomputed —
401 /// reproduced here so tests can verify the JIT result equals the
402 /// old eager result and that the partition invariants hold. Each
403 /// returned mask is materialized through [`Self::get_or_compute_mask`],
404 /// i.e. the lazy path. Test-only.
405 #[cfg(test)]
406 pub(crate) fn all_reachable_masks(&self) -> Vec<(ParserState, Arc<AdaptiveTokenMask>)> {
407 use crate::earley::NO_PREV_INPUT_POS;
408
409 let mut out = Vec::new();
410 if self.pimpl.tokenizer_info.vocab_size() == 0 {
411 return out;
412 }
413 let grammar = &self.pimpl.grammar;
414 let root_rule_id = grammar.root_rule_id();
415 for rule_id in 0..grammar.num_rules() {
416 let rule = grammar.rule(rule_id);
417 let fsm = grammar.per_rule_fsms[rule_id as usize]
418 .as_ref()
419 .expect("optimized grammar must have a per-rule FSM");
420 let mut reachable = ahash::AHashSet::new();
421 fsm.reachable_states(&mut reachable);
422 let is_root = rule_id == root_rule_id;
423 for state_id in reachable {
424 let scanable = fsm
425 .fsm()
426 .edges(state_id as usize)
427 .iter()
428 .any(|e| e.is_char_range());
429 if !scanable {
430 continue;
431 }
432 let canonical =
433 ParserState::new(rule_id, rule.body_expr_id, state_id, NO_PREV_INPUT_POS, 0);
434 let mask = self.get_or_compute_mask(canonical, is_root);
435 out.push((canonical, mask));
436 }
437 }
438 out
439 }
440}