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)的

三个核心步骤:

  1. forward pass 的实际计算,每生成一个token 前都需要一次 forward 计算。对应code:llama_decode(ctx, batch)

  2. 使用采样器,采样 token 作为当前步骤的新 token。code:new_token_id = llama_sampler_sample(smpl, ctx, -1);

  3. 将新的 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 函数本身不直接执行实际的矩阵乘法等底层操作。相反,它是一个封装了所有关键步骤的接口,包括:

  1. 准备数据:将当前 batch 中的 tokens 转换为适合模型处理的格式。
  2. 前向传播:调用模型的内部实现来进行实际的数学运算,如矩阵乘法和激活函数的计算。
  3. 输出处理:从模型中提取出用于下一步采样的概率分布或直接的 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_decodectx->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 应用于最后一层输出。 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