llama.cpp是一个高性能的CPU/GPU大语言模型推理框架,适用于消费级设备或边缘设备。开发者可以通过工具将各类开源大语言模型转换并量化成gguf格式的文件,然后通过llama.cpp实现本地推理。经过我的调研,相比较其它大模型落地方案,中小型研发企业使用llama.cpp可能是唯一的产品落地方案。关键词:“中小型研发企业”,“产品落地方案”。

中小型研发企业:相较动辄千万+的硬件投入,中小型研发企业只能支撑少量硬件投入,并且也缺少专业的研发人员。

产品落地方案:项目需要具备在垂直领域落地的能力,大多数情况下还需要私有化部署。

网上有不少介绍的文章,B站上甚至有一些收费课程。但是版本落后较多,基本已经没有参考价值。本文采用b3669版本,发布日期是2024年9月,参考代码:examples/main.cpp。由于作者(Georgi Gerganov)没有提供详细的接口文档,examples的代码质量也确实不高,因此学习曲线比较陡峭。本文旨在介绍如何使用llama.cpp进行推理和介绍重点函数,帮助开发人员入门,深入功能还有待研究。

一、推理流程

1. 过程描述

以常见的交互推理为例,程序大概可以分成5个子功能模块。

初始化:模型和系统提示词初始化。其实从程序处理过程上分析,并没有特别区分系统提示词与用户输入,实际项目开发中完全可以放在一起处理。后面会再解释它们在概念上的区别。

用户输入:等待用户输入文本信息。大语言模型其实就是对人类的文本信息进行分析和理解的过程,而产品落地的本质就是借助大模型的理解进一步完成一些指定任务。在这个过程中,互联网上又造了许多概念,什么agent,function等。其实本质上都是在研究如何将大模型与程序进一步结合并完成交互。至少目前,我的观点是:大模型仅具备语义分析,语义推理的能力。

分析预测:这个是大语言模型的核心能力之一,它需要分析上下文(系统提示词、用户输入、已推理的内容)再进一步完成下一个词语(token)的预测。

推理采样:这个是大语言模型的另一个核心能力,它需要从分析预测的结果中随机选择一个token,并将它作为输入反向发送给分析预测模块继续进行,直到输出结束(EOS)。

输出:这个模块严格说不属于大模型,但是它又是完成用户交互必须模块。从产品设计上,可以选择逐字输出(token-by-token)或者一次性输出(token-by-once)。

2. 概念介绍

角色(roles):大语言模型通常会内置三种角色:系统(system),用户(user),助手(assistant)。这三种角色并非所有模型统一指定,但是基本目前所有开源的大模型都兼容这三种角色的交互,它有助于大模型更好的理解人类语境并完成任务。system表示系统提示词,就是我们常说的prompt。网上有不少课程将写系统提示词描述为提示词工程,还煞有介事的进行分类,其实大可不必。从我的使用经验看,一个好的系统提示词(prompt)应具备三个要点即可:语义明确,格式清晰,任务简单。语义明确即在系统提示词中尽量不要使用模棱两可的词语,用人话说就是“把问题说清楚”。格式清晰即可以使用markdown或者json指定一些重要概念。如果你需要让大模型按照某个固定流程进行分析,可以使用markdown的编号语法,如果你需要将大模型对推理结果进行结构化处理,可以使用json语法。任务简单即不要让大模型处理逻辑太复杂或者流程太多的任务。大模型的推理能力完全基于语义理解,它并不具备严格意义上的程序执行逻辑和数学运算逻辑。这就是为什么,当你问大模型:1.11和1.8谁大的时候,它会一本正经的告诉你,当整数部分一样大的时候,仅需要比较小数部分,因为11大于8,因此1.11大于1.8。那么如果我们现实中确实有一些计算任务或复杂的流程需要处理怎么办?我的解决方案是,与程序交互和动态切换上下文。除了系统角色以外,用户一般代表输入和助手一般代表输出。

token:这里不要理解为令牌,它的正确解释应该是一组向量的id。就是常见的描述大模型上下文长度的单位。一个token代表什么?互联网上有很多错误的解释,比较常见说法是:一个英文单词为1个token,一个中文通常是2-3个token。上面的流程介绍一节,我已经解释了“分析预测”与“采样推理”如何交互。“推理采样”生成1个token,反向输送给“分析预测”进行下一个token的预测,而输出模块可以选择token-by-token的方式向用户输出。实际上,对于中文而言,一个token通常表示一个分词。例如:“我爱中国”可能的分词结果是“我”,“爱”,“中国”也可能是“我”,“爱”,“中”,“国”。前者代表3个token,后者代表4个token。具体如何划分,取决于大模型的中文指令训练。除了常见的代表词语的token以外,还有一类特殊token(special token),例如上文提到的,大模型一个字一个字的进行推理生成,程序怎么知道何时结束?其实是有个eos-token,当读到这个token的时候,即表示本轮推理结束了。

3. 程序结构

llama.cpp的程序结构比较清晰,核心模块是llama和ggmll。ggml通过llama进行调用,开发通常不会直接使用。在llama中定义了常用的结构体和函数。common是对llama中函数功能的再次封装,有时候起到方便调用的目的。但是版本迭代上,common中的函数变化较快,最好的方法是看懂流程后直接调用llama.h中的函数。

4. 源码分析

下面我以examples/main/main.cpp作为基础做重点分析。

(1) 初始化

全局参数,这个结构体主要用来接收用户输入和后续用来初始化模型与推理上下文。

gpt_params params;

系统初始化函数:

llama_backend_init();
llama_numa_init(params.numa);

系统资源释放函数:

llama_backend_free();

创建模型和推理上下文:

llama_init_result llama_init = llama_init_from_gpt_params(params);

llama_model *model = llama_init.model;
llama_context *ctx = llama_init.context; 

它声明在common.h中。如果你需要将模型和上下文分开创建可以使用llama.h中的另外两对函数:

llama_model_params model_params = llama_model_params_from_gpt_params(gpt_params_);
llama_model_ = llama_load_model_from_file(param.model.c_str(), model_params); llama_context_params ctx_eval_params = llama_context_params_from_gpt_params(gpt_params_);
llama_context *ctx_eval = llama_new_context_with_model(llama_model_, ctx_eval_params);

创建ggml的线程池,这个过程可能和模型加速有关,代码中没有对它的详细解释:

struct ggml_threadpool * threadpool = ggml_threadpool_new(&tpp);

llama_attach_threadpool(ctx, threadpool, threadpool_batch);

除了完成一般的推理任务,llama.cpp还实现了上下文存储与读取。上下文切换的前提是不能换模型,且仅首次推理接收用户输入的prompt。利用这个特性,可以实现上下文的动态切换。

std::string path_session = params.path_prompt_cache;
std::vector<llama_token> session_tokens;

至此,有关系统初始化模块的过程已经完成。

(2) 用户输入

为了接收用户输入和推理输出,源码集中定义了几个变量:

std::vector<llama_token> embd_inp;

std::vector<llama_token> embd;

检查编码器,现代模型大多都没有明确定义的encodec

if (llama_model_has_encoder(model)) {
int enc_input_size = embd_inp.size();
llama_token * enc_input_buf = embd_inp.data();
if (llama_encode(ctx, llama_batch_get_one(enc_input_buf, enc_input_size, 0, 0))) {
LOG_TEE("%s : failed to eval\n", __func__);
return 1;
}
llama_token decoder_start_token_id = llama_model_decoder_start_token(model);
if (decoder_start_token_id == -1) {
decoder_start_token_id = llama_token_bos(model);
} embd_inp.clear();
embd_inp.push_back(decoder_start_token_id);
}

(3) 分析预测

分析预测部分的核心代码如下,我将处理关注力和session的逻辑删除,仅保留推理部分的逻辑。

// predict
if (!embd.empty()) {
// Note: (n_ctx - 4) here is to match the logic for commandline prompt handling via
// --prompt or --file which uses the same value.
int max_embd_size = n_ctx - 4; // Ensure the input doesn't exceed the context size by truncating embd if necessary.
if ((int) embd.size() > max_embd_size) {
const int skipped_tokens = (int) embd.size() - max_embd_size;
embd.resize(max_embd_size); console::set_display(console::error);
printf("<<input too long: skipped %d token%s>>", skipped_tokens, skipped_tokens != 1 ? "s" : "");
console::set_display(console::reset);
fflush(stdout);
} for (int i = 0; i < (int) embd.size(); i += params.n_batch) {
int n_eval = (int) embd.size() - i;
if (n_eval > params.n_batch) {
n_eval = params.n_batch;
} LOG("eval: %s\n", LOG_TOKENS_TOSTR_PRETTY(ctx, embd).c_str()); if (llama_decode(ctx, llama_batch_get_one(&embd[i], n_eval, n_past, 0))) {
LOG_TEE("%s : failed to eval\n", __func__);
return 1;
} n_past += n_eval; LOG("n_past = %d\n", n_past);
// Display total tokens alongside total time
if (params.n_print > 0 && n_past % params.n_print == 0) {
LOG_TEE("\n\033[31mTokens consumed so far = %d / %d \033[0m\n", n_past, n_ctx);
}
}
} embd.clear();

逻辑的重点是:首先,如果推理的上下文长度超限,会丢弃超出部分。实际开发中可以考虑重构这个部分的逻辑。其次,每次推理都有一个处理数量限制(n_batch),这主要是为了当一次性输入的内容太多,系统不至于长时间无响应。最后,每次推理完成,embd都会被清理,推理完成后的信息会保存在ctx中。

(4) 推理采样

采样推理部分的源码分两个部分:

if ((int) embd_inp.size() <= n_consumed && !is_interacting) {
// optionally save the session on first sample (for faster prompt loading next time)
if (!path_session.empty() && need_to_save_session && !params.prompt_cache_ro) {
need_to_save_session = false;
llama_state_save_file(ctx, path_session.c_str(), session_tokens.data(), session_tokens.size()); LOG("saved session to %s\n", path_session.c_str());
} const llama_token id = llama_sampling_sample(ctx_sampling, ctx, ctx_guidance); llama_sampling_accept(ctx_sampling, ctx, id, /* apply_grammar= */ true); LOG("last: %s\n", LOG_TOKENS_TOSTR_PRETTY(ctx, ctx_sampling->prev).c_str()); embd.push_back(id); // echo this to console
input_echo = true; // decrement remaining sampling budget
--n_remain; LOG("n_remain: %d\n", n_remain);
} else {
// some user input remains from prompt or interaction, forward it to processing
LOG("embd_inp.size(): %d, n_consumed: %d\n", (int) embd_inp.size(), n_consumed);
while ((int) embd_inp.size() > n_consumed) {
embd.push_back(embd_inp[n_consumed]); // push the prompt in the sampling context in order to apply repetition penalties later
// for the prompt, we don't apply grammar rules
llama_sampling_accept(ctx_sampling, ctx, embd_inp[n_consumed], /* apply_grammar= */ false); ++n_consumed;
if ((int) embd.size() >= params.n_batch) {
break;
}
}

首先要关注第2部分,这一段的逻辑是将用户的输入载入上下文中,由于用户的输入不需要推理,因此只需要调用llama_sampling_accept函数。第1部分只有当用户输入都完成以后才会进入,每次采样一个token,写进embd。这个过程和分析预测交替进行,直到遇到eos。

if (llama_token_is_eog(model, llama_sampling_last(ctx_sampling))) {
LOG("found an EOG token\n"); if (params.interactive) {
if (params.enable_chat_template) {
chat_add_and_format(model, chat_msgs, "assistant", assistant_ss.str());
}
is_interacting = true;
printf("\n");
}
}

chat_add_and_format函数只负责将所有交互过程记录在char_msgs中,对整个推理过程没有影响。如果要实现用户输出,可以在这里处理。

二、关键函数

通过gpt_params初始化llama_model_params

struct llama_model_params     llama_model_params_from_gpt_params    (const gpt_params & params);

创建大模型指针

LLAMA_API struct llama_model * llama_load_model_from_file(
const char * path_model,
struct llama_model_params params);

创建ggml线程池和设置线程池

GGML_API struct ggml_threadpool*         ggml_threadpool_new          (struct ggml_threadpool_params  * params);
LLAMA_API void llama_attach_threadpool(
struct llama_context * ctx,
ggml_threadpool_t threadpool,
ggml_threadpool_t threadpool_batch);

通过gpt_params初始化llama_context_params

struct llama_context_params   llama_context_params_from_gpt_params  (const gpt_params & params);

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

对输入进行分词并转换成token

std::vector<llama_token> llama_tokenize(
const struct llama_context * ctx,
const std::string & text,
bool add_special,
bool parse_special = false);

获取特殊token

LLAMA_API llama_token llama_token_bos(const struct llama_model * model); // beginning-of-sentence
LLAMA_API llama_token llama_token_eos(const struct llama_model * model); // end-of-sentence
LLAMA_API llama_token llama_token_cls(const struct llama_model * model); // classification
LLAMA_API llama_token llama_token_sep(const struct llama_model * model); // sentence separator
LLAMA_API llama_token llama_token_nl (const struct llama_model * model); // next-line
LLAMA_API llama_token llama_token_pad(const struct llama_model * model); // padding

批量处理token并进行预测

LLAMA_API struct llama_batch llama_batch_get_one(
llama_token * tokens,
int32_t n_tokens,
llama_pos pos_0,
llama_seq_id seq_id); LLAMA_API int32_t llama_decode(
struct llama_context * ctx,
struct llama_batch batch);

执行采样和接收采样

llama_token llama_sampling_sample(
struct llama_sampling_context * ctx_sampling,
struct llama_context * ctx_main,
struct llama_context * ctx_cfg,
int idx = -1); void llama_sampling_accept(
struct llama_sampling_context * ctx_sampling,
struct llama_context * ctx_main,
llama_token id,
bool apply_grammar);

将token转成自然语言

std::string llama_token_to_piece(
const struct llama_context * ctx,
llama_token token,
bool special = true);

判断推理是否结束,注意,这个token可能和llama_token_eos获取的不一致。因此一定要通过这个函数判断

// Check if the token is supposed to end generation (end-of-generation, eg. EOS, EOT, etc.)
LLAMA_API bool llama_token_is_eog(const struct llama_model * model, llama_token token);

三、总结

本文旨在介绍llama.cpp的基础用法,由于Georgi Gerganov更新较快,且缺少文档。因此可能有些解释不够准确。如果大家对框架和本文敢兴趣可以给我留言深入讨论。

llama.cpp推理流程和常用函数介绍的更多相关文章

  1. MySQL常用函数介绍

    MySQL常用函数介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.操作符介绍 1>.操作符优先级 mysql; +----------+ | +----------+ ...

  2. MySQL 常用函数介绍

    MySQL 基础篇 三范式 MySQL 军规 MySQL 配置 MySQL 用户管理和权限设置 MySQL 常用函数介绍 MySQL 字段类型介绍 MySQL 多列排序 MySQL 行转列 列转行 M ...

  3. [MFC美化] SkinMagic使用详解2- SkinMagic常用函数介绍

    SkinMagic常用函数介绍 (1)InitSkinMagicLib函数:初始化SkinMagic int InitSkinMagicLib( //初始化SkinMagic工具库 HINSTANCE ...

  4. go语言之进阶篇字符串操作常用函数介绍

    下面这些函数来自于strings包,这里介绍一些我平常经常用到的函数,更详细的请参考官方的文档. 一.字符串操作常用函数介绍 1.Contains func Contains(s, substr st ...

  5. SQL SERVER系统表和常用函数介绍

    sysaltfiles 主数据库 保存数据库的文件 syscharsets 主数据库 字符集与排序顺序sysconfigures 主数据库 配置选项syscurconfigs 主数据库 当前配置选项s ...

  6. (转)postgis常用函数介绍(一)

    http://blog.csdn.net/gisshixisheng/article/details/47701237 概述: 在进行地理信息系统开发的过程中,常用的空间数据库有esri的sde,po ...

  7. promql 常用函数介绍

    Metrics类型 根据不同监控指标之间的差异,Prometheus定义了4中不同的指标类型(metric type):Counter(计数器).Gauge(仪表盘).Histogram(直方图).S ...

  8. 性能测试基础-开门篇3(LR常用函数介绍)

    LR常用的函数,协议不一样函数会不一样,这里简单的介绍下HTTP\WEBSERVICE\SOCKET协议常用函数: HTTP: web_set_max_html_param_len("102 ...

  9. Python数据分析--Numpy常用函数介绍(2)

    摘要:本篇我们将以分析历史股价为例,介绍怎样从文件中载入数据,以及怎样使用NumPy的基本数学和统计分析函数.学习读写文件的方法,并尝试函数式编程和NumPy线性代数运算,来学习NumPy的常用函数. ...

  10. (转)postgis常用函数介绍(二)

    http://blog.csdn.net/gisshixisheng/article/details/47903151 概述: 书接上文,本文继续讲解Postgres中常用的空间函数的使用. 常用函数 ...

随机推荐

  1. linux一行执行多条命令 shell

    要实现在一行执行多条Linux命令,分三种情况: 1.&& 举例: lpr /tmp/t2 && rm /tmp/t2 第2条命令只有在第1条命令成功执行之后才执行.当 ...

  2. web3 产品介绍:metamask 钱包 使用dapp更方便的钱包

    Metamask钱包是一种基于区块链技术的数字货币钱包,它允许用户在安全.便捷的环境下管理自己的加密资产.Metamask钱包是以太坊生态系统中最流行的钱包之一,它具有易于使用.安全性高和功能强大等优 ...

  3. ROS(机器人操作系统)的基本了解

    参考: https://blog.csdn.net/qq_51963216/article/details/125754175 https://zhuanlan.zhihu.com/p/5956062 ...

  4. 强化学习入门书籍《DeepReinforcementLearningHands-On-SecondEdition》

    前段时间在网上买了本强化学习入门的书籍,即<Deep-Reinforcement-Learning-Hands-On>,虽然是影印版的,但是感觉还是可以看看的,说的也蛮易懂的,感觉比现在市 ...

  5. openAI的仿真环境Gym Retro的Game Integration——新游戏融合(2)( 示例 demo )

    内容接前文: openAI的仿真环境Gym Retro的Game Integration--新游戏融合(将retro中没有融合的ROM游戏加入其中) 前文大致简单的介绍了gym retro 库对新游戏 ...

  6. 数据库存储时间数据用timestamp 好还是 varchar好

    表示日期数据基本是date型,只有年月的用varchar2或者char,好处见下:1.数据规范.date对合法日期型会校验,包括闰年2月这种.避免字符型变量产生的某月32号,日期长度不对,日期格式不统 ...

  7. 利用Linux系统提供的和调度器相关的接口让进程或线程对某个处理器进行绑定

    目录 设置进程与CPU的亲和性 设置线程与CPU的亲和性 设置进程与CPU的亲和性 taskset命令允许你查看或设置运行中的进程的CPU亲和性(即该进程可以在哪些CPU上运行). 要将一个已经运行的 ...

  8. br4gOnB4ll靶机笔记

    br4gOnB4ll靶机笔记 这是一台vulnhub上的免费靶机,比较简单. 1.主机发现 主机发现 -sn 只做ping扫描,不做端口扫描 nmap -sn 192.168.84.1/24 Star ...

  9. Linux内核如何判断地址是否位于用户空间?

    一. 问题描述 access_ok函数是什么原理? 二.问题分析 我们在内核空间和用户空间进行数据拷贝的时候必须判断用户空间地址是否合法. 主要通过偶函数access_ok来判断. 1. Linux用 ...

  10. 闲鱼卖2000元的带腾讯备案的 gaapqcloud.com.cn 域名低成本获取方法!

    最近在闲鱼看到有人卖域名,声称是腾讯的备案,还卖1000多元!逆天了!这个信息差是真能割韭菜,我一查,这不就是腾讯云的全球应用加速域名吗?????这样也能赚到钱?? 获取方法 进入腾讯云全球应用加速 ...