llama.cpp初探:simple示例代码简要分析
加载后端
void ggml_backend_load_all() {
ggml_backend_load_all_from_path(nullptr);
}
void ggml_backend_load_all_from_path(const char * dir_path) {
#ifdef NDEBUG
bool silent = true;
#else
bool silent = false;
#endif
ggml_backend_load_best("blas", silent, dir_path);
ggml_backend_load_best("zendnn", silent, dir_path);
ggml_backend_load_best("cann", silent, dir_path);
ggml_backend_load_best("cuda", silent, dir_path);
ggml_backend_load_best("hip", silent, dir_path);
ggml_backend_load_best("metal", silent, dir_path);
ggml_backend_load_best("rpc", silent, dir_path);
ggml_backend_load_best("sycl", silent, dir_path);
ggml_backend_load_best("vulkan", silent, dir_path);
ggml_backend_load_best("opencl", silent, dir_path);
ggml_backend_load_best("hexagon", silent, dir_path);
ggml_backend_load_best("musa", silent, dir_path);
ggml_backend_load_best("cpu", silent, dir_path);
// check the environment variable GGML_BACKEND_PATH to load an out-of-tree backend
const char * backend_path = std::getenv("GGML_BACKEND_PATH");
if (backend_path) {
ggml_backend_load(backend_path);
}
}
动态加载所有可用的计算后端(CPU、CUDA、Metal 等),让 llama.cpp 能在不同硬件上运行。通过评分机制选择最适合的后端进行注册。每个后端会提供一个获取评分的函数,使用该函数进行计算得到分值,比如使用位运算累加看看性能。注册后端注释是加载其对应后端的动态库,将后端添加到backends向量。
加载模型
先看看看GGUF的模型文件。
(1)文件头: 位于文件的最开始位置,用于快速校验文件是否合法。
- Magic Number (4 bytes): 0x47 0x47 0x55 0x46,对应的 ASCII 码就是 “GGUF”。加载器首先读取这4个字节,如果不对,直接报错。
- Version (4 bytes): 版本号(图中是 3)。这允许加载器处理不同版本的格式变化(向后兼容)。
- Tensor Count (8 bytes, UINT64): 模型中包含的张量(权重矩阵)总数。比如一个 7B 模型可能有几百个 Tensor。
- Metadata KV Count (8 bytes, UINT64): 元数据键值对的数量。这是 GGUF 最核心的改进点。
(2)元数据键值对:接在头部之后,这部分定义了模型的所有非权重信息
以前的格式(如 GGML)通常把超参数(层数、维度等)写死在代码或特定的结构体里。GGUF 改用 Key-Value 映射。
- 架构信息: general.architecture = “llama”(告诉加载器用哪套逻辑加载)。
- 模型参数: llama.context_length = 4096, llama.embedding_length = 4096 等。
- Tokenizer (词表): 这一点非常重要。GGUF 将词表(tokenizer.ggml.tokens)直接打包在文件里,不再需要额外的 tokenizer.json 或 vocab.model 文件。这实现了真正的“单文件发布”。
- 其他: 作者信息、量化方法描述等。
(3)张量信息表:索引目录
在读取完所有元数据后,就是 Tensor 的索引列表。这里不包含实际的权重数据,只是数据的“描述”和“指针”。对于每一个 Tensor(共 tensor_count 个),包含以下字段:
- Name: 字符串,例如 blk.0.ffn_gate.weight。这对应了模型层中的具体权重名称。
- n_dimensions: 维度数量(例如 2 表示矩阵,1 表示向量)。
- dimensions: 具体的形状数组,例如 [4096, 32000]。
- Type: 数据类型,例如 GGML_TYPE_Q2_K(2-bit 量化)、F16 等。
- Offset (8 bytes, UINT64): 这是最关键的字段。它是一个指针(偏移量),告诉程序:“这个张量的实际二进制数据,位于文件数据区的第 X 个字节处”。
(4)数据区:图中右侧箭头指向的 10110… 部分。
这是文件体积最大的部分,包含所有权重矩阵的实际二进制数据。内存映射 (mmap) 的奥秘,因为有了上面的 Offset,llama_model_load_from_file 不需要把整个几 GB 的文件一次性读入 RAM。 它只需要调用系统级 API mmap,将文件映射到虚拟内存。当模型推理需要用到 blk.0 的权重时,CPU/GPU 根据 Offset 直接去磁盘(或文件系统缓存)里拿数据。这就是为什么 GGUF 模型启动速度极快且内存占用低的原因。
模型加载主要是将GGUF加载进来,主要的流程是先检查是否有注册后端,然后选择对应的设备,接着解析GGUF元信息包括KV/张量表,接着按照n_gpu_layers/tensor_split分配层与buffer,再者就是读取/映射/上传权重数据,返回llama_model对象指针。
其中最核心的函数就是llama_mode_load,这个函数主要的作用如下:
- load_arch:先知道是什么架构,后面的 KV key 解释/张量命名规则才对。
- load_hparams:读 GGUF KV 填充超参。
- load_vocab:构建 tokenizer/vocab。
- load_tensors:决定设备/缓冲区并实际装载权重数据。
load_arch
load_arch加载的是模型的架构类型,比如LLaMA、Falcon、Qwen、CLIP等大类,存储到llama_mode::arch字段里面,这个来源是GGUF的metadata key。
get_key(llm_kv(LLM_KV_GENERAL_ARCHITECTURE), arch_name, false);
llm_kv = LLM_KV(llm_arch_from_string(arch_name));
load_arch本身只是把loader里解析好的枚举出来。
void llama_model::load_arch(llama_model_loader & ml) {
arch = ml.get_arch();
if (arch == LLM_ARCH_UNKNOWN) {
throw std::runtime_error("unknown model architecture: '" + ml.get_arch_name() + "'");
}
}
之所以要解析加载arch是因为后面所以得操作都依赖着arch,比如怎么解释GGUF、怎么建张量的逻辑依赖arch。l
- oad_hparams 需要 arch 才知道该读哪些 KV、如何解释超参,而且它还用 arch 做特殊分支(例如 CLIP 直接跳过 hparams)。
- load_vocab 会用 arch 构造 LLM_KV(arch),影响 vocab/tokenizer 的加载规则。
- load_tensors 更是直接 switch (arch) 来决定要创建哪些权重张量、张量命名/shape/op 映射等。
load_hparams
struct llama_hparams {
bool vocab_only;
bool no_alloc;
bool rope_finetuned;
bool use_par_res;
bool swin_norm;
uint32_t n_ctx_train; // context size the model was trained on
uint32_t n_embd;
uint32_t n_embd_features = 0;
uint32_t n_layer;
// ...
uint32_t n_rot;
uint32_t n_embd_head_k;
uint32_t n_embd_head_v;
uint32_t n_expert = 0;
uint32_t n_expert_used = 0;
// ...
};
这里的 hparams 指的是 llama_model 里的 模型超参数/结构参数(hyper-parameters):决定模型“长什么样”(层数、隐藏维度、头数、RoPE/ALiBi/SWA/MoE 等配置)。它对应的结构体是 struct llama_hparams
llama_model::load_hparams(ml) 会从 GGUF 的 KV(metadata)里读取并填充 model.hparams(以及一些辅助状态,比如 gguf_kv 字符串表、type 等)
- 模型结构: 训练时的context长度、embedding/hidden size、transformer block 数、MoE等。
- 注意力/FFN:支持 KV 是标量或数组:有些模型每层不同。
- RoPE相关:rope_finetuned / rope_freq_base_train / rope_freq_scale_train / rope_scaling_type_train / n_rot / n_embd_head_k/v 等
- 架构特化项:比如 LLM_ARCH_WAVTOKENIZER_DEC 会额外读 posnet/convnext 参数。
load_vocab
load_vocab 加载的是 模型的词表/分词器(tokenizer + vocab):包括每个 token 的文本、score、属性(normal/control/user_defined/unknown/byte 等),以及各种 special token(BOS/EOS/EOT/PAD/…)和不同 tokenizer(SPM/BPE/WPM/UGM/RWKV/…)所需的附加数据(如 BPE merges、charsmap 等)。
struct llama_vocab {
struct token_data {
std::string text;
float score;
llama_token_attr attr;
};
void load(llama_model_loader & ml, const LLM_KV & kv);
std::string get_tokenizer_model() const;
std::string get_tokenizer_pre() const;
enum llama_vocab_type get_type() const;
enum llama_vocab_pre_type get_pre_type() const;
uint32_t n_tokens() const;
uint32_t n_token_types() const;
// ...
};
模型的推理需要做文本到token id的双向转换,输入的prompt必须先tokenize 才能喂给llama_decode,输出的token id必须detokenize 才能打印成字符串。不同模型(LLaMA / Falcon / ChatGLM / …)的 tokenizer 规则不同(SPM/BPE/WPM/UGM…,以及 pre-tokenization 规则),如果 vocab 没加载或加载错,模型就“无法正确理解输入/输出”
load_tensor
这里的tensor是指模型推理所需的所有权重张量(weghts),比如:
- token embedding:tok_embd
- 每层 attention 的 Wq/Wk/Wv/Wo(以及可选 bias、norm 权重等)
- 每层 FFN 的 Wgate/Wup/Wdown(MoE 还会有专家张量、router 等)
- 输出层/输出 embedding(有些模型与 tok_embd 共享,需要 duplicated 处理)
在load tensors会按arch分支创建这些张量,比如已LLAMA类分支片段。
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
// output
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED);
if (output == NULL) {
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
}
for (int i = 0; i < n_layer; ++i) {
auto & layer = layers[i];
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), {n_embd, n_embd_head_k * n_head}, 0);
layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", i), {n_embd, n_embd_k_gqa}, 0);
layer.wv = create_tensor(tn(LLM_TENSOR_ATTN_V, "weight", i), {n_embd, n_embd_v_gqa}, 0);
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head_k * n_head, n_embd}, 0);
}
(1)tensor对象(ggml_tensor *)存在哪里?
- 按语义分组存到 llama_model 的成员里:比如 tok_embd / output_norm / output,以及每层的 layers[i].wq/wk/wv/… 等(这些都是 ggml_tensor * 指针)。
struct llama_model {
// ...
struct ggml_tensor * tok_embd = nullptr;
// ...
struct ggml_tensor * output_norm = nullptr;
struct ggml_tensor * output = nullptr;
// ...
std::vector<llama_layer> layers;
// ...
std::vector<std::pair<std::string, struct ggml_tensor *>> tensors_by_name;
// ...
};
- 按名字查 tensor:load_tensors() 完成后会把每个 ggml_context 里的 tensor 都放进 tensors_by_name(主要用于内部/统计与 get_tensor(name) 这种按名访问)。
// populate tensors_by_name
for (auto & [ctx, _] : pimpl->ctxs_bufs) {
for (auto * cur = ggml_get_first_tensor(ctx.get()); cur != NULL; cur = ggml_get_next_tensor(ctx.get(), cur)) {
tensors_by_name.emplace_back(ggml_get_name(cur), cur);
}
}
对应按名取
const ggml_tensor * llama_model::get_tensor(const char * name) const {
auto it = std::find_if(tensors_by_name.begin(), tensors_by_name.end(),
[name](const std::pair<std::string, ggml_tensor *> & it) { return it.first == name; });
if (it == tensors_by_name.end()) {
return nullptr;
}
return it->second;
}
(2)tensor数据(权重内容)存在哪里?
ggml_tensor 自身只是一个“描述 + 指针”,真正的权重数据会落在三类地方之一:
- CPU/GPU 后端 buffer 里:load_tensors() 会为不同 buffer type 创建对应的 backend buffer,并把它们与承载 tensor metadata 的 ggml_context 绑在一起。这些 buffer 句柄被保存在:pimpl->ctxs_bufs
struct llama_model::impl {
// contexts where the model tensors metadata is stored as well as the corresponding buffers:
std::vector<std::pair<ggml_context_ptr, std::vector<ggml_backend_buffer_ptr>>> ctxs_bufs;
// ...
};
- mmap 映射区域(文件映射内存):如果启用 mmap,loader 会把模型文件映射进内存,某些 tensor 的 data 会直接指向映射区域,或者后端 buffer 会通过 buffer_from_host_ptr 包装映射内存。映射对象被保存在loader侧:ml.mappings,model侧:pimpl->mappings。
- no_alloc 模式:ml.no_alloc 时只是创建 tensor 元信息与“dummy buffer”,不装载数据,用于某些统计/规划场景。
(3)tensor是怎么分配到那个设备上的?
在GPU上还是CPU上,决策分为两层:
- 层级/设备选择(dev):决定每一层(input/repeating/output)用哪个 ggml_backend_dev_t(CPU / GPU / RPC / IGPU…)。
- buffer type 选择(buft):在选定设备后,再决定该 tensor 用哪个 ggml_backend_buffer_type_t(同一设备也可能有多种 buffer type;并且还会把 CPU buffer type 作为 fallback)。
流程梳理(从“设备列表”到“权重进后端 buffer”)如下:
┌──────────────────────────────────────────────────────────────┐
│ llama_model_load_from_file_impl(path, params) │
│ - 选择/构建 model->devices (GPU/RPC/IGPU...) │
└───────────────┬──────────────────────────────────────────────┘
│
v
┌──────────────────────────────────────────────────────────────┐
│ llama_model_load(...) │
│ - llama_model_loader ml(...) 读取 GGUF 元信息、索引 tensors │
│ - model.load_arch/load_hparams/load_vocab │
│ - model.load_tensors(ml) ← 重点 │
└───────────────┬──────────────────────────────────────────────┘
│
v
┌──────────────────────────────────────────────────────────────┐
│ llama_model::load_tensors(ml) │
│ A) 构建候选 buft 列表 │
│ cpu_buft_list = make_cpu_buft_list(...) │
│ gpu_buft_list[dev] = make_gpu_buft_list(dev,...) + CPU fb │
│ │
│ B) 决定每层放哪台设备(层→dev) │
│ splits = tensor_split 或按 dev free-mem 默认计算 │
│ i_gpu_start = (n_layer+1 - n_gpu_layers) │
│ dev_input=CPU, dev_layer[il]=CPU/GPU..., dev_output=... │
│ │
│ C) 决定每个 tensor 用哪个 buft,并创建 ggml_tensor 元信息 │
│ 对每个权重 tensor: │
│ - 根据层(input/repeating/output)选 buft_list │
│ - buft = select_weight_buft(...) (考虑 op/type/override) │
│ - ctx = ctx_map[buft] (同 buft 归到同 ggml_context) │
│ - ml.create_tensor(ctx, ...) (创建 tensor meta) │
│ ml.done_getting_tensors() 校验数量 │
└───────────────┬──────────────────────────────────────────────┘
│
v
┌──────────────────────────────────────────────────────────────┐
│ D) 分配后端 buffer(真正“存储权重”的地方) │
│ ml.init_mappings(...) (mmap 时建立映射、算 size_data) │
│ 对每个 (buft -> ctx): │
│ - 若满足条件: ggml_backend_dev_buffer_from_host_ptr(...) │
│ (把 mmap 的区间包装成后端 buffer,少拷贝) │
│ - 否则: ggml_backend_alloc_ctx_tensors_from_buft(ctx,buft)│
│ - 结果保存到 model.pimpl->ctxs_bufs 维持生命周期 │
│ - 标记 buffer usage = WEIGHTS (帮助调度) │
└───────────────┬──────────────────────────────────────────────┘
│
v
┌──────────────────────────────────────────────────────────────┐
│ E) 装载权重数据到后端(mmap/读文件/异步上传) │
│ 对每个 ctx: ml.load_all_data(ctx, buf_map, progress_cb) │
│ - progress_cb 返回 false => 取消 => load_tensors 返回 false│
│ 若 use_mmap_buffer: model.pimpl->mappings 接管 ml.mappings │
│ 同时填充 model.tensors_by_name 供按名查询/统计 │
└──────────────────────────────────────────────────────────────┘
│
v
┌──────────────────────────────────────────────────────────────┐
│ 推理阶段 build_graph / llama_decode │
│ - 直接引用 model.tok_embd / model.layers[i].wq... 等 ggml_tensor│
│ - 后端根据 tensor 绑定的 buffer 决定在 CPU/GPU 执行与取数 │
└──────────────────────────────────────────────────────────────┘
buft 是什么?
buft 是 ggml_backend_buffer_type_t(backend buffer type),可以理解为“某个后端设备上,用哪一种内存/分配方式来存放 tensor 数据”的类型句柄。
在 load_tensors() 里它的作用是:给每个权重 tensor 选择一个合适的 buffer type,并据此把 tensor 归到对应的 ggml_context,最终用该 buft 去创建真实的后端 buffer(CPU RAM / GPU VRAM / 远端 RPC buffer / host pinned buffer 等)。
- load_tensors() 里按 tensor 所在层选一个 buft_list,再用 select_weight_buft(…) 选出最终 buft:
if (!buft) {
buft = select_weight_buft(hparams, t_meta, op, *buft_list);
if (!buft) {
throw std::runtime_error(format("failed to find a compatible buffer type for tensor %s", tn.str().c_str()));
}
}
- 随后用 buft 决定该 tensor 属于哪个 ggml_context(ctx_for_buft(buft)),并最终分配后端 buffer(例如 ggml_backend_alloc_ctx_tensors_from_buft(ctx, buft) 或 mmap 的 ggml_backend_dev_buffer_from_host_ptr(…))。
buft 和 dev(device)的区别
– dev(ggml_backend_dev_t):是哪块设备(CPU / 某张 GPU / RPC 设备…)
– buft(ggml_backend_buffer_type_t):在这块设备上“用哪种 buffer 类型/内存形式”来存 tensor(默认 device buffer、host buffer、特殊优化 buffer 等)
(4)推理阶段如何使用这些权重
后续推理构图时(model.build_graph(…) ——>各 src/models/xx.cpp),会直接用 llama_model 里的这些 ggml_tensor x 作为权重输入,和激活值做 ggml 运算(matmul/add/norm/rope…)。例如 src/models/stablelm.cpp 在构图时直接用:
model.tok_embd 做输入 embedding
– model.layers[il].wq/wk/wv 做 Q/K/V 投影
– model.layers[il].bq/bk/bv(可选)加 bias
– ggml_rope_ext 用 rope 参数对 Q/K 做 RoPE
Tokenization
const llama_vocab * vocab = llama_model_get_vocab(model);
// tokenize the prompt
// find the number of tokens in the prompt
const int n_prompt = -llama_tokenize(vocab, prompt.c_str(), prompt.size(), NULL, 0, true, true);
// allocate space for the tokens and tokenize the prompt
std::vector<llama_token> prompt_tokens(n_prompt);
if (llama_tokenize(vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), true, true) < 0) {
fprintf(stderr, "%s: error: failed to tokenize the prompt\n", __func__);
return 1;
}
这段代码主要的作用是做文本->token ID的转换。整段的流程可以总结一下。
- 拿vocab:llama_model_get_vocab(model) 获取 tokenizer/词表句柄;
- 预估token数:第一次 llama_tokenize(…, NULL, 0, …),只算需要多少 token,返回负数取反得到 n_prompt;
- 分配缓存:std::vector
prompt_tokens(n_prompt); 为 prompt 分配精确长度的 token 缓冲区; - 真正 tokenize:第二次 llama_tokenize 把 token id 写进 prompt_tokens,如果失败或容量不够返回 < 0 则报错退出。
llama_tokenize() tokenize 的对象是 一段输入文本(UTF-8 字符串),输出是 token id 序列(llama_token):也就是模型能直接消费的离散符号编号。它依赖 llama_vocab(模型绑定的 tokenizer + 词表 + special token 规则),把文本切分/编码成 token ids。
llama_tokenize会转发到vocab->tokenize,先做 special token 分段,再按 vocab type 分发到具体 tokenizer session。
std::vector<llama_token> llama_vocab::impl::tokenize(const std::string & raw_text, bool add_special, bool parse_special) const {
// 1) special token partition(把 raw_text 切成「普通文本片段」和「已识别的特殊 token」)
tokenizer_st_partition(fragment_buffer, parse_special);
// 2) 按 vocab type 分发到具体 tokenizer session
switch (get_type()) {
case LLAMA_VOCAB_TYPE_SPM: llm_tokenizer_spm_session(...).tokenize(...); break;
case LLAMA_VOCAB_TYPE_BPE: llm_tokenizer_bpe_session(...).tokenize(...); break;
case LLAMA_VOCAB_TYPE_WPM: llm_tokenizer_wpm_session(...).tokenize(...); break;
case LLAMA_VOCAB_TYPE_UGM: llm_tokenizer_ugm_session(...).tokenize(...); break;
case LLAMA_VOCAB_TYPE_RWKV: llm_tokenizer_rwkv_session(...).tokenize(...); break;
case LLAMA_VOCAB_TYPE_PLAMO2: llm_tokenizer_plamo2_session(...).tokenize(...); break;
default: GGML_ABORT(...);
}
return output;
}
SPM
SentencePiece BPE-ish 合并,项目里叫 SPM tokenizer,入口类是llm_tokenizer_spm_session,其核心思路是把文本先按 UTF-8 字符拆成链表 symbol,然后不断从优先队列取“得分最高”的可合并 bigram,做合并,最后把每个合并后的片段映射为 token。
struct llm_tokenizer_spm_session {
void tokenize(const std::string & text, std::vector<llama_token> & output) {
// split into utf8 chars -> symbols
// seed work_queue with all 2-char tokens -> try_add_bigram
// pop highest score, merge, and push new bigrams
// resegment each final symbol into token ids / byte tokens
}
}
在 impl::tokenize 的 SPM 分支里,会先把空格转义成 “▁”(U+2581),并处理 add_space_prefix、BOS/EOS:
if (add_space_prefix && is_prev_special) { text = ' '; }
text += fragment...
llama_escape_whitespace(text); // ' ' -> ▁
llm_tokenizer_spm_session session(vocab);
session.tokenize(text, output);
BEP
Byte Pair Encoding,入口类:llm_tokenizer_bpe_session,其核心思路是
- 1) 用“pre-tokenizer regex”(不同模型 pre_type 不同)把文本切成词片段(word_collection)。
- 2) 每个词片段再按 UTF-8 拆成 symbols。
- 3) 用 vocab.find_bpe_rank(left,right) 查 merges rank(越小越优先),不断合并。
- 4) 合并结束后把每个最终 symbol 映射为 token(找不到就回退到逐字节 token)。
WPM
WordPiece,入口类:llm_tokenizer_wpm_session,核心思路是:
- 先做 NFD 归一化 + 按 whitespace/标点切词(preprocess)
- 每个词前加 “▁”
- 对每个词做 最长匹配(从当前位置往后尽量取最长、且在 vocab 中存在的 token)
- 若某个词无法完全分解,回退整个词为unk
UGM
入口类:llm_tokenizer_ugm_session,核心思路:典型 unigram LM 的 Viterbi 最优切分:
- 先 normalize 输入
- 每个位置沿 trie 找所有可能 token,做动态规划选择 “累计得分最大”的路径
- 走不通就用 unknown token + penalty
- 最后从末尾回溯得到 token 序列
初始化上下文
llama_context_params ctx_params = llama_context_default_params();
// n_ctx is the context size
ctx_params.n_ctx = n_prompt + n_predict - 1;
// n_batch is the maximum number of tokens that can be processed in a single call to llama_decode
ctx_params.n_batch = n_prompt;
// enable performance counters
ctx_params.no_perf = false;
llama_context * ctx = llama_init_from_model(model, ctx_params);
if (ctx == NULL) {
fprintf(stderr , "%s: error: failed to create the llama_context\n" , __func__);
return 1;
}
这段代码主要的作用是创建llama_context,用于管理推理的状态KV cache、batch 分配器、backend 调度器等)。
配置上下文参数、batch大小、性能统计。创建llama_context包括初始化计算后端,分配KV cache内存,创建batch分配器和调度器,预留计算图内存。llama_context是推理的核心对象,后续的llama_decode调用都会使用它来管理状态和执行计算。
初始化采样器
auto sparams = llama_sampler_chain_default_params();
sparams.no_perf = false;
llama_sampler * smpl = llama_sampler_chain_init(sparams);
llama_sampler_chain_add(smpl, llama_sampler_init_greedy());
构造一个“采样器链”(sampler chain),并在其中添加一个贪心采样器(greedy sampler),后面主循环用它从 logits 里选下一个 token。下面按调用顺序拆开。采样器接收一个 llama_token_data_array(包含所有候选 token 的 id、logit、概率),经过处理(过滤、排序、选择等),最终确定一个 token。
处理提示词
// print the prompt token-by-token
for (auto id : prompt_tokens) {
char buf[128];
int n = llama_token_to_piece(vocab, id, buf, sizeof(buf), 0, true);
if (n < 0) {
fprintf(stderr, "%s: error: failed to convert token to piece\n", __func__);
return 1;
}
std::string s(buf, n);
printf("%s", s.c_str());
}
// prepare a batch for the prompt
llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size());
if (llama_model_has_encoder(model)) {
if (llama_encode(ctx, batch)) {
fprintf(stderr, "%s : failed to eval\n", __func__);
return 1;
}
llama_token decoder_start_token_id = llama_model_decoder_start_token(model);
if (decoder_start_token_id == LLAMA_TOKEN_NULL) {
decoder_start_token_id = llama_vocab_bos(vocab);
}
batch = llama_batch_get_one(&decoder_start_token_id, 1);
}
llama_token_to_piece是将token ID转换为文本并输出,llama_batch_get_one是为prompt创建batch结构。通过llama_model_has_encoder检查模型是不是编码-解码模型,如果是先进行prompt,在准备decoder的起始batch。
总结一下就是先调用llama_encode编码prompt的token,做完这一步,encoder部分就结束了,接着让decoder开始工作,我们知道大模型是一个自回归模型,需要有第一个token作为起点,类似机器翻译里的 BOS。
最后的llama_batch_get_one这里就是准备decoder起始batch。
llama_encode
llama_decode
生成循环
for (int n_pos = 0; n_pos + batch.n_tokens < n_prompt + n_predict; ) {
// evaluate the current batch with the transformer model
if (llama_decode(ctx, batch)) {
fprintf(stderr, "%s : failed to eval, return code %d\n", __func__, 1);
return 1;
}
n_pos += batch.n_tokens;
// sample the next token
{
new_token_id = llama_sampler_sample(smpl, ctx, -1);
// is it an end of generation?
if (llama_vocab_is_eog(vocab, new_token_id)) {
break;
}
char buf[128];
int n = llama_token_to_piece(vocab, new_token_id, buf, sizeof(buf), 0, true);
if (n < 0) {
fprintf(stderr, "%s: error: failed to convert token to piece\n", __func__);
return 1;
}
std::string s(buf, n);
printf("%s", s.c_str());
fflush(stdout);
// prepare the next batch with the sampled token
batch = llama_batch_get_one(&new_token_id, 1);
n_decode += 1;
}
}
这段代码是自回归文本生成的核心循环,实现decoder->sample->print的循环。llama_decode进行decoder,然后调用llamap_sampler_sample采样下一个token,获取对应的token ID,通过调用llama_vocab_is_eog来检查结束条件,判断采样到的token是否为结束token,是则退出循环。最后调用llama_token_to_piece将token ID转换为文本并打印。
核心的思想其实跟之前写的:从零实现 Transformer:中英文翻译实例文章是一样的。





