- 人工智能
- 大模型
- 推理引擎
- 本地部署
【免费下载链接】PowerInfer
High-speed Large Language Model Serving for Local Deployment
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; } }逐步拆解:
取 logits:
llama_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。构造候选数组:把 logits 逐项包装成
llama_token_data{ token_id, logit, 0.0f },构成llama_token_data_array。p(概率)字段初始为 0,由采样函数内部计算。贪心采样:
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);终止判断:若采到 EOS(
llama_token_eos(model),即句末标记)或达到n_len长度上限,退出循环。llama_token_eos在 llama.h 中与llama_token_bos(句首)、llama_token_nl(换行)并列,属于特殊 token 查询接口。单 token 续接:
llama_batch_clear清空 batch,再llama_batch_add把新 token 以位置n_cur、logits = true加入,随后llama_decode。这里体现了自回归生成的标准形态:每个新生成的 token 都作为下一次解码的输入,而 KV Cache 会保存此前所有 token 的键值状态,因此无需重新计算历史。
llama_batch_clear与llama_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 msn_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_mirostat、llama_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 的最小推理闭环:
llama_backend_init→ 初始化后端;llama_load_model_from_file+llama_new_context_with_model→ 装载模型与上下文;llama_tokenize→ prompt 分词;llama_batch_init+llama_batch_add+llama_decode→ 预填充并取得 logits;llama_get_logits_ith+llama_sample_token_greedy→ 贪心采样;- 循环"单 token 解码 → 采样 → 回填 batch",直到 EOS 或长度上限;
llama_print_timings输出分阶段耗时,对称清理资源。
理解这七步,就等于掌握了整个 PowerInfer/llama.cpp 生态的通用推理范式:batch 提交、KV Cache 管理、logits 采样与自回归续接。无论是改造成交互式聊天、接入批处理服务,还是换成更复杂的采样管线,都是在这条主链路上做加法。
- 人工智能
- 大模型
- 推理引擎
- 本地部署
【免费下载链接】PowerInfer
High-speed Large Language Model Serving for Local Deployment
相关推荐
PowerInfer smallthinker 最小示例解析:用 llama-simple 跑通本地 LLM 文本生成全流程
PowerInfer smallthinker 最小示例解析:用 llama simple 跑通本地 LLM 文本生成全流程 llama simple 是 Po
人工智能大模型推理引擎本地部署PowerInfer 中基于猜测解码(Speculative Decoding)的快速生成:speculative-simple 示例实战与原理剖析
PowerInfer 中基于猜测解码(Speculative Decoding)的快速生成:speculative simple 示例实战与原理剖析 导读 本文
人工智能大模型推理引擎本地部署LLM本地推理全流程:基于LMDeploy的pipeline实战指南
在大语言模型(LLM)应用落地过程中,本地环境的高效推理部署是开发者面临的核心挑战之一。LMDeploy作为一款轻量级推理框架,通过其pipeline API为
大模型多模态深度学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考