use sha2::{Digest, Sha256};
use super::error::SetFitError;
pub const MAX_SEQUENCE_LENGTH: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TruncationFact {
pub truncated: bool,
pub original_len: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputProvenance {
pub index: usize,
pub text_sha256: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SentenceBatch {
pub(crate) input_ids: Vec<u32>,
pub(crate) token_type_ids: Vec<u32>,
pub(crate) attention_mask: Vec<u8>,
pub(crate) batch: usize,
pub(crate) seq: usize,
pub(crate) truncation: Vec<TruncationFact>,
pub(crate) provenance: Vec<InputProvenance>,
pub(crate) tokenizer_sha256: String,
}
impl SentenceBatch {
#[must_use]
pub fn input_ids(&self) -> &[u32] {
&self.input_ids
}
#[must_use]
pub fn token_type_ids(&self) -> &[u32] {
&self.token_type_ids
}
#[must_use]
pub fn attention_mask(&self) -> &[u8] {
&self.attention_mask
}
#[must_use]
pub fn batch(&self) -> usize {
self.batch
}
#[must_use]
pub fn seq(&self) -> usize {
self.seq
}
#[must_use]
pub fn truncation(&self) -> &[TruncationFact] {
&self.truncation
}
#[must_use]
pub fn provenance(&self) -> &[InputProvenance] {
&self.provenance
}
#[must_use]
pub fn tokenizer_sha256(&self) -> &str {
&self.tokenizer_sha256
}
}
pub const PADDING_MODE: &str = "batch_longest";
pub struct MiniLmTokenizer {
inner: tokenizers::Tokenizer,
untruncated: tokenizers::Tokenizer,
source_bytes: Vec<u8>,
tokenizer_sha256: String,
}
impl std::fmt::Debug for MiniLmTokenizer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MiniLmTokenizer")
.field("tokenizer_sha256", &self.tokenizer_sha256)
.field("source_bytes_len", &self.source_bytes.len())
.field("max_sequence_length", &MAX_SEQUENCE_LENGTH)
.finish()
}
}
pub(crate) fn sha256_hex(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
}
impl MiniLmTokenizer {
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self, SetFitError> {
let mut inner =
tokenizers::Tokenizer::from_bytes(bytes).map_err(|e| SetFitError::TokenizerLoad {
reason: e.to_string(),
})?;
inner
.with_truncation(Some(tokenizers::TruncationParams {
max_length: MAX_SEQUENCE_LENGTH,
strategy: tokenizers::TruncationStrategy::LongestFirst,
stride: 0,
direction: tokenizers::TruncationDirection::Right,
}))
.map_err(|e| SetFitError::TokenizerLoad {
reason: format!("cannot configure truncation: {e}"),
})?;
debug_assert_eq!(PADDING_MODE, "batch_longest");
inner.with_padding(Some(tokenizers::PaddingParams {
strategy: tokenizers::PaddingStrategy::BatchLongest,
direction: tokenizers::PaddingDirection::Right,
pad_to_multiple_of: None,
pad_id: 0,
pad_type_id: 0,
pad_token: "[PAD]".to_string(),
}));
let mut untruncated =
tokenizers::Tokenizer::from_bytes(bytes).map_err(|e| SetFitError::TokenizerLoad {
reason: e.to_string(),
})?;
untruncated
.with_truncation(None)
.map_err(|e| SetFitError::TokenizerLoad {
reason: format!("cannot clear truncation: {e}"),
})?;
untruncated.with_padding(None);
Ok(Self {
inner,
untruncated,
source_bytes: bytes.to_vec(),
tokenizer_sha256: sha256_hex(bytes),
})
}
#[must_use]
pub fn tokenizer_sha256(&self) -> &str {
&self.tokenizer_sha256
}
#[must_use]
pub fn source_bytes(&self) -> &[u8] {
&self.source_bytes
}
pub fn encode_batch(&self, texts: &[&str]) -> Result<SentenceBatch, SetFitError> {
if texts.is_empty() {
return Err(SetFitError::BatchInvalid {
reason: "empty text list: a batch needs at least one input".to_string(),
});
}
let encodings = self.inner.encode_batch(texts.to_vec(), true).map_err(|e| {
SetFitError::TokenizerLoad {
reason: format!("encode_batch failed: {e}"),
}
})?;
if encodings.len() != texts.len() {
return Err(SetFitError::BatchInvalid {
reason: format!(
"tokenizer returned {} encodings for {} inputs",
encodings.len(),
texts.len()
),
});
}
let batch = texts.len();
let seq = encodings[0].get_ids().len();
if seq == 0 {
return Err(SetFitError::BatchInvalid {
reason: "tokenizer produced a zero-length row".to_string(),
});
}
for (i, e) in encodings.iter().enumerate() {
if e.get_ids().len() != seq {
return Err(SetFitError::BatchInvalid {
reason: format!(
"row {i} has length {} but row 0 has {seq}; padding did not apply",
e.get_ids().len()
),
});
}
}
let cut: Vec<usize> = encodings
.iter()
.enumerate()
.filter(|(_, e)| !e.get_overflowing().is_empty())
.map(|(i, _)| i)
.collect();
let mut original_lens: Vec<usize> = encodings
.iter()
.map(|e| e.get_attention_mask().iter().filter(|m| **m == 1).count())
.collect();
if !cut.is_empty() {
let cut_texts: Vec<&str> = cut.iter().map(|i| texts[*i]).collect();
let full = self
.untruncated
.encode_batch(cut_texts, true)
.map_err(|e| SetFitError::TokenizerLoad {
reason: format!("untruncated pass failed: {e}"),
})?;
if full.len() != cut.len() {
return Err(SetFitError::BatchInvalid {
reason: "untruncated pass returned a different row count".to_string(),
});
}
for (slot, e) in cut.iter().zip(full.iter()) {
original_lens[*slot] = e.get_ids().len();
}
}
let n = batch
.checked_mul(seq)
.ok_or_else(|| SetFitError::BatchInvalid {
reason: format!("batch {batch} x seq {seq} overflows usize"),
})?;
let mut input_ids = Vec::with_capacity(n);
let mut token_type_ids = Vec::with_capacity(n);
let mut attention_mask = Vec::with_capacity(n);
let mut truncation = Vec::with_capacity(batch);
let mut provenance = Vec::with_capacity(batch);
for (i, e) in encodings.iter().enumerate() {
input_ids.extend_from_slice(e.get_ids());
token_type_ids.extend_from_slice(e.get_type_ids());
for (pos, m) in e.get_attention_mask().iter().enumerate() {
let bit = u8::try_from(*m).map_err(|_| SetFitError::BatchInvalid {
reason: format!("attention mask value {m} at row {i} position {pos}"),
})?;
if bit > 1 {
return Err(SetFitError::BatchInvalid {
reason: format!(
"non-binary attention mask value {bit} at row {i} position {pos}"
),
});
}
attention_mask.push(bit);
}
truncation.push(TruncationFact {
truncated: !e.get_overflowing().is_empty(),
original_len: original_lens[i],
});
provenance.push(InputProvenance {
index: i,
text_sha256: sha256_hex(texts[i].as_bytes()),
});
}
Ok(SentenceBatch {
input_ids,
token_type_ids,
attention_mask,
batch,
seq,
truncation,
provenance,
tokenizer_sha256: self.tokenizer_sha256.clone(),
})
}
}
#[cfg(all(test, feature = "setfit"))]
#[path = "tokenizer_tests.rs"]
mod tokenizer_tests;