news 2026/9/24 18:14:59

PowerInfer 极简文本生成指南:基于 examples/simple 源码剖析最小 LLM 推理流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PowerInfer 极简文本生成指南:基于 examples/simple 源码剖析最小 LLM 推理流程
  • 人工智能
  • 大模型
  • 推理引擎
  • 本地部署

【免费下载链接】PowerInfer

High-speed Large Language Model Serving for Local Deployment

项目地址:https://gitcode.com/gh_mirrors/po/PowerInfer
点击查看免费下载

PowerInfer 是一个面向本地部署的高性能大语言模型(LLM)推理引擎,其核心思路是利用 LLM 推理中神经元激活的幂律分布特性(少量"热"神经元被持续激活、绝大多数"冷"神经元随输入变化),通过 GPU-CPU 混合推理实现单消费级显卡上的高速文本生成。examples/simple是仓库中最小的端到端示例,它不带任何交互逻辑、采样策略或并行调度,只保留"加载模型 → 分词 → 解码 → 贪心采样 → 输出文本"这条最纯粹的主链路。阅读本文后,你将掌握 PowerInfer/llama.cpp 兼容 API 的最核心调用序列,理解llama_batch、KV Cache、logits 与贪心采样之间的协作关系,并能基于此最小骨架自行扩展出更多示例。

一、示例定位与运行方式

1.1 这个示例解决什么问题

examples/simple/README.md明确指出:该示例的用途是演示使用给定 prompt 生成文本的最小 llama.cpp(及其兼容引擎 PowerInfer)用法。它没有交互模式、没有批处理、没有 grammar 约束,甚至没有温度采样——代码里只用贪心策略选取概率最高的下一个 token。

对应源码位于 examples/simple/simple.cpp。从main函数开头可以看出它的接口极其简单:

if (argc == 1 || argv[1][0] == '-') { printf("usage: %s MODEL_PATH [PROMPT]\n" , argv[0]); return 1; }

只接受两个位置参数:模型路径和(可选的)prompt。examples/simple/README.md中的运行示例为:

./simple ./models/llama-7b-v2/ggml-model-f16.gguf "Hello my name is"

1.2 构建方式与产物

simple示例通过 CMake 构建。其编译配置见 examples/simple/CMakeLists.txt:

set(TARGET 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)
  • 依赖common(common/CMakeLists.txt 定义的参数解析与工具库)和llama(核心推理库);
  • 需要 C++11 标准;
  • 在 examples/CMakeLists.txt 中被add_subdirectory(simple)引入,随整个 examples 树一起构建。

从仓库根目录构建(以 NVIDIA GPU 场景为例,详见 README.md 的 Setup and Installation 一节):

cmake -S . -B build -DLLAMA_CUBLAS=ON cmake --build build --config Release -j

构建完成后,可执行文件位于build/bin/simple(或构建目录的对应 bin 子目录)。

1.3 参数默认值

simple示例没有使用gpt_params_parse那套完整的 CLI 解析(那是main示例的能力),而是直接读取argv[1]argv[2]

if (argc >= 2) { params.model = argv[1]; } if (argc >= 3) { params.prompt = argv[2]; } if (params.prompt.empty()) { params.prompt = "Hello my name is"; }

也就是说:

  • 第一个参数:模型文件路径,必填;
  • 第二个参数:prompt,可省略;省略时使用默认 prompt"Hello my name is"
  • 生成长度:硬编码为n_len = 32,即总序列长度(含 prompt)最多 32 个 token。

这些字段来自gpt_params结构体(common/common.h),其中params.model默认指向"models/7B/ggml-model-f16.gguf"params.prompt默认为空字符串,params.n_threads默认取物理核心数(get_num_physical_cores())。

二、初始化:从后端到模型与上下文

2.1 后端初始化

llama_backend_init(params.numa);

对应 llama.h 中的声明:

// Initialize the llama + ggml backend // If numa is true, use NUMA optimizations // Call once at the start of the program LLAMA_API void llama_backend_init(bool numa);

它负责初始化 llama 与 ggml 底层后端,程序开始时调用一次。numa参数用于启用 NUMA 优化(对多路 CPU 服务器有帮助)。程序结束时对应的清理函数是llama_backend_free()

2.2 加载模型

llama_model_params model_params = llama_model_default_params(); // model_params.n_gpu_layers = 99; // offload all layers to the GPU llama_model * model = llama_load_model_from_file(params.model.c_str(), model_params);
  • llama_model_default_params()返回模型加载参数的默认值;
  • 代码中注释掉的model_params.n_gpu_layers = 99是 GPU 卸载的关键开关:在 PowerInfer 的 GPU-CPU 混合推理设计里,更多层卸载到 GPU 通常意味着更高吞吐,但受显存限制;
  • llama_load_model_from_file()在 llama.h 中的签名为:
LLAMA_API struct llama_model * llama_load_model_from_file( const char * path_model, struct llama_model_params params);

加载失败时返回NULL,示例会打印unable to load model并退出。

2.3 创建上下文(Context)

llama_context_params ctx_params = llama_context_default_params(); ctx_params.seed = 1234; ctx_params.n_ctx = 2048; ctx_params.n_threads = params.n_threads; ctx_params.n_threads_batch = params.n_threads_batch == -1 ? params.n_threads : params.n_threads_batch; llama_context * ctx = llama_new_context_with_model(model, ctx_params);

三个关键字段:

  • seed = 1234:随机数种子固定,保证每次运行结果可复现(由于采样是贪心的,种子影响相对有限,但在引入随机采样后会起作用);
  • n_ctx = 2048:上下文窗口大小,即 KV Cache 能容纳的最大 token 数;
  • n_threads/n_threads_batch:解码线程数。n_threads_batch == -1时回退为n_threads,即批处理线程数默认跟随生成线程数。

llama_new_context_with_model()的声明同样在 llama.h:

LLAMA_API struct llama_context * llama_new_context_with_model( struct llama_model * model, struct llama_context_params params);

创建失败同样返回NULL并报错退出。

三、分词与 KV Cache 容量校验

3.1 分词

std::vector<llama_token> tokens_list; tokens_list = ::llama_tokenize(ctx, params.prompt, true);

第三个参数add_bos = true表示在 prompt 前自动加上句首标记(BOS)。llama_tokenize是 common/common.h 提供的便捷封装,其头注释明确说明行为应与 Python 的tokenizer.encode类似。

3.2 KV Cache 容量校验

const int n_ctx = llama_n_ctx(ctx); const int n_kv_req = tokens_list.size() + (n_len - 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_parallel or increase n_ctx\n", __func__); return 1; }

这里n_kv_req = tokens_list.size() + (n_len - tokens_list.size()),实际上恒等于n_len(当 prompt token 数小于n_len时)。它的含义是:要生成到n_len长度,KV Cache 总共需要容纳的 token 数。如果该需求超过了n_ctx,就无法继续,示例会提示"要么降低并行度、要么增大上下文窗口"。这正是 KV Cache 设计的基本约束:prompt 与已生成 token 的 KV 状态都要驻留在缓存中,直到序列结束。

随后示例把 prompt 逐 token 打印到 stderr:

for (auto id : tokens_list) { fprintf(stderr, "%s", llama_token_to_piece(ctx, id).c_str()); }

llama_token_to_piece把 token id 还原为文本片段,相当于 Python 的tokenizer.id_to_piece

四、核心解码循环:batch → decode → sample

4.1 初始化 batch

llama_batch batch = llama_batch_init(512, 0, 1);

llama_batch_init在 llama.h 中这样定义:

// Allocates a batch of tokens on the heap that can hold a maximum of n_tokens // Each token can be assigned up to n_seq_max sequence ids // The batch has to be freed with llama_batch_free() // If embd != 0, llama_batch.embd will be allocated with size of n_tokens * embd * sizeof(float) // Otherwise, llama_batch.token will be allocated to store n_tokens llama_token LLAMA_API struct llama_batch llama_batch_init( int32_t n_tokens, int32_t embd, int32_t n_seq_max);

三个参数分别表示:batch 最大可容纳 token 数(512)、嵌入维度(0 表示普通 token 模式而非 embedding 输入)、每个 token 最多可关联的序列数(1,即单序列场景)。

4.2 提交 prompt 并解码

for (size_t i = 0; i < tokens_list.size(); i++) { llama_batch_add(batch, tokens_list[i], i, { 0 }, false); } // llama_decode will output logits only for the last token of the prompt batch.logits[batch.n_tokens - 1] = true; if (llama_decode(ctx, batch) != 0) { LOG_TEE("%s: llama_decode() failed\n", __func__); return 1; }
  • llama_batch_add逐个把 prompt token 放入 batch,位置pos = i,序列 id 为{0}logits = false表示默认不输出该 token 的 logits;
  • 关键优化点:只有 batch 中最后一个 token 的logits被置为 true。注释解释了原因——llama_decode只为最后那个 token 输出 logits,这正是预测"下一个 token"所必需的,从而避免为整个 prompt 计算并保留 logits 的开销;
  • llama_decode的返回值语义见 llama.h:
// Positive return values does not mean a fatal error, but rather a warning. // 0 - success // 1 - could not find a KV slot for the batch (try reducing the size of the batch or increase the context) // < 0 - error LLAMA_API int llama_decode( struct llama_context * ctx, struct llama_batch batch);

返回 1 表示 KV Cache 中没有足够槽位容纳该 batch——这是减小 batch 或增大n_ctx的信号。

4.3 采样与生成循环

int n_cur = batch.n_tokens; // 当前序列长度(从 prompt 长度开始) int n_decode = 0; while (n_cur <= n_len) { auto n_vocab = llama_n_vocab(model); auto * logits = llama_get_logits_ith(ctx, batch.n_tokens - 1); std::vector<llama_token_data> candidates; candidates.reserve(n_vocab); 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 }; const llama_token new_token_id = llama_sample_token_greedy(ctx, &candidates_p); if (new_token_id == llama_token_eos(model) || n_cur == n_len) { break; } LOG_TEE("%s", llama_token_to_piece(ctx, new_token_id).c_str()); llama_batch_clear(batch); llama_batch_add(batch, new_token_id, n_cur, { 0 }, true); n_decode += 1; n_cur += 1; if (llama_decode(ctx, batch)) { fprintf(stderr, "%s : failed to eval, return code %d\n", __func__, 1); return 1; } }

逐步拆解:

  1. 取 logitsllama_get_logits_ith(ctx, batch.n_tokens - 1)返回最近一次解码 batch 中第batch.n_tokens - 1个 token 的 logits 指针。按 llama.h 的说明,它等价于llama_get_logits(ctx) + i*n_vocab,是一个n_vocab长度的浮点数组,每个元素对应词表中的一个候选 token。

  2. 构造候选数组:把 logits 逐项包装成llama_token_data{ token_id, logit, 0.0f },构成llama_token_data_arrayp(概率)字段初始为 0,由采样函数内部计算。

  3. 贪心采样llama_sample_token_greedy在 llama.h 中说明为"选择概率最高的 token,不计算 token 概率":

/// @details Selects the token with the highest probability. /// Does not compute the token probabilities. Use llama_sample_softmax() instead. LLAMA_API llama_token llama_sample_token_greedy( struct llama_context * ctx, llama_token_data_array * candidates);
  1. 终止判断:若采到 EOS(llama_token_eos(model),即句末标记)或达到n_len长度上限,退出循环。llama_token_eos在 llama.h 中与llama_token_bos(句首)、llama_token_nl(换行)并列,属于特殊 token 查询接口。

  2. 单 token 续接llama_batch_clear清空 batch,再llama_batch_add把新 token 以位置n_curlogits = true加入,随后llama_decode。这里体现了自回归生成的标准形态:每个新生成的 token 都作为下一次解码的输入,而 KV Cache 会保存此前所有 token 的键值状态,因此无需重新计算历史。

llama_batch_clearllama_batch_add同样是 common/common.h 声明的 batch 工具函数,负责内部数组的重置与追加。

4.4 计时与清理

llama_print_timings(ctx); ... llama_batch_free(batch); llama_free(ctx); llama_free_model(model); llama_backend_free();
  • llama_print_timings输出分阶段的耗时统计(load time、sample time、prompt eval time、eval time、total time);
  • 四步清理与初始化严格对称:释放 batch → 释放上下文 → 释放模型 → 释放后端。

五、输出解读:一次真实运行的性能指标

examples/simple/README.md给出了示例输出,逐行解读如下:

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
  • n_kv_req = 32:即前文计算的 KV Cache 需求(prompt 的 10 个 token + 待生成的 22 个 token 之和不超过 32,实际输出 27 个新 token 后命中 EOS 或长度条件);
  • decoded 27 tokens in 2.31 s, speed: 11.68 t/s:这是示例自己统计的生成吞吐(不含 prompt 预填充阶段);
  • prompt eval time:prompt 预填充(prefill)阶段,10 个 token 共 655.63 ms,这是并行计算的,因此单 token 65.56 ms 但整体吞吐 15.25 tokens/s;
  • eval time:自回归解码(decode)阶段,27 个 token 共 2180.97 ms,每 token 80.78 ms,12.38 tokens/s——逐 token 串行,所以单 token 耗时与整体吞吐直接对应;
  • sample time:采样本身极快(0.72 ms / 28 runs),几乎不构成瓶颈。

需要说明的是:这些数字来自该文档在示例机器(模型为 llama-7b 系 f16 量化)上的真实输出,具体数值会随硬件、模型、量化方式与线程数变化,不具备跨环境可比性。读者在自己的机器上运行时会得到不同的绝对数值,但"prefill 并行、decode 串行、采样开销极低"这一规律是稳定的。

六、从最小示例到完整应用:扩展路径

simple之所以叫 simple,是因为它刻意省略了生产级能力。对照仓库中的其他示例与公共库,可以沿以下方向扩展:

  • 完整 CLI 参数main示例(examples/main/main.cpp)通过gpt_params_parse(common/common.h 声明)支持-n(生成 token 数)、-c(上下文大小)、-t(线程数)、--seed-ngl(GPU 层数)等全套参数;
  • 更丰富的采样策略:本示例只用llama_sample_token_greedy。C API 还提供llama_sample_token_mirostatllama_sample_token_mirostat_v2(Mirostat 1.0/2.0,目标困惑度控制)、llama_sample_token(按概率随机采样)等,见 llama.h;common/sampling.h中的llama_sampling_context则封装了 temperature、top-k、top-p、penalty 等完整采样管线;
  • 多序列批处理:把llama_batch_init的第三个参数n_seq_max从 1 改为更大值、为不同 token 分配不同seq_id,即可并行处理多条序列——这是 examples/batched/batched.cpp 展示的用法;
  • 服务化部署:以simple的模型加载与解码逻辑为骨架,套上 HTTP 层,就是 examples/server/server.cpp 所做的事情,支持 OpenAI 兼容接口的本地服务;
  • GPU 加速:PowerInfer 的 GPU-CPU 混合推理(README.md 中描述的 hot/cold 神经元分工)依赖 CUDA 后端,构建时通过-DLLAMA_CUBLAS=ON开启,示例源码中注释掉的model_params.n_gpu_layers = 99即提示了层级卸载入口。

七、小结

examples/simple以约 180 行 C++ 代码完整呈现了 PowerInfer/llama.cpp 兼容 API 的最小推理闭环:

  1. llama_backend_init→ 初始化后端;
  2. llama_load_model_from_file+llama_new_context_with_model→ 装载模型与上下文;
  3. llama_tokenize→ prompt 分词;
  4. llama_batch_init+llama_batch_add+llama_decode→ 预填充并取得 logits;
  5. llama_get_logits_ith+llama_sample_token_greedy→ 贪心采样;
  6. 循环"单 token 解码 → 采样 → 回填 batch",直到 EOS 或长度上限;
  7. llama_print_timings输出分阶段耗时,对称清理资源。

理解这七步,就等于掌握了整个 PowerInfer/llama.cpp 生态的通用推理范式:batch 提交、KV Cache 管理、logits 采样与自回归续接。无论是改造成交互式聊天、接入批处理服务,还是换成更复杂的采样管线,都是在这条主链路上做加法。

  • 人工智能
  • 大模型
  • 推理引擎
  • 本地部署

【免费下载链接】PowerInfer

High-speed Large Language Model Serving for Local Deployment

项目地址:https://gitcode.com/gh_mirrors/po/PowerInfer
点击查看免费下载

相关推荐

上一篇:Android-Image-Cropper终极迁移指南:从旧版本升级到最新版本的完整教程
下一篇:Zotero Style:学术文献管理的视觉化革命与智能工作流构建

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 18:14:47

SSM+Layui+ECharts酒店系统实战:Vo层设计与图表数据对接

简介&#xff1a;这是一套面向JavaWeb初学者与课程设计实践者的酒店管理系统完整项目源码&#xff0c;聚焦预订、入住、退房及客房统计等核心业务场景&#xff0c;助力掌握SSM框架整合开发、前后端协同与数据可视化落地能力。资源共523个文件&#xff0c;涵盖103个Java后端逻辑…

作者头像 李华
网站建设 2026/9/24 18:13:27

真实省域PM2.5时序预测:LSTM全流程实战与避坑指南

简介&#xff1a;本资源是一份面向计算机及相关专业学生的高分期末大作业实战项目&#xff0c;基于Python实现空气质量数据的LSTM时序建模、预测与可视化分析&#xff0c;适用于课程设计、毕业设计及AI项目入门实践。资源包共260个文件&#xff0c;含15个核心Python脚本&#x…

作者头像 李华
网站建设 2026/9/24 18:13:23

高光谱图像PCA-KNN处理全流程与避坑指南

简介&#xff1a;本资源是一个面向遥感、农业、地质等领域的高光谱图像处理MATLAB工具包&#xff0c;聚焦PCA降维、KNN分类与CNN深度学习三大核心算法的工程化实现&#xff0c;适用于具备基础信号处理与机器学习知识的科研人员及高校研究生开展高光谱图像分类、目标识别与异常检…

作者头像 李华
网站建设 2026/9/24 18:13:10

OPNET OSPF动态路由实验配置验证与排错指南

简介&#xff1a;OSPF 是内部网关协议中广泛应用的链路状态路由协议&#xff0c;而 Riverbed OpNet 是业界常用的网络仿真与性能分析平台。该资源是一份面向网络工程师、运维人员及高校学生的 OSPF 仿真项目包&#xff0c;基于 OpNet 环境搭建了完整的 OSPF 网络模型&#xff0…

作者头像 李华
网站建设 2026/9/24 18:12:48

HTML 的 <table> 元素

1. 引言 在网页开发中&#xff0c;表格是展示结构化数据最直观的方式之一。无论是商品列表、成绩单、财务报表&#xff0c;还是后台管理系统的数据展示&#xff0c;<table> 元素都扮演着不可或缺的角色。本文将带你系统学习 HTML 表格的完整知识体系&#xff0c;从基础语…

作者头像 李华
网站建设 2026/9/24 18:12:02

基于Python与OpenCV的人脸识别门禁系统开发实战

简介&#xff1a;这是一套基于Python的人脸识别智能小区门禁管理系统源码&#xff0c;面向Python学习者、计算机专业学生及安防系统开发者&#xff0c;用于解决小区出入身份验证与门禁自动化管理问题。资源包共101个文件&#xff0c;大小约12.17MB&#xff0c;文件类型涵盖.py源…

作者头像 李华