概述
Llama.cpp是一个基于C++编写的高性能大模型推理框架,旨在提供快速、稳定且易于使用的计算工具,原本的目标是允许在MacBook上使用INT4量化的LLaMA模型,但现在Llama.cpp支持多种计算模式,包括向量计算、矩阵运算、图算法等,可广泛应用于机器学习、图像处理、数据分析等领域。
目录结构
src是构建模型架构的基础库文件夹
examples是部分案例模型的源文件
ggml是计算操作的库文件
源码分析
llama.cpp运行机制分析
可如下参考链接(注意:函数名有所变动):
CodeLeaner@微信公众号:llama.cpp源码解析
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_illama_tokenize(ctx, prompt, true, true) // 将prompt tokenizewhile // 循环产生tokenllama_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); // 打印性能结果
llama.cpp模型构建函数
// 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-attentionstruct 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 networkcur = 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,并调用cpu_backend进行计算,继而调用ggml_graph_compute计算// 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(¶ms, 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
所示,如果type
是GGML_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 elementsvint8m1_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 removedGGML_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
这些函数作为不同量化模式的成员函数,在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 之间的参数则完全不同并不共享[^2]。
在实现中对于一个 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和显存消耗[^1]。