CoolFace
Modelpublic

RedHatAI/Kimi-K3-NVFP4

sourceHugging Faceotherupdated 10d agoView on Hugging Face
12likes1.2kdownloads
tokenization_kimi.py409 linesDownload Raw Back to root
1import os2from logging import getLogger3from pathlib import Path4from shutil import copyfile5from typing import Dict, Iterator, List, Optional, Tuple, Union, cast6 7import tiktoken8from tiktoken.load import load_tiktoken_bpe9from tokenizers import AddedToken10from transformers.convert_slow_tokenizer import bytes_to_unicode11from transformers.tokenization_utils import PreTrainedTokenizer12 13try:14    from .encoding_k3 import build_chat_segments, is_batched_conversation15except ImportError:  # pragma: no cover - supports direct file execution/import.16    from encoding_k3 import build_chat_segments, is_batched_conversation17 18logger = getLogger(__name__)19VOCAB_FILES_NAMES = {"vocab_file": "tiktoken.model"}20 21 22class TikTokenTokenizer(PreTrainedTokenizer):23    """24    Tokenizing and encoding/decoding text using the Tiktoken tokenizer. See megatron/tokenizer/tiktoken_tokenizer.py.25 26    This tokenizer inherits from [`PreTrainedTokenizer`] which contains most of the main methods. Users should refer to27    this superclass for more information regarding those methods.28 29    Args:30        vocab_file (`str`):31            The path to the Tiktoken model file.32        bos_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|begin_of_text|>",`):33            The beginning of sequence token that was used during pretraining. Can be used a sequence classifier token.34        eos_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|end_of_text|>"`):35            The end of sequence token.36        unk_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|reserved_special_token_249|>"`):37            The unknown token. A token that is not in the vocabulary cannot be converted to an ID and is set to be this38            token instead. The second to last item in special_tokens.39        pad_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|reserved_special_token_250|>"`):40            The token used for padding, for example when batching sequences of different lengths.41        additional_special_tokens (list of `str`, *optional*):42            A tuple or a list of additional tokens, which will be marked as `special`, meaning that they will be43            skipped when decoding if `skip_special_tokens` is set to `True`.44    """45 46    vocab_files_names = VOCAB_FILES_NAMES47 48    model_input_names = ["input_ids", "attention_mask"]49 50    special_tokens: Dict[str, int]51 52    num_reserved_special_tokens = 25653 54    pat_str = "|".join([55        r"""[\p{Han}]+""",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"""[^\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)?""",58        r"""\p{N}{1,3}""",59        r""" ?[^\s\p{L}\p{N}]+[\r\n]*""",60        r"""\s*[\r\n]+""",61        r"""\s+(?!\S)""",62        r"""\s+""",63    ])64 65    def __init__(66        self,67        vocab_file,68        bos_token: Union[str, AddedToken] = "[BOS]",69        eos_token: Union[str, AddedToken] = "[EOS]",70        unk_token: Union[str, AddedToken, None] = None,71        pad_token: Union[str, AddedToken, None] = None,72        additional_special_tokens: List[str] = None,73        added_tokens_decoder: Optional[dict] = None,74        **kwargs,75    ):76        assert os.path.isfile(vocab_file), vocab_file77 78        if additional_special_tokens is None:79            additional_special_tokens = [80                "<|im_end|>",81                "<|im_user|>",82                "<|im_assistant|>",83                "<|start_header_id|>",84                "<|end_header_id|>",85                "[EOT]",86                "<|im_system|>",87                "<|im_middle|>",88            ]89 90        if added_tokens_decoder:91            special_tokens_mapping = {92                i: added_tokens_decoder[i].content93                for i in added_tokens_decoder94            }95        else:96            special_tokens_mapping = {}97 98        self.vocab_file = vocab_file99        mergeable_ranks = load_tiktoken_bpe(vocab_file)100        num_base_tokens = len(mergeable_ranks)101        self.special_tokens = {102            special_tokens_mapping.get(i, f"<|reserved_token_{i}|>"): i103            for i in range(num_base_tokens, num_base_tokens +104                           self.num_reserved_special_tokens)105        }106 107        self.model = tiktoken.Encoding(108            name=Path(vocab_file).name,109            pat_str=self.pat_str,110            mergeable_ranks=mergeable_ranks,111            special_tokens=self.special_tokens,112        )113        logger.info(f"Reloaded tiktoken model from {vocab_file}")114 115        self.n_words: int = self.model.n_vocab116        # BOS / EOS token IDs117        self.bos_id: int = self.special_tokens[str(bos_token)]118        self.eos_id: int = self.special_tokens[str(eos_token)]119        logger.info(120            f"#words: {self.n_words} - BOS ID: {self.bos_id} - EOS ID: {self.eos_id}"121        )122 123        self.pad_id: int = self.special_tokens[str(pad_token)]124        self.unk_id: int = self.special_tokens[str(unk_token)]125 126        self.byte_encoder = bytes_to_unicode()127        self.byte_decoder = {v: k for k, v in self.byte_encoder.items()}128 129        self.decoder = {}130        for i in range(self.n_words):131            # Taken from https://gist.github.com/xenova/a452a6474428de0182b17605a98631ee132            decoding = ''.join([133                self.byte_encoder[ord(char)] for char in134                self.model.decode_single_token_bytes(i).decode('latin-1')135            ])136            self.decoder[i] = decoding137 138        self.encoder = {}139        for i in range(self.n_words):140            if i in self.decoder:141                self.encoder[self.decoder[i]] = i142 143        super().__init__(144            bos_token=bos_token,145            eos_token=eos_token,146            unk_token=unk_token,147            pad_token=pad_token,148            additional_special_tokens=additional_special_tokens,149            added_tokens_decoder=added_tokens_decoder,150            **kwargs,151        )152        self.all_special_ids_set = set(self.all_special_ids)153 154    def _encode_text_piece(self, text: str,155                           allow_special_tokens: bool = True) -> List[int]:156        # The tiktoken tokenizer can handle <=400k chars without157        # pyo3_runtime.PanicException.158        TIKTOKEN_MAX_ENCODE_CHARS = 400_000159 160        # https://github.com/openai/tiktoken/issues/195161        # Here we iterate over subsequences and split if we exceed the limit162        # of max consecutive non-whitespace or whitespace characters.163        MAX_NO_WHITESPACES_CHARS = 25_000164 165        t: List[int] = []166        for i in range(0, len(text), TIKTOKEN_MAX_ENCODE_CHARS):167            for substr in self._split_whitespaces_or_nonwhitespaces(168                text[i:i + TIKTOKEN_MAX_ENCODE_CHARS],169                MAX_NO_WHITESPACES_CHARS,170            ):171                if allow_special_tokens:172                    t.extend(173                        # structural markers: encode <|...|> as their special token IDs174                        self.model.encode(175                            substr,176                            allowed_special="all",177                        ))178                else:179                    t.extend(180                        # user/tool text: encode any <|...|> as ordinary BPE tokens (never as control tokens)181                        self.model.encode(182                            substr,183                            disallowed_special=(),184                        ))185 186        return t187 188    def encode(self,189               text: str,190               allow_special_tokens: bool = True,191               **kwargs) -> List[int]:192        """193        Encodes a string into a list of token IDs.194 195        Args:196            text (str): The input string to be encoded.197 198        Returns:199            list[int]: A list of token IDs.200        """201        # If there are other args, we should call super().encode because there are a lot of code202        # to handle those args. supper().encode finally will call _tokenize and _convert_token_to_id.203        # NOTE: our encode method is not compatible with the super().encode method,204        #   e.g. split_special_tokens' default is True in our encode method.205        if len(kwargs) > 0:206            logger.warning(f"Calling super().encode with {kwargs}")207            return super().encode(text, **kwargs)208 209        assert type(text) is str210        return self._encode_text_piece(text,211                                       allow_special_tokens=allow_special_tokens)212 213    def decode(self, token_ids: Union[int, List[int]], **kwargs) -> str:214        """215        Decodes a list of token IDs into a string.216 217        Args:218            token_ids (List[int]): The list of token IDs to be decoded.219 220        Returns:221            str: The decoded string.222        """223        # If there are other args, we should call super().decode because there are a lot of code224        # to handle those args. supper().encode finally will call convert_tokens_to_string and _convert_id_to_token.225        if len(kwargs) > 0:226            return super().decode(token_ids, **kwargs)227 228        if type(token_ids) is int:229            token_ids = [token_ids]230 231        return self.model.decode(cast(List[int], token_ids))232 233    @staticmethod234    def _split_whitespaces_or_nonwhitespaces(235            s: str, max_consecutive_slice_len: int) -> Iterator[str]:236        """237        Splits the string `s` so that each substring contains no more than `max_consecutive_slice_len`238        consecutive whitespaces or consecutive non-whitespaces.239        """240        current_slice_len = 0241        current_slice_is_space = s[0].isspace() if len(s) > 0 else False242        slice_start = 0243 244        for i in range(len(s)):245            is_now_space = s[i].isspace()246 247            if current_slice_is_space ^ is_now_space:248                current_slice_len = 1249                current_slice_is_space = is_now_space250            else:251                current_slice_len += 1252                if current_slice_len > max_consecutive_slice_len:253                    yield s[slice_start:i]254                    slice_start = i255                    current_slice_len = 1256        yield s[slice_start:]257 258    def _encode_chat_segments(self, segments) -> List[int]:259        token_ids: List[int] = []260        for segment in segments:261            token_ids.extend(262                self._encode_text_piece(263                    segment.text,264                    allow_special_tokens=segment.allow_special,265                ))266        return token_ids267 268    @staticmethod269    def _truncate(ids: List[int],270                  truncation: bool = False,271                  max_length: Optional[int] = None) -> List[int]:272        if truncation and max_length is not None:273            return ids[:max_length]274        return ids275 276    def _format_chat_token_output(self,277                                  encoded_inputs: List[List[int]],278                                  *,279                                  is_batched: bool,280                                  padding=False,281                                  truncation: bool = False,282                                  max_length: Optional[int] = None,283                                  return_tensors=None,284                                  return_dict: bool = False):285        encoded_inputs = [286            self._truncate(ids, truncation=truncation, max_length=max_length)287            for ids in encoded_inputs288        ]289 290        needs_batch_encoding = (291            is_batched or padding or return_tensors is not None or return_dict)292        if not needs_batch_encoding:293            return encoded_inputs[0]294 295        features = [{296            "input_ids": ids,297            "attention_mask": [1] * len(ids)298        } for ids in encoded_inputs]299        batch = self.pad(features,300                         padding=padding,301                         max_length=max_length if padding else None,302                         return_attention_mask=True,303                         return_tensors=return_tensors)304 305        if return_dict:306            return batch307        if is_batched:308            return batch["input_ids"]309        return batch["input_ids"][0] if return_tensors is None else batch[310            "input_ids"]311 312    """ ----- Below are the abstract methods required by PreTrainedTokenizer ----- """313 314    @property315    def vocab_size(self) -> int:316        return self.n_words317 318    def get_vocab(self) -> Dict[str, int]:319        return self.encoder320 321    def _tokenize(self, text: str, **kwargs) -> List[str]:322        return [self.decoder[t] for t in self.encode(text)]323 324    def _convert_token_to_id(self, token: str) -> int:325        return self.encoder.get(token, self.unk_id)326 327    def _convert_id_to_token(self, index: int) -> str:328        return self.decoder.get(index)329 330    @staticmethod331    def clean_up_tokenization(out_string: str) -> str:332        return out_string333 334    def convert_tokens_to_string(self, tokens: List[str]) -> str:335        text = ''.join(tokens)336        text = bytearray([self.byte_decoder[c]337                          for c in text]).decode('utf-8', 'replace')338        return text339 340    def save_vocabulary(self,341                        save_directory: str,342                        filename_prefix: Optional[str] = None) -> Tuple[str]:343        if not os.path.isdir(save_directory):344            raise ValueError(345                f"vocabulary path ({save_directory}) should be a directory")346        out_vocab_file = os.path.join(347            save_directory,348            (filename_prefix + "-" if filename_prefix else "") +349            VOCAB_FILES_NAMES["vocab_file"])350 351        if os.path.abspath(self.vocab_file) != os.path.abspath(352                out_vocab_file) and os.path.isfile(self.vocab_file):353            copyfile(self.vocab_file, out_vocab_file)354 355        return (out_vocab_file, )356 357    def apply_chat_template(self,358                            conversation,359                            tools: Optional[list[dict]] = None,360                            tokenize: bool = False,361                            add_generation_prompt: bool = True,362                            thinking: bool = True,363                            padding=False,364                            truncation: bool = False,365                            max_length: Optional[int] = None,366                            return_tensors=None,367                            return_dict: bool = False,368                            **kwargs):369        # Tokenizer-level rendering reorders tool result messages to match370        # assistant tool_calls, normalizes per-call arguments and response371        # schema, then encodes the resulting XTML structure segment-by-segment.372        is_batched = is_batched_conversation(conversation)373        conversations = conversation if is_batched else [conversation]374        image_prompts = kwargs.pop("image_prompts", None)375        if is_batched and image_prompts is not None:376            raise ValueError("image_prompts is only supported for one chat.")377        378        # by default set thinking effort to max379        kwargs.setdefault("thinking_effort", "max")380 381        segment_batches = [382            build_chat_segments(383                messages,384                tools=tools,385                add_generation_prompt=add_generation_prompt,386                thinking=thinking,387                image_prompts=image_prompts,388                **kwargs,389            ) for messages in conversations390        ]391 392        if not tokenize:393            rendered = ["".join(segment.text for segment in segments)394                        for segments in segment_batches]395            return rendered if is_batched else rendered[0]396 397        encoded_inputs = [398            self._encode_chat_segments(segments) for segments in segment_batches399        ]400        return self._format_chat_token_output(401            encoded_inputs,402            is_batched=is_batched,403            padding=padding,404            truncation=truncation,405            max_length=max_length,406            return_tensors=return_tensors,407            return_dict=return_dict,408        )409