816 lines
36 KiB
Python
816 lines
36 KiB
Python
import os
|
|
import base64
|
|
from transformers import PreTrainedTokenizer, AutoTokenizer
|
|
|
|
class LlmTokenizer(PreTrainedTokenizer):
|
|
def __init__(self, tokenizer_path, model_type, **kwargs):
|
|
try:
|
|
self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, trust_remote_code=True, use_fast=False)
|
|
except:
|
|
self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, trust_remote_code=True, use_fast=True)
|
|
self.tokenizer_path = tokenizer_path
|
|
self.model_type = model_type
|
|
# stop_ids
|
|
self.stop_ids = []
|
|
self.stop_ids.append(self.tokenizer.eos_token_id)
|
|
if hasattr(self.tokenizer, 'im_end_id'):
|
|
self.stop_ids.append(self.tokenizer.im_end_id)
|
|
try:
|
|
eot_id = self.tokenizer.encode('<|eot_id|>')
|
|
if len(eot_id) == 1:
|
|
self.stop_ids.append(eot_id[0])
|
|
eot_id = self.tokenizer.encode('<end_of_turn>')
|
|
if len(eot_id) == 2 and eot_id[0] == 2:
|
|
self.stop_ids.append(eot_id[1])
|
|
except:
|
|
pass
|
|
from collections.abc import Iterable
|
|
if hasattr(self.tokenizer, 'generation_config') and self.tokenizer.generation_config is not None:
|
|
eos_token_id = self.tokenizer.generation_config.eos_token_id
|
|
if isinstance(eos_token_id, int):
|
|
self.stop_ids.append(eos_token_id)
|
|
elif isinstance(eos_token_id, Iterable):
|
|
for id in eos_token_id:
|
|
self.stop_ids.append(id)
|
|
gen_cfg_path = os.path.join(tokenizer_path, 'generation_config.json')
|
|
if os.path.isfile(gen_cfg_path):
|
|
import json
|
|
try:
|
|
with open(gen_cfg_path, 'r') as f:
|
|
gen_cfg = json.load(f)
|
|
eos_token_id = gen_cfg.get('eos_token_id')
|
|
if isinstance(eos_token_id, int):
|
|
self.stop_ids.append(eos_token_id)
|
|
elif isinstance(eos_token_id, Iterable):
|
|
for id in eos_token_id:
|
|
self.stop_ids.append(id)
|
|
except Exception:
|
|
pass
|
|
# gemma4: <turn|> (token 106) is end-of-turn
|
|
try:
|
|
turn_ids = self.tokenizer.encode('<turn|>', add_special_tokens=False)
|
|
if len(turn_ids) == 1 and turn_ids[0] not in self.stop_ids:
|
|
self.stop_ids.append(turn_ids[0])
|
|
except:
|
|
pass
|
|
if model_type == 'glm_ocr':
|
|
user_ids = self.tokenizer.encode('<|user|>', add_special_tokens=False)
|
|
if len(user_ids) == 1:
|
|
self.stop_ids.append(user_ids[0])
|
|
self.stop_ids = [stop_id for stop_id in self.stop_ids if stop_id is not None]
|
|
self.stop_ids = list(set(self.stop_ids))
|
|
super().__init__(**kwargs)
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
return self.tokenizer(*args, **kwargs)
|
|
|
|
def __getattr__(self, name):
|
|
if self.tokenizer and hasattr(self.tokenizer, name):
|
|
return getattr(self.tokenizer, name)
|
|
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
|
|
def _tokenize(self, text, **kwargs):
|
|
return self.tokenizer.tokenize(text, **kwargs)
|
|
|
|
def _convert_token_to_id(self, token):
|
|
return self.tokenizer.convert_tokens_to_ids(token)
|
|
|
|
def _convert_id_to_token(self, index):
|
|
return self.tokenizer.convert_ids_to_tokens(index)
|
|
|
|
def get_vocab(self):
|
|
return self.tokenizer.get_vocab()
|
|
|
|
@property
|
|
def vocab_size(self):
|
|
return self.tokenizer.vocab_size
|
|
|
|
def id_to_str(self, token_id):
|
|
try:
|
|
word = self.tokenizer.decode(int(token_id))
|
|
except:
|
|
def contains_replacement(text): return '\uFFFD' in text
|
|
def decode_id(token_id):
|
|
return self.tokenizer.convert_tokens_to_string(
|
|
self.tokenizer._convert_id_to_token(int(token_id)))
|
|
def decode_ids(token_ids):
|
|
return self.tokenizer.convert_tokens_to_string(
|
|
self.tokenizer.convert_ids_to_tokens(token_ids))
|
|
word = decode_id(int(token_id))
|
|
# Smollm tokenizer will produce half chinese character, using buffer to decode
|
|
if contains_replacement(word):
|
|
self.decode_buffer.append(token_id)
|
|
buffer_txt = decode_ids(self.decode_buffer)
|
|
if not contains_replacement(buffer_txt):
|
|
word = buffer_txt
|
|
self.decode_buffer.clear()
|
|
else:
|
|
word = ''
|
|
return word
|
|
|
|
@classmethod
|
|
def from_pretrained(cls, pretrained_model_name_or_path, model_type, **kwargs):
|
|
return cls(pretrained_model_name_or_path, model_type, **kwargs)
|
|
|
|
def apply_chat_template(self, conversation, **kwargs):
|
|
if hasattr(self.tokenizer, 'apply_chat_template'):
|
|
return self.tokenizer.apply_chat_template(conversation, tokenize=False, add_generation_prompt=True, **kwargs)
|
|
raise RuntimeError('Tokenizer no `apply_chat_template` funtion.')
|
|
|
|
def save_vocabulary(self, save_directory, **kwargs):
|
|
file_path = os.path.join(save_directory, "tokenizer.txt")
|
|
return (file_path,)
|
|
|
|
def get_chat_template(self, chat_template = None, tools = None):
|
|
if chat_template is None and not getattr(self.tokenizer, 'chat_template', None):
|
|
return None
|
|
try:
|
|
return self.tokenizer.get_chat_template(chat_template, tools)
|
|
except ValueError:
|
|
return None
|
|
|
|
@staticmethod
|
|
def _generate_nfkc_table():
|
|
import unicodedata
|
|
entries = []
|
|
for cp in range(0x110000):
|
|
try:
|
|
ch = chr(cp)
|
|
normalized = unicodedata.normalize('NFKC', ch)
|
|
if normalized != ch:
|
|
entries.append((cp, normalized.encode('utf-8')))
|
|
except (ValueError, OverflowError):
|
|
pass
|
|
return entries
|
|
|
|
@staticmethod
|
|
def _generate_nfc_table():
|
|
import unicodedata
|
|
entries = []
|
|
for cp in range(0x110000):
|
|
try:
|
|
ch = chr(cp)
|
|
normalized = unicodedata.normalize('NFC', ch)
|
|
if normalized != ch:
|
|
entries.append((cp, normalized.encode('utf-8')))
|
|
except (ValueError, OverflowError):
|
|
pass
|
|
return entries
|
|
|
|
@staticmethod
|
|
def _generate_nfd_table():
|
|
import unicodedata
|
|
entries = []
|
|
for cp in range(0x110000):
|
|
try:
|
|
ch = chr(cp)
|
|
decomposed = unicodedata.normalize('NFD', ch)
|
|
if decomposed != ch:
|
|
entries.append((cp, decomposed.encode('utf-8')))
|
|
except (ValueError, OverflowError):
|
|
pass
|
|
return entries
|
|
|
|
@staticmethod
|
|
def _write_norm_table(fp, entries):
|
|
import struct
|
|
fp.write(struct.pack('<I', len(entries)))
|
|
for cp, utf8 in entries:
|
|
fp.write(struct.pack('<I', cp))
|
|
fp.write(struct.pack('<H', len(utf8)))
|
|
fp.write(utf8)
|
|
|
|
def export_mtok(self, save_directory, tokenizer_json_path):
|
|
"""Export tokenizer in binary .mtok format (PipelineTokenizer)."""
|
|
import json
|
|
import struct
|
|
|
|
with open(tokenizer_json_path, 'r', encoding='utf-8') as f:
|
|
tj = json.load(f)
|
|
|
|
file_path = os.path.join(save_directory, "tokenizer.mtok")
|
|
MAGIC_NUMBER = 430
|
|
PIPELINE = 4
|
|
|
|
def pack_str(s):
|
|
if isinstance(s, str):
|
|
s = s.encode('utf-8')
|
|
return struct.pack('<H', len(s)) + s
|
|
|
|
with open(file_path, "w", encoding="utf8") as fp:
|
|
# Text header: magic number + type
|
|
fp.write(f'{MAGIC_NUMBER} {PIPELINE}\n')
|
|
|
|
# Special tokens info (same as text format)
|
|
special_list = list(set(self.tokenizer.all_special_ids)) if hasattr(self.tokenizer, 'all_special_ids') else []
|
|
if 'added_tokens' in tj:
|
|
for at in tj['added_tokens']:
|
|
if at.get('special', False) and at.get('id', -1) not in special_list:
|
|
special_list.append(at['id'])
|
|
special_list = [s for s in special_list if s is not None]
|
|
|
|
prefix_list = []
|
|
if hasattr(self.tokenizer, 'get_prefix_tokens'):
|
|
prefix_list = self.tokenizer.get_prefix_tokens()
|
|
if len(prefix_list) == 0:
|
|
try:
|
|
ids = self.tokenizer.encode('A')
|
|
get_txt = self.tokenizer.decode(ids[-1])
|
|
if len(ids) > 1 and get_txt == 'A':
|
|
prefix_list = ids[:-1]
|
|
except:
|
|
pass
|
|
|
|
fp.write(f'{len(special_list)} {len(self.stop_ids)} {len(prefix_list)}\n')
|
|
tokens_line = ' '.join(str(t) for t in (special_list + self.stop_ids + prefix_list))
|
|
fp.write(tokens_line + '\n' if tokens_line else '\n')
|
|
|
|
# Now write binary body
|
|
with open(file_path, "ab") as fp:
|
|
# --- Normalizer ---
|
|
norm = tj.get('normalizer')
|
|
def write_normalizer_bin(fp, norm):
|
|
if norm is None:
|
|
fp.write(struct.pack('<B', 0))
|
|
return
|
|
ntype = norm.get('type', '')
|
|
if ntype in ('NFKC', 'Precompiled', 'NFKD'):
|
|
fp.write(struct.pack('<B', 6))
|
|
self._write_norm_table(fp, self._generate_nfkc_table())
|
|
elif ntype == 'NFC':
|
|
fp.write(struct.pack('<B', 6))
|
|
self._write_norm_table(fp, self._generate_nfc_table())
|
|
elif ntype == 'Prepend':
|
|
fp.write(struct.pack('<B', 2))
|
|
fp.write(pack_str(norm.get('prepend', '')))
|
|
elif ntype == 'Replace':
|
|
fp.write(struct.pack('<B', 3))
|
|
pattern = ''
|
|
if isinstance(norm.get('pattern'), dict):
|
|
pattern = norm['pattern'].get('String', '')
|
|
elif isinstance(norm.get('pattern'), str):
|
|
pattern = norm['pattern']
|
|
fp.write(pack_str(pattern))
|
|
fp.write(pack_str(norm.get('content', '')))
|
|
elif ntype == 'Sequence':
|
|
fp.write(struct.pack('<B', 4))
|
|
normalizers = norm.get('normalizers', [])
|
|
fp.write(struct.pack('<I', len(normalizers)))
|
|
for n in normalizers:
|
|
write_normalizer_bin(fp, n)
|
|
elif ntype == 'BertNormalizer':
|
|
sa = norm.get('strip_accents', False)
|
|
# In HuggingFace, strip_accents=None with lowercase=True means strip accents
|
|
if sa is None and norm.get('lowercase', True):
|
|
sa = True
|
|
strip_accents = int(sa or False)
|
|
if strip_accents:
|
|
fp.write(struct.pack('<B', 7))
|
|
else:
|
|
fp.write(struct.pack('<B', 5))
|
|
fp.write(struct.pack('<BBBB',
|
|
int(norm.get('clean_text', True)),
|
|
int(norm.get('handle_chinese_chars', True)),
|
|
strip_accents,
|
|
int(norm.get('lowercase', True))))
|
|
if strip_accents:
|
|
self._write_norm_table(fp, self._generate_nfd_table())
|
|
elif ntype == 'Lowercase':
|
|
fp.write(struct.pack('<B', 5))
|
|
fp.write(struct.pack('<BBBB', 0, 0, 0, 1))
|
|
elif ntype == 'StripAccents':
|
|
fp.write(struct.pack('<B', 7))
|
|
fp.write(struct.pack('<BBBB', 0, 0, 1, 0))
|
|
self._write_norm_table(fp, self._generate_nfd_table())
|
|
elif ntype == 'Strip':
|
|
fp.write(struct.pack('<B', 8))
|
|
fp.write(struct.pack('<BB',
|
|
int(norm.get('strip_left', True)),
|
|
int(norm.get('strip_right', True))))
|
|
else:
|
|
fp.write(struct.pack('<B', 0))
|
|
write_normalizer_bin(fp, norm)
|
|
|
|
# --- PreTokenizer ---
|
|
pt = tj.get('pre_tokenizer')
|
|
def write_pre_tokenizer_bin(fp, pt):
|
|
if pt is None:
|
|
fp.write(struct.pack('<B', 0))
|
|
return
|
|
ptype = pt.get('type', '')
|
|
if ptype == 'ByteLevel':
|
|
fp.write(struct.pack('<BB', 1, int(pt.get('use_regex', True))))
|
|
elif ptype == 'Digits':
|
|
fp.write(struct.pack('<BB', 2, int(pt.get('individual_digits', False))))
|
|
elif ptype == 'Metaspace':
|
|
fp.write(struct.pack('<B', 3))
|
|
rep = pt.get('replacement', '\u2581')
|
|
if pt.get('str_rep'):
|
|
rep = pt['str_rep']
|
|
fp.write(pack_str(rep))
|
|
fp.write(struct.pack('<B', int(pt.get('add_prefix_space', True))))
|
|
elif ptype == 'Split':
|
|
fp.write(struct.pack('<B', 4))
|
|
pattern = ''
|
|
if isinstance(pt.get('pattern'), dict):
|
|
pattern = pt['pattern'].get('Regex', pt['pattern'].get('String', ''))
|
|
elif isinstance(pt.get('pattern'), str):
|
|
pattern = pt['pattern']
|
|
fp.write(pack_str(pattern))
|
|
behavior = pt.get('behavior', 'Isolated')
|
|
behavior_id = 0 if behavior == 'Isolated' else (2 if behavior == 'MergedWithPrevious' else 1)
|
|
fp.write(struct.pack('<BB', int(pt.get('invert', False)), behavior_id))
|
|
elif ptype == 'BertPreTokenizer':
|
|
fp.write(struct.pack('<B', 5))
|
|
elif ptype == 'Sequence':
|
|
fp.write(struct.pack('<B', 6))
|
|
pretokenizers = pt.get('pretokenizers', [])
|
|
fp.write(struct.pack('<I', len(pretokenizers)))
|
|
for p in pretokenizers:
|
|
write_pre_tokenizer_bin(fp, p)
|
|
elif ptype == 'WhitespaceSplit':
|
|
fp.write(struct.pack('<B', 4))
|
|
fp.write(pack_str('\\s+'))
|
|
fp.write(struct.pack('<BB', 0, 1))
|
|
else:
|
|
fp.write(struct.pack('<B', 0))
|
|
write_pre_tokenizer_bin(fp, pt)
|
|
|
|
# --- Model ---
|
|
model = tj.get('model', {})
|
|
mtype = model.get('type', '')
|
|
if not mtype:
|
|
# Infer model type from fields
|
|
if 'continuing_subword_prefix' in model and 'merges' not in model:
|
|
mtype = 'WordPiece'
|
|
elif isinstance(model.get('vocab'), list):
|
|
mtype = 'Unigram'
|
|
else:
|
|
mtype = 'BPE'
|
|
|
|
if mtype == 'BPE':
|
|
vocab = model.get('vocab', {})
|
|
merges = model.get('merges', [])
|
|
byte_fallback = int(model.get('byte_fallback', False))
|
|
|
|
byte_level = 0
|
|
if pt and pt.get('type') == 'ByteLevel':
|
|
byte_level = 0
|
|
elif pt and pt.get('type') == 'Sequence':
|
|
has_bl_pt = any(p.get('type') == 'ByteLevel' for p in pt.get('pretokenizers', []))
|
|
if not has_bl_pt:
|
|
dec = tj.get('decoder')
|
|
if dec and dec.get('type') == 'ByteLevel':
|
|
byte_level = 1
|
|
elif dec and dec.get('type') == 'Sequence':
|
|
if any(d.get('type') == 'ByteLevel' for d in dec.get('decoders', [])):
|
|
byte_level = 1
|
|
|
|
# Sort vocab by token string for binary search in C++
|
|
sorted_vocab = sorted(vocab.items(), key=lambda x: x[0])
|
|
vocab_size = len(sorted_vocab)
|
|
|
|
fp.write(struct.pack('<B', 0)) # type=BPE
|
|
fp.write(struct.pack('<I', vocab_size))
|
|
fp.write(struct.pack('<BB', byte_fallback, byte_level))
|
|
fp.write(struct.pack('<I', len(merges)))
|
|
|
|
for token, tid in sorted_vocab:
|
|
fp.write(pack_str(token))
|
|
fp.write(struct.pack('<I', tid))
|
|
|
|
# Build merge pairs with rank, sort by merge_key for binary search in C++
|
|
merge_pairs = []
|
|
for i, m in enumerate(merges):
|
|
if isinstance(m, str):
|
|
parts = m.split(' ', 1)
|
|
if len(parts) == 2:
|
|
id1 = vocab.get(parts[0], -1)
|
|
id2 = vocab.get(parts[1], -1)
|
|
merge_pairs.append((id1, id2, i))
|
|
elif isinstance(m, list) and len(m) >= 2:
|
|
id1 = vocab.get(m[0], -1)
|
|
id2 = vocab.get(m[1], -1)
|
|
merge_pairs.append((id1, id2, i))
|
|
# Sort by merge_key = (id1 << 32) | id2
|
|
merge_pairs.sort(key=lambda x: (x[0] << 32) | (x[1] & 0xFFFFFFFF))
|
|
for id1, id2, rank in merge_pairs:
|
|
fp.write(struct.pack('<III', id1, id2, rank))
|
|
|
|
elif mtype == 'WordPiece':
|
|
vocab = model.get('vocab', {})
|
|
unk_token = model.get('unk_token', '[UNK]')
|
|
prefix = model.get('continuing_subword_prefix', '##')
|
|
max_chars = model.get('max_input_chars_per_word', 100)
|
|
sorted_vocab = sorted(vocab.items(), key=lambda x: x[0])
|
|
vocab_size = len(sorted_vocab)
|
|
|
|
fp.write(struct.pack('<B', 1)) # type=WordPiece
|
|
fp.write(struct.pack('<I', vocab_size))
|
|
fp.write(pack_str(unk_token))
|
|
fp.write(pack_str(prefix))
|
|
fp.write(struct.pack('<I', max_chars))
|
|
|
|
for token, tid in sorted_vocab:
|
|
fp.write(pack_str(token))
|
|
fp.write(struct.pack('<I', tid))
|
|
|
|
elif mtype == 'Unigram':
|
|
vocab = model.get('vocab', [])
|
|
unk_id = model.get('unk_id', 0)
|
|
byte_fallback = int(model.get('byte_fallback', False))
|
|
|
|
# Build (token, id, score) and sort by token string
|
|
indexed_vocab = []
|
|
for i, item in enumerate(vocab):
|
|
if isinstance(item, list) and len(item) >= 2:
|
|
indexed_vocab.append((item[0], i, item[1]))
|
|
indexed_vocab.sort(key=lambda x: x[0])
|
|
|
|
fp.write(struct.pack('<B', 2)) # type=Unigram
|
|
fp.write(struct.pack('<I', len(indexed_vocab)))
|
|
fp.write(struct.pack('<I', unk_id))
|
|
fp.write(struct.pack('<B', byte_fallback))
|
|
|
|
for token, tid, score in indexed_vocab:
|
|
fp.write(pack_str(token))
|
|
fp.write(struct.pack('<I', tid))
|
|
fp.write(struct.pack('<d', score))
|
|
|
|
# --- Decoder ---
|
|
dec = tj.get('decoder')
|
|
def write_decoder_bin(fp, dec):
|
|
if dec is None:
|
|
fp.write(struct.pack('<B', 0))
|
|
return
|
|
dtype = dec.get('type', '')
|
|
if dtype == 'ByteLevel':
|
|
fp.write(struct.pack('<B', 0))
|
|
elif dtype == 'ByteFallback':
|
|
fp.write(struct.pack('<B', 1))
|
|
elif dtype == 'Metaspace':
|
|
fp.write(struct.pack('<B', 2))
|
|
fp.write(pack_str(dec.get('replacement', '\u2581')))
|
|
fp.write(struct.pack('<B', int(dec.get('add_prefix_space', True))))
|
|
elif dtype == 'WordPiece':
|
|
fp.write(struct.pack('<B', 3))
|
|
fp.write(pack_str(dec.get('prefix', '##')))
|
|
fp.write(struct.pack('<B', int(dec.get('cleanup', True))))
|
|
elif dtype == 'Fuse':
|
|
fp.write(struct.pack('<B', 4))
|
|
elif dtype == 'Replace':
|
|
fp.write(struct.pack('<B', 5))
|
|
pattern = ''
|
|
if isinstance(dec.get('pattern'), dict):
|
|
pattern = dec['pattern'].get('String', '')
|
|
elif isinstance(dec.get('pattern'), str):
|
|
pattern = dec['pattern']
|
|
fp.write(pack_str(pattern))
|
|
fp.write(pack_str(dec.get('content', '')))
|
|
elif dtype == 'Strip':
|
|
fp.write(struct.pack('<B', 6))
|
|
fp.write(pack_str(dec.get('content', '')))
|
|
fp.write(struct.pack('<II', dec.get('start', 0), dec.get('stop', 0)))
|
|
elif dtype == 'Sequence':
|
|
fp.write(struct.pack('<B', 7))
|
|
decoders = dec.get('decoders', [])
|
|
fp.write(struct.pack('<I', len(decoders)))
|
|
for d in decoders:
|
|
write_decoder_bin(fp, d)
|
|
else:
|
|
fp.write(struct.pack('<B', 0))
|
|
write_decoder_bin(fp, dec)
|
|
|
|
# --- Added Tokens ---
|
|
added_tokens = tj.get('added_tokens', [])
|
|
fp.write(struct.pack('<I', len(added_tokens)))
|
|
for at in added_tokens:
|
|
aid = at.get('id', -1)
|
|
special = int(at.get('special', False))
|
|
lstrip = int(at.get('lstrip', False))
|
|
rstrip = int(at.get('rstrip', False))
|
|
content = at.get('content', '')
|
|
fp.write(struct.pack('<I', aid))
|
|
fp.write(struct.pack('<BBB', special, lstrip, rstrip))
|
|
fp.write(pack_str(content))
|
|
|
|
# --- Chat Template & Flags ---
|
|
chat_template = ''
|
|
eos_token = ''
|
|
bos_token = ''
|
|
flags = 0
|
|
tokenizer_config_path = os.path.join(os.path.dirname(tokenizer_json_path), 'tokenizer_config.json')
|
|
if os.path.exists(tokenizer_config_path):
|
|
with open(tokenizer_config_path, 'r', encoding='utf-8') as tc:
|
|
tc_json = json.load(tc)
|
|
chat_template = tc_json.get('chat_template', '')
|
|
eos = tc_json.get('eos_token', '')
|
|
if isinstance(eos, dict):
|
|
eos_token = eos.get('content', '')
|
|
else:
|
|
eos_token = str(eos) if eos else ''
|
|
bos = tc_json.get('bos_token', '')
|
|
if isinstance(bos, dict):
|
|
bos_token = bos.get('content', '')
|
|
else:
|
|
bos_token = str(bos) if bos else ''
|
|
if tc_json.get('clean_up_tokenization_spaces', False) is True:
|
|
flags |= 0x01
|
|
tpl_bytes = chat_template.encode('utf-8') if chat_template else b''
|
|
eos_bytes = eos_token.encode('utf-8') if eos_token else b''
|
|
fp.write(struct.pack('<I', len(tpl_bytes)))
|
|
fp.write(tpl_bytes)
|
|
fp.write(struct.pack('<H', len(eos_bytes)))
|
|
fp.write(eos_bytes)
|
|
|
|
# --- Flags ---
|
|
fp.write(struct.pack('<B', flags))
|
|
|
|
# --- BOS token ---
|
|
bos_bytes = bos_token.encode('utf-8') if bos_token else b''
|
|
fp.write(struct.pack('<H', len(bos_bytes)))
|
|
fp.write(bos_bytes)
|
|
|
|
return file_path
|
|
|
|
def export(self, save_directory, model_path=None, model_type=None):
|
|
"""
|
|
Export tokenizer to MNN format with comprehensive tokenizer type support.
|
|
|
|
Args:
|
|
save_directory: Directory to save the exported tokenizer
|
|
model_path: Optional model path for tokenizer file discovery
|
|
model_type: Optional model type for special handling
|
|
|
|
Returns:
|
|
str: Path to the exported tokenizer file
|
|
"""
|
|
import os
|
|
import base64
|
|
|
|
# Use provided values or fall back to instance values
|
|
if model_path is None:
|
|
model_path = self.tokenizer_path
|
|
if model_type is None:
|
|
model_type = self.model_type
|
|
|
|
# Create directory if it doesn't exist
|
|
os.makedirs(save_directory, exist_ok=True)
|
|
|
|
# Try .mtok format first (pipeline tokenizer) if tokenizer.json exists
|
|
tokenizer_json_path = os.path.join(model_path, 'tokenizer.json')
|
|
if os.path.exists(tokenizer_json_path):
|
|
result = self.export_mtok(save_directory, tokenizer_json_path)
|
|
if result:
|
|
return result
|
|
|
|
# TOKENIZER MAGIC NUMBER
|
|
MAGIC_NUMBER = 430
|
|
# TOKENIZER TYPE
|
|
SENTENCEPIECE = 0; TIKTOIKEN = 1; BERT = 2; HUGGINGFACE = 3
|
|
|
|
def write_line(fp, *args):
|
|
for arg in args:
|
|
for token in arg:
|
|
fp.write(str(token) + ' ')
|
|
fp.write('\n')
|
|
|
|
def write_header(fp, type, speicals, prefix=[]):
|
|
fp.write(f'{MAGIC_NUMBER} {type}\n')
|
|
fp.write(f'{len(speicals)} {len(self.stop_ids)} {len(prefix)}\n')
|
|
write_line(fp, speicals, self.stop_ids, prefix)
|
|
|
|
file_path = os.path.join(save_directory, "tokenizer.txt")
|
|
|
|
# Collect special tokens from various sources
|
|
special_list = list(self.tokenizer.added_tokens_decoder.keys())
|
|
if hasattr(self.tokenizer, 'special_tokens'):
|
|
for k, v in self.tokenizer.special_tokens.items():
|
|
special_list.append(v)
|
|
if hasattr(self.tokenizer, 'all_special_ids'):
|
|
special_list.extend(self.tokenizer.all_special_ids)
|
|
if hasattr(self.tokenizer, 'gmask_token_id'):
|
|
special_list.append(self.tokenizer.gmask_token_id)
|
|
|
|
# Handle generation_config special tokens
|
|
if hasattr(self.tokenizer, 'generation_config') and self.tokenizer.generation_config is not None:
|
|
generation_config = self.tokenizer.generation_config
|
|
if hasattr(generation_config, 'user_token_id'):
|
|
special_list.append(generation_config.user_token_id)
|
|
if hasattr(generation_config, 'assistant_token_id'):
|
|
special_list.append(generation_config.assistant_token_id)
|
|
|
|
vocab_list = []
|
|
prefix_list = []
|
|
|
|
# Get prefix tokens
|
|
if hasattr(self.tokenizer, 'get_prefix_tokens'):
|
|
prefix_list = self.tokenizer.get_prefix_tokens()
|
|
|
|
# Simple prefix token detection
|
|
if len(prefix_list) == 0:
|
|
try:
|
|
test_txt = 'A'
|
|
ids = self.tokenizer.encode(test_txt)
|
|
get_txt = self.tokenizer.decode(ids[-1])
|
|
if len(ids) > 1 and get_txt == test_txt:
|
|
prefix_list += ids[:-1]
|
|
except Exception:
|
|
pass
|
|
|
|
# Load SentencePiece model if available
|
|
sp_model = None
|
|
tokenizer_model = os.path.join(model_path, 'tokenizer.model')
|
|
ice_text_model = os.path.join(model_path, 'ice_text.model')
|
|
|
|
try:
|
|
import sentencepiece as spm
|
|
if os.path.exists(tokenizer_model):
|
|
sp_model = spm.SentencePieceProcessor(tokenizer_model)
|
|
elif os.path.exists(ice_text_model):
|
|
sp_model = spm.SentencePieceProcessor(ice_text_model)
|
|
except Exception:
|
|
sp_model = None
|
|
|
|
# Check for merge file (BERT/HuggingFace tokenizers)
|
|
merge_file = os.path.join(model_path, 'merges.txt')
|
|
merge_txt = merge_file if os.path.exists(merge_file) else None
|
|
|
|
if sp_model is not None:
|
|
# SentencePiece tokenizer export
|
|
NORMAL = 1; UNKNOWN = 2; CONTROL = 3
|
|
USER_DEFINED = 4; UNUSED = 5; BYTE = 6
|
|
|
|
for i in range(sp_model.GetPieceSize()):
|
|
token = sp_model.IdToPiece(i)
|
|
score = sp_model.GetScore(i)
|
|
token_type = NORMAL
|
|
if sp_model.IsUnknown(i):
|
|
token_type = UNKNOWN
|
|
elif sp_model.IsControl(i):
|
|
token_type = CONTROL
|
|
elif sp_model.IsUnused(i):
|
|
token_type = UNUSED
|
|
elif sp_model.IsByte(i):
|
|
token_type = BYTE
|
|
|
|
# Handle special cases for specific models
|
|
if model_path == 'Chatglm_6b':
|
|
if '<n>' in token: token = '\n'
|
|
if '<|tab|>' in token: token = '\t'
|
|
if '<|blank_' in token: token = ' ' * int(token[8:token.find('|>')])
|
|
if '▁' in token: token = token.replace('▁', ' ')
|
|
|
|
token_encode = base64.b64encode(token.encode("utf-8")).decode("utf8")
|
|
vocab_list.append(f'{token_encode} {score} {token_type}\n')
|
|
|
|
# Add special tokens to vocab_list
|
|
for index in special_list:
|
|
if index >= len(vocab_list):
|
|
try:
|
|
token = self.tokenizer.decode(index)
|
|
token_encode = base64.b64encode(token.encode("utf-8")).decode("utf8")
|
|
vocab_list.append(f'{token_encode} {0} {NORMAL}\n')
|
|
except:
|
|
pass
|
|
|
|
# Write SentencePiece format
|
|
with open(file_path, "w", encoding="utf8") as fp:
|
|
write_header(fp, SENTENCEPIECE, special_list, prefix_list)
|
|
if model_type == "gemma3" or model_type == "gemma3-text":
|
|
fp.write(f'{len(vocab_list) + 1}\n') # +1 for image_soft_token
|
|
else:
|
|
fp.write(f'{len(vocab_list)}\n')
|
|
for vocab in vocab_list:
|
|
fp.write(vocab)
|
|
|
|
elif hasattr(self.tokenizer, 'mergeable_ranks'):
|
|
# TikToken tokenizer export
|
|
vocab_list = []
|
|
for k, v in self.tokenizer.mergeable_ranks.items():
|
|
line = base64.b64encode(k).decode("utf8") + "\n"
|
|
vocab_list.append(line)
|
|
if hasattr(self.tokenizer, 'special_tokens'):
|
|
for k, v in self.tokenizer.special_tokens.items():
|
|
line = base64.b64encode(k.encode("utf-8")).decode("utf8") + "\n"
|
|
vocab_list.append(line)
|
|
if hasattr(self.tokenizer, 'added_tokens_decoder'):
|
|
for k, v in self.tokenizer.added_tokens_decoder.items():
|
|
line = base64.b64encode(v.__str__().encode("utf-8")).decode("utf8") + "\n"
|
|
vocab_list.append(line)
|
|
|
|
# Write TikToken format
|
|
with open(file_path, "w", encoding="utf8") as fp:
|
|
write_header(fp, TIKTOIKEN, special_list, prefix_list)
|
|
fp.write(f'{len(vocab_list)}\n')
|
|
for vocab in vocab_list:
|
|
fp.write(vocab)
|
|
|
|
elif merge_txt is not None:
|
|
# HuggingFace/BERT tokenizer export
|
|
merge_list = []
|
|
vocab = self.tokenizer.get_vocab()
|
|
special_list = list(self.tokenizer.added_tokens_decoder.keys())
|
|
vocab_list = ['<unk>' for i in range(len(vocab))]
|
|
|
|
# Load vocab
|
|
for k, v in vocab.items():
|
|
vocab_list[int(v)] = k
|
|
|
|
# Load merge
|
|
with open(merge_txt, 'rt') as merge:
|
|
for line in merge.readlines():
|
|
merge_list.append(line)
|
|
|
|
# Write HuggingFace format
|
|
with open(file_path, "w", encoding="utf8") as fp:
|
|
write_header(fp, HUGGINGFACE, special_list)
|
|
fp.write(f'{len(vocab_list)} {len(merge_list)}\n')
|
|
for v in vocab_list:
|
|
fp.write(v + '\n')
|
|
for m in merge_list:
|
|
fp.write(m)
|
|
else:
|
|
# Auto-detect tokenizer type and export
|
|
tokenizer_class_name = type(self.tokenizer).__name__.lower()
|
|
vocab = self.tokenizer.get_vocab()
|
|
|
|
# Check for SentencePiece-based tokenizers
|
|
if ('xlmroberta' in tokenizer_class_name or
|
|
'roberta' in tokenizer_class_name or
|
|
'sentencepiece' in tokenizer_class_name or
|
|
hasattr(self.tokenizer, 'sp_model') or
|
|
(hasattr(self.tokenizer, 'vocab_file') and
|
|
self.tokenizer.vocab_file and 'sentencepiece' in self.tokenizer.vocab_file.lower()) or
|
|
# Check for SentencePiece patterns (▁ prefix)
|
|
(len(vocab) > 0 and any('▁' in token for token in list(vocab.keys())[:100]))):
|
|
tokenizer_type = SENTENCEPIECE
|
|
print(f"Detected SentencePiece-based tokenizer: {tokenizer_class_name}")
|
|
elif 'bert' in tokenizer_class_name:
|
|
tokenizer_type = BERT
|
|
print(f"Detected BERT tokenizer: {tokenizer_class_name}")
|
|
else:
|
|
tokenizer_type = TIKTOIKEN
|
|
print(f"Detected TikToken tokenizer: {tokenizer_class_name}")
|
|
|
|
vocab = self.tokenizer.get_vocab()
|
|
|
|
if tokenizer_type == SENTENCEPIECE:
|
|
# Handle SentencePiece tokenizer
|
|
vocab_list = []
|
|
NORMAL = 1
|
|
|
|
for token, token_id in sorted(vocab.items(), key=lambda x: x[1]):
|
|
try:
|
|
token_bytes = token.encode('utf-8')
|
|
token_b64 = base64.b64encode(token_bytes).decode('utf-8')
|
|
vocab_list.append(f'{token_b64} 0.0 {NORMAL}\n')
|
|
except Exception as e:
|
|
print(f"Warning: Failed to encode SentencePiece token '{token}': {e}")
|
|
token_b64 = base64.b64encode('▁'.encode('utf-8')).decode('utf-8')
|
|
vocab_list.append(f'{token_b64} 0.0 {NORMAL}\n')
|
|
|
|
with open(file_path, "w", encoding="utf8") as fp:
|
|
write_header(fp, SENTENCEPIECE, special_list, prefix_list)
|
|
fp.write(f'{len(vocab_list)}\n')
|
|
for vocab_line in vocab_list:
|
|
fp.write(vocab_line)
|
|
else:
|
|
# Handle BERT or TikToken tokenizer
|
|
def unicode_to_byte(u: int):
|
|
# Handle special unicode mappings for BERT tokenizers
|
|
if u >= 256 and u <= 288:
|
|
return u - 256
|
|
if u >= 289 and u <= 322:
|
|
return u - 162
|
|
if u == 323:
|
|
return 173
|
|
return u
|
|
|
|
vocab_list = ['<unk>' for i in range(len(vocab))]
|
|
|
|
for k, v in vocab.items():
|
|
if tokenizer_type == BERT:
|
|
try:
|
|
vocab_list[int(v)] = k.encode('utf-8')
|
|
except Exception as e:
|
|
try:
|
|
vocab_list[int(v)] = bytes([unicode_to_byte(ord(c)) for c in k])
|
|
except Exception as e2:
|
|
print(f"Warning: Failed to encode token '{k}' with id {v}: {e2}")
|
|
vocab_list[int(v)] = k.encode('utf-8', errors='replace')
|
|
else:
|
|
try:
|
|
vocab_list[int(v)] = bytes([unicode_to_byte(ord(c)) for c in k])
|
|
except Exception as e2:
|
|
print(f"Warning: Failed to encode token '{k}' with id {v}: {e2}")
|
|
vocab_list[int(v)] = k.encode('utf-8', errors='replace')
|
|
|
|
with open(file_path, "w", encoding="utf8") as fp:
|
|
write_header(fp, tokenizer_type, special_list)
|
|
fp.write(f'{len(vocab_list)}\n')
|
|
for v in vocab_list:
|
|
line = base64.b64encode(v).decode("utf8") + "\n"
|
|
fp.write(line)
|
|
|
|
return file_path |