LoRA-llama.cpp

概述

LoRA(Low-Rank Adaption)是微软研究人员2021年提出的一种高效的微调技术,其核心思想就是将大的矩阵分解为两个低秩矩阵表示,从而减少了权重的数据量。如下图所示,将矩阵 \(\Bbb{R}^{1024\times 1024}\)表示为两个低秩矩阵 \(\Bbb{R}^{1024\times 2}\)\(\Bbb{R}^{2\times 1024}\)相乘,其中的大小为2,可以减少256倍的数据体积。

矩阵分解

LoRA

如下图所示,使用高斯随机初始化矩阵 \(A\) ,并将矩阵 \(B\) 初始化为零,因此在训练开始时 \(\Delta W=BA\) 为零。其中, \(W_0 \in \Bbb{R}^{d\times k}\),且矩阵\(B\in\Bbb{R}^{d\times r}\)和矩阵\(A\in \Bbb{R}^{r\times k}\)的秩满足 \(r \ll \min(d,k)\)

\[\large h=W_{0}x+\Delta Wx=W_{0}x + BAx\]

可以使用缩放因子 \(\frac{\alpha}{r}\)\(\Delta Wx\)进行缩放:

\[\large h=W_{0}x+\frac{\alpha}{r}\Delta Wx \]

LoRA
  • LoRA 应用于哪些权重矩阵?

需要注意的是,若将所有参数皆纳入\(\Delta W_q\)\(\Delta W_k\),将会致使精度性能大幅下降,而同时对\(\Delta W_q\)和或\(\Delta W_v\)进行调整,则会产生最优结果。这意味着,即便秩为 4,在 \(\Delta W\) 中亦能够捕捉到充足的信息,故而相较使用秩更大的单一类型的权重,调整多个权重矩阵更为可取。

应用LoRA于不同类型的注意力权重

Loading LoRA

目前llama.cpp中有两种加载lora文件的选项:

  • --lora

指定LoRA适配器文件的路径

  • --lora-scaled

指定LoRA适配器文件的路径和用户定义的缩放系数

  1. 可以多次指定 --lora 选项可实现加载多个LoRA适配器;
  2. 支持动态加载LoRA文件;
  3. 支持将LoRA文件参数与基础模型合并;

LoRA Adapter Init

1
2
3
4
5
6
7
8
9
10
11
12
13
for (auto & la : params.lora_adapters) {
common_lora_adapter_container loaded_la;
loaded_la.path = la.path;
loaded_la.scale = la.scale;
loaded_la.adapter = llama_lora_adapter_init(model, la.path.c_str());
if (loaded_la.adapter == nullptr) {
LOG_ERR("%s: failed to apply lora adapter '%s'\n", __func__, la.path.c_str());
llama_free(lctx);
llama_free_model(model);
return iparams;
}
iparams.lora_adapters.push_back(loaded_la); // copy to list of loaded adapters
}

上述代码其中,llama_lora_adapter_init 函数从lora格式的文件中加载相关信息,判断是否和当前框架匹配,并加载BA矩阵的相关权重参数。

LoRA Format

如下图所示,是LoRA文件的结构示意图,主要结构与GGUF格式一致,主要是KV字典中保存与LoRA适配器相关的内容:

LoRA文件格式

llama.cpp中关于LoRA权重的加载实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static struct ggml_tensor * llm_build_lora_mm(
struct llama_context & lctx,
struct ggml_context * ctx0,
struct ggml_tensor * w,
struct ggml_tensor * cur) {
struct ggml_tensor * res = ggml_mul_mat(ctx0, w, cur);
for (auto & it : lctx.lora_adapters) {
struct llama_lora_weight * lora = it.first->get_weight(w);
if (lora == nullptr) {
continue;
}
const float alpha = it.first->alpha;
const float rank = (float) lora->b->ne[0];
const float scale = alpha ? it.second * alpha / rank : it.second;
struct ggml_tensor * ab_cur = ggml_mul_mat(
ctx0, lora->b,
ggml_mul_mat(ctx0, lora->a, cur)
);
ab_cur = ggml_scale(ctx0, ab_cur, scale);
res = ggml_add(ctx0, res, ab_cur);
}
return res;
}

其中,权重tensor(w)表示匹\(W_Q\)\(W_K\)\(W_V\)\(W_O\)中的任意一个,如果存在则加载,否则跳过。 关于动态加载的实现,基于判断是否存在 lctx.lora_adapters 对象。如果存在,则在计算图中添加BA矩阵的计算分支,这会增加模型的计算量,但由于BA矩阵的比较,增加的计算量可以接受。