Skip to main content

tpt_tokenizer_core/
encoding.rs

1//! Batch encoding, special-token insertion, padding, and truncation.
2//!
3//! These conveniences layer on top of the raw [`Tokenizer::encode`] output.
4//! They are scheme-agnostic (they work for both BPE and WordPiece) and live in
5//! the shared [`Tokenizer`] trait as provided methods, so every tokenizer gets
6//! them for free.
7
8use alloc::vec;
9use alloc::vec::Vec;
10
11use crate::error::TokenizerError;
12use crate::tokenizer::{TokenId, Tokenizer};
13
14/// How a sequence (or batch of sequences) should be padded to a common length.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum Padding {
17    /// Do not pad. Sequences keep their natural length.
18    #[default]
19    None,
20    /// Pad every sequence to a fixed length. Sequences already at least this
21    /// long are left unchanged (truncation is controlled separately).
22    Fixed(usize),
23    /// Pad every sequence in a batch to the length of the longest sequence.
24    /// Behaves like [`Padding::None`] for a single
25    /// [`encode_with`](TokenizerExt::encode_with) call.
26    Longest,
27}
28
29/// How an over-long sequence should be shortened.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum Truncation {
32    /// Never truncate.
33    #[default]
34    None,
35    /// Truncate to at most `n` ids, *including* any special tokens added by
36    /// [`EncodeConfig::add_special_tokens`].
37    Fixed(usize),
38}
39
40/// Configuration for [`encode_with`](TokenizerExt::encode_with) /
41/// [`encode_batch`](TokenizerExt::encode_batch).
42///
43/// Build it fluently:
44///
45/// ```
46/// use tpt_tokenizer_core::encoding::{EncodeConfig, Padding, Truncation};
47///
48/// let cfg = EncodeConfig::new()
49///     .with_bos(1)
50///     .with_eos(2)
51///     .with_pad(0)
52///     .padding(Padding::Longest)
53///     .truncation(Truncation::Fixed(128));
54/// ```
55#[derive(Debug, Clone, Default)]
56pub struct EncodeConfig {
57    bos: Option<TokenId>,
58    eos: Option<TokenId>,
59    pad: Option<TokenId>,
60    padding: Padding,
61    truncation: Truncation,
62    add_special_tokens: bool,
63}
64
65impl EncodeConfig {
66    /// A config that adds no special tokens and applies no padding or
67    /// truncation — equivalent to a plain [`Tokenizer::encode`], but wrapped in
68    /// an [`Encoding`] with an attention mask.
69    #[must_use]
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// Set the beginning-of-sequence token id to prepend (enables
75    /// special-token insertion).
76    #[must_use]
77    pub fn with_bos(mut self, id: TokenId) -> Self {
78        self.bos = Some(id);
79        self.add_special_tokens = true;
80        self
81    }
82
83    /// Set the end-of-sequence token id to append (enables special-token
84    /// insertion).
85    #[must_use]
86    pub fn with_eos(mut self, id: TokenId) -> Self {
87        self.eos = Some(id);
88        self.add_special_tokens = true;
89        self
90    }
91
92    /// Set the padding token id. Required for any [`Padding`] other than
93    /// [`Padding::None`].
94    #[must_use]
95    pub fn with_pad(mut self, id: TokenId) -> Self {
96        self.pad = Some(id);
97        self
98    }
99
100    /// Enable or disable BOS/EOS insertion without changing the ids.
101    #[must_use]
102    pub fn add_special_tokens(mut self, yes: bool) -> Self {
103        self.add_special_tokens = yes;
104        self
105    }
106
107    /// Set the padding strategy.
108    #[must_use]
109    pub fn padding(mut self, padding: Padding) -> Self {
110        self.padding = padding;
111        self
112    }
113
114    /// Set the truncation strategy.
115    #[must_use]
116    pub fn truncation(mut self, truncation: Truncation) -> Self {
117        self.truncation = truncation;
118        self
119    }
120
121    /// Number of special tokens this config would add to a sequence.
122    fn num_special(&self) -> usize {
123        if !self.add_special_tokens {
124            return 0;
125        }
126        usize::from(self.bos.is_some()) + usize::from(self.eos.is_some())
127    }
128}
129
130/// The result of encoding a single sequence with an [`EncodeConfig`].
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct Encoding {
133    /// The token ids, including any special/padding tokens.
134    pub ids: Vec<TokenId>,
135    /// A mask that is `1` for real (or special) tokens and `0` for padding.
136    pub attention_mask: Vec<u32>,
137}
138
139impl Encoding {
140    /// Length of the (possibly padded) sequence.
141    #[must_use]
142    pub fn len(&self) -> usize {
143        self.ids.len()
144    }
145
146    /// Whether the encoding is empty.
147    #[must_use]
148    pub fn is_empty(&self) -> bool {
149        self.ids.is_empty()
150    }
151}
152
153/// Assembles a single [`Encoding`] from raw ids: truncate content to leave room
154/// for specials, insert BOS/EOS, then build the attention mask. Padding is *not*
155/// applied here (it depends on the batch), so all mask entries are `1`.
156pub(crate) fn assemble(mut ids: Vec<TokenId>, cfg: &EncodeConfig) -> Encoding {
157    if let Truncation::Fixed(max) = cfg.truncation {
158        let room = max.saturating_sub(cfg.num_special());
159        if ids.len() > room {
160            ids.truncate(room);
161        }
162    }
163
164    let mut out = Vec::with_capacity(ids.len() + cfg.num_special());
165    if cfg.add_special_tokens {
166        if let Some(bos) = cfg.bos {
167            out.push(bos);
168        }
169    }
170    out.extend(ids);
171    if cfg.add_special_tokens {
172        if let Some(eos) = cfg.eos {
173            out.push(eos);
174        }
175    }
176
177    let attention_mask = vec![1u32; out.len()];
178    Encoding {
179        ids: out,
180        attention_mask,
181    }
182}
183
184/// Pads `enc` up to `target` ids using `pad_id`, extending the attention mask
185/// with zeros. A no-op if already at least `target` long.
186pub(crate) fn pad_to(enc: &mut Encoding, target: usize, pad_id: TokenId) {
187    if enc.ids.len() >= target {
188        return;
189    }
190    let deficit = target - enc.ids.len();
191    enc.ids.extend(core::iter::repeat(pad_id).take(deficit));
192    enc.attention_mask
193        .extend(core::iter::repeat(0u32).take(deficit));
194}
195
196/// Applies the batch-level padding strategy across `encodings` in place.
197///
198/// # Errors
199/// Returns [`TokenizerError::MalformedFile`] if padding is requested but no pad
200/// token id was configured.
201pub(crate) fn apply_padding(
202    encodings: &mut [Encoding],
203    cfg: &EncodeConfig,
204) -> Result<(), TokenizerError> {
205    let target = match cfg.padding {
206        Padding::None => return Ok(()),
207        Padding::Fixed(n) => n,
208        Padding::Longest => encodings.iter().map(Encoding::len).max().unwrap_or(0),
209    };
210    let Some(pad_id) = cfg.pad else {
211        return Err(TokenizerError::MalformedFile(alloc::string::String::from(
212            "padding requested without a pad token id (call EncodeConfig::with_pad)",
213        )));
214    };
215    for enc in encodings.iter_mut() {
216        pad_to(enc, target, pad_id);
217    }
218    Ok(())
219}
220
221/// Provided-method extensions shared by every [`Tokenizer`].
222impl<T: Tokenizer + ?Sized> TokenizerExt for T {}
223
224/// Higher-level encoding conveniences (special tokens, padding, truncation,
225/// batching) available on every [`Tokenizer`].
226pub trait TokenizerExt: Tokenizer {
227    /// Encode a single sequence, applying special-token insertion, truncation
228    /// and (fixed) padding from `cfg`.
229    ///
230    /// [`Padding::Longest`] has no effect on a single sequence — use
231    /// [`encode_batch`](TokenizerExt::encode_batch) for that.
232    ///
233    /// # Errors
234    /// Propagates [`Tokenizer::encode`] errors, or returns
235    /// [`TokenizerError::MalformedFile`] if fixed padding is requested without a
236    /// pad token id.
237    fn encode_with(&self, text: &str, cfg: &EncodeConfig) -> Result<Encoding, TokenizerError> {
238        let ids = self.encode(text)?;
239        let mut enc = assemble(ids, cfg);
240        if let Padding::Fixed(n) = cfg.padding {
241            let Some(pad_id) = cfg.pad else {
242                return Err(TokenizerError::MalformedFile(alloc::string::String::from(
243                    "padding requested without a pad token id (call EncodeConfig::with_pad)",
244                )));
245            };
246            pad_to(&mut enc, n, pad_id);
247        }
248        Ok(enc)
249    }
250
251    /// Encode a batch of sequences, applying special tokens and truncation to
252    /// each and then the batch-level [`Padding`] strategy across all of them.
253    ///
254    /// # Errors
255    /// Propagates [`Tokenizer::encode`] errors, or returns
256    /// [`TokenizerError::MalformedFile`] if padding is requested without a pad
257    /// token id.
258    fn encode_batch(
259        &self,
260        texts: &[&str],
261        cfg: &EncodeConfig,
262    ) -> Result<Vec<Encoding>, TokenizerError> {
263        let mut encodings = Vec::with_capacity(texts.len());
264        for text in texts {
265            let ids = self.encode(text)?;
266            encodings.push(assemble(ids, cfg));
267        }
268        apply_padding(&mut encodings, cfg)?;
269        Ok(encodings)
270    }
271}