use std::collections::HashSet; use pyo3::{ IntoPyObjectExt, PyResult, exceptions, prelude::*, pybacked::PyBackedStr, types::{PyBytes, PyList}, }; use rustc_hash::FxHashMap as HashMap; use crate::{CoreBPE, Rank, byte_pair_encode}; #[pymethods] impl CoreBPE { #[new] fn py_new( encoder: HashMap, Rank>, special_tokens_encoder: HashMap, pattern: &str, ) -> PyResult { Self::new_internal(encoder, special_tokens_encoder, pattern) .map_err(|e| PyErr::new::(e.to_string())) } // ==================== // Encoding // ==================== #[pyo3(name = "encode_ordinary")] fn py_encode_ordinary(&self, py: Python, text: &str) -> Vec { py.detach(|| self.encode_ordinary(text)) } #[pyo3(name = "encode")] fn py_encode( &self, py: Python, text: &str, allowed_special: HashSet, ) -> PyResult> { py.detach(|| { let allowed_special: HashSet<&str> = allowed_special.iter().map(|s| s.as_ref()).collect(); match self.encode(text, &allowed_special) { Ok((tokens, _)) => Ok(tokens), Err(e) => Err(PyErr::new::(e.message)), } }) } fn encode_to_tiktoken_buffer( &self, py: Python, text: &str, allowed_special: HashSet, ) -> PyResult> { let tokens_res = py.detach(|| { let allowed_special: HashSet<&str> = allowed_special.iter().map(|s| s.as_ref()).collect(); self.encode(text, &allowed_special) }); let tokens = match tokens_res { Ok((tokens, _)) => tokens, Err(e) => return Err(PyErr::new::(e.message)), }; let buffer = TiktokenBuffer { tokens }; buffer.into_py_any(py) } fn _encode_bytes(&self, py: Python, bytes: &[u8]) -> Vec { py.detach(|| { match std::str::from_utf8(bytes) { // Straightforward case Ok(text) => self.encode_ordinary(text), // Oops, don't actually have UTF-8. But we need to do the regex splitting in // Unicode space, so we make our best guess at where we would have splits Err(e) => { let text = unsafe { std::str::from_utf8_unchecked(&bytes[..e.valid_up_to()]) }; let (tokens, last_piece_token_len) = self.encode(text, &HashSet::new()).unwrap(); let (mut tokens, last_piece_token_len) = self._increase_last_piece_token_len(tokens, last_piece_token_len); let mut unstable_bytes; if !tokens.is_empty() && last_piece_token_len > 0 { // Lop off the tokens from the last piece and run BPE on the remaining bytes // This likely matches what models see better, e.g. if you assume we're // dealing with truncated UTF-8 bytes. // Niche, but note this may not be correct if we'd have had a regex // split between the valid UTF-8 and the invalid bytes. unstable_bytes = self .decode_bytes(&tokens[tokens.len() - last_piece_token_len..]) .unwrap(); unstable_bytes.extend_from_slice(&bytes[e.valid_up_to()..]); tokens.truncate(tokens.len() - last_piece_token_len); } else { unstable_bytes = bytes[e.valid_up_to()..].to_vec(); } if !unstable_bytes.is_empty() { match self.encoder.get(&unstable_bytes) { Some(token) => tokens.push(*token), None => { tokens.extend(&byte_pair_encode(&unstable_bytes, &self.encoder)) } } } tokens } } }) } #[pyo3(name = "encode_with_unstable")] fn py_encode_with_unstable( &self, py: Python, text: &str, allowed_special: HashSet, ) -> PyResult<(Vec, Py)> { let (tokens, completions): (Vec, HashSet>) = py.detach(|| { let allowed_special: HashSet<&str> = allowed_special.iter().map(|s| s.as_ref()).collect(); self._encode_unstable_native(text, &allowed_special) }); let py_completions = PyList::new(py, completions.into_iter())?; Ok((tokens, py_completions.into())) } fn encode_single_token(&self, piece: &[u8]) -> PyResult { if let Some(token) = self.encoder.get(piece).copied() { return Ok(token); } if let Ok(piece_str) = std::str::from_utf8(piece) { if let Some(token) = self.special_tokens_encoder.get(piece_str).copied() { return Ok(token); } } Err(PyErr::new::(piece.to_owned())) } fn encode_single_piece(&self, piece: &[u8]) -> Vec { if let Some(token) = self.encoder.get(piece) { return vec![*token]; } byte_pair_encode(piece, &self.encoder) } // ==================== // Decoding // ==================== #[pyo3(name = "decode_bytes")] fn py_decode_bytes(&self, py: Python, tokens: Vec) -> Result, PyErr> { match py.detach(|| self.decode_bytes(&tokens)) { Ok(bytes) => Ok(PyBytes::new(py, &bytes).into()), Err(e) => Err(pyo3::exceptions::PyKeyError::new_err(format!("{}", e))), } } fn decode_single_token_bytes(&self, py: Python, token: Rank) -> PyResult> { if let Some(bytes) = self.decoder.get(&token) { return Ok(PyBytes::new(py, bytes).into()); } if let Some(bytes) = self.special_tokens_decoder.get(&token) { return Ok(PyBytes::new(py, bytes).into()); } Err(PyErr::new::(token.to_string())) } // ==================== // Miscellaneous // ==================== fn token_byte_values(&self, py: Python) -> Vec> { self.sorted_token_bytes .iter() .map(|x| PyBytes::new(py, x).into()) .collect() } } #[pyclass(frozen)] struct TiktokenBuffer { tokens: Vec, } #[pymethods] impl TiktokenBuffer { // Based on https://github.com/PyO3/pyo3/blob/v0.22.2/tests/test_buffer_protocol.rs#L25 unsafe fn __getbuffer__( slf: Bound<'_, Self>, view: *mut pyo3::ffi::Py_buffer, flags: std::os::raw::c_int, ) -> PyResult<()> { if view.is_null() { return Err(pyo3::exceptions::PyBufferError::new_err("View is null")); } if (flags & pyo3::ffi::PyBUF_WRITABLE) == pyo3::ffi::PyBUF_WRITABLE { return Err(pyo3::exceptions::PyBufferError::new_err( "Object is not writable", )); } unsafe { let view_ref = &mut *view; view_ref.obj = slf.clone().into_any().into_ptr(); let data = &slf.borrow().tokens; view_ref.buf = data.as_ptr() as *mut std::os::raw::c_void; view_ref.len = (data.len() * std::mem::size_of::()) as isize; view_ref.readonly = 1; view_ref.itemsize = std::mem::size_of::() as isize; view_ref.format = if (flags & pyo3::ffi::PyBUF_FORMAT) == pyo3::ffi::PyBUF_FORMAT { let msg = std::ffi::CString::new("I").unwrap(); msg.into_raw() } else { std::ptr::null_mut() }; view_ref.ndim = 1; view_ref.shape = if (flags & pyo3::ffi::PyBUF_ND) == pyo3::ffi::PyBUF_ND { &mut view_ref.len } else { std::ptr::null_mut() }; view_ref.strides = if (flags & pyo3::ffi::PyBUF_STRIDES) == pyo3::ffi::PyBUF_STRIDES { &mut view_ref.itemsize } else { std::ptr::null_mut() }; view_ref.suboffsets = std::ptr::null_mut(); view_ref.internal = std::ptr::null_mut(); } Ok(()) } unsafe fn __releasebuffer__(&self, view: *mut pyo3::ffi::Py_buffer) { // Note that Py_buffer doesn't have a Drop impl unsafe { let view_ref = &mut *view; if !view_ref.format.is_null() { std::mem::drop(std::ffi::CString::from_raw(view_ref.format)); } } } } #[pymodule(gil_used = false)] fn _tiktoken(_py: Python, m: &Bound) -> PyResult<()> { m.add_class::()?; Ok(()) }