【项目分析】llama.cpp工程

概述

Llama.cpp是一个基于C++编写的高性能大模型推理框架,旨在提供快速、稳定且易于使用的计算工具,原本的目标是允许在MacBook上使用INT4量化的LLaMA模型,但现在Llama.cpp支持多种计算模式,包括向量计算、矩阵运算、图算法等,可广泛应用于机器学习、图像处理、数据分析等领域。

目录结构

src是构建模型架构的基础库文件夹
examples是部分案例模型的源文件
ggml是计算操作的库文件
在这里插入图片描述

源码分析

llama.cpp运行机制分析

可如下参考链接(注意:函数名有所变动):
CodeLeaner@微信公众号:llama.cpp源码解析
以下代码基于tag: b4033分支分析。

llama.cpp运行入口函数

// llama  @examples/main/main.cpp
main()  
	gpt_params_parse(argc, argv, params, LLAMA_EXAMPLE_MAIN, print_usage)	// 解析传递进来的模型参数
	llama_init_from_gpt_params()
		llama_load_model_from_file(params.model.c_str(), mparams);		// 加载model参数
		llama_new_context_with_model(model, cparams);			// 
			ggml_backend_cpu_init();
				*cpu_backend				// 定义指针指向 cpu_backend_i
		llama_tokenize(ctx, prompt, true, true)				// 将prompt tokenize
	while   // 循环产生token
		   	llama_decode(ctx, llama_batch_get_one(&embd[i], n_eval, n_past, 0))	// 生成token函数
   			llama_token_to_piece(ctx, id, params.special)
 	gpt_perf_print(ctx, smpl);						// 打印性能结果

模型计算图构建函数


// decode 函数体 @src/llama.cpp
int32_t llama_decode(        struct llama_context * ctx,          struct llama_batch   batch)
	llama_decode_internal(*ctx, batch)
		while (lctx.sbatch.n_tokens > 0) 
			llama_build_graph(lctx, ubatch, false);   // 构建计算图,包括self-attention、ffn等,计算图将计算方式赋值,但不输入数据计算,在构建计算图时定义计算类型
			llama_graph_compute(lctx, gf, n_threads, threadpool);    // 创建线程,并基于前期构建的计算图调用对应计算类型的函数进行计算
				ggml_backend_cpu_set_n_threads(lctx.backend_cpu, n_threads);
        			ggml_backend_cpu_set_threadpool(lctx.backend_cpu, threadpool);
        			ggml_backend_cpu_set_abort_callback(lctx.backend_cpu, lctx.abort_callback, lctx.abort_callback_data);	
				ggml_backend_sched_graph_compute_async(lctx.sched, gf);

// 计算图构建函数 
static struct ggml_cgraph * llama_build_graph(  llama_context & lctx,  const llama_ubatch & batch,                  bool   worst_case)
	llm.init();
	result = llm.build_llama();
		inpL = llm_build_inp_embd(ctx0, lctx, hparams, batch, model.tok_embd, cb);
		for (int il = 0; il < n_layer; ++il) {
		cur = llm_build_norm(ctx0, inpL, hparams, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, cb, il);
	 	// self-attention
	 	struct ggml_tensor * Qcur = llm_build_lora_mm(lctx, ctx0, model.layers[il].wq, cur);
	 	struct ggml_tensor * Kcur = llm_build_lora_mm(lctx, ctx0, model.layers[il].wk, cur);
	 	struct ggml_tensor * Vcur = llm_build_lora_mm(lctx, ctx0, model.layers[il].wv, cur);
	 	cur = llm_build_kv(ctx0, lctx, kv_self, gf,  model.layers[il].wo, model.layers[il].bo,  Kcur, Vcur, Qcur, KQ_mask, n_tokens, kv_head, n_kv, 1.0f/sqrtf(float(n_embd_head)), cb, il);
	 		llm_build_kv_store(ctx, hparams, cparams, kv, graph, k_cur, v_cur, n_tokens, kv_head, cb, il);
	 		cur  = llm_build_kqv(ctx, lctx, kv, graph, wo, wo_b, q_cur, kq_mask, n_tokens, n_kv, kq_scale, cb, il);
	 			struct ggml_tensor * kq = ggml_mul_mat(ctx, k, q);
	 			kq = ggml_soft_max_ext(ctx, kq, kq_mask, kq_scale, hparams.f_max_alibi_bias);
	 			struct ggml_tensor * kqv = ggml_mul_mat(ctx, v, kq);
	 	// feed-forward network
	 	cur = llm_build_norm(ctx0, ffn_inp, hparams, model.layers[il].ffn_norm, NULL, LLM_NORM_RMS, cb, il);
	 	cur = llm_build_ffn(ctx0, lctx, cur, model.layers[il].ffn_up,   model.layers[il].ffn_up_b,   NULL, model.layers[il].ffn_gate, model.layers[il].ffn_gate_b, NULL, model.layers[il].ffn_down, model.layers[il].ffn_down_b, NULL, NULL, LLM_FFN_SILU, LLM_FFN_PAR, cb, il);
		}
		cur = llm_build_norm(ctx0, cur, hparams, model.output_norm, NULL, LLM_NORM_RMS, cb, -1);
		cur = llm_build_lora_mm(lctx, ctx0, model.output, cur);
		ggml_build_forward_expand(gf, cur);



ggml计算库函数


// backend计算图执行函数   ggml/src/ggml-backend.c
enum ggml_status ggml_backend_sched_graph_compute_async(ggml_backend_sched_t sched, struct ggml_cgraph * graph)
	ggml_backend_sched_alloc_graph(sched, graph)
		ggml_backend_sched_split_graph(sched, graph);			// 该函数划分graph
	ggml_backend_sched_compute_split(sched);
		ggml_backend_graph_compute_async(split_backedn, &split->graph)
			backend->iface.graph_compute(backend,cgraph);  //根据iface结构体中的信息,调用对应平台的ggml_graph_compute计算
				ggml_backend_cpu_graph_compute()					// 例如cpu平台,则调用该函数
					ggml_graph_compute(cgraph, &cplan);
				



// ggml计算图中各类计算选通的主体函数  ggml/src/ggml.c
enum ggml_status ggml_graph_compute(struct ggml_cgraph * cgraph, struct ggml_cplan * cplan)
	ggml_graph_compute_thread(&threadpool->workers[0])
		ggml_compute_forward(&params, node);
			ggml_compute_forward_dup(params, tensor);
			ggml_compute_forward_add1(params, tensor);
			ggml_compute_forward_repeat(params, tensor);
			ggml_compute_forward_mul_mat(params, tensor);   // 函数计算时根据ggml_type选择对应精度的vec_dot函数执行
			ggml_compute_forward_soft_max(params, tensor);
			ggml_compute_forward_rms_norm(params, tensor);

量化操作函数

模型的计算图构建好后调用ggml_compute_forward函数进行计算,计算时会根据源操作数的数据类型ggml_type调用对应的运算函数,如下函数ggml_compute_forward_mul_mat_one_chunk所示,如果typeGGML_TYPE_Q8_0,则运算函数vec_dot则是ggml_vec_dot_q8_0_q8_0

static const ggml_type_traits_t type_traits[GGML_TYPE_COUNT] = {
    [GGML_TYPE_Q8_0] = {
        .type_name                = "q8_0",
        .blck_size                = QK8_0,
        .type_size                = sizeof(block_q8_0),
        .is_quantized             = true,
        .to_float                 = (ggml_to_float_t) dequantize_row_q8_0,
        .from_float               = quantize_row_q8_0,
        .from_float_ref           = (ggml_from_float_t) quantize_row_q8_0_ref,
        .from_float_to_mat        = quantize_mat_q8_0,
        .vec_dot                  = ggml_vec_dot_q8_0_q8_0,
        .vec_dot_type             = GGML_TYPE_Q8_0,
    ......
}

static void ggml_compute_forward_mul_mat_one_chunk(...)
	const enum ggml_type type = src0->type;			// 源操作数据类型
	ggml_vec_dot_t const vec_dot      = type_traits[type].vec_dot;  // 调用对应的运算函数
	for (int64_t iir1 = ir1_start; iir1 < ir1_end; iir1 += blck_1) {
    		for (int64_t iir0 = ir0_start; iir0 < ir0_end; iir0 += blck_0) {
        		for (int64_t ir1 = iir1; ir1 < iir1 + blck_1 && ir1 < ir1_end; ir1 += num_rows_per_vec_dot) {
						...
						for (int64_t ir0 = iir0; ir0 < iir0 + blck_0 && ir0 < ir0_end; ir0 += num_rows_per_vec_dot) {
               		vec_dot(ne00, &tmp[ir0 - iir0], (num_rows_per_vec_dot > 1 ? 16 : 0), src0_row + ir0 * nb01, (num_rows_per_vec_dot > 1 ? nb01 : 0), src1_col, (num_rows_per_vec_dot > 1 ? src1_col_stride : 0), num_rows_per_vec_dot);
           		}
           		...
					}
			}
	}

void ggml_vec_dot_q8_0_q8_0(int n, float * restrict s, size_t bs, const void * restrict vx, size_t bx, const void * restrict vy, size_t by, int nrc) 
    size_t vl = __riscv_vsetvl_e8m1(qk);
    for (; ib < nb; ++ib) {
    // load elements
    vint8m1_t bx_0 = __riscv_vle8_v_i8m1(x[ib].qs, vl);
    vint8m1_t by_0 = __riscv_vle8_v_i8m1(y[ib].qs, vl);
    vint16m2_t vw_mul = __riscv_vwmul_vv_i16m2(bx_0, by_0, vl);
    vint32m1_t v_zero = __riscv_vmv_v_x_i32m1(0, vl);
    vint32m1_t v_sum = __riscv_vwredsum_vs_i16m2_i32m1(vw_mul, v_zero, vl);
    int sumi = __riscv_vmv_x_s_i32m1_i32(v_sum);
    sumf += sumi*(GGML_FP16_TO_FP32(x[ib].d)*GGML_FP16_TO_FP32(y[ib].d));
    }

数据结构

// cpu执行数据流结构体,将函数作为结构体成员。
cpu_backend_i	// @ggml/src/ggml-backend.c   
	ggml_backend_cpu_graph_plan_create()
		ggml_graph_plan()
	ggml_backend_cpu_graph_plan_compute()
		ggml_graph_compute()
	ggml_backend_cpu_graph_compute()
		ggml_graph_plan()
		ggml_graph_compute()
// ggml tensor
struct ggml_tensor {
        enum ggml_type         type;   // 通过type选择计算的数据精度
        enum ggml_op 				op;		// 通过op选择计算函数
        ...
	}

enum ggml_type {
        GGML_TYPE_F32     = 0,
        GGML_TYPE_F16     = 1,
        GGML_TYPE_Q4_0    = 2,
        GGML_TYPE_Q4_1    = 3,
        // GGML_TYPE_Q4_2 = 4, support has been removed
        // GGML_TYPE_Q4_3 = 5, support has been removed
        GGML_TYPE_Q5_0    = 6,
        GGML_TYPE_Q5_1    = 7,
        GGML_TYPE_Q8_0    = 8,
        GGML_TYPE_Q8_1    = 9,
        GGML_TYPE_Q2_K    = 10,
        GGML_TYPE_Q3_K    = 11,
        ...
}
enum ggml_op {
        GGML_OP_NONE = 0,
        GGML_OP_DUP,
        GGML_OP_ADD,
        GGML_OP_ADD1,
        GGML_OP_ACC,
        GGML_OP_SUB,
        GGML_OP_MUL,
        GGML_OP_DIV,
        GGML_OP_SQR,
        GGML_OP_SQRT,
        GGML_OP_LOG,
        GGML_OP_SIN,
        ...
}

rvv移植代码分析

Github相关链接:
Llama.cpp中利用GGML中对RVV的支持1
Llama.cpp中利用GGML中对RVV的支持2
Tameem-10xE@llama.cpp Github:Added RISC-V Vector Intrinsics Support

起初移植代码在ggml.c中,后续迁移至ggml-quants.c文件中。

修改函数包括12个:
量化转换
quantize_row_q8_0
quantize_row_q8_1
向量点乘:
ggml_vec_dot_q4_0_q8_0
ggml_vec_dot_q4_1_q8_1
ggml_vec_dot_q5_0_q8_0
ggml_vec_dot_q5_1_q8_1
ggml_vec_dot_q8_0_q8_0
ggml_vec_dot_q2_K_q8_K
ggml_vec_dot_q3_K_q8_K
ggml_vec_dot_q4_K_q8_K
ggml_vec_dot_q5_K_q8_K
ggml_vec_dot_q6_K_q8_K

中科院软件所PLCT实验室做了gemv和gemm的量化工作。
矩阵向量乘
gemv
ggml_gemv_q4_0_8x8_q8_0

矩阵矩阵乘
gemm
ggml_gemm_q4_0_8x8_q8_0

这些函数作为不同量化模式的成员函数,在ggml不同算子计算函数中被调用。

llama.cpp的运行机制

量化

llama.cpp的训练后量化使用convert-hf-to-gguf.py脚本对模型进行格式转换,以及数据量化;也可以使用llama-quantize命令。

量化操作

使用llama-quantize

./llama-quantize ggml-model-f16.gguf ggml-model-q4_0.gguf   Q4_0

量化选项Qn_0、Qn_1等含义

  • Q后面的第一个数字n表示了量化到 n bit
  • 下划线后为数字:表示简单量化方法,为0时表示对称量化,没有零点;为1时表示非对称量化,每个scale还有一个zero point;
  • 下划线后为K:表示K-quant量化方法,K后面的字母表示量化模型的参数规模:Small, Meduim,Large
    在这里插入图片描述

sgsprog@hackmd.io: Linux 核心專題: llama.cpp 效能分析

简单量化

在llama.cpp中,32 个矩阵参数为一个 block,每个 block 内完成一次量化操作。也就是常说的 Block-wise Quantization,一个 block 中同一个放缩和平移参数,但不同 block 之间的参数则完全不同并不共享1
在实现中对于一个 tensor (llama7B中常见的 4096x4096)的量化过程中,最外一层循环会将 tensor 分为 chunk 层(每个 chunk 有 4096 x 4 = 16384 个数值,一共 1024 个 chunk),这层循环一般是可以多线程并行处理的。单个chunk 中的数据会进一步被分为 64 个 32 数值块的 block,接下来我们只分析一个 block 内的量化操作。

后缀 _0 的方法(quantize_row_q8_0_reference)步骤为:

  • 1)分块,这里 llama.cpp 做好了较好的并行处理机制;分为 chunk 和 block,chunk 的尺寸一般为 row size 的 4 倍,这里 chunk 主要是为了并行的,chunk 之间并行,chunk 内串行执行。
  • 2)每个 block 内求绝对值 max 。
  • 3)每个数值按 0 到 max 的范围内切开成 127 份,也就是 -max 到 max 切开为 254 份。间隔即量化放缩系数,格式为 FP16,并且保存在新的参数文件中。
  • 4)将每个参数 fp 值通过直接的四舍五入 round 函数映射到 int 值上,并存储。

K-quant量化

K-quant量化使用了 16 x 8的“块”进行量化,每个“块”共有16个行。每 8 个权重为一组使用同一个量化参数scale,因此有 16 个一级量化参数。此外,为了进一步的降低资源消耗,还有 1 个 fp16 的二级量化参数K_2,用于量化16个一级量化参数,相当于“量化参数的量化”,这可以进一步减小模型size和显存消耗2

批处理基准

评估原理

模型推理速度评估

指标英文释义中文释义
PPprompt tokens per batch
TGgenerated tokens per batch
Bnumber of batches
N_KVrequired KV cache size
T_PPprompt processing time (i.e. time to first token)首次token处理的时间
S_PPprompt processing speed ( (B*PP)/T_PP Or PP/T_PP )prompt处理速度
T_TGtime to generate all batches
S_TGtext generation speed((B*TG)/T_TG )token生成速度
Ttotal time
Stotal speed (i.e. all tokens /total time)

模型困惑度评估

困惑度一般用于生成式语言模型的指标。它衡量模型预测数据样本的能力。困惑度得分越低表示语言模型预测下个词的能力越强,而得分越高则表示模型对下个词的预测越不确定或越“困惑”。
武辰@知乎:深入理解语言模型的困惑度perplexity

参考文献


  1. 刀刀宁@知乎:笔记:Llama.cpp 代码浅析(四):量化那些事 ↩︎

  2. meton@知乎:CNN量化 vs. LLM量化 ↩︎

### 使用 Visual Studio 命令行工具 Visual Studio 提供了一系列强大的命令行工具来支持开发人员完成各种任务。这些工具允许开发者在不打开集成开发环境的情况下编译项目、运行构建脚本以及管理解决方案。 #### 启动 Developer Command Prompt 为了方便访问所有的命令行功能,安装 Visual Studio 之后会自动配置一个名为 "Developer Command Prompt for VS" 的快捷方式。通过这个提示符可以轻松调用 MSBuild、devenv.exe 和其他必要的工具[^1]。 ```bash # 打开开始菜单并找到 'Developer Command Prompt for VS' ``` #### 编译 C++ 项目 对于基于 Makefile 的工程,比如 `llama2.c` 文件,在 Visual Studio Code 中可以通过指定路径下的 make 工具来进行编译操作。而在原生的 Visual Studio 环境下,则通常依赖于 nmake 或者直接利用 devenv 来处理整个解决方案文件(.sln)。 ```cmd :: 在命令行中编译单个C/C++源码文件 cl /EHsc /Fe:test.exe test.cpp :: 构建完整的解决方案 "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.com" MySolution.sln /build Release ``` #### 运行 Python 脚本并与调试器交互 当涉及到 Python 开发时,除了常规解释器外还可以借助 Visual Studio 自带的功能将调试器连接至正在运行的 REPL 实例上以便更好地诊断程序行为[^2]。 ```powershell # 启动Python交互模式,并准备接受来自VS的远程调试请求 python -m ptvsd --wait --port 5678 myscript.py ``` #### 设置线程名称(特定于 Xbox) 某些情况下可能需要强制设置线程名用于更详细的日志记录或是性能分析目的;然而需要注意的是此选项仅适用于 Xbox 平台上的应用程序开发,并且可能会干扰到 XDK COM 接口的工作机制因此需谨慎使用[^3]。 ```cpp // 宏定义控制是否开启线程命名特性 #define SETTHREADNAMES 1 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KGback

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值