Files
vllm-omni/vllm_omni/tokenizers/mammoth_moda2_tokenizer.py
T
1612948062 add support for MammothModa2 model (#336)
Signed-off-by: HonestDeng <2958906959@qq.com>
Signed-off-by: iwzbi <iwzbi@zju.edu.cn>
Signed-off-by: iwzbi <wzbi@zju.edu.cn>
Signed-off-by: Divyansh Singhvi <divyanshsinghvi@gmail.com>
Signed-off-by: gcanlin <canlinguosdu@gmail.com>
Signed-off-by: yinpeiqi <yinpeiqi809@gmail.com>
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Signed-off-by: 汪志鹏 <wangzhipeng628@gmail.com>
Co-authored-by: catcat <108673086+iwzbi@users.noreply.github.com>
Co-authored-by: iwzbi <iwzbi@zju.edu.cn>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: dsinghvi <divyanshsinghvi@gmail.com>
Co-authored-by: Canlin Guo <canlinguosdu@gmail.com>
Co-authored-by: Peiqi Yin <60515999+yinpeiqi@users.noreply.github.com>
Co-authored-by: 汪志鹏 <wangzhipeng628@gmail.com>
2026-03-04 22:52:56 +08:00

402 lines
14 KiB
Python

# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# SPDX-License-Identifier: Apache-2.0
#
# This file has been modified by ByteDance Ltd. on 2025-09-30.
#
# Original file was released under Apache-2.0, with the full license text
# available at https://www.apache.org/licenses/LICENSE-2.0
#
# This modified file is released under the same license.
#
# --- Upstream header preserved below ---
#
# Copyright 2025 The Qwen Team and The HuggingFace Inc. team. All rights reserved.
#
# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
# and OPT implementations in this library. It has been modified from its
# original forms to accommodate minor architectural differences compared
# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import os
import unicodedata
from collections.abc import Collection
import tiktoken
from loguru import logger
from transformers import AddedToken, PreTrainedTokenizer
VOCAB_FILES_NAMES = {
"vocab_file": "mammothu.tiktoken",
"special_tokens_file": "mammothu_vision_tokens.txt",
}
PAT_STR = (
r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?"""
r"""[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+"""
)
ENDOFTEXT = "<|endoftext|>"
IMSTART = "<|im_start|>"
IMEND = "<|im_end|>"
# as the default behavior is changed to allow special tokens in
# regular texts, the surface forms of special tokens need to be
# as different as possible to minimize the impact
QWEN_SPECIAL_TOKENS = (
"<|object_ref_start|>",
"<|object_ref_end|>",
"<|box_start|>",
"<|box_end|>",
"<|quad_start|>",
"<|quad_end|>",
"<|vision_start|>",
"<|vision_end|>",
"<|vision_pad|>",
"<|image_pad|>",
"<|video_pad|>",
"<tool_call>",
"</tool_call>",
"<|fim_prefix|>",
"<|fim_middle|>",
"<|fim_suffix|>",
"<|fim_pad|>",
"<|repo_name|>",
"<|file_sep|>",
)
# align to qwen2.5 tokenizer length (151846)
EXTRAS = [f"<|extra_{i}|>" for i in range(181)] # 205 - 19[len(QWEN_SPECIAL_TOKENS)] - 5
# align to qwen2.5 embedding size (152064)
EXTRAS += [f"<|extra_margin_{i}|>" for i in range(152064 - 151846)]
# append new token in gen embedding range
EXTRAS += ["<|endofline|>", "<|endoffile|>", "<|gen_placeholder|>", "<|useless token|>", "<|beginoftext|>"]
EXTRAS = tuple(EXTRAS)
# changed to use actual index to avoid misconfiguration with vocabulary expansion
SPECIAL_START_ID = 151643
def _load_tiktoken_bpe(tiktoken_bpe_file: str) -> dict[bytes, int]:
with open(tiktoken_bpe_file, "rb") as f:
contents = f.read()
return {
base64.b64decode(token): int(rank) for token, rank in (line.split() for line in contents.splitlines() if line)
}
class MammothUTokenizer(PreTrainedTokenizer):
"""MammothU tokenizer."""
vocab_files_names = VOCAB_FILES_NAMES
def __init__(
self,
vocab_file: str,
special_tokens_file: str,
errors: str = "replace",
bos_token: str = "<|beginoftext|>",
eos_token: str = "<|endoftext|>",
pad_token: str = "<|endoftext|>",
img_token: str = "<|image token|>",
boi_token: str = "<|image start|>",
eoi_token: str = "<|image end|>",
eol_token: str = "<|endofline|>",
eof_token: str = "<|endoffile|>",
**kwargs,
) -> None:
super().__init__(**kwargs)
# how to handle errors in decoding UTF-8 byte sequences
# use ignore if you are in streaming inference
self.errors = errors
self.mergeable_ranks = _load_tiktoken_bpe(vocab_file)
with open(special_tokens_file) as f:
vision_tokens = [t.strip() for t in f.readlines() if len(t.strip()) > 0]
SPECIAL_TOKENS = tuple(
enumerate(
(
(
ENDOFTEXT,
IMSTART,
IMEND,
)
+ QWEN_SPECIAL_TOKENS
+ EXTRAS
+ tuple(vision_tokens)
),
start=SPECIAL_START_ID,
)
)
self.special_tokens = {token: index for index, token in SPECIAL_TOKENS}
self.special_tokens_set = set(t for _, t in SPECIAL_TOKENS)
enc = tiktoken.Encoding(
"mammothu",
pat_str=PAT_STR,
mergeable_ranks=self.mergeable_ranks,
special_tokens=self.special_tokens,
)
assert len(self.mergeable_ranks) + len(self.special_tokens) == enc.n_vocab, (
f"{len(self.mergeable_ranks) + len(self.special_tokens)} != {enc.n_vocab} in encoding"
)
self.decoder = {v: k for k, v in self.mergeable_ranks.items()}
self.decoder.update({v: k for k, v in self.special_tokens.items()})
self.tokenizer = enc
self.eod_id = self.tokenizer.eot_token
self.bos_token = bos_token
self.eos_token = eos_token
self.pad_token = pad_token
self.img_token = img_token
self.boi_token = boi_token
self.eoi_token = eoi_token
self.eol_token = eol_token
self.eof_token = eof_token
self.image_content_token = "<|image_pad|>" # come from Qwen2.5-VL
self.gen_image_token = "<|gen_image_pad|>"
self.gen_image_placeholder_token = "<|gen_placeholder|>"
self.visual_tokens = ["<|image_pad|>", "<|video_pad|>", "<|vision_start|>", "<|vision_end|>"]
self.visual_tokens_ids = [self.get_vocab()[token] for token in self.visual_tokens]
self.vision_range = (self.get_vocab()[self.boi_token], self.tokenizer.n_vocab - 1)
logger.info(f"MammothUTokeniser Vision range: {self.vision_range}")
def __getstate__(self):
# for pickle lovers
state = self.__dict__.copy()
del state["tokenizer"]
return state
def __setstate__(self, state):
# tokenizer is not python native; don't pass it; rebuild it
self.__dict__.update(state)
enc = tiktoken.Encoding(
"mammothu",
pat_str=PAT_STR,
mergeable_ranks=self.mergeable_ranks,
special_tokens=self.special_tokens,
)
self.tokenizer = enc
def __len__(self) -> int:
return self.tokenizer.n_vocab
def get_vocab(self) -> dict[bytes | str, int]:
vocab = self.mergeable_ranks.copy()
vocab.update(self.special_tokens)
return vocab
@property
def gen_placeholder_id(self):
return self.get_vocab()[self.gen_image_placeholder_token]
def convert_tokens_to_ids(self, tokens: bytes | str | list[bytes | str]) -> list[int]:
if isinstance(tokens, (str, bytes)):
if tokens in self.special_tokens:
return self.special_tokens[tokens]
else:
return self.mergeable_ranks.get(tokens)
ids = []
for token in tokens:
if token in self.special_tokens:
ids.append(self.special_tokens[token])
else:
ids.append(self.mergeable_ranks.get(token))
return ids
def _add_tokens(
self,
new_tokens: list[str] | list[AddedToken],
special_tokens: bool = False,
) -> int:
if not special_tokens and new_tokens:
raise ValueError("Adding regular tokens is not supported")
added_tokens = 0
for token in new_tokens:
surface_form = token.content if isinstance(token, AddedToken) else token
if surface_form in self.special_tokens_set:
# Token already exists in our special tokens set
added_tokens += 1
else:
logger.warning(f"Token {surface_form} is not in the predefined special tokens set and cannot be added")
return added_tokens
def add_special_tokens(self, special_tokens_dict: dict[str, str | AddedToken]) -> int:
"""
Add special tokens to the tokenizer and update the special tokens mapping.
Only adds tokens that are already in the special_tokens_set.
Args:
special_tokens_dict: dictionary of special tokens to add.
The key is the token type and the value is the token to add.
Returns:
Number of tokens added to the vocabulary.
"""
added_tokens = 0
for token_type, token in special_tokens_dict.items():
if token_type == "additional_special_tokens" and isinstance(token, list):
added_tokens += self._add_tokens(token, special_tokens=True)
else:
token_value = token.content if isinstance(token, AddedToken) else token
if token_value in self.special_tokens_set:
setattr(self, token_type, token_value)
added_tokens += 1
else:
logger.warning(
f"Token {token_value} is not in the predefined special tokens set and cannot be added"
)
return added_tokens
def save_vocabulary(self, save_directory: str, **kwargs) -> tuple[str]:
"""
Save only the vocabulary of the tokenizer (vocabulary).
Returns:
`tuple(str)`: Paths to the files saved.
"""
regular_file_path = os.path.join(save_directory, self.vocab_files_names["vocab_file"])
with open(regular_file_path, "w", encoding="utf8") as w:
for k, v in self.mergeable_ranks.items():
line = base64.b64encode(k).decode("utf8") + " " + str(v) + "\n"
w.write(line)
excluded_special_tokens = set(
(
ENDOFTEXT,
IMSTART,
IMEND,
)
+ EXTRAS
)
special_file_path = os.path.join(save_directory, self.vocab_files_names["special_tokens_file"])
with open(special_file_path, "w", encoding="utf8") as w:
for k in self.special_tokens:
if k not in excluded_special_tokens:
print(k, file=w)
return (regular_file_path, special_file_path)
def tokenize(
self,
text: str,
allowed_special: set | str = "all",
disallowed_special: Collection | str = (),
**kwargs,
) -> list[bytes | str]:
"""
Converts a string in a sequence of tokens.
Args:
text (`str`):
The sequence to be encoded.
allowed_special (`Literal["all"]` or `set`):
The surface forms of the tokens to be encoded as special tokens in regular texts.
Default to "all".
disallowed_special (`Literal["all"]` or `Collection`):
The surface forms of the tokens that should not be in regular texts and trigger errors.
Default to an empty tuple.
kwargs (additional keyword arguments, *optional*):
Will be passed to the underlying model specific encode method.
Returns:
`list[bytes|str]`: The list of tokens.
"""
tokens = []
text = unicodedata.normalize("NFC", text)
# this implementation takes a detour: text -> token id -> token surface forms
for t in self.tokenizer.encode(text, allowed_special=allowed_special, disallowed_special=disallowed_special):
tokens.append(self.decoder[t])
return tokens
def convert_tokens_to_string(self, tokens: list[bytes | str]) -> str:
"""
Converts a sequence of tokens in a single string.
"""
text = ""
temp = b""
for t in tokens:
if isinstance(t, str):
if temp:
text += temp.decode("utf-8", errors=self.errors)
temp = b""
text += t
elif isinstance(t, bytes):
temp += t
else:
raise TypeError("token should only be of type types or str")
if temp:
text += temp.decode("utf-8", errors=self.errors)
return text
@property
def vocab_size(self) -> int:
return self.tokenizer.n_vocab
def _convert_id_to_token(self, index: int) -> bytes | str:
"""Converts an id to a token, special tokens included"""
if index in self.decoder:
return self.decoder[index]
raise ValueError("unknown ids")
def _convert_token_to_id(self, token: bytes | str) -> int:
"""Converts a token to an id using the vocab, special tokens included"""
if token in self.special_tokens:
return self.special_tokens[token]
if token in self.mergeable_ranks:
return self.mergeable_ranks[token]
raise ValueError("unknown token")
def _decode(
self,
token_ids: int | list[int],
skip_special_tokens: bool = False,
errors: str | None = None,
**kwargs,
) -> str:
if isinstance(token_ids, int):
token_ids = [token_ids]
if skip_special_tokens:
token_ids = [i for i in token_ids if i < self.eod_id]
return self.tokenizer.decode(token_ids, errors=errors or self.errors)
def bytes_to_str(self, byte_tokens: dict) -> str:
"""Convert byte tokens to string representation.
Args:
byte_tokens: A dictionary where keys are byte objects and values are integers,
or a single byte object.
Returns:
If input is a dictionary, returns a new dictionary with byte keys converted to strings.
If input is a single byte object, returns the string representation.
"""
if isinstance(byte_tokens, dict):
return {k.decode("utf-8", errors=self.errors): v for k, v in byte_tokens.items() if isinstance(k, bytes)}
if isinstance(byte_tokens, bytes):
return byte_tokens.decode("utf-8", errors=self.errors)
return byte_tokens