Skip to content

分词与词表 — 代码走读

src/llama-vocab.cpp — 分词器实现

核心类结构

llama_vocab 类管理整个词表和分词逻辑。它采用 PIMPL 模式(struct llama_vocabsrc/llama-vocab.h 中声明,实现细节藏在 src/llama-vocab.cppimpl 中),对外只暴露访问器方法,没有任何公开的数据成员:

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::tokenizesrc/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_piecetoken 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