Appearance
分词与词表 — 代码走读
src/llama-vocab.cpp — 分词器实现
核心类结构
llama_vocab 类管理整个词表和分词逻辑。它采用 PIMPL 模式(struct llama_vocab 在 src/llama-vocab.h 中声明,实现细节藏在 src/llama-vocab.cpp 的 impl 中),对外只暴露访问器方法,没有任何公开的数据成员:
cpp
// src/llama-vocab.h (~72) —— 实际接口(节选)
struct llama_vocab {
// 词表/类型
enum llama_vocab_type get_type() const;
uint32_t n_tokens() const;
// 特殊 token
llama_token token_bos() const;
llama_token token_eos() const;
llama_token token_pad() const;
// 文本/属性
const char * token_get_text (llama_token id) const;
float token_get_score(llama_token id) const;
// BPE 合并规则
std::vector<std::string> get_bpe_merges() const;
int find_bpe_rank(const std::string & token_left,
const std::string & token_right) const;
// 分词 / 反分词
int32_t tokenize(const char * text, int32_t text_len,
llama_token * tokens, int32_t n_tokens_max,
bool add_special, bool parse_special) const;
int32_t token_to_piece(llama_token token, char * buf,
int32_t length, int32_t lstrip, bool special) const;
private:
struct impl;
std::unique_ptr<impl> pimpl; // 真正的词表数据都在这里
};说明:早期版本里
llama_vocab是一个直接持有tokens/token_to_id/bos_token_id等公开字段的聚合体;当前版本已把这些数据下沉到pimpl,外部代码只能通过上面的访问器拿到。如果你在别处看到旧的"字段式"写法,那是过时的概念示意,并非字面源码。
分词入口
cpp
// include/llama.h —— 注意首参是 llama_vocab *,不是 llama_model *
int32_t llama_tokenize(
const struct llama_vocab * vocab,
const char * text,
int32_t text_len,
llama_token * tokens,
int32_t n_tokens_max,
bool add_special,
bool parse_special);拿到 vocab 指针的方式:
const llama_vocab * vocab = llama_model_get_vocab(model);(include/llama.h)。add_special控制是否按模型配置加 BOS/EOS;parse_special控制是否把<...>形式的特殊/控制 token 当作真实 token 而非纯文本。
BPE 编码流程
下面的
unicode_normalize/pre_tokenize/bpe_encode只是教学示意名,并不存在于源码里。真实实现是llm_tokenizer_bpe_session::tokenize(src/llama-vocab.cpp),它内部用unicode_regex_split(...)同时完成规范化与正则预切分,再走 bigram 合并循环(见下)。
cpp
// 示意(非字面源码):
// 1. 预处理 + 预分词:Unicode 规范化并按正则切成 word(实际由 unicode_regex_split 一次完成)
auto words = pre_tokenize(text);
// 2. 对每个 word 执行 BPE 合并(真实方法:llm_tokenizer_bpe_session::tokenize)
for (auto & word : words) {
auto tokens = bpe_encode(word);
result.insert(result.end(), tokens.begin(), tokens.end());
}
// 3. 按需添加特殊 token(实际由 append_bos/append_eos 处理)
if (add_special) append_bos(result);BPE 合并
同样是教学示意。真实实现里,"找最优合并"对应
llama_vocab::find_bpe_rank(left, right)(返回合并表的 rank,越小越优先),合并调度由llm_tokenizer_bpe_session::add_new_bigram+ 一个优先队列llm_bigram_bpe::queue驱动。
cpp
// 示意(非字面源码):BPE 核心 —— 重复合并 rank 最小的相邻 token 对
while (优先队列非空) {
auto best = queue.pop(); // 真实:llm_bigram_bpe::queue
int rank = vocab.find_bpe_rank( // 真实:llama_vocab::find_bpe_rank
best.left_token, best.right_token);
if (rank < 0) break; // 没有可合并的对了
merge_pair(best.left, best.right); // 把右符号并入左符号
queue 重新评估相邻的新 bigram;
}HybridDNA 分词器
llm_tokenizer_bpe_session 现在有虚析构函数和虚 tokenize() 方法,支持子类化:
cpp
// HybridDNA 继承 BPE session,重写 tokenize() 处理 DNA 段(注意类名:hybriddna 连写,无下划线)
struct llm_tokenizer_hybriddna_session : llm_tokenizer_bpe_session {
// 检测 <dna>...</dna> 标签
// 文本段走标准 BPE
// DNA 段使用 6-base k-mer 分词
// 非 ACGT 字符 → <oov>
};Detokenization
cpp
// include/llama.h —— 首参同样是 llama_vocab *,不是 llama_model *
int32_t llama_token_to_piece(
const struct llama_vocab * vocab,
llama_token token,
char * buf,
int32_t length,
int32_t lstrip,
bool special);关键函数索引
| 函数 | 说明 |
|---|---|
llama_tokenize | 文本 → token IDs |
llama_token_to_piece | token ID → 文本片段 |
llama_vocab_get_text | 获取 token 的文本表示 |
llama_vocab_get_type | 获取分词器类型 |
llama_vocab::load | 从 GGUF 加载词表(load(llama_model_loader & ml, const LLM_KV & kv),src/llama-vocab.h) |