-
Notifications
You must be signed in to change notification settings - Fork 2.3k
tokenizer
MNN LLM 的 Tokenizer 负责文本编码(encode)和解码(decode),是 LLM 推理的核心组件。当前支持两种文件格式:
-
.mtok二进制格式(推荐):Pipeline Tokenizer,高性能二进制格式 -
.txt文本格式(旧版):兼容早期导出的模型
.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) │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
前 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 零拷贝引用
文本归一化器,对输入文本进行预处理(大小写、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 归一化表
.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),对每个码点执行归一化,仅记录归一化结果与原始字符不同的条目:
# 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):
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功能,分解后去除组合标记(categoryMn)实现去变音符号
预分词器,将输入文本拆分为子串,再分别送入 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]
核心分词模型,支持三种算法:
[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
[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
[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
解码器,将 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]
模型额外添加的特殊 token(如 <|im_start|>、<tool_call> 等)。
[u32 count]
For each token:
[u32 id]
[u8 special] // 是否为特殊 token
[u8 lstrip] // 匹配时是否去左空格
[u8 rstrip] // 匹配时是否去右空格
[str content] // token 文本
内嵌 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 文本
MNN Tokenizer 内置了 Jinja2 模板引擎,用于将多轮对话格式化为模型所需的 prompt 格式。
| 格式 | 模板来源 |
|---|---|
.mtok |
内嵌在文件尾部,从 tokenizer_config.json 中提取并写入 |
.txt |
从 llm_config.json 的 jinja.chat_template 字段读取 |
// 简单对话
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 核心语法子集:
{# 变量 #}
{{ 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) |
旧版采用纯文本格式,以 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) │
│ ... │
└─────────────────────────────────────┘
适用于 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
...
适用于 Qwen、GPT-4 等使用 Tiktoken 的模型。
Line 4: "{vocab_size}"
Lines 5 ~ 4+vocab_size:
"{base64_token}"
按行序排列,行号即 token ID
适用于 BERT、MiniLM 等模型。
Line 4: "{vocab_size}"
Lines 5 ~ 4+vocab_size:
"{base64_token}"
按行序排列,行号即 token ID
使用 WordPiece 算法编码,"##" 前缀表示子词
适用于使用 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<string, int> |
| 词表查找 | 二分查找 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 |
# 导出模型(自动选择格式,优先 .mtok)
cd transformers/llm/export
python llmexport.py --path /path/to/model --export mnn
# 产物:
# 有 tokenizer.json → 导出 tokenizer.mtok
# 无 tokenizer.json → 导出 tokenizer.txt