C/C++版LLM推理框架Llama.cpp——入门与编码实战

C/C++版LLM推理框架Llama.cpp——入门与编码实战

一、Llama.cpp框架简介

llama.cpp是由Georgi Gerganov创建的轻量级推理引擎,它是基于C/C++语言编码实现的LLM框架,支持大模型的训练和推理,专注于在本地硬件环境(比如个人电脑、树莓派等)上高效运行LLM模型。

llama.cpp框架目前支持的大模型有LLaMA系列、Qwen系列、Gemma系列、LLaVA系列等。

llama.cpp框架支持运行在CPU、GPU、 嵌入式等设备上,对消费级硬件和资源受限的边缘计算设备支持较好。

由于llama.cpp的实现代码以C/C++为主,因此它也具备跨平台兼容性(支持Windows、Linux、macOS等平台),并支持Python编码调用,比如llama-cpp-python第三方库。

Georgi Gerganov本人是AI开源社区知名的开发者,专注于高性能机器学习框架的C/C++编程实现,在Georgi Gerganov创建llama.cpp项目之前,此前已经开发出了以极简主义和高性能而闻名的whisper.cpp框架和专为机器学习优化的张量计算库ggml。

2023年初,Meta公司发布了LLaMA模型,但原版LLaMA模型的推理运行需要消耗大量计算资源,且依赖Pytorch和GPU算力,此时Georgi Gerganov开始意识到他可以将LLaMA模型移植到基于C语言开发的ggml张量库中,并实现基于CPU处理器本地运行大模型的推理和训练过程,由此导致业界闻名的llama.cpp框架的诞生。

llama.cpp的应用场景:

边缘人工智能:在计算能力有限的设备(例如智能手机、物联网设备)上部署 LLaMA 或类似 GPT 的模型。

模型研究:研究人员可以快速迭代量化模型,而无需担心资源密集型硬件设置。

离线推理:将人工智能服务部署在本地离线环境,不与云服务器交互,用来确保数据隐私和商业机密。

二、关于GGUF模型文件

llama.cpp框架不能直接使用PyTorch或TensorFlow生成的原始模型文件,它主要使用的文件格式是GGUF(GGML Unified Format),用于存储LLM大模型的权重和配置。

llama.cpp早期采用的是GGML文件格式, 后来改为使用GGUF文件格式。

在GGML/GGUF出现之前,最早期的机器学习模型文件格式主要侧重于存储未量化的模型,并确保不同的AI框架和硬件平台之间的兼容性。

最早期的机器学习模型文件格式有".pb"文件格式(应用于TensorFlow框架),".pt"或".pth"文件格式(应用于PyTorch框架),虽然这些格式适用于较小的AI模型,但它们有很大的局限性,比如:

1.不支持量化,这些模型存储了全精度(32位浮点)权重,导致模型文件过大。

2.内存使用率高,由于模型未压缩,它们需要大量内存来存储和推理,在消费级硬件上部署时占用了过高的内存。

引入GGML文件格式(通用GPT模型语言)是为了满足LLaMA等大型LLM模型的量化和压缩需求。GGML允许以较低精度的格式存储模型权重,例如8位整型(INT8)或4位整型(INT4),从而大大减少了模型的大小和内存占用。

GGUF是在GGML格式的基础上做的优化,GGUF包含了更多的元数据,并且设计上更易于扩展和使用,GGUF格式将权重、分词器和元数据等集成到一个高效的二进制文件中,该二进制文件加载速度快,可跨平台工作。

GGUF格式的核心特性:

硬件兼容性强:支持在消费级硬件(例如CPU)上运行LLM模型,无需昂贵的GPU配置。

内存占用低:通过量化显著减少内存占用,使大模型更易于部署。

支持自定义量化:通过选择量化级别来微调模型的性能和精度之间的平衡。

模型可扩展性强:GGUF能够存储和运行巨大规模的LLM模型(例如LLaMA 2),而不会遇到GGML格式下的可扩展性问题。

易于部署:GGUF可以部署在云服务器、边缘设备和移动端设备上,即使在算力较低的硬件设备上也能高效完成推理过程。

丰富的元数据信息:GGUF能够存储LLM模型的更广泛的元数据信息,这些元数据包括模型的层级结构、配置参数和量化级别等。

GGUF模型文件的获取和转换

开发者可以使用llama.cpp提供的convert.py脚本,将原始的PyTorch模型转换为GGUF格式,命令示例:

python convert.py path/to/model --outtype gguf --outfile model.gguf

llama.cpp中加载GGUF文件的代码实现: 

struct gguf_context * gguf_init_from_file(const char * fname, struct gguf_init_params params) { FILE * file = ggml_fopen(fname, "rb"); if (!file) { GGML_LOG_ERROR("%s: failed to open GGUF file '%s'\n", __func__, fname); return nullptr; } struct gguf_context * result = gguf_init_from_file_impl(file, params); fclose(file); return result; }
struct gguf_context { uint32_t version = GGUF_VERSION; std::vector<struct gguf_kv> kv; std::vector<struct gguf_tensor_info> info; size_t alignment = GGUF_DEFAULT_ALIGNMENT; // offset of `data` from beginning of file size_t offset = 0; // size of `data` in bytes size_t size = 0; void * data = nullptr; };

用户可以从Hugging Face Hub下载预转换的GGUF模型:

三、LLM模型的量化

量化(Quantization)就是通过使用较低精度的整型格式来压缩LLM模型文件的大小,比如将LLM模型参数(比如权重、偏差等)的精度从32位浮点型(FP32)等较高精度格式降低到8位整型(INT8)或4位整型(INT4)等较低精度的过程,这大大减少了模型的存储大小和推理计算时的内存负载。

量化技术的目的就是在适当降低模型的精度后,达到既没有显著降低模型的效果,又能大幅度优化模型的内存占用。

量化技术对于在内存资源有限的边缘硬件设备上部署LLM模型至关重要。

1.量化的分类

(1).按量化等级分类:

Q8级别量化:

8位量化 (INT8),保留了LLM模型更多的原始精度,但模型文件较大,且内存占用仍然很高,适合应用在对话生成等通用场景。

Q4级别量化:

4位量化 (INT4),最节省内存的量化方案,可显著提高内存和推理速度,但会降低模型的精度,适合应用在关键字提取、语义搜索等专用场景。

(2).按量化方法分类: 

简单量化:

简单量化统一降低所有参数的精度。

K均值量化:

K均值量化是通过划分聚类的方式来实现量化。

llama.cpp支持多种量化级别,常用的级别如下:

Q8_0: 8位整型量化,文件较大,质量接近FP16,但运行速度较快。

Q5_K_M, Q5_K_S: 5位K均值量化,平衡了模型文件大小、运行速度和精度,折衷方案。

Q4_K_M, Q4_K_S: 5位K均值量化,模型文件更小,内存占用更低,但精度有所下降,Q4_K_M是许多用户首选的入门级别,因为它能在消费级硬件上较好地运行。

Q2_K: 2位K均值量化,文件最小,运行速度最快,但精度损失较大。

量化的应用举例:

基于LLaMA-7B模型测试发现,Q4_K_M级别相比FP16级别,模型体积减小了63%,推理速度提升了2.5倍,但LLaMA-7B模型的精度损失仅0.8%。

llama.cpp量化级别中的"Q#_K_M"的含义:

*Q:代表量化。

*#:表示量化过程中使用的位数。

*K:表示在量化中使用 k 均值聚类。

*M:表示量化后的模型大小。S=小,M=中,L=大。

2.量化命令实战

用户可以通过llama.cpp的quantize工具进行模型的量化。

参考示例:llama.cpp/tools/quantize

./llama-quantize ./models/ggml-vocab-deepseek-llm.gguf Q4_K_M

量化过程打印: 

四、llama.cpp命令行

llama.cpp的核心命令行工具包括模型推理工具(main) 和量化工具(quantize),main是llama.cpp的核心命令行工具,用于加载GGUF模型并执行推理任务。基本语法为:

./main [选项] -m <模型路径>

常用命令行参数如下:

全量的命令行参数参考官方文档: https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md

五、llama.cpp本地部署实战

step.01:从github仓库下载llama.cpp项目源码

git clone https://github.com/ggml-org/llama.cpp

step.02:编译llama.cpp项目源码

cmake -B build cmake --build build --config Release

step.03:安装HuggingFace_Hub Python环境,推荐使用env虚拟环境安装。

python3 -m venv .env source .env/bin/activate pip install huggingface_hub

安装完成以后可以使用"hf"命令进行验证:

step.04:从HuggingFace_Hub下载大模型到本地,以阿里的Qwen大模型为例。

https://huggingface.co/Qwen/Qwen3-0.6B-GGUF/

step.05:使用llama.cpp加载Qwen3大模型,并生成Restful服务。

./llama-server -m /home/dev/LLM/llama.cpp/models/Qwen3-0.6B-Q8_0.gguf --host 0.0.0.0 --port 8080

Restful服务的后台日志打印:

访问LLM对应的Restful服务:

http://127.0.0.1:8080/

也可以不生成Restful服务,直接用llama.cpp命令行与模型进行聊天: 

./llama-cli -m /home/dev/LLM/llama.cpp/models/Qwen3-0.6B-Q8_0.gguf

六、llama.cpp Python编码实战

Python编码之前,需要先安装依赖库"llama-cpp-python":

pip install llama-cpp-python

Demo1:

from llama_cpp import Llama # Load the model llm = Llama( model_path="/home/dev/LLM/llama.cpp/models/Qwen3-0.6B-Q8_0.gguf", n_ctx=512, n_threads=4 ) # Provide a prompt prompt = "What is LLM?" # Generate the response output = llm(prompt, max_tokens=250) # Print the response print(output["choices"][0]["text"].strip())

运行结果: 

七、llama.cpp C++编码实战

Demo2:

#include "arg.h" #include "common.h" #include "log.h" #include "llama.h" #include <algorithm> #include <cstdio> #include <string> #include <vector> int main(int argc, char ** argv) { common_params params; params.model.path = "/home/dev/LLM/llama.cpp/models/Qwen3-0.6B-Q8_0.gguf"; params.prompt = "What is LLM?"; params.n_predict = 128; common_init(); // number of parallel batches int n_parallel = params.n_parallel; // total length of the sequences including the prompt int n_predict = params.n_predict; // init LLM llama_backend_init(); llama_numa_init(params.numa); // initialize the model llama_model_params model_params = common_model_params_to_llama(params); llama_model *model = llama_model_load_from_file(params.model.path.c_str(), model_params); if (model == NULL) { LOG_ERR("%s: error: unable to load model\n" , __func__); return 1; } const llama_vocab * vocab = llama_model_get_vocab(model); // tokenize the prompt std::vector<llama_token> tokens_list; tokens_list = common_tokenize(vocab, params.prompt, true); const int n_kv_req = tokens_list.size() + (n_predict - tokens_list.size())*n_parallel; // initialize the context llama_context_params ctx_params = common_context_params_to_llama(params); ctx_params.n_ctx = n_kv_req; ctx_params.n_batch = std::max(n_predict, n_parallel); llama_context * ctx = llama_init_from_model(model, ctx_params); auto sparams = llama_sampler_chain_default_params(); sparams.no_perf = false; llama_sampler * smpl = llama_sampler_chain_init(sparams); llama_sampler_chain_add(smpl, llama_sampler_init_top_k(params.sampling.top_k)); llama_sampler_chain_add(smpl, llama_sampler_init_top_p(params.sampling.top_p, params.sampling.min_keep)); llama_sampler_chain_add(smpl, llama_sampler_init_temp (params.sampling.temp)); llama_sampler_chain_add(smpl, llama_sampler_init_dist (params.sampling.seed)); if (ctx == NULL) { LOG_ERR("%s: error: failed to create the llama_context\n" , __func__); return 1; } const int n_ctx = llama_n_ctx(ctx); LOG_INF("\n%s: n_predict = %d, \ n_ctx = %d, \ n_batch = %u, \ n_parallel = %d, \ n_kv_req = %d\n", __func__, n_predict, n_ctx, ctx_params.n_batch, n_parallel, n_kv_req); // make sure the KV cache is big enough to hold all the prompt and generated tokens if (n_kv_req > n_ctx) { LOG_ERR("the required KV cache size is not big enough\n"); return 1; } // print the prompt token-by-token LOG("\n"); for (auto id : tokens_list) { LOG("%s", common_token_to_piece(ctx, id).c_str()); } // create a llama_batch // we use this object to submit token data for decoding llama_batch batch = llama_batch_init(std::max(tokens_list.size(), (size_t) n_parallel), 0, n_parallel); std::vector<llama_seq_id> seq_ids(n_parallel, 0); for (int32_t i = 0; i < n_parallel; ++i) { seq_ids[i] = i; } // evaluate the initial prompt for (size_t i = 0; i < tokens_list.size(); ++i) { common_batch_add(batch, tokens_list[i], i, seq_ids, false); } GGML_ASSERT(batch.n_tokens == (int) tokens_list.size()); if (llama_model_has_encoder(model)) { if (llama_encode(ctx, batch)) { LOG_ERR("%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 == LLAMA_TOKEN_NULL) { decoder_start_token_id = llama_vocab_bos(vocab); } common_batch_clear(batch); common_batch_add(batch, decoder_start_token_id, 0, seq_ids, 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_ERR("%s: llama_decode() failed\n", __func__); return 1; } if (n_parallel > 1) { LOG("\n\n%s: generating %d sequences ...\n", __func__, n_parallel); } // main loop // we will store the parallel decoded sequences in this vector std::vector<std::string> streams(n_parallel); std::vector<int32_t> i_batch(n_parallel, batch.n_tokens - 1); int n_cur = batch.n_tokens; int n_decode = 0; const auto t_main_start = ggml_time_us(); while (n_cur <= n_predict) { // prepare the next batch common_batch_clear(batch); // sample the next token for each parallel sequence / stream for (int32_t i = 0; i < n_parallel; ++i) { if (i_batch[i] < 0) { // the stream has already finished continue; } const llama_token new_token_id = llama_sampler_sample(smpl, ctx, i_batch[i]); if (llama_vocab_is_eog(vocab, new_token_id) || n_cur == n_predict) { i_batch[i] = -1; LOG("\n"); if (n_parallel > 1) { LOG_INF("%s: stream %d finished at n_cur = %d", __func__, i, n_cur); } continue; } // if there is only one stream, we print immediately to stdout if (n_parallel == 1) { LOG("%s", common_token_to_piece(ctx, new_token_id).c_str()); } streams[i] += common_token_to_piece(ctx, new_token_id); i_batch[i] = batch.n_tokens; common_batch_add(batch, new_token_id, n_cur, { i }, true); n_decode += 1; } if (batch.n_tokens == 0) { break; } n_cur += 1; if (llama_decode(ctx, batch)) { LOG_ERR("%s : failed to eval, return code %d\n", __func__, 1); return 1; } } if (n_parallel > 1) { LOG("\n"); for (int32_t i = 0; i < n_parallel; ++i) { LOG("sequence %d:\n\n%s%s\n\n", i, params.prompt.c_str(), streams[i].c_str()); } } const auto t_main_end = ggml_time_us(); LOG_INF("%s: decoded %d tokens in %.2f s, speed: %.2f t/s\n", __func__, n_decode, (t_main_end - t_main_start) / 1000000.0f, n_decode / ((t_main_end - t_main_start) / 1000000.0f)); LOG("\n"); llama_perf_sampler_print(smpl); llama_perf_context_print(ctx); fprintf(stderr, "\n"); llama_batch_free(batch); llama_sampler_free(smpl); llama_free(ctx); llama_model_free(model); llama_backend_free(); return 0; }

运行结果:

参考阅读:

https://medium.com/@vimalkansal/understanding-the-gguf-format-a-comprehensive-guide-67de48848256

https://qwen.readthedocs.io/zh-cn/latest/run_locally/llama.cpp.html

https://learn.arm.com/learning-paths/servers-and-cloud-computing/llama_cpp_streamline/2_llama.cpp_intro/

https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-quantization

http://www.bimant.com/blog/llama-cpp-quantization-manual/

Read more

【C++笔记】STL详解:string的实现

【C++笔记】STL详解:string的实现

前言:                 在前面的学习中,我们已经初步掌握了string类接口函数的使用方法,本文将带领大家从零开始,逐步实现一个完整的string类。          一、string类总览                 温馨提示: 为了避免与标准库中的string产生命名冲突,我们使用mystd命名空间进行封装。 namespace mystd { class string { public: //迭代器 typedef char* iterator; typedef const char* const_iterator; //默认成员函数 string(); string(const char* str); //构造函数 string(const string& s); //拷贝构造函数 string& operator=(const string& s); //赋值运算符重载函数 ~string(); //析构函数 //迭代器相关函数 iterator begin(

By Ne0inhk
【2024 Year-End Summary】C++自学分享

【2024 Year-End Summary】C++自学分享

目录 [ C 语言 ] [ 数据结构 ] [ 算法 ] [ C++ ] [Linux] [Mysql] [Redis 文档学习] [Docker 云原生] [Git] [Qt] 转眼大学就过了一年半,希望自己可以保持学习₍₍Ϡ(੭•̀ω•́)੭✧⃛ 在刚上大一的时候用的是纸质笔记本,后来东西越学越多,就开始使用语雀文档,文章也有部分同步到 ZEEKLOG 上了,很高兴能够对大家有所帮助~ 博客之星的文章一直不知道写些什么,想着对专栏做一个整理叭 下面的标题/网课名 就是 学习链接的传送门,自学的资料也都是免费的,开头就不多说了,学就好啦 [ C 语言 ] hh 这是多少小伙伴梦开始的地方 网课: * 【浙江大学】C语言入门与进阶 翁恺(全129讲)_哔哩哔哩_bilibili 书籍: * C Primer Plus * C

By Ne0inhk
华为OD机试双机位C卷:采购订单 (Py/Java/C/C++/Js/Go)

华为OD机试双机位C卷:采购订单 (Py/Java/C/C++/Js/Go)

采购订单 华为OD机试双机位C卷 - 华为OD上机考试双机位C卷 100分题型 华为OD机试双机位c卷真题目录点击查看: 华为OD机试双机位C卷真题题库目录|机考题库 + 算法考点详解 题目描述 在一个采购系统中,采购申请(PR)需要经过审批后才能生成采购订单(PO)。每个PR包含商品的单价(假设相同商品的单价一定是一样的)及数量信息。系统要求对商品进行分类处理:单价高于100元的商品需要单独处理,单价低于或等于100元的相同商品可以合并到同一采购订单PO中。针对单价低于100的小额订单,如果量大可以打折购买。 具体规则如下: 如果PR状态为"审批通过",则将其商品加入到PO中。如果PR的状态为"审批拒绝"或"待审批",则忽略改PR。 对于单价高于100元的商品,每个商品单独生成一条PO记录。对于单价低于100元的商品,将相同商品的数量合并到一条PO记录中。 如果商品单价<100且商品数量>=100,则单价打9折。 输入描述 第一行包含整数N,

By Ne0inhk
C++的IO流和C++的类型转换----《Hello C++ Wrold!》(29)--(C/C++)

C++的IO流和C++的类型转换----《Hello C++ Wrold!》(29)--(C/C++)

文章目录 * 前言 * C++的类型转换 * 四种命名的强制类型转换操作符 * static_cast * reinterpret_cast * const_cast * dynamic_cast * RTTI(这个了解一下就行了) * C++的IO流 * C++文件的IO流 * stringstream 前言 在 C++ 编程体系中,类型转换与 IO 流是支撑程序数据处理与交互的两大核心环节。类型转换关乎数据在不同类型间的安全传递与运算适配,而 IO 流则负责程序与外部设备(如键盘、屏幕、文件)之间的数据输入与输出,二者共同构成了 C++ 程序实现功能、交互信息的基础框架。 C 语言中的类型转换方式虽简洁,却存在可视性差、难以追踪的问题,容易在复杂程序中引发潜在的逻辑错误。为解决这一痛点,C++ 引入了四种命名明确的强制类型转换操作符 ——static_cast、reinterpret_

By Ne0inhk