metrale_grammar/matcher/bitmask.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// TokenBitmask — the per-token accept/reject mask the matcher fills.
4//
5// Port of the C++ token-bitmask representation. The C++ side packs the
6// mask into a `DLTensor` of `int32`, one bit per vocab token, with the
7// buffer size given by `GetBitmaskSize(vocab_size)`. This module
8// reproduces the same packed layout in safe Rust:
9// * `bitmask_size(vocab_size)` — number of `i32` words, identical to
10// the C++ `DynamicBitset::GetBufferSize`;
11// * [`TokenBitmask`] — an owned, vocab-sized packed mask;
12// * helpers to view / fill a caller-supplied `&mut [i32]` slice so
13// Metrale Engine's W7 shim can hand the matcher whatever buffer it owns.
14//
15// Bit `t` of the mask is set iff token id `t` is grammar-legal for the
16// next decode position. Word `t / 32`, bit `t % 32` (LSB-first), the
17// exact layout the C++ `DynamicBitset` uses so a bitmask produced here
18// is byte-compatible with the upstream CUDA `apply_token_bitmask`.
19
20/// Bits packed per `i32` word — fixed by the C++ `DynamicBitset`.
21pub const BITS_PER_WORD: usize = 32;
22
23/// Number of `i32` words needed to hold a bit-per-token mask for a
24/// vocabulary of `vocab_size` tokens.
25///
26/// Port of `GetBitmaskSize` / `DynamicBitset::GetBufferSize`:
27/// `ceil(vocab_size / 32)`.
28#[must_use]
29pub fn bitmask_size(vocab_size: usize) -> usize {
30 vocab_size.div_ceil(BITS_PER_WORD)
31}
32
33/// Set bit `token` in a packed `i32` mask slice.
34#[inline]
35fn set_bit(words: &mut [i32], token: usize, value: bool) {
36 let word = token / BITS_PER_WORD;
37 let bit = token % BITS_PER_WORD;
38 let mask = 1u32 << bit;
39 let cur = words[word] as u32;
40 words[word] = if value { cur | mask } else { cur & !mask } as i32;
41}
42
43/// Read bit `token` from a packed `i32` mask slice.
44#[inline]
45#[must_use]
46fn get_bit(words: &[i32], token: usize) -> bool {
47 let word = token / BITS_PER_WORD;
48 let bit = token % BITS_PER_WORD;
49 (words[word] as u32 >> bit) & 1 == 1
50}
51
52/// Fill every bit `0..vocab_size` of `words` with `value`, leaving the
53/// padding bits of the final word cleared (so `count`/`all` over the
54/// logical vocabulary are exact).
55fn fill(words: &mut [i32], vocab_size: usize, value: bool) {
56 for w in words.iter_mut() {
57 *w = if value { -1 } else { 0 };
58 }
59 if value {
60 // Clear padding bits beyond `vocab_size` in the last word.
61 let used = vocab_size % BITS_PER_WORD;
62 if used != 0 {
63 let last = bitmask_size(vocab_size) - 1;
64 let keep = (1u32 << used) - 1;
65 words[last] = (words[last] as u32 & keep) as i32;
66 }
67 }
68}
69
70/// A packed bit-per-token accept mask owned by a [`super::GrammarMatcher`].
71///
72/// This is the Rust equivalent of the C++ `DLTensor` int32 bitmask. It
73/// is `vocab_size` bits wide, stored LSB-first in `i32` words. The
74/// matcher fills it in place each decode step; callers read which
75/// tokens are legal via [`Self::is_set`] or apply it to logits.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct TokenBitmask {
78 words: Vec<i32>,
79 vocab_size: usize,
80}
81
82impl TokenBitmask {
83 /// Allocate an all-rejecting (all-zero) mask for `vocab_size` tokens.
84 #[must_use]
85 pub fn new(vocab_size: usize) -> Self {
86 Self {
87 words: vec![0; bitmask_size(vocab_size)],
88 vocab_size,
89 }
90 }
91
92 /// The vocabulary size this mask was sized for.
93 #[must_use]
94 pub fn vocab_size(&self) -> usize {
95 self.vocab_size
96 }
97
98 /// The packed `i32` words — the layout Metrale Engine hands to the kernel.
99 #[must_use]
100 pub fn as_words(&self) -> &[i32] {
101 &self.words
102 }
103
104 /// Mutable access to the packed words (for the W7 shim / kernels).
105 pub fn as_words_mut(&mut self) -> &mut [i32] {
106 &mut self.words
107 }
108
109 /// True if token id `token` is accepted (its bit is set).
110 ///
111 /// Panics if `token >= vocab_size`.
112 #[must_use]
113 pub fn is_set(&self, token: usize) -> bool {
114 assert!(token < self.vocab_size, "token id out of range");
115 get_bit(&self.words, token)
116 }
117
118 /// Set / clear token id `token`. Panics if `token >= vocab_size`.
119 pub fn set(&mut self, token: usize, value: bool) {
120 assert!(token < self.vocab_size, "token id out of range");
121 set_bit(&mut self.words, token, value);
122 }
123
124 /// Reject every token (clear all bits).
125 pub fn clear(&mut self) {
126 fill(&mut self.words, self.vocab_size, false);
127 }
128
129 /// Accept every token (set every logical bit).
130 pub fn fill_all(&mut self) {
131 fill(&mut self.words, self.vocab_size, true);
132 }
133
134 /// Number of accepted (set) tokens over the logical vocabulary.
135 #[must_use]
136 pub fn count_set(&self) -> usize {
137 (0..self.vocab_size)
138 .filter(|&t| get_bit(&self.words, t))
139 .count()
140 }
141
142 /// True if every logical token bit is set.
143 #[must_use]
144 pub fn all_set(&self) -> bool {
145 (0..self.vocab_size).all(|t| get_bit(&self.words, t))
146 }
147
148 /// Collect the rejected (cleared) token ids — the
149 /// `_DebugGetMaskedTokensFromBitmask` helper of the C++ matcher.
150 #[must_use]
151 pub fn rejected_tokens(&self) -> Vec<i32> {
152 (0..self.vocab_size)
153 .filter(|&t| !get_bit(&self.words, t))
154 .map(|t| t as i32)
155 .collect()
156 }
157}
158
159/// A mutable, vocab-sized view over a caller-owned `i32` slice.
160///
161/// `FillNextTokenBitmask` in the C++ takes a `DLTensor*`; Metrale Engine owns
162/// the backing buffer. This newtype lets the matcher fill that buffer
163/// directly without copying through a [`TokenBitmask`]. The slice must
164/// be at least `bitmask_size(vocab_size)` words long.
165pub struct BitmaskSlice<'a> {
166 words: &'a mut [i32],
167 vocab_size: usize,
168}
169
170impl<'a> BitmaskSlice<'a> {
171 /// Wrap `words` as a `vocab_size`-bit mask.
172 ///
173 /// Returns `None` if `words` is too short to hold the mask.
174 #[must_use]
175 pub fn new(words: &'a mut [i32], vocab_size: usize) -> Option<Self> {
176 if words.len() < bitmask_size(vocab_size) {
177 return None;
178 }
179 Some(Self { words, vocab_size })
180 }
181
182 /// The vocabulary size this view covers.
183 #[must_use]
184 pub fn vocab_size(&self) -> usize {
185 self.vocab_size
186 }
187
188 /// Set / clear token id `token`.
189 pub fn set(&mut self, token: usize, value: bool) {
190 debug_assert!(token < self.vocab_size);
191 set_bit(self.words, token, value);
192 }
193
194 /// Read token id `token`.
195 #[must_use]
196 pub fn is_set(&self, token: usize) -> bool {
197 debug_assert!(token < self.vocab_size);
198 get_bit(self.words, token)
199 }
200
201 /// The packed `i32` words backing this view — exactly
202 /// `bitmask_size(vocab_size)` of them.
203 ///
204 /// Padding bits beyond `vocab_size` in the final word are not
205 /// guaranteed cleared (only [`Self::fill_all`] / [`Self::clear`]
206 /// normalize them); callers that popcount must mask the tail or
207 /// only trust [`Self::vocab_size`] logical bits.
208 #[must_use]
209 pub fn words(&self) -> &[i32] {
210 self.words
211 }
212
213 /// Reject every token.
214 pub fn clear(&mut self) {
215 fill(self.words, self.vocab_size, false);
216 }
217
218 /// Accept every token.
219 pub fn fill_all(&mut self) {
220 fill(self.words, self.vocab_size, true);
221 }
222
223 /// True if every logical token bit is set.
224 #[must_use]
225 pub fn all_set(&self) -> bool {
226 (0..self.vocab_size).all(|t| get_bit(self.words, t))
227 }
228}
229
230impl<'a> From<&'a mut TokenBitmask> for BitmaskSlice<'a> {
231 fn from(b: &'a mut TokenBitmask) -> Self {
232 let vocab_size = b.vocab_size;
233 Self {
234 words: &mut b.words,
235 vocab_size,
236 }
237 }
238}