我们知道,一般运行大语言模型都是在Python上运行的,可是Python的性能太差了,不适合用于生产环境,因此可以采用llama.cpp提供的API在C语言上运行大模型。
llama.cpp的下载
Windows下的下载
我们需要下载llama.cpp的两个部分,分别是它的源代码和windows预编译包。它的源代码直接在github上下载即可:
GitHub - ggerganov/llama.cpp: LLM inference in C/C++
它的预编译包在这里下载:
Releases · ggerganov/llama.cpp
Linux下的下载
linux下只需要下载源代码,然后编译即可:
make
如果想下载GPU加速的,则输入:
make GGML_CUDA=1
gguf文件的获取
从huggingface中下载
在llama.cpp上运行大语言模型需要一个gguf格式的文件,存储模型的相关信息。gguf文件可以从huggingface上直接获取,如:bartowski/Llama-3.2-1B-Instruct-GGUF · HF Mirror,然后选择一个合适的镜像即可。
从transformers中转换
当然,也可以从transformers模型中转换。在llama.cpp的源代码包下输入如下命令:
pip install -r requirements.txt
python convert_hf_to_gguf.py <transfomers模型路径> --outtype f16 --outfile <格式转换后的模型路径.gguf>
在其中,transformers模型路径是一个目录,目录里包括模型信息和分词器信息,–outtype指定的是量化信息,用于减小推理时的显存资源消耗,可以选择f32,f16,q8_0,q4_k_m等。–outfile是转换后的gguf路径,是一个.gguf格式的文件
API接口的使用
在使用API接口前,我们需要先创建一个文件夹,作为项目文件夹。然后把源代码包中的include/llama.h
,ggml/src
下的所有头文件全部复制到这个项目文件夹中,接着把预编译包中的所有dll文件复制进去(Linux下复制函数库),然后创建main.c,编写main函数。
在使用API完成推理的过程中,需要依次经历以下几步:
-
加载模型
-
创建上下文
-
获得词汇表
-
处理提示词
-
创建批次
-
设置采样器
-
循环进行解码和采样
-
释放资源
接下来对每一步用到的函数进行讲解:
加载模型
在加载模型时,需要先设定参数,在加载模型。通常获取默认参数即可。
获取默认参数的函数是llama_model_default_params
,其原型如下:
struct llama_model_params llama_model_default_params(void);
它需要一个llama_model_params
结构体来接它的返回值,有了这个返回值,就可以调用llama_model_load_from_file
函数,用于加载模型,这个函数的原型如下:
struct llama_model * llama_model_load_from_file(
const char * path_model,
struct llama_model_params params);
它返回一个llama_model
结构体的指针,就是从路径中获取到的模型的指针,path_model
表示gguf文件的路径,params
是加载模型时的参数。
创建上下文
创建上下文时同样需要参数,获取其默认参数的函数是llama_context_default_params
,其原型如下:
struct llama_context_params llama_context_default_params(void);
需要一个llama_context_params
的结构体来接它的返回值,有了这个返回值,就可以调用llama_init_from_model
函数,用于创建上下文,这个函数的原型如下:
struct llama_context * llama_init_from_model(
struct llama_model * model,
struct llama_context_params params);
它的第一个参数就是加载后的模型。第二个参数就是刚创建的参数,返回一个llama_context
结构体的指针,表示初始化的上下文。
获得词汇表
获得词汇表采用llama_model_get_vocab
函数,其原型如下:
const struct llama_vocab *llama_model_get_vocab(const struct llama_model* model);
它接受一个模型,返回这个模型中的词汇表,存储到llama_vocab
结构体中,并返回地址。
处理提示词
在将提示词传入模型前,需要对其进行标记化(tokenize,又叫序列化),将文字转换为一个数组,这样才可以让模型理解这段文字。
处理提示词的关键函数是llama_tokenize
,它用于标记化一段文字,的原型如下:
int32_t llama_tokenize(
const struct llama_vocab * vocab,
const char * text,
int32_t text_len,
llama_token * tokens,
int32_t n_tokens_max,
bool add_special,
bool parse_special);
vocab
是词汇表,text
是文字,text_len
是文字长度,tokens
指向的地址是标记化之后的存储位置,n_tokens_max
是序列化地址的最大容纳长度,add_special
是是否增加特殊标记,即段首标识和段末标识,parse_special
表示是否解析特殊表示,包括段首标识、段末标识等。
如果成功,这个函数将返回标记化的数量,即tokens
的有效长度;如果失败,这个函数将返回负数。
提示词不仅包括语言本身,还包括一些特殊标识,如llama的提示词样例如下:
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
Cutting Knowledge Date: December 2023
Today Date: 26 Jul 2024
{system_prompt}<|eot_id|><|start_header_id|>user<|end_header_id|>
{prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
在创建提示词的时候,需要注意包括这些特使标识。
创建批次
模型推理前,需要为推理创建一个批次。
创建批次采用llama_batch_get_one
函数,这个函数的原型如下:
struct llama_batch llama_batch_get_one(
llama_token * tokens,
int32_t n_tokens);
它会返回一个llama_batch
结构体,表示创建的批次。参数tokens
表示标记化后的数据,可以是一个数组,n_tokens
是这个数组的有效长度。这两个参数都可以从llama_tokenize
中获得。
设置采样器
采样器用于指定采样的方式,决定了以什么样的方式确定候选词。为了让采样方式多样化,同时进行多种采样,可以采取采样器链。如下代码就定义了一个采样器链:
struct llama_sampler_chain_params sparams = llama_sampler_chain_default_params();
struct llama_sampler *sampler = llama_sampler_chain_init(sparams);
llama_sampler_chain_add(sampler, llama_sampler_init_temp(0.8));
llama_sampler_chain_add(sampler, llama_sampler_init_top_k(50));
llama_sampler_chain_add(sampler, llama_sampler_init_top_p(0.9, 1));
long seed = time(NULL);
llama_sampler_chain_add(sampler, llama_sampler_init_dist(seed));
采样器链中,可以增加如下类型的采样器:
基础采样器
-
贪婪采样器
LLAMA_API struct llama_sampler * llama_sampler_init_greedy(void);
每次选择当前概率最高的词元作为输出,不考虑随机性。
-
随机采样器
LLAMA_API struct llama_sampler * llama_sampler_init_dist(uint32_t seed);
基于随机分布进行采样,
seed
用于初始化随机数生成器。
概率调整采样器
-
Softmax 采样器
DEPRECATED(LLAMA_API struct llama_sampler * llama_sampler_init_softmax(void));
按照词元的 logits 对候选词元进行降序排序,并计算基于 logits 的概率。注意:不推荐在完整词汇表上使用,因为排序操作可能很慢,建议先进行 top-k 或 top-p 采样。
基于截断的采样器
-
Top-K 采样器
LLAMA_API struct llama_sampler * llama_sampler_init_top_k(int32_t k);
选择概率最高的前 K 个词元进行采样,
k
是截断的词元数量。 -
Nucleus 采样器
LLAMA_API struct llama_sampler * llama_sampler_init_top_p(float p, size_t min_keep);
选择累积概率达到阈值
p
的最小词元集合进行采样,min_keep
是保留的最小词元数量。 -
Min-P 采样器
LLAMA_API struct llama_sampler * llama_sampler_init_min_p(float p, size_t min_keep);
选择概率至少为
p
的词元进行采样,min_keep
是保留的最小词元数量。 -
局部典型采样器
LLAMA_API struct llama_sampler * llama_sampler_init_typical(float p, size_t min_keep);
选择与模型条件熵接近的词元进行采样,
p
是截断阈值,min_keep
是保留的最小词元数量。
温度调整采样器
-
温度采样器
LLAMA_API struct llama_sampler * llama_sampler_init_temp(float t);
对 logits 进行缩放,公式为 li′=li/t。当
t <= 0.0f
时,保留最大 logit 的值,其余设置为负无穷。 -
动态温度采样器
LLAMA_API struct llama_sampler * llama_sampler_init_temp_ext(float t, float delta, float exponent);
动态调整温度,
t
是基础温度,delta
和exponent
是动态调整参数。
特殊采样器
-
XTC 采样器
LLAMA_API struct llama_sampler * llama_sampler_init_xtc(float p, float t, size_t min_keep, uint32_t seed);
排除最可能的词元以增加创造性,
p
是截断阈值,t
是温度,min_keep
是保留的最小词元数量,seed
是随机种子。 -
Mirostat 1.0 采样器
LLAMA_API struct llama_sampler * llama_sampler_init_mirostat( int32_t n_vocab, uint32_t seed, float tau, float eta, int32_t m, float mu);
控制生成文本的交叉熵(surprise),
n_vocab
是词汇表大小,tau
是目标交叉熵,eta
是学习率,m
是用于估计s_hat
的词元数量,mu
是最大交叉熵。 -
Mirostat 2.0 采样器
LLAMA_API struct llama_sampler * llama_sampler_init_mirostat_v2(uint32_t seed, float tau, float eta, float mu);
Mirostat 2.0 算法,参数与 Mirostat 1.0 类似,但实现更通用。
其他采样器
-
语法采样器
LLAMA_API struct llama_sampler * llama_sampler_init_grammar( const struct llama_vocab * vocab, const char * grammar_str, const char * grammar_root);
根据语法规则进行采样,
vocab
是词汇表,grammar_str
是语法字符串,grammar_root
是语法根节点。 -
惩罚采样器
LLAMA_API struct llama_sampler * llama_sampler_init_penalties( int32_t penalty_last_n, float penalty_repeat, float penalty_freq, float penalty_present);
对重复词元进行惩罚,
penalty_last_n
是考虑的最近 n 个词元,penalty_repeat
是重复惩罚,penalty_freq
是频率惩罚,penalty_present
是存在惩罚。 -
DRY 采样器
LLAMA_API struct llama_sampler * llama_sampler_init_dry( const struct llama_vocab * vocab, int32_t n_ctx_train, float dry_multiplier, float dry_base, int32_t dry_allowed_length, int32_t dry_penalty_last_n, const char ** seq_breakers, size_t num_breakers);
用于减少重复和增强多样性的采样器,参数用于控制重复惩罚和序列中断。
-
Logit 偏置采样器
LLAMA_API struct llama_sampler * llama_sampler_init_logit_bias( int32_t n_vocab, int32_t n_logit_bias, const llama_logit_bias * logit_bias);
对特定词元的 logits 进行偏置调整,
n_vocab
是词汇表大小,n_logit_bias
是偏置词元数量,logit_bias
是偏置数组。 -
填空采样器
LLAMA_API struct llama_sampler * llama_sampler_init_infill(const struct llama_vocab * vocab);
用于填空任务的采样器,主要用于在文本中间填充内容。
在采样器链的最后,必须是贪婪采样器、随机采样器和Mirostat采样器中的任意一种。
循环进行解码采样
在这里面,需要用到llama_decode
函数进行解码,llama_sampler_sample
函数进行采样,llama_detokenize
函数进行反标记化(即将模型的输出转换为自然语言),最后需要将批次更新,增加刚输出的标识。
llama_decode
的原型如下:
int32_t llama_decode(
struct llama_context * ctx,
struct llama_batch batch);
它接受上下文和批次作为参数,返回值如果为0则成功,非0则失败。在成功解码后,就可以调用llama_sampler_sample
函数进行采样,其原型如下:
llama_token llama_sampler_sample(struct llama_sampler * smpl, struct llama_context * ctx, int32_t idx);
它将会进行采样。它会对第idx
个元素进行采样,如果idx
为-1,则会采样最后一个。smpl
是定义的采样器或采样器链,ctx
是上下文。
llama_detokenize
是反序列化函数,它的原型如下:
int32_t llama_detokenize(
const struct llama_vocab * vocab,
const llama_token * tokens,
int32_t n_tokens,
char * text,
int32_t text_len_max,
bool remove_special,
bool unparse_special);
和llama_tokenize
相反,它将tokens
内的序列化数据转换为text
内的文本数据,返回的是反序列化的长度。如果出错,返回负数。
接下来需要更新批次数据,这里面的更新指清除批次数据,并写入当前采样的数据:
batch.token[0] = next_token;
batch.n_tokens = 1;
释放资源
上面申请的模型、上下文、采样器、批次等都需要释放,代码如下:
llama_sampler_free(sampler);
llama_batch_free(batch);
llama_free(context);
llama_model_free(model);
完整代码
从[bartowski/Llama-3.2-1B-Instruct-GGUF · HF Mirror](bartowski/Llama-3.2-1B-Instruct-GGUF · HF Mirror)下载一个GGUF模型,进行测试。
完整代码如下:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include "llama.h"
#define MAX_TOKEN 10000
int main(void){
// 创建模型
struct llama_model_params model_params = llama_model_default_params();
struct llama_model *model = llama_model_load_from_file("./Llama-3.2-1B-Instruct-f16.gguf", model_params);
printf("create model down\n");
// 创建上下文
struct llama_context_params context_params = llama_context_default_params();
struct llama_context *context = llama_init_from_model(model, context_params);
printf("create context down\n");
// 获得词汇表
const struct llama_vocab *vocab = llama_model_get_vocab(model);
printf("create vocab down\n");
// 定义提示词
char *prompt =
"<|begin_of_text|><|start_header_id|>user<|end_header_id|>Who are you?<|eot_id|><|start_header_id|>assistant<|end_header_id|>";
// 对提示词进行标记化(tokenize)
llama_token *tokens = (llama_token *)malloc(sizeof(llama_token) * MAX_TOKEN);
int len = llama_tokenize(vocab, prompt, strlen(prompt), tokens, MAX_TOKEN, false, true);
if (len < 0){
fprintf(stderr, "Error:tokenize error\n");
return -1;
}
printf("tokenize prompt down\n");
// 创建批次
struct llama_batch batch = llama_batch_get_one(tokens, len);
printf("create batch down\n");
// 初始化采样器链
struct llama_sampler_chain_params sparams = llama_sampler_chain_default_params();
struct llama_sampler *sampler = llama_sampler_chain_init(sparams);
llama_sampler_chain_add(sampler, llama_sampler_init_temp(0.8));
llama_sampler_chain_add(sampler, llama_sampler_init_top_k(50));
llama_sampler_chain_add(sampler, llama_sampler_init_top_p(0.9, 1));
long seed = time(NULL);
llama_sampler_chain_add(sampler, llama_sampler_init_dist(seed));
printf("create sampler chain down\n");
// 循环
llama_token next_token = LLAMA_TOKEN_NULL;
llama_token eos = llama_vocab_eos(vocab);
while (next_token != eos) {
// 解码
if(llama_decode(context, batch)){
fprintf(stderr, "Error: decode error\n");
return -1;
}
// 采样
next_token = llama_sampler_sample(sampler, context, -1);
// 反标记化
char deprompt[100] = {0};
if(llama_detokenize(vocab, &next_token, 1, deprompt, sizeof(deprompt) / sizeof(deprompt[0]), false, false) < 0){
fprintf(stderr, "Error: detokenize error\n");
return -1;
}
printf("%s", deprompt);
// 更新 batch 以包含新生成的 token
batch.token[0] = next_token;
batch.n_tokens = 1;
}
// 释放资源
llama_sampler_free(sampler);
llama_batch_free(batch);
llama_free(context);
llama_model_free(model);
free(tokens);
return 0;
}
输出结果:
I'm an artificial intelligence model known as Llama. Llama stands for "Large Language Model Meta AI."
可以看到,模型没有问题。