ik_llama.cpp 最小示例 llama-simple 深度解析:从零构建文本生成管线
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
本篇文章围绕 ik_llama.cpp 仓库中 examples/simple 目录下的最小示例展开,完整剖析llama-simple这个可执行程序:它用最少的代码量串联起"加载模型 → 分词 → 构建 batch → 解码 → 采样 → 生成文本"的完整推理管线。读完本文,你将理解 llama.cpp 系 C API 的核心调用顺序、llama_batch的提交机制、贪心采样与 EOG 判断的实现方式,并能读懂llama-simple输出的每一条性能指标,从而为阅读llama-server、main等更复杂的示例打下基础。
llama-simple 是什么
llama-simple是 ik_llama.cpp 中刻意保持"最小化"的示例程序,其官方定位在 examples/simple/README.md 中写得非常明确:演示使用 llama.cpp 以给定 prompt 生成文本的最小用法("demonstrate a minimal usage of llama.cpp for generating text with a given prompt")。
它与 examples/main 的定位完全不同:
main(llama-cli)是功能完备的交互式 CLI,支持对话模板、采样参数微调、语法约束、prompt 缓存等数十个开关;llama-simple只做一件事:输入 prompt,输出续写结果,全程只依赖 C API 加少量common辅助函数,不涉及任何对话历史、模板渲染或高级采样。
因此,它非常适合作为理解 llama.cpp 类推理引擎工作流程的第一份源码教材。
构建与运行
构建方式
llama-simple随示例整体一起构建,其构建规则见 examples/simple/CMakeLists.txt:
set(TARGET llama-simple) add_executable(${TARGET} simple.cpp) install(TARGETS ${TARGET} RUNTIME) target_link_libraries(${TARGET} PRIVATE common llama ${CMAKE_THREAD_LIBS_INIT}) target_compile_features(${TARGET} PRIVATE cxx_std_11)要点:
- 生成的可执行文件名为
llama-simple,且会被install安装; - 链接目标是
common和llama两个库,外加线程库;common提供参数解析与批量操作辅助函数,llama提供全部推理 C API; - 编译标准为 C++11;
- 该示例通过 examples/CMakeLists.txt 中的
add_subdirectory(simple)被纳入整体构建。
因此在仓库根目录执行常规的 CMake 配置与构建后,即可在构建目录中得到llama-simple可执行文件。
运行命令
官方 README 给出的运行命令如下(模型路径请替换为你本地的 GGUF 文件):
./llama-simple -m ./models/llama-7b-v2/ggml-model-f16.gguf -p "Hello my name is"其中:
-m指定模型文件(GGUF 格式);-p指定输入提示词(prompt)。
由于simple.cpp在代码里已经把默认参数预设为params.prompt = "Hello my name is"和params.n_predict = 32(见下文源码分析),所以即使只传-m不传-p,程序也会使用这个默认 prompt 续写 32 个 token。
运行输出逐行解读
README 给出了一个典型运行输出,我们逐段解释其含义:
./llama-simple -m ./models/llama-7b-v2/ggml-model-f16.gguf -p "Hello my name is" ... main: n_len = 32, n_ctx = 2048, n_parallel = 1, n_kv_req = 32 Hello my name is Shawn and I'm a 20 year old male from the United States. I'm a 20 year old main: decoded 27 tokens in 2.31 s, speed: 11.68 t/s llama_print_timings: load time = 579.15 ms llama_print_timings: sample time = 0.72 ms / 28 runs ( 0.03 ms per token, 38888.89 tokens per second) llama_print_timings: prompt eval time = 655.63 ms / 10 tokens ( 65.56 ms per token, 15.25 tokens per second) llama_print_timings: eval time = 2180.97 ms / 27 runs ( 80.78 ms per token, 12.38 tokens per second) llama_print_timings: total time = 2891.13 ms- 第一行日志是 simple.cpp 中打印的:
n_predict = 32(计划生成 32 个 token)、n_ctx = 2048(模型上下文窗口)、n_kv_req = 32(本次请求实际需要的 KV cache 空间),并检查n_kv_req > n_ctx以确保 KV cache 足够容纳 prompt 与生成 token; - 中间两行是真实生成结果:模型在 "Hello my name is" 之后续写出了 "Shawn and I'm a 20 year old male...";
decoded 27 tokens in 2.31 s, speed: 11.68 t/s是主循环统计:实际解码了 27 个 token(因为遇 EOG 提前结束),吞吐约 11.68 token/s;- 最后 5 行
llama_print_timings是llama_print_timings(ctx)打印的详细计时(对应源码 simple.cpp):load time:模型文件加载耗时;sample time:采样(贪心选择)耗时,28 runs 累计 0.72 ms;prompt eval time:prompt 预填充(prefill)阶段耗时,10 个 token 用了 655.63 ms,约 15.25 token/s;eval time:逐 token 生成(decode)阶段耗时,27 次解码 2180.97 ms,约 12.38 token/s;total time:总计 2.89 s。
注意:上述数字是 README 记录的一次参考运行结果,受 CPU/GPU 环境与模型影响极大,你在本机跑出的数值通常不同;
prompt eval与eval的吞吐差异正是"并行预填充 vs 逐 token 自回归"两种计算模式的典型体现。
源码级剖析:llama-simple 的七步推理管线
simple.cpp 全文只有 175 行,主流程清晰划分为七个阶段。下面结合源码逐段讲解。
第一步:参数解析与默认值
gpt_params params; params.prompt = "Hello my name is"; params.n_predict = 32; if (!gpt_params_parse(argc, argv, params)) { print_usage(argc, argv, params); return 1; }gpt_params定义于 common/common.h,是common库统一使用的参数聚合结构。程序先把prompt与n_predict设好默认值,再用gpt_params_parse覆盖为命令行传入值;解析失败时调用gpt_params_print_usage打印用法(simple.cpp)。
gpt_params_parse在 common/common.cpp 中实现,支持大量参数。与本示例直接相关的有:
| 参数 | 含义 | 默认值(gpt_params结构) |
|---|---|---|
-m, --model | 模型文件路径 | 空字符串 |
-p, --prompt | 提示词 | 空(simple 内覆盖为 "Hello my name is") |
-n, --predict, --n-predict | 生成 token 数,-1表示无限,-2表示直到填满上下文 | -1(见 common/common.h) |
-c, --ctx-size | 上下文大小 | 0(使用模型默认) |
-t, --threads | CPU 计算线程数 | cpu_get_num_math() |
-b, --batch-size | 逻辑批大小(prompt 处理) | 2048 |
-ub, --ubatch-size | 物理批大小 | 512 |
--numa | 启用 NUMA 支持 | 关闭 |
-n的解析逻辑在 common/common.cpp 中可见:if (arg == "-n" || arg == "--predict" || arg == "--n-predict") { ... params.n_predict = std::stoi(argv[i]); }。
第二步:后端初始化
llama_backend_init(); llama_numa_init(params.numa);llama_backend_init()负责初始化后端(CPU 线程池、BLAS 等),llama_numa_init则按参数启用 NUMA 感知调度(仅在--numa开启时有实际作用)。
第三步:加载模型
llama_model_params model_params = common_model_params_to_llama(params); llama_model * model = llama_model_load_from_file(params.model.c_str(), model_params);common_model_params_to_llama(声明见 common/common.h)把gpt_params转换为底层 C API 的llama_model_params结构,随后llama_model_load_from_file从磁盘加载 GGUF 模型。若返回NULL,程序打印 "unable to load model" 并退出。
第四步:创建推理上下文
llama_context_params ctx_params = common_context_params_to_llama(params); llama_context * ctx = llama_init_from_model(model, ctx_params);上下文(llama_context)承载 KV cache 与运行时状态。这一步会把n_ctx(上下文窗口)等参数固化到实际运行环境。
第五步:分词与 KV 容量预检
std::vector<llama_token> tokens_list; tokens_list = ::common_tokenize(ctx, params.prompt, true); const int n_ctx = llama_n_ctx(ctx); const int n_kv_req = tokens_list.size() + (n_predict - tokens_list.size()); if (n_kv_req > n_ctx) { LOG_TEE("%s: error: n_kv_req > n_ctx, the required KV cache size is not big enough\n", __func__); LOG_TEE("%s: either reduce n_predict or increase n_ctx\n", __func__); return 1; }common_tokenize(common/common.h)把 prompt 字符串切成 token 序列,第三个参数add_special = true表示自动附加特殊 token(如 BOS);n_kv_req估算本次请求需要占用的 KV 位置数:prompt token 数 + 待生成 token 数(此处假设会生成满n_predict个);- 若超出
n_ctx,程序给出两条修复建议:减小n_predict或增大n_ctx(通过-c参数)。
随后程序还会把 prompt 逐 token 还原打印到 stderr(调用common_token_to_piece),方便确认分词是否正确。
第六步:构建 batch 并预填充 prompt
llama_batch batch = llama_batch_init(512, 0, 1); for (size_t i = 0; i < tokens_list.size(); i++) { common_batch_add(batch, tokens_list[i], i, { 0 }, false); } batch.logits[batch.n_tokens - 1] = true; if (llama_decode(ctx, batch) != 0) { LOG_TEE("%s: llama_decode() failed\n", __func__); return 1; }这是全程序最关键的部分,理解它就能理解 llama.cpp 的批处理模型:
llama_batch_init(512, 0, 1)分配一个最多容纳 512 个 token、每个 token 最多属于 1 个序列的 batch(API 说明见 include/llama.h),embd = 0表示不使用 embedding 输入;common_batch_add把 prompt 的每个 token 按位置i依次填入 batch(辅助函数声明见 common/common.h);batch.logits[batch.n_tokens - 1] = true是刻意为之:只要求解码器为 batch 中最后一个 token 输出 logits,因为生成阶段只需要最后一个位置的概率分布;llama_decode(ctx, batch)一次性并行处理整个 prompt(即 prefill/预填充阶段),这也是为什么 README 输出中 prompt eval 10 个 token 只花了 655 ms——并行预填充远快于逐 token 生成。
第七步:自回归生成主循环
while (n_cur <= n_predict) { // 1. 取最后一个 token 的 logits auto n_vocab = llama_n_vocab(model); auto * logits = llama_get_logits_ith(ctx, batch.n_tokens - 1); // 2. 构造候选数组 std::vector<llama_token_data> candidates; for (llama_token token_id = 0; token_id < n_vocab; token_id++) { candidates.emplace_back(llama_token_data{ token_id, logits[token_id], 0.0f }); } llama_token_data_array candidates_p = { candidates.data(), candidates.size(), false }; // 3. 贪心采样 const llama_token new_token_id = llama_sample_token_greedy(ctx, &candidates_p); // 4. EOG 判断 if (llama_token_is_eog(model, new_token_id) || n_cur == n_predict) { break; } // 5. 输出 token 并放入新 batch LOG_TEE("%s", common_token_to_piece(ctx, new_token_id).c_str()); common_batch_clear(batch); common_batch_add(batch, new_token_id, n_cur, { 0 }, true); // 6. 解码新 token if (llama_decode(ctx, batch)) { ... } }循环内每轮迭代执行"取 logits → 采样 → 判 EOG → 提交单 token → 解码"的闭环:
llama_get_logits_ith(include/llama.h)取第i个位置的 logits 指针(-1表示最后一个位置);程序把整个词表的 logits 全部包装进llama_token_data_array;llama_sample_token_greedy(include/llama.h)执行贪心采样——直接选概率最高的 token。API 注释明确指出它"不计算 token 概率"(即不做 softmax 归一化也能取 argmax),因此速度极快,这也解释了 README 中sample time只有 0.72 ms;llama_token_is_eog(include/llama.h)判断当前 token 是否为"结束生成"类 token(EOS、EOT 等),命中即终止,所以示例中实际只解码了 27 个 token 而非满 32 个;common_batch_clear+common_batch_add复用同一块 batch 内存,每轮只提交一个新 token(add_special参数此时为true,表示需要计算 logits);- 主循环结束时调用
llama_print_timings(ctx)输出计时,随后依次llama_batch_free、llama_free(ctx)、llama_free_model(model)、llama_backend_free()释放全部资源——这是每个 llama.cpp 程序都应当遵循的对称清理顺序。
从 llama-simple 出发:下一步进阶路线
llama-simple刻意省略了真实应用需要的许多环节,阅读源码时你可以对照以下清单观察它"没做什么",并据此规划进阶方向:
- 采样策略:simple 只用贪心。更丰富的采样(top-k、top-p、温度、重复惩罚等)由
llama_sample_*系列 API 与common_sampler提供,可参考 common/sampling.cpp 与 examples/main/main.cpp; - 多序列与批处理复用:simple 单序列单请求,
llama_batch_init(512, 0, 1)的第三个参数n_seq_max = 1;多序列并行、KV cache 复用的完整实现见 examples/server(llama-server)与 examples/batched; - 对话与模板:simple 无聊天模板渲染;
llama-server通过 models/templates 下的 jinja 模板支持多轮对话与工具调用; - 性能测量:
llama_print_timings给出的五段计时是通用指标,配合 examples/llama-bench 可以做更系统的基准对比。
如果你想把llama-simple改成自己的第一个推理程序,最自然的三个实验是:把llama_sample_token_greedy换成llama_sample_token(ctx, &candidates_p)(配合common_sampler设定温度/top-p);用-c 4096扩大上下文并观察 KV 预检行为;或把common_tokenize的add_special改为false观察特殊 token 对生成质量的影响。
小结
llama-simple用约 175 行代码完整呈现了 llama.cpp 系推理引擎的最小可行管线:参数解析 → 后端初始化 → 模型加载 → 上下文创建 → 分词与 KV 容量检查 → batch 预填充 → 自回归采样生成。它的每个环节都对应一组明确的 C API 调用(llama_decode、llama_get_logits_ith、llama_sample_token_greedy、llama_token_is_eog),这些调用同样构成了llama-cli、llama-server等重量级程序的地基。理解了这个最小闭环,你就掌握了阅读 ik_llama.cpp 全部推理代码的钥匙。
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考