# Tokenizer MNN LLM 的 Tokenizer 负责文本编码(encode)和解码(decode),是 LLM 推理的核心组件。当前支持两种文件格式: - **`.mtok` 二进制格式**(推荐):Pipeline Tokenizer,高性能二进制格式 - **`.txt` 文本格式**(旧版):兼容早期导出的模型 ## .mtok 文件格式 `.mtok` 是 MNN 自定义的二进制 Tokenizer 格式,采用「Python 导出时预计算 + C++ 加载时零拷贝」的设计,具有加载快、编码效率高、无外部依赖等优点。 ### 文件结构总览 ``` ┌─────────────────────────────────────┐ │ Text Header │ ← 文本行,与旧格式兼容 │ Line 1: "430 4\n" │ │ Line 2: "special stop prefix\n" │ │ Line 3: "id1 id2 ... \n" │ ├─────────────────────────────────────┤ │ Binary Body │ ← 二进制数据,顺序读取 │ ┌─────────────────────────────┐ │ │ │ Normalizer │ │ │ │ (可含 NFKC/NFD 归一化表) │ │ │ ├─────────────────────────────┤ │ │ │ PreTokenizer │ │ │ ├─────────────────────────────┤ │ │ │ Model (BPE/WP/Unigram) │ │ │ ├─────────────────────────────┤ │ │ │ Decoder │ │ │ ├─────────────────────────────┤ │ │ │ Added Tokens │ │ │ ├─────────────────────────────┤ │ │ │ Chat Template (optional) │ │ │ ├─────────────────────────────┤ │ │ │ EOS Token (optional) │ │ │ ├─────────────────────────────┤ │ │ │ Flags (optional) │ │ │ ├─────────────────────────────┤ │ │ │ BOS Token (optional) │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘ ``` ### Text Header 前 3 行为文本格式,与旧版 `.txt` 格式的 Header 结构相同: | 行 | 内容 | 说明 | |----|------|------| | 1 | `430 4` | magic number (430) + tokenizer type (4=PIPELINE) | | 2 | `{special_num} {stop_num} {prefix_num}` | 各类特殊 token 的数量 | | 3 | `{id1} {id2} ...` | 所有特殊、stop、prefix token 的 ID | ### 基础二进制类型 所有整数均为**小端序 (little-endian)**,字符串格式为 `[uint16 len][bytes]`: ``` u8 = 1 字节无符号整数 u16 = 2 字节无符号整数 (little-endian) u32 = 4 字节无符号整数 (little-endian) f64 = 8 字节双精度浮点 (IEEE 754) str = [u16 len][len bytes] // 变长字符串 str_ref = [u16 len][len bytes] // 同 str,C++ 端用 StringRef 零拷贝引用 ``` ### Normalizer 文本归一化器,对输入文本进行预处理(大小写、Unicode 归一化等)。 ``` [u8 type] 0 = None // 无归一化 1 = NFKC // NFKC 归一化(无内嵌表,使用旧路径) 2 = Prepend // 前缀添加 [str prepend_str] 3 = Replace // 字符串替换 [str pattern] [str content] 4 = Sequence // 多个归一化器组合 [u32 count] [Normalizer × count] 5 = BertNormalizer // BERT 风格归一化 [u8 clean] [u8 handle_chinese] [u8 strip_accents] [u8 lowercase] 6 = NFKC+Table // NFKC 归一化(内嵌预计算表) [NormTable] 7 = BertNormalizer+NFD // BERT + NFD 归一化 [u8 clean] [u8 handle_chinese] [u8 strip_accents] [u8 lowercase] if strip_accents: [NormTable] // NFD 归一化表 ``` #### Unicode 归一化表 (NormTable) `.mtok` 将 Unicode 归一化所需的映射表直接内嵌到文件中,使得 C++ 端无需依赖 ICU 等外部 Unicode 库。 **表结构:** ``` [u32 count] // 条目数量 For each entry: [u32 codepoint] // 原始 Unicode 码点 [u16 utf8_len] // 归一化后的 UTF-8 字节长度 [utf8_len bytes] // 归一化后的 UTF-8 字节序列 ``` **生成方式(Python 导出时预计算):** 导出时遍历整个 Unicode 码点空间(U+0000 ~ U+10FFFF),对每个码点执行归一化,仅记录归一化结果与原始字符不同的条目: ```python # NFKC 表:用于 SentencePiece 等模型的归一化 for cp in range(0x110000): ch = chr(cp) normalized = unicodedata.normalize('NFKC', ch) if normalized != ch: entries.append((cp, normalized.encode('utf-8'))) # NFD 表:用于 BERT 的 strip_accents(去除变音符号) for cp in range(0x110000): ch = chr(cp) decomposed = unicodedata.normalize('NFD', ch) if decomposed != ch: entries.append((cp, decomposed.encode('utf-8'))) ``` **C++ 端查找(二分查找):** 表按 codepoint 升序排列,C++ 加载后通过二分查找完成归一化,单次查找 O(log n): ```cpp size_t lo = 0, hi = table_.size(); while (lo < hi) { size_t mid = (lo + hi) / 2; if ((uint32_t)cp > table_[mid].first) lo = mid + 1; else if ((uint32_t)cp < table_[mid].first) hi = mid; else { /* 命中,返回归一化结果 */ break; } } ``` **典型场景:** - **NFKC 表** (type=6):将兼容字符映射到标准形式,如 `fi` → `fi`,`①` → `1`。用于 SentencePiece (Llama/Gemma) 等模型 - **NFD 表** (type=7):将字符分解为基字符 + 组合字符,如 `é` → `e` + `◌́`。用于 BERT 的 `strip_accents` 功能,分解后去除组合标记(category `Mn`)实现去变音符号 ### PreTokenizer 预分词器,将输入文本拆分为子串,再分别送入 Model 进行编码。 ``` [u8 type] 0 = None 1 = ByteLevel // GPT-2 / Llama3 风格 [u8 use_regex] // 是否使用正则/scanner 拆分 2 = Digits // 数字拆分 [u8 individual_digits] // 是否逐位拆分 3 = Metaspace // SentencePiece 风格 [str replacement] [u8 add_prefix_space] 4 = Split // 自定义正则拆分 [str pattern] [u8 invert] [u8 behavior] 5 = BertPreTokenizer // BERT 风格(空格+标点拆分) 6 = Sequence // 多个预分词器组合 [u32 count] [PreTokenizer × count] ``` ### Model 核心分词模型,支持三种算法: #### BPE (type=0) ``` [u8 type=0] [u32 vocab_size] [u8 byte_fallback] // 是否支持 byte fallback [u8 byte_level] // 是否为 byte-level BPE [u32 merge_size] // merge 规则数量 // 词表(按 token 字符串字典序预排序,支持二分查找) [str_ref token, u32 id] × vocab_size // Merge 规则(按 key=(id1<<32)|id2 预排序,支持二分查找) [u32 id1, u32 id2, u32 rank] × merge_size ``` #### WordPiece (type=1) ``` [u8 type=1] [u32 vocab_size] [str unk_token] // 未知 token,如 "[UNK]" [str continuing_subword_prefix] // 子词前缀,如 "##" [u32 max_chars] // 单词最大字符数 // 词表(按 token 字符串字典序预排序) [str_ref token, u32 id] × vocab_size ``` #### Unigram (type=2) ``` [u8 type=2] [u32 vocab_size] [u32 unk_id] // 未知 token ID [u8 byte_fallback] // 是否支持 byte fallback // 词表(按 token 字符串字典序预排序) [str_ref token, u32 id, f64 score] × vocab_size ``` ### Decoder 解码器,将 token ID 序列还原为文本。 ``` [u8 type] 0 = ByteLevel // byte → unicode 反映射 1 = ByteFallback // byte token 还原 2 = Metaspace // '▁' → 空格 [str replacement] [u8 add_prefix_space] 3 = WordPiece // 移除 "##" 前缀 [str prefix] [u8 cleanup] 4 = Fuse // 拼接所有 token 5 = Replace // 字符串替换 [str pattern] [str content] 6 = Strip // 去除首尾字符 [str content] [u32 start] [u32 stop] 7 = Sequence // 多个解码器组合 [u32 count] [Decoder × count] ``` ### Added Tokens 模型额外添加的特殊 token(如 `<|im_start|>`、`` 等)。 ``` [u32 count] For each token: [u32 id] [u8 special] // 是否为特殊 token [u8 lstrip] // 匹配时是否去左空格 [u8 rstrip] // 匹配时是否去右空格 [str content] // token 文本 ``` ### Chat Template(可选) 内嵌 Jinja2 chat template,加载后无需额外读取 `tokenizer_config.json`。 ``` [u32 tpl_len] [tpl_len bytes] // Jinja2 模板 UTF-8 文本 [u16 eos_len] [eos_len bytes] // EOS token 文本 [u8 flags] // bit0: clean_up_tokenization_spaces [u16 bos_len] [bos_len bytes] // BOS token 文本 ``` ## Chat Template 支持 MNN Tokenizer 内置了 Jinja2 模板引擎,用于将多轮对话格式化为模型所需的 prompt 格式。 ### 模板来源 | 格式 | 模板来源 | |------|---------| | `.mtok` | 内嵌在文件尾部,从 `tokenizer_config.json` 中提取并写入 | | `.txt` | 从 `llm_config.json` 的 `jinja.chat_template` 字段读取 | ### 使用方式 ```cpp // 简单对话 std::string prompt = tokenizer->apply_chat_template("你好"); // 多轮对话 ChatMessages messages = { {"system", "You are a helpful assistant."}, {"user", "Hello!"}, {"assistant", "Hi there!"}, {"user", "What's the weather?"} }; std::string prompt = tokenizer->apply_chat_template(messages); ``` ### 模板语法 支持 Jinja2 核心语法子集: ```jinja {# 变量 #} {{ messages }} {{ eos_token }} {{ bos_token }} {# 控制流 #} {% for message in messages %} {% if message.role == 'user' %} <|im_start|>user\n{{ message.content }}<|im_end|>\n {% endif %} {% endfor %} {% if add_generation_prompt %} <|im_start|>assistant\n {% endif %} {# 过滤器 #} {{ value | tojson }} {{ value | trim }} {{ value | length }} {{ value | upper }} {# 循环变量 #} {{ loop.index }} {# 1-based 序号 #} {{ loop.first }} {# 是否第一次迭代 #} {{ loop.last }} {# 是否最后一次迭代 #} ``` ### 模板上下文变量 | 变量 | 类型 | 说明 | |------|------|------| | `messages` | array | 消息数组,每项包含 `role` 和 `content` | | `add_generation_prompt` | bool | 是否在末尾添加 assistant 提示 | | `eos_token` | string | EOS 特殊 token 文本 | | `bos_token` | string | BOS 特殊 token 文本 | | `tools` | array | 工具定义(可选,用于 function calling) | --- ## 旧版 tokenizer.txt 文件格式 旧版采用纯文本格式,以 base64 编码存储 token。支持 4 种 tokenizer 类型。 ### 文件结构 ``` ┌─────────────────────────────────────┐ │ Line 1: "430 {type}" │ ← type: 0/1/2/3 │ Line 2: "{special} {stop} {pfx}" │ │ Line 3: "id1 id2 ..." │ ├─────────────────────────────────────┤ │ Vocab Section (type-specific) │ │ ... │ └─────────────────────────────────────┘ ``` ### Type 0: SENTENCEPIECE 适用于 Llama、Baichuan、ChatGLM 等使用 SentencePiece 的模型。 ``` Line 4: "{vocab_size}" Lines 5 ~ 4+vocab_size: "{base64_token} {score} {type}" score: float,Unigram 概率分数 type: 1=NORMAL, 2=UNKNOWN, 3=CONTROL, 4=USER_DEFINED, 5=UNUSED, 6=BYTE ``` 示例: ``` 430 0 5 2 1 0 1 2 3 4 100 101 151643 32000 PHM+ -1000 3 PA== -1000 2 PGJ5dGVfMHgwMD4= 0 6 ... ``` ### Type 1: TIKTOKEN 适用于 Qwen、GPT-4 等使用 Tiktoken 的模型。 ``` Line 4: "{vocab_size}" Lines 5 ~ 4+vocab_size: "{base64_token}" 按行序排列,行号即 token ID ``` ### Type 2: BERT 适用于 BERT、MiniLM 等模型。 ``` Line 4: "{vocab_size}" Lines 5 ~ 4+vocab_size: "{base64_token}" 按行序排列,行号即 token ID 使用 WordPiece 算法编码,"##" 前缀表示子词 ``` ### Type 3: HUGGINGFACE 适用于使用 HuggingFace Tokenizers 库的模型(BPE 算法)。 ``` Line 4: "{vocab_size} {merge_size}" Lines 5 ~ 4+vocab_size: "{token}" // 未经 base64 编码的原始 token Lines 5+vocab_size ~ 4+vocab_size+merge_size: "{token1} {token2}" // BPE merge 规则,按优先级排列 ``` ### 类型对比 | 特性 | `.mtok` (type=4) | `.txt` (type=0-3) | |------|-------------------|-------------------| | 文件格式 | 二进制 | 纯文本 | | 词表存储 | 预排序数组 + 零拷贝 | `unordered_map` | | 词表查找 | 二分查找 O(log n) | 哈希表 O(1)~O(n) | | Merge 规则 | 预计算 uint64 key + 二分 | wstring pair 哈希表 | | 预分词 | 手写 Unicode scanner | `std::regex` | | Unicode 归一化 | 预计算表内嵌文件,无外部依赖 | 运行时依赖 Unicode 库或不支持 | | Chat Template | 内嵌文件 | 需从 config 读取 | | 文件 I/O | 单次读取,零拷贝 | 逐行解析 | | 字符串分配 | StringRef 指向 buffer | 每个 token 分配 string | ### 导出方式 ```bash # 导出模型(自动选择格式,优先 .mtok) cd transformers/llm/export python llmexport.py --path /path/to/model --export mnn # 产物: # 有 tokenizer.json → 导出 tokenizer.mtok # 无 tokenizer.json → 导出 tokenizer.txt ```