ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Day 18·2 tokenizer.json → vocab.bin——build_vocab_bin.py 在干嘛

Day 18·2 tokenizer.json → vocab.bin——build_vocab_bin.py 在干嘛 真机实测通过本文实验已在 RK3588 板端实测完成2026-09方法学与原始记录见仓库 docs 与《实验脚本》目录一句话导读build_vocab_bin.py 词表编译把 7MB 的 tokenizer.json 编成 1,950,864 字节的 vocab.bindecode_token_str 字节解码闸门收拾 0xAD 等三处历史坑板端重跑逐位一致可审计。18-1 提到引擎运行时加载的是vocab.bin1,950,864 字节而不是 7MB 的tokenizer.json。这一篇拆开tools/build_vocab_bin.py它把 151,643 条基础词条 26 个特殊 token 编译成二进制并对 byte-level 词条做字节解码——其中0xAD、0x80–0xA0、0x7F三个区间是历史上反复踩坑的地方。板端重跑一次生成sha256与线上vocab.bin逐位一致。1. 知识点为什么不能直接读 tokenizer.json1.1 四个现实约束体积与解析成本tokenizer.json7MBJSON 解析 字符串构建 merge 表加载板载单核启动要付出不可忽略的固定成本——而本项目冷启动卖点是 ~2s字节语义不能在运行时反复算官方词条是字符映射串Ġapple里的Ġ代表空格字节。若运行时再反转bytes_to_unicode每次加载都要过一遍 15 万条映射merge 表用不上引擎是贪心最长前缀18-1根本不读merges特殊 token 不在 vocab 里|endoftext|(151643)、|im_start|(151644)、|im_end|(151645) 等在 tokenizer.json 的added_tokens区需要另外的来源。结论把词表编译成一份紧凑、定长语义的二进制loader 两遍扫描直接落成strings[151669]指针数组编码时零字符串处理、零 JSON。1.2 vocab.bin 的二进制布局vllm_tokenizer_qwen.c的 loader27–153 行读的就是这个格式u32 词条数 N 151669 重复 N 次 u32 token_id u16 字节串长度 L L 字节token 的原始字节串 ← 不是映射字符串是解码后的字节loader 的做法是两遍扫描第一遍只读tid/slen把每个词条的offset记下、累加总字节数93–118 行第二遍fseek回去按tid落位到一块连续内存str_datastrings[tid]直接指进去129–142 行。这样词条顺序无关、内存零拷贝解析之后find_longest_match就是memcmp线性扫描。1.3 词表里到底存什么字符串——byte-level 的解码问题Qwen3 词条在 tokenizer.json 里长这样model.vocabid 0…151642!: 0, …, Ġ: 220, Ċ: 198, Ń: 255, Ġapple: 23268, …键里的Ġ/Ċ/Ń都不是真字符而是 GPT-2bytes_to_unicode给单个字节起的代称。映射规则16 进制字节字节区间映射到说明0x21–0x7E原样ASCII直接存0xA1–0xAC、0xAE–0xFF原样latin-1 区直接存0x00–0x20U0100–U0120控制符 空格其中0x0A→Ċ、0x20→Ġ0x7F–0xA0U0121–U0142删除符与 C2 区字节0xADU0143‘Ń’一个游离字节历史修复点关键问题引擎 decode 是把 token 字节串直接拼回输出不做反向映射18-1 §3.3编码是拿原始字节匹配词表。所以vocab.bin里存的必须贴近真实字节语义而不是把U0120之类再 UTF-8 编码一次。build_vocab_bin.py的decode_token_str32–49 行就是这道转换闸门。2. 对应代码decode_token_str 的三种处理读build_vocab_bin.py32–49 行注意它对映射字符分三类defdecode_token_str(s):outbytearray()forchins:cpord(ch)ifcp0x0143:# Ń → 字节 0xADout.append(0xAD)elifcp0x0121:# ġ → 字节 0x7Fout.append(0x7F)elif0x0122cp0x0142:out.append(cp-0xA2)# 0x80..0xA0 → 原始单字节elif0x00A1cp0x00ACor0x00AEcp0x00FF:out.append(cp)# latin-1 原样else:out.extend(ch.encode(utf-8))# Ġ/Ċ/CJK 等保持原样returnbytes(out)三类处理各对应一种工程语义解码回单字节0x7F、0x80–0xA0、0xAD这些字节若以字符 UTF-8 形式存进 vocab.bin会变成C2 xx/C5 83这类双字节串与原文的单字节永不相等——贪心匹配要么错过、要么把两个词条缝成错位历史上表现为 “ðŁĺ/İ” 类乱码脚本头注释 2–16 行记录了这轮修复早先的fix_vocab_local.py漏了0xAD→U0143这一条。latin-1 原样¡–¬、®–ÿU00A1…本身就是字节即字符直接out.append(cp)保持 UTF-8 原样Ġ(U0120)、Ċ(U010A) 与所有 CJK。这是故意的——Ġ/Ċ是引擎 encode 的词首/换行标记18-1 §2 的C4 A0前缀就是这么来的CJK 是 3 字节真字符直接 UTF-8 编码即可。main() 里对每条词条做rec ! raw统计69–72 行最后把id ≥ 151643 的 26 个特殊 token从旧vocab.bin原样拷回75–83 行——因为它们不在 tokenizer.json 的model.vocab里只有既有二进制里才有完整字节。3. 改动后果板端重跑一次生成实测口径板端 RK3588 / aarch64 / 2026-09-07。命令python3 tools/build_vocab_bin.py tokenizer.json 旧 vocab.bin vocab_regen.bin源为Qwen3-VL-2B-Instruct/tokenizer.json、旧件为线上build-rk3588/vocab.bin。3.1 重生成日志与统计total entries: 151669 converted: 57286 kept: 94357 special copied: 26 -- common char coverage -- (no MISSING lines above full coverage) 回答: 2 hits [102104, 111423] 你好: 1 hits [108386] Ġgood: 6 hits [1661, 11561, 38426] Ċ: 2179 hits [198, 271, 280] DONE - /mnt/emmc/day18_logs/vocab_regen.bin数字对得上151,669 151,643基础 26特殊。57,286 条被解码修正即rec ! raw主要是含0x80–0xA0/0xAD/0x7F的碎片词条与字节词条94,357 条原样保留。内置校验还顺带打印了常见汉字覆盖无一 MISSING与Ġ/Ċ的命中示例。3.2 与线上文件的逐位一致性299e1c156bff1b85b5b4dc098c1d7f2e33e0b76fe1967d0d21df328985637a73 build-rk3588/vocab.bin 299e1c156bff1b85b5b4dc098c1d7f2e33e0b76fe1967d0d21df328985637a73 vocab_regen.binsha256相等两个文件都是 1,950,864 字节。这证明线上那份vocab.bin正是同一份脚本同一份输入生成的——词表编译是纯函数可复现、可审计与 Day 17 的权重转换同理确定性来自无随机 输入相同。3.3 一个错修反例看修复点在数据里长什么样用本地脚本在 tokenizer.json 里抓几个带映射字符的键验证 §1.3 的映射字节 0x20空格 : id 220 str Ġ utf-8 字节 c4a0 → 保留引擎词首标记 字节 0x0A换行 : id 198 str Ċ utf-8 字节 c48a → 保留引擎换行标记 字节 0xAD : id 255 str Ń utf-8 字节 c583 → 应解码成单字节 ad 字节 0x7FDEL : id 221 str ġ utf-8 字节 c4a1 → 应解码成单字节 7f若把decode_token_str里的0x0143分支删掉模拟漏修id 255 会以c5 83落盘此后凡是文本流中出现字节0xAD的位置贪心匹配都找不到对应的单字节词条——文本与词表在字节维度上错位。loader 侧的安全网只有一层vocab_size越界与条目截断检测loader 74–78、105–110 行它管不了语义对错所以这道闸门必须在生成期把好。4. 学员调试任务A 档板端动手复刻 3.1/3.2重跑build_vocab_bin.py核对 151669/57286/94357/26 四个数字与sha256一致故意删掉decode_token_str的0x0143分支或把0x0122..0x0142整段注释掉重生成后跑tokprobe的 15 条探针观察并记录哪些行开始错、错成什么样重点看含特殊字节的碎片词条路径用tok_inspect.py核对 3.3 中 id 220/198/255/221 的落盘字节养成先验字节再谈语义的习惯。B 档纯读源码通读qwen_tokenizer_loadvllm_tokenizer_qwen.c27–153 行回答① 为什么 loader 要两遍扫描 offset 表而不是顺序读一遍直接塞进strings②max_str_len打印为max_len256在代码里只出现在 loader 的统计与诊断打印里104/115/151 行encode 路径并没有用它剪枝——试从find_longest_match_qwen的实现说明为什么有最长词条长度却无法用来加速匹配以及若要加速该换什么数据结构③ 若某人把vocab.bin的u32 N改成 200000loader 的哪些检查会先拦下它预期输出一次可复现的词表重生成记录统计 sha256加一张改坏某分支 → 哪条探针哪几个 token 变错的对照表。收尾本篇源码点名build_vocab_bin.pydecode_token_str 32–49、统计 59–72、特殊拷贝 75–83、vllm_tokenizer_qwen.cloader 27–153、encode 195–272开源仓库Kestrel-LLM (Gitee)AGPL-3.0-or-later 或商业许可二选一下篇预告词表就位但 Qwen3-VL 有个纯文本模型没有的问题——视觉 token 和文本 token 混在一个序列里位置编码怎么排Day 18 收尾篇进 MRoPE[24,20,20]的三段式旋转、pos_t/h/w三套坐标以及把 3D 改回 1D会在旋转上差出多少弧度。关键词tokenizer.json、vocab.bin、词表编译、build_vocab_bin.py、sha256
返回列表