metrale_grammar/matcher/
batch.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// BatchGrammarMatcher — batched bitmask fill / token acceptance.
4//
5// Port of `class BatchGrammarMatcher` from `cpp/grammar_matcher.cc`.
6// The C++ version uses a hand-rolled `ThreadPool`; this port uses the
7// crate's existing `rayon` dependency for the parallel fill path.
8//
9// SIMPLIFICATION vs C++
10// ---------------------
11// The C++ `BatchFillNextTokenBitmask` lets `indices` remap matcher `i`
12// to an arbitrary tensor slice. This port keeps that capability for the
13// sequential path; the parallel path requires the *natural* mapping
14// (`matchers[i]` -> slice `i`) so the per-thread mutable slices are
15// provably disjoint without `unsafe`. With an explicit `indices`
16// permutation the fill falls back to sequential, which is always
17// correct.
18
19use rayon::prelude::*;
20
21use super::bitmask::bitmask_size;
22use super::fill::FillError;
23use super::matcher::GrammarMatcher;
24
25/// A batched driver over many [`GrammarMatcher`]s — fills one packed
26/// bitmask tensor for a whole decode batch in parallel.
27#[derive(Debug, Clone)]
28pub struct BatchGrammarMatcher {
29    /// Upper bound on worker threads; `1` forces the sequential path.
30    max_threads: usize,
31}
32
33impl BatchGrammarMatcher {
34    /// Construct a batched matcher.
35    ///
36    /// `max_threads`: `None` picks `available_parallelism / 2` (the C++
37    /// `"auto"` default); `Some(n)` caps at `n` (must be >= 1).
38    #[must_use]
39    pub fn new(max_threads: Option<usize>) -> Self {
40        let max_threads = match max_threads {
41            Some(n) => {
42                assert!(n >= 1, "max_threads must be >= 1");
43                n
44            }
45            None => std::thread::available_parallelism()
46                .map(|n| (n.get() / 2).max(1))
47                .unwrap_or(1),
48        };
49        Self { max_threads }
50    }
51
52    /// The configured worker-thread cap.
53    #[must_use]
54    pub fn max_threads(&self) -> usize {
55        self.max_threads
56    }
57
58    /// Fill `bitmask` for every matcher in `matchers`.
59    ///
60    /// `bitmask` is one flat buffer of `matchers.len() *
61    /// bitmask_size(vocab)` words; matcher `i` writes slice `i`. Every
62    /// matcher must share the same vocabulary size. Port of
63    /// `BatchFillNextTokenBitmask` (natural-index form).
64    ///
65    /// Returns one `Result` per matcher (same order).
66    pub fn fill_next_token_bitmask(
67        &self,
68        matchers: &mut [GrammarMatcher],
69        bitmask: &mut [i32],
70        debug: bool,
71    ) -> Vec<Result<bool, FillError>> {
72        if matchers.is_empty() {
73            return Vec::new();
74        }
75        let vocab = matchers[0].tokenizer_info().vocab_size();
76        let words = bitmask_size(vocab);
77        assert!(
78            bitmask.len() >= matchers.len() * words,
79            "bitmask buffer too small for the batch"
80        );
81
82        if self.max_threads <= 1 || matchers.len() == 1 {
83            return matchers
84                .iter_mut()
85                .zip(bitmask.chunks_mut(words))
86                .map(|(m, slice)| m.fill_next_token_bitmask(slice, 0, debug))
87                .collect();
88        }
89
90        // Parallel path: matcher `i` <-> disjoint slice `i`.
91        matchers
92            .par_iter_mut()
93            .zip(bitmask.par_chunks_mut(words))
94            .map(|(m, slice)| m.fill_next_token_bitmask(slice, 0, debug))
95            .collect()
96    }
97
98    /// Accept one token per matcher. `token_ids[i]` is fed to
99    /// `matchers[i]`. Port of `BatchAcceptToken`.
100    pub fn accept_token(
101        matchers: &mut [GrammarMatcher],
102        token_ids: &[i32],
103        debug: bool,
104    ) -> Vec<bool> {
105        assert_eq!(
106            matchers.len(),
107            token_ids.len(),
108            "matchers and token_ids must have equal length"
109        );
110        matchers
111            .iter_mut()
112            .zip(token_ids)
113            .map(|(m, &t)| m.accept_token(t, debug))
114            .collect()
115    }
116
117    /// Accept one string per matcher. Port of `BatchAcceptString`.
118    pub fn accept_string(
119        matchers: &mut [GrammarMatcher],
120        input_strs: &[&str],
121        debug: bool,
122    ) -> Vec<bool> {
123        assert_eq!(
124            matchers.len(),
125            input_strs.len(),
126            "matchers and input_strs must have equal length"
127        );
128        matchers
129            .iter_mut()
130            .zip(input_strs)
131            .map(|(m, &s)| m.accept_string(s, debug))
132            .collect()
133    }
134}