概述
Llama.cpp是一个基于C++编写的高性能大模型推理框架,旨在提供快速、稳定且易于使用的计算工具,原本的目标是允许在MacBook上使用INT4量化的LLaMA模型,但现在Llama.cpp支持多种计算模式,包括向量计算、矩阵运算、图算法等,可广泛应用于机器学习、图像处理、数据分析等领域。
目录结构
src是构建模型架构的基础库文件夹
examples是部分案例模型的源文件
ggml是计算操作的库文件
源码分析
可如下参考链接(注意:函数名有所变动):
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(¶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 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。
批处理基准
评估原理
模型推理速度评估
指标 | 英文释义 | 中文释义 |
---|---|---|
PP | prompt tokens per batch | |
TG | generated tokens per batch | |
B | number of batches | |
N_KV | required KV cache size | |
T_PP | prompt processing time (i.e. time to first token) | 首次token处理的时间 |
S_PP | prompt processing speed ( (B*PP)/T_PP Or PP/T_PP ) | prompt处理速度 |
T_TG | time to generate all batches | |
S_TG | text generation speed((B*TG)/T_TG ) | token生成速度 |
T | total time | |
S | total speed (i.e. all tokens /total time) |
模型困惑度评估
困惑度一般用于生成式语言模型的指标。它衡量模型预测数据样本的能力。困惑度得分越低表示语言模型预测下个词的能力越强,而得分越高则表示模型对下个词的预测越不确定或越“困惑”。
武辰@知乎:深入理解语言模型的困惑度perplexity