llama-simple -m ./models/Qwen3-1.7B-Q4_K_M.gguf -n 30 "What is the result of 5/0 in math?"
推理过程 in code
int main () {
ggml_backend_load_all();
// 初始化 model 参数
llama_model_params model_params = llama_model_default_params();
model_params.n_gpu_layers = ngl;
// 创建 model 对象
llama_model * model = llama_model_load_from_file(model_path.c_str(), model_params);
// 得到 vocab, 得到特殊 token id,得到token-to-id 列表,包括了padding token id
// 得到 id-to-token 列表,得到 cached-token-to-piece 列表
// Tokenizer model是gpt2, 类型是 BPE,最长Token lenght = 256,等信息(包括了BPE合并规则)
const llama_vocab * vocab = llama_model_get_vocab(model);
// prompt 进行 tokenize,得到 id 表示的 prompt : prompt_tokens。
std::vector<llama_token> prompt_tokens(n_prompt);
// 然后根据Vocab进行tokenization,对于BEP,还有合并规则,这个规则存在于 llama_vocab对象中:
// std::vector<std::string> get_bpe_merges() const;
// 将输入prompt 分词后得到 prompt_tokens
llama_tokenize(vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), true, true)
// 首先得到 ctx 参数
llama_context_params ctx_params = llama_context_default_params();
ctx_params.n_ctx = n_prompt + n_predict - 1;
// 用上述 ctx_param 和 model 参数创建一个 ctx 对象 ***********
// llama_model_params model_params = llama_model_default_params();
// llama_model * model = llama_model_load_from_file(model_path.c_str(), model_params);
// llama_context_params ctx_params = llama_context_default_params();
llama_context * ctx = llama_init_from_model(model, ctx_params);
// 初始化 sampler 参数
auto sparams = llama_sampler_chain_default_params();
// 创建 samplers 对象
llama_sampler * smpl = llama_sampler_chain_init(sparams);
llama_sampler_chain_add(smpl, llama_sampler_init_greedy());
// 输出 prompt one by one
// 循环 prompt_tokens 中每一个id,
for (auto id : prompt_tokens) {
char buf[128]; // 假设每个 token 至多 127 个字符
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); // 从 bug中读前n个写入s。
printf("%s", s.c_str());
}
// prepare a batch ?总不能是空的batch
llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size());
llama_token new_token_id;
// generate token by token
for (int n_pos = 0; n_pos + batch.n_tokens < n_prompt + n_predict; ) {
// 1. 这里是 forward pass 的实际计算,每生成一个token 前都需要一次forward 计算
llama_decode(ctx, batch)
// 2. 这里都开始 采样了,所以forward pass 在其之前
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;}
int n = llama_token_to_piece(vocab, new_token_id, buf,...)
// show generated
std::string s(buf, n); // 确保每个 token 立即打印到终端,适合实时交互或调试。
printf("%s", s.c_str());
fflush(stdout);
// 3. prepare the next batch
// 包含这个 token 的新输入
batch = llama_batch_get_one(&new_token_id, 1);
}
// perf 相关,clean up
}
关于cuda的信息:
llama_model_load_from_file_impl: using device CUDA0 (Orin) - 1082 MiB free
load_tensors: offloading 28 repeating layers to GPU
load_tensors: offloading output layer to GPU
load_tensors: offloaded 29/29 layers to GPU
load_tensors: CUDA0 model buffer size = 1050.43 MiB
load_tensors: CPU_Mapped model buffer size = 166.92 MiB
n_batch 是一次性调用 llama_decode 可处理的最多 token 数。
KAQ:model param 和 ctx param 的区别?
两者服务的对象就不同。
model param 是从 GGUF 文件中得到的,用于控制模型行为(如何加载 model 结构,weight 和 meta data)、硬件分配(存储在显存中的层数 n_gpu_layers 等)、内存优化(是否使用mmap等)、数据验证等。
ctx param 用于控制推理上下文的行为。如 n_ctx(上下文长度)、n_batch(批大小)、n_threads(线程数)。attention_type(注意力类型)、rope_scaling_type(RoPE 缩放)。如 type_k/type_v(KV 缓存数据类型)、kv_unified(统一 KV 缓存)。还包括性能优化如 flash_attn(启用 Flash Attention)、offload_kqv(KV 缓存加载到 GPU)等内容控制。
所以需要 ctx 对象才能进行推理(decode)。
KAQ:model 对应的 Graph 的搭建发生在哪里?
Graph 的搭建发生在 ctx->decode 时,具体是其中的 llama_context::process_ubatch 中的 build_graph(gparams)。
KAQ:如何逐个 token 计算生成,如何实际计算模型的推理(forward pass)的
三个核心步骤:
forward pass 的实际计算,每生成一个token 前都需要一次 forward 计算。对应code:
llama_decode(ctx, batch)使用采样器,采样 token 作为当前步骤的新 token。code:
new_token_id = llama_sampler_sample(smpl, ctx, -1);将新的 token 包含进下一个 batch。在生成每个新 token 后,都需要准备一个新的 batch 来包含这个新的 token 作为下一次计算的输入。所以需要创建一个只包含单个新 token 的
llama_batch对象。code:batch = llama_batch_get_one(&new_token_id, 1);
当然,中间需要将 generated token 输出到屏幕。
KAQ:llama_batch 是什么
llama_batch 表示一批输入 tokens 的结构体。在文本生成过程中,每次调用 llama_decode 时都需要一个 batch 来提供当前的上下文信息给模型。这个 batch 可以包含多个 token,但在一次迭代中只处理一个新生成的 token。
要创建一个仅包含单个新 token 的 llama_batch,你可以使用 llama_batch_get_one 函数,如下所示:
int new_token_id = ...; // 新生成的 token ID
const llama_token *tokens = &new_token_id;
size_t n_tokens = 1;
// 创建新的 batch 只包含这一个 token
llama_batch batch = llama_batch_get_one(tokens, n_tokens);
这样你就可以将这个新的 token 作为下一次调用的输入的一部分了。
KAQ:llama_decode 如何实际计算的? 比如 matmul 等?
llama_decode 函数本身不直接执行实际的矩阵乘法等底层操作。相反,它是一个封装了所有关键步骤的接口,包括:
- 准备数据:将当前 batch 中的 tokens 转换为适合模型处理的格式。
- 前向传播:调用模型的内部实现来进行实际的数学运算,如矩阵乘法和激活函数的计算。
- 输出处理:从模型中提取出用于下一步采样的概率分布或直接的 token ID。
具体的矩阵乘法和其他数值计算是在模型的内部实现的,通常是使用优化的库(例如 cuBLAS 或类似的高性能线性代数库),这些库能够利用 GPU 并行性来实现高效的计算。
KAQ:如何进入具体计算的,比如 matmul 等
int32_t llama_decode(llama_context * ctx, llama_batch batch) {
const int ret = ctx->decode(batch);
if (ret != 0 && ret != 1) {
LLAMA_LOG_ERROR("%s: failed to decode, ret = %d\n", __func__, ret);
}
return ret;
}
可以看到,llama_decode 即 ctx->decode(batch),所有的实际计算应该是发生在这里了。
KAQ:ctx->decode 阅读, 大概做了什么
输入验证:
确保 batch 有效(token 或嵌入,非空)。确保输入 batch 要么包含 token(batch_inp.token),要么包含嵌入(batch_inp.embd),但不能两者都有,符合自回归推理或嵌入计算的互斥性。互斥是因为 两者的计算图不同。前者从 token ID 开始,通过嵌入层生成嵌入,再执行 Transformer 层;后者提供嵌入向量,跳过嵌入层直接输入 Transformer 或池化层。
初始化模型参数(词汇表、嵌入维度)。内存检查,如果没有内存上下文(memory),调用 encode。
batch 初始化:
使用 balloc 分配 batch,设置 token/embedding,检查输出一致性。balloc->init(batch_inp, vocab, memory.get(), n_embd, cparams.kv_unified ? LLAMA_MAX_SEQ : cparams.n_seq_max, output_all)
KV 缓存管理:
更新 KV 缓存,初始化 mctx,处理碎片整理。kv_self_update(false);。 处理待处理的 KV 缓存碎片整理或滑动窗口(kv_self_update),确保缓存状态正确。
memory->init_batch 初始化 ubatch。
前向传播:
output_reserve(n_outputs_all) 分配输出缓冲区。
构建并计执行算图: const auto * res = process_ubatch(ubatch, LLM_GRAPH_TYPE_DECODER, mctx.get(), status); 包括步骤:
model.build_graph()
compute_graph()
- 选择CPU 或 GPU:ggml_backend_sched_graph_compute_async()
执行 Transformer 层(嵌入 -> 多头注意力 -> 层归一化 -> 前馈网络),更新 KV 缓存。
结果提取:
提取 logits(用于采样)或嵌入(token 或序列级,基于池化类型)。
ggml_backend_tensor_get_async(backend_res, t_logits, logits_out, 0, n_outputs*n_vocab*sizeof(float)); 异步拷贝到输出缓冲区(logits 或 embd)。
错误处理 和 输出排序 和 清理:
失败时回滚 KV 缓存。调整输出顺序,匹配输入 batch。
重置调度器,为下次解码准备。 ggml_backend_sched_reset(sched.get());
KAQ:llama_context::process_ubatch 做了什么
主要两个动作:model.build_graph(gparams); 和 graph_compute()
llm_graph_result * llama_context::process_ubatch(const llama_ubatch & ubatch,
llm_graph_type gtype,
llama_memory_context_i * mctx,
ggml_status & ret) {
if (mctx && !mctx->apply()) {
LLAMA_LOG_ERROR("%s: failed to apply memory context\n", __func__);
ret = GGML_STATUS_FAILED;
return nullptr;
}
// 获取上一次的计算图结果(gf_res_prev)和计算图(gf)。
auto * res = gf_res_prev.get();
auto * gf = res->get_gf();
// 根据 ubatch、mctx 和图类型生成新的 图参数(gparams)。
const auto gparams = graph_params(res, ubatch, mctx, gtype);
if (!graph_reuse_disable && res->can_reuse(gparams)) {
n_reused++;
} else {
// 重置 结果 和 调度器
res->reset();
ggml_backend_sched_reset(sched.get());
ggml_backend_sched_set_eval_callback(sched.get(), cparams.cb_eval, cparams.cb_eval_user_data);
// 用新的图参数构建新图
gf = model.build_graph(gparams);
if (!gf) {
LLAMA_LOG_ERROR("%s: failed to initialize graph\n", __func__);
ret = GGML_STATUS_FAILED;
return nullptr;
}
if (!ggml_backend_sched_alloc_graph(sched.get(), gf)) {
LLAMA_LOG_ERROR("%s: failed to allocate graph\n", __func__);
ret = GGML_STATUS_ALLOC_FAILED;
return nullptr;
}
}
// 这次计算的 数据作为input
res->set_inputs(&ubatch);
// 计算图 执行计算图
const auto status = graph_compute(res->get_gf(), ubatch.n_tokens > 1);
if (status != GGML_STATUS_SUCCESS) {
LLAMA_LOG_ERROR("%s: failed to compute graph, compute status: %d\n", __func__, status);
ret = status;
return nullptr;
}
ret = GGML_STATUS_SUCCESS;
return res;
}
KAQ:如何构建计算图的 build_graph(gparams)
各个架构的组成是提前写好的,在 llama-model.cpp 中都有定义。对于 arch 是 QWEN3,其类型是 llm_build_qwen3 。最后 append 一个 pooling 层:
llm = std::make_unique<llm_build_qwen3>(*this, params);
llm->build_pooling(cls, cls_b, cls_out, cls_out_b);
KAQ:why要额外的 pooling 层
KAQ:qwen3 28 层结构是如何搭建的
struct llm_build_qwen3 : public llm_graph_context {
llm_build_qwen3(const llama_model & model, const llm_graph_params & params) : llm_graph_context(params) {
const int64_t n_embd_head = hparams.n_embd_head_v;
GGML_ASSERT(n_embd_head == hparams.n_embd_head_k);
GGML_ASSERT(n_embd_head == hparams.n_rot);
ggml_tensor * cur;
ggml_tensor * inpL;
// 将输入 token ID 转换为嵌入向量(n_tokens × n_embd)***
// build_inp_embd 使用 ggml_matmul 将 token ID 映射到嵌入矩阵(model.tok_embd)
inpL = build_inp_embd(model.tok_embd);
// 生成位置编码张量(inp_pos),用于 RoPE 编码
ggml_tensor * inp_pos = build_inp_pos();
// 初始化 KV 缓存输入(inp_attn),支持统一 KV 缓存管理(cparams.kv_unified)
// build_attn_inp_kv_unified 为注意力机制准备 KV 缓存张量,存储历史键值对
auto * inp_attn = build_attn_inp_kv_unified();
// 构建输出 ID 索引(inp_out_ids),用于提取特定输出的嵌入向量
ggml_tensor * inp_out_ids = build_inp_out_ids();
// 遍历 28 层 Transformer,每层包含norm、self-attension、FFN 和归一化
for (int il = 0; il < n_layer; ++il) {
ggml_tensor * inpSA = inpL;
// RMSNorm 归一化层, 根均方归一化, 用于注意力机制前的输入向量,减少梯度爆炸/消失
cur = build_norm(inpL,
model.layers[il].attn_norm, NULL,
LLM_NORM_RMS, il);
cb(cur, "attn_norm", il);
// self-attention
{
// build_lora_mm 使用 ggml_matmul,将输入 cur 与权重(wq, wk, wv)相乘,支持 LoRA
ggml_tensor * Qcur = build_lora_mm(model.layers[il].wq, cur);
cb(Qcur, "Qcur", il);
ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur);
cb(Kcur, "Kcur", il);
ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur);
cb(Vcur, "Vcur", il);
Qcur = ggml_reshape_3d(ctx0, Qcur, n_embd_head, n_head, n_tokens);
Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens);
// 归一化 Qcur 和 Kcur, 提升注意力稳定性
Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, NULL, LLM_NORM_RMS, il);
cb(Qcur, "Qcur_normed", il);
// 应用 RoPE 位置编码,旋转 Q、K 张量,融入位置信息, 提升模型对长文本的理解能力
Qcur = ggml_rope_ext(
ctx0, Qcur, inp_pos, nullptr,
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
ext_factor, attn_factor, beta_fast, beta_slow
);
Kcur = build_norm(Kcur, model.layers[il].attn_k_norm, NULL, LLM_NORM_RMS, il);
cb(Kcur, "Kcur_normed", il);
Kcur = ggml_rope_ext(
ctx0, Kcur, inp_pos, nullptr,
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
ext_factor, attn_factor, beta_fast, beta_slow
);
cb(Qcur, "Qcur", il);
cb(Kcur, "Kcur", il);
cb(Vcur, "Vcur", il);
// 执行多头注意力,计算注意力分数,输出通过权重 wo 和偏置 bo 投影
// build_attn 调用 ggml_softmax 和 ggml_matmul,结合 KV 缓存完成注意力计算
cur = build_attn(inp_attn,
model.layers[il].wo, model.layers[il].bo,
Qcur, Kcur, Vcur, nullptr, nullptr, 1.0f/sqrtf(float(n_embd_head)), il);
}
// 如果当前层是最后一层,并且有输出 ID 索引(inp_out_ids),则只计算特定输出的嵌入向量
// 最后一层,选择特定 token 的输出(如 CLS 或最后 token)
if (il == n_layer - 1 && inp_out_ids) {
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids);
}
// 计算前一层的输出与当前层输入的和,用于残差连接, 残差连接增强训练稳定性,符合 Transformer 设计
ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA);
cb(ffn_inp, "ffn_inp", il);
// 对 FFN 输入应用 RMSNorm
cur = build_norm(ffn_inp,
model.layers[il].ffn_norm, NULL,
LLM_NORM_RMS, il);
cb(cur, "ffn_norm", il);
// 构建前馈网络(FFN),包括线性变换、激活函数和残差连接
// build_ffn 使用 ggml_matmul 和 ggml_silu
cur = build_ffn(cur,
model.layers[il].ffn_up, NULL, NULL,
model.layers[il].ffn_gate, NULL, NULL,
model.layers[il].ffn_down, NULL, NULL,
NULL,
LLM_FFN_SILU, LLM_FFN_PAR, il);
cb(cur, "ffn_out", il);
// 残差连接
cur = ggml_add(ctx0, cur, ffn_inp);
cur = build_cvec(cur, il);
cb(cur, "l_out", il);
// 当前层输出作为下一层的输入
inpL = cur;
}
cur = inpL;
cur = build_norm(cur,
model.output_norm, NULL,
LLM_NORM_RMS, -1);
cb(cur, "result_norm", -1);
res->t_embd = cur;
// lm_head
cur = build_lora_mm(model.output, cur);
cb(cur, "result_output", -1);
res->t_logits = cur;
// 将所有节点加入计算图(gf),完成构建. 并没有计算
ggml_build_forward_expand(gf, cur);
}
};
inpL = build_inp_embd(model.tok_embd); 这里将输入 token ID 转换为嵌入向量(n_tokens × n_embd)***。与原理对齐。
build_xxx 函数就是在图添加一层。所以 Qwen3-1.7B 的结构是
28层 Transformer每层包含:
- RMSNorm(Attention 前)
build_norm - Multi head self Attention(是带causal Mask的,带 LoRA、RoPE、Q/K 归一化、KV 缓存)
build_attn。更具体是 flash attention - 残差连接
ggml_add(ctx0, cur, inpSA); - RMSNorm(FFN 前)
build_norm - FFN(带 SiLU 激活、门控机制)
build_ffn - 残差连接
ggml_add(ctx0, cur, ffn_inp);
- RMSNorm(Attention 前)
最终归一化:RMSNorm 应用于最后一层输出。
build_norm输出层:线性变换生成 logits,支持 LoRA。
build_lora_mm
KAQ:每一次的 build_xxx 之后都有 cb(),什么作用?
表示callback,用于调试,不影响graph的构建。cb的类型是:using llm_graph_cb = std::function<void(const llama_ubatch & ubatch, ggml_tensor * cur, const char * name, int il)>;, 是一个函数对象。
KAQ:kv-cache 计算在哪里
在这里:llm_graph_context::build_attn()
KAQ:对于 self-attention 组件的构建,确认有 Causal Mask
具体见 llm_graph_context::build_attn_mha,其中有attention的逻辑 raw 实现(没有调用现成的函数)。
// Q和K的矩阵乘法(相似度计算)
ggml_tensor * kq = ggml_mul_mat(ctx0, k, q);
// Softmax 归一化,包括 Scale和 Mask
kq = ggml_soft_max_ext(ctx0, kq, kq_mask, kq_scale, hparams.f_max_alibi_bias);
// 加权求和(V与Attention权重相乘)
ggml_tensor * kqv = ggml_mul_mat(ctx0, v, kq);
这就是 Attention 的核心计算了。
到此为止是 graph 的构建
没有涉及实际计算。实际计算在 compute_graph, 比如一个计算 kernel 见quant-cuda-kernel