""" Wrapper around piece_tokenizer that provides a HuggingFace-like interface. Used by eval.py and finetune_muon.py. """ import os import json import piece_tokenizer as pt # 词表和中文分词词典的文件名,**按优先级排列**。 # # 首选带出处的名字(与 ../Summer/ 的产出同名)—— 这样从任何一个 checkpoint # 目录就能看出词表出自哪里。sha256 核过:Summer-Tokenizer.pt 与旧名 # piece.model 逐字节相同,dict.txt 同理,所以改名不改内容。 # # **旧名必须保留为回退**:已发布的 Ismantic/Qwen3-1.7B-Base-ReTok 和 # Ismantic/Interpreter-Qwen3-1.7B-ReTok 里都是旧名,现存 checkpoint 也是。 # 一刀切改名会让它们全部加载失败,而失败方式是「找不到就回退到别处的分词器」 # —— 正是这个仓库一路在修的那类静默错误。piece_mt.model 是更早的 HYMT 遗留。 VOCAB_NAMES = ["Summer-Tokenizer.pt", "piece.model", "piece_mt.model"] DICT_NAMES = ["Summer-Tokenizer.dict.txt", "dict.txt"] def _resolve(model_dir, names, what): for n in names: p = os.path.join(model_dir, n) if os.path.exists(p): return p raise FileNotFoundError( f"{model_dir} 里找不到{what},试过 {names}\n" f"checkpoint 目录必须自包含 —— 检查 src/artifacts.py 的五件套" f"是否都拷过来了。\n" f"注意中文词典**不是可选项**:少了它中文 token id 会变,而 round-trip " f"照样正确,从输出完全看不出来。") class PieceTokenizerWrapper: def __init__(self, model_dir): """Load tokenizer from a model directory containing piece.model and token_mapping.json.""" self._tok = pt.Tokenizer() model_file = _resolve(model_dir, VOCAB_NAMES, "piece 词表") # 中文分词词典是**必需的,不是可选的**。 # # 这里原来的注释写的是「可选,少了只是 O(n^2) 慢」——实测是错的: # 少了它中文的 token id 会**变**。同一句话 # # 带词典 … 另一(5066) 种(1066) 语言(5028) … # 无词典 … 另(3406) 一种(2612) 语言(5028) … # # 长度一样、**两边 round-trip 解码都正确**,所以从输出完全看不出来 —— # 模型只是悄悄吃到了与训练时不同的 id。这正是「改错了不报错」那一类。 # Summer 那边踩过同一个坑,它的 prepare/tokenizer.py 也是直接报错。 cn_dict = _resolve(model_dir, DICT_NAMES, "中文分词词典") self._tok.load(model_file, cn_dict) # Load token mapping mapping_file = os.path.join(model_dir, "token_mapping.json") if os.path.exists(mapping_file): with open(mapping_file) as f: mapping = json.load(f) self.pad_token_id = mapping["pad_id"] self.bos_token_id = mapping["bos_id"] self.eos_token_id = mapping["eos_id"] self.user_token_id = mapping.get("user_id") self.assistant_token_id = mapping.get("assistant_id") self.system_token_id = mapping.get("system_id") else: # Fallback to piece_to_id lookups self.bos_token_id = self._tok.piece_to_id("") self.eos_token_id = self._tok.piece_to_id("") self.pad_token_id = self._tok.piece_to_id("") self.user_token_id = self._tok.piece_to_id("") self.assistant_token_id = self._tok.piece_to_id("") self.system_token_id = self._tok.piece_to_id("") if self.pad_token_id < 0: self.pad_token_id = 0 @property def vocab_size(self): return self._tok.vocab_size() def encode(self, text, add_special_tokens=False): ids = self._tok.encode_as_ids(text) if add_special_tokens: ids = [self.bos_token_id] + ids + [self.eos_token_id] return ids def decode(self, ids, skip_special_tokens=True): if skip_special_tokens: special = {self.bos_token_id, self.eos_token_id, self.pad_token_id, self.user_token_id, self.assistant_token_id, self.system_token_id} ids = [i for i in ids if i not in special] try: return self._tok.decode(ids) except UnicodeDecodeError: # Model emitted byte-fallback piece(s) that don't form valid UTF-8. # Per-piece fallback: keep ids that decode cleanly, drop the rest. parts = [] for i in ids: try: parts.append(self._tok.id_to_piece(i)) except UnicodeDecodeError: continue return "".join(parts).replace("▁", " ") def apply_chat_template(self, messages, tokenize=True, add_generation_prompt=False, **kwargs): """Build chat-formatted token sequence from messages.""" ids = [] # Check for system message start = 0 if messages and messages[0]["role"] == "system": ids.append(self.bos_token_id) ids.extend(self._tok.encode_as_ids(messages[0]["content"])) ids.append(self.system_token_id) start = 1 else: ids.append(self.bos_token_id) for msg in messages[start:]: if msg["role"] == "user": ids.append(self.user_token_id) ids.extend(self._tok.encode_as_ids(msg["content"])) elif msg["role"] == "assistant": ids.append(self.assistant_token_id) ids.extend(self._tok.encode_as_ids(msg["content"])) ids.append(self.eos_token_id) if add_generation_prompt: ids.append(self.assistant_token_id) if tokenize: return ids else: # Return as string (rarely needed) return self._tok.decode(ids) def save_pretrained(self, output_dir): """Save tokenizer files to directory (for checkpoint saving).""" import shutil os.makedirs(output_dir, exist_ok=True) # Copy piece.model src = os.path.join(os.path.dirname(output_dir), "piece.model") if os.path.exists(src): shutil.copy2(src, os.path.join(output_dir, "piece.model")) # Save mapping mapping = { "bos_id": self.bos_token_id, "eos_id": self.eos_token_id, "pad_id": self.pad_token_id, "user_id": self.user_token_id, "assistant_id": self.assistant_token_id, "system_id": self.system_token_id, } with open(os.path.join(output_dir, "token_mapping.json"), "w") as f: json.dump(mapping, f, indent=2)