LittleDesignSolution/Kimi-K2.5
017
1import os2from collections import OrderedDict3from logging import getLogger4from pathlib import Path5from shutil import copyfile6from typing import Any, Dict, Iterator, List, Optional, Tuple, Union, cast7 8import tiktoken9from tiktoken.load import load_tiktoken_bpe10from tokenizers import AddedToken11 12from transformers.convert_slow_tokenizer import bytes_to_unicode13from transformers.tokenization_utils import PreTrainedTokenizer14 15from .tool_declaration_ts import encode_tools_to_typescript_style16 17logger = getLogger(__name__)18VOCAB_FILES_NAMES = {"vocab_file": "tiktoken.model"}19 20 21class TikTokenTokenizer(PreTrainedTokenizer):22 """23 Tokenizing and encoding/decoding text using the Tiktoken tokenizer. See megatron/tokenizer/tiktoken_tokenizer.py.24 25 This tokenizer inherits from [`PreTrainedTokenizer`] which contains most of the main methods. Users should refer to26 this superclass for more information regarding those methods.27 28 Args:29 vocab_file (`str`):30 The path to the Tiktoken model file.31 bos_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|begin_of_text|>",`):32 The beginning of sequence token that was used during pretraining. Can be used a sequence classifier token.33 eos_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|end_of_text|>"`):34 The end of sequence token.35 unk_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|reserved_special_token_249|>"`):36 The unknown token. A token that is not in the vocabulary cannot be converted to an ID and is set to be this37 token instead. The second to last item in special_tokens.38 pad_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|reserved_special_token_250|>"`):39 The token used for padding, for example when batching sequences of different lengths.40 additional_special_tokens (list of `str`, *optional*):41 A tuple or a list of additional tokens, which will be marked as `special`, meaning that they will be42 skipped when decoding if `skip_special_tokens` is set to `True`.43 """44 45 vocab_files_names = VOCAB_FILES_NAMES46 47 model_input_names = ["input_ids", "attention_mask"]48 49 special_tokens: Dict[str, int]50 51 num_reserved_special_tokens = 25652 53 pat_str = "|".join([54 r"""[\p{Han}]+""",55 r"""[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?""",56 r"""[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?""",57 r"""\p{N}{1,3}""",58 r""" ?[^\s\p{L}\p{N}]+[\r\n]*""",59 r"""\s*[\r\n]+""",60 r"""\s+(?!\S)""",61 r"""\s+""",62 ])63 64 def __init__(65 self,66 vocab_file,67 bos_token: Union[str, AddedToken] = "[BOS]",68 eos_token: Union[str, AddedToken] = "[EOS]",69 unk_token: Union[str, AddedToken, None] = None,70 pad_token: Union[str, AddedToken, None] = None,71 additional_special_tokens: List[str] = None,72 added_tokens_decoder: Optional[dict] = None,73 **kwargs,74 ):75 assert os.path.isfile(vocab_file), vocab_file76 77 if additional_special_tokens is None:78 additional_special_tokens = [79 "<|im_end|>",80 "<|im_user|>",81 "<|im_assistant|>",82 "<|start_header_id|>",83 "<|end_header_id|>",84 "[EOT]",85 "<|im_system|>",86 "<|im_middle|>",87 ]88 89 if added_tokens_decoder:90 special_tokens_mapping = {91 i: added_tokens_decoder[i].content92 for i in added_tokens_decoder93 }94 else:95 special_tokens_mapping = {}96 97 self.vocab_file = vocab_file98 mergeable_ranks = load_tiktoken_bpe(vocab_file)99 num_base_tokens = len(mergeable_ranks)100 self.special_tokens = {101 special_tokens_mapping.get(i, f"<|reserved_token_{i}|>"): i102 for i in range(num_base_tokens, num_base_tokens +103 self.num_reserved_special_tokens)104 }105 106 self.model = tiktoken.Encoding(107 name=Path(vocab_file).name,108 pat_str=self.pat_str,109 mergeable_ranks=mergeable_ranks,110 special_tokens=self.special_tokens,111 )112 logger.info(f"Reloaded tiktoken model from {vocab_file}")113 114 self.n_words: int = self.model.n_vocab115 # BOS / EOS token IDs116 self.bos_id: int = self.special_tokens[str(bos_token)]117 self.eos_id: int = self.special_tokens[str(eos_token)]118 logger.info(119 f"#words: {self.n_words} - BOS ID: {self.bos_id} - EOS ID: {self.eos_id}"120 )121 122 self.pad_id: int = self.special_tokens[str(pad_token)]123 self.unk_id: int = self.special_tokens[str(unk_token)]124 125 self.byte_encoder = bytes_to_unicode()126 self.byte_decoder = {v: k for k, v in self.byte_encoder.items()}127 128 self.decoder = {}129 for i in range(self.n_words):130 # Taken from https://gist.github.com/xenova/a452a6474428de0182b17605a98631ee131 decoding = ''.join([132 self.byte_encoder[ord(char)] for char in133 self.model.decode_single_token_bytes(i).decode('latin-1')134 ])135 self.decoder[i] = decoding136 137 self.encoder = {}138 for i in range(self.n_words):139 if i in self.decoder:140 self.encoder[self.decoder[i]] = i141 142 self._token_config_cache = OrderedDict()143 self._cache_max_size = 128144 145 super().__init__(146 bos_token=bos_token,147 eos_token=eos_token,148 unk_token=unk_token,149 pad_token=pad_token,150 additional_special_tokens=additional_special_tokens,151 added_tokens_decoder=added_tokens_decoder,152 **kwargs,153 )154 self.all_special_ids_set = set(self.all_special_ids)155 156 def encode(self,157 text: str,158 allow_special_tokens: bool = True,159 **kwargs) -> List[int]:160 """161 Encodes a string into a list of token IDs.162 163 Args:164 text (str): The input string to be encoded.165 166 Returns:167 list[int]: A list of token IDs.168 """169 # If there are other args, we should call super().encode because there are a lot of code170 # to handle those args. supper().encode finally will call _tokenize and _convert_token_to_id.171 # NOTE: our encode method is not compatible with the super().encode method,172 # e.g. split_special_tokens' default is True in our encode method.173 if len(kwargs) > 0:174 logger.warning(f"Calling super().encode with {kwargs}")175 return super().encode(text, **kwargs)176 177 assert type(text) is str178 179 # The tiktoken tokenizer can handle <=400k chars without180 # pyo3_runtime.PanicException.181 TIKTOKEN_MAX_ENCODE_CHARS = 400_000182 183 # https://github.com/openai/tiktoken/issues/195184 # Here we iterate over subsequences and split if we exceed the limit185 # of max consecutive non-whitespace or whitespace characters.186 MAX_NO_WHITESPACES_CHARS = 25_000187 188 texts = self.pre_tokenizer_process(text)189 190 all_substrs = []191 for text in texts:192 substrs = (193 substr for i in range(0, len(text), TIKTOKEN_MAX_ENCODE_CHARS)194 for substr in self._split_whitespaces_or_nonwhitespaces(195 text[i:i +196 TIKTOKEN_MAX_ENCODE_CHARS], MAX_NO_WHITESPACES_CHARS))197 all_substrs.extend(substrs)198 199 t: List[int] = []200 for substr in all_substrs:201 if allow_special_tokens:202 t.extend(203 # we should consider special token as a common token204 self.model.encode(205 substr,206 allowed_special="all",207 ))208 else:209 t.extend(210 # we should consider special token as a common token211 self.model.encode(212 substr,213 disallowed_special=(),214 ))215 216 return t217 218 def decode(self, token_ids: Union[int, List[int]], **kwargs) -> str:219 """220 Decodes a list of token IDs into a string.221 222 Args:223 token_ids (List[int]): The list of token IDs to be decoded.224 225 Returns:226 str: The decoded string.227 """228 # If there are other args, we should call super().decode because there are a lot of code229 # to handle those args. supper().encode finally will call convert_tokens_to_string and _convert_id_to_token.230 if len(kwargs) > 0:231 return super().decode(token_ids, **kwargs)232 233 if type(token_ids) is int:234 token_ids = [token_ids]235 236 return self.model.decode(cast(List[int], token_ids))237 238 @staticmethod239 def _split_whitespaces_or_nonwhitespaces(240 s: str, max_consecutive_slice_len: int) -> Iterator[str]:241 """242 Splits the string `s` so that each substring contains no more than `max_consecutive_slice_len`243 consecutive whitespaces or consecutive non-whitespaces.244 """245 current_slice_len = 0246 current_slice_is_space = s[0].isspace() if len(s) > 0 else False247 slice_start = 0248 249 for i in range(len(s)):250 is_now_space = s[i].isspace()251 252 if current_slice_is_space ^ is_now_space:253 current_slice_len = 1254 current_slice_is_space = is_now_space255 else:256 current_slice_len += 1257 if current_slice_len > max_consecutive_slice_len:258 yield s[slice_start:i]259 slice_start = i260 current_slice_len = 1261 yield s[slice_start:]262 263 def pre_tokenizer_process(self, text: str) -> List[str]:264 """265 pre-tokenizes the input text into a list of tokens.266 This method is used to split the input text into smaller chunks for internal processing.267 """268 return [text]269 270 """ ----- Below are the abstract methods required by PreTrainedTokenizer ----- """271 272 @property273 def vocab_size(self) -> int:274 return self.n_words275 276 def get_vocab(self) -> Dict[str, int]:277 return self.encoder278 279 def _tokenize(self, text: str, **kwargs) -> List[str]:280 return [self.decoder[t] for t in self.encode(text)]281 282 def _convert_token_to_id(self, token: str) -> int:283 return self.encoder.get(token, self.unk_id)284 285 def _convert_id_to_token(self, index: int) -> str:286 return self.decoder.get(index)287 288 @staticmethod289 def clean_up_tokenization(out_string: str) -> str:290 return out_string291 292 def convert_tokens_to_string(self, tokens: List[str]) -> str:293 text = ''.join(tokens)294 text = bytearray([self.byte_decoder[c]295 for c in text]).decode('utf-8', 'replace')296 return text297 298 def save_vocabulary(self,299 save_directory: str,300 filename_prefix: Optional[str] = None) -> Tuple[str]:301 if not os.path.isdir(save_directory):302 raise ValueError(303 f"vocabulary path ({save_directory}) should be a directory")304 out_vocab_file = os.path.join(305 save_directory,306 (filename_prefix + "-" if filename_prefix else "") +307 VOCAB_FILES_NAMES["vocab_file"])308 309 if os.path.abspath(self.vocab_file) != os.path.abspath(310 out_vocab_file) and os.path.isfile(self.vocab_file):311 copyfile(self.vocab_file, out_vocab_file)312 313 return (out_vocab_file, )314 315 def apply_chat_template(self,316 conversation,317 tools: Optional[list[dict]] = None,318 tokenize: bool = False,319 add_generation_prompt: bool = True,320 thinking: bool = True,321 **kwargs):322 323 tools = deep_sort_dict(tools)324 325 # Convert tools to TypeScript style string if tools are provided326 tools_ts_str = None327 if tools:328 try:329 tools_ts_str = encode_tools_to_typescript_style(tools)330 331 except Exception as e:332 print(f"Failed to convert tools to TypeScript style: {e}")333 tools_ts_str = None334 335 # Store the TypeScript string in kwargs so it can be accessed by the template336 if tools_ts_str is not None:337 kwargs['tools_ts_str'] = tools_ts_str338 return super().apply_chat_template(339 conversation,340 tools=tools,341 tokenize=tokenize,342 add_generation_prompt=add_generation_prompt,343 thinking=thinking,344 **kwargs)345 346 347def deep_sort_dict(obj: Any) -> Any:348 if isinstance(obj, dict):349 return {k: deep_sort_dict(v) for k, v in sorted(obj.items())}350 if isinstance(obj, list):351 return [deep_sort_dict(item) for item in obj]352 return obj353 