一文入门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 kv;
std::vector 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/

五、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

include

include

include

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__);
    return1;
}

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);

constint 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__);
    return1;
}

constint 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");
    return1;
}

// 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__);
        return1;
    }

    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__);
    return1;
}

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;
constauto 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);
        return1;
    }
}

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());
    }
}

constauto 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();
return0;

}
运行结果:

参考阅读:
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/

声明:来自程序员与背包客,仅代表创作者观点。链接:https://eyangzhen.com/4027.html

程序员与背包客的头像程序员与背包客

相关推荐

关注我们
关注我们
购买服务
购买服务
返回顶部