lora论文地址:arxiv.org/pdf/2106.09685
dora论文地址:DoRA: Weight-Decomposed Low-Rank Adaptation (arxiv.org)
lora github地址:microsoft/LoRA: Code for loralib, an implementation of "LoRA: Low-Rank Adaptation of Large Language Models" (github.com)
peft github地址:huggingface/peft: PEFT: State-of-the-art Parameter-Efficient Fine-Tuning. (github.com)
运行微调使用的peft版本是v0.11.1, 代码解析使用的peft版本是0.11.2.dev0,这个版本单独将dora提取出来了。
目录
一、peft lora微调代码解析
1.inject_adapter
src/peft/tuners/tuners_utils.py —— def inject_adapter
def inject_adapter(self, model: nn.Module, adapter_name: str, autocast_adapter_dtype: bool = True) -> None:
// ......
for key in key_list:
# Check for modules_to_save in case
if _check_for_modules_to_save and any(
key.endswith(f"{module_to_save}") for module_to_save in peft_config.modules_to_save
):
# Optionally set the modules to save
parent, target, target_name = _get_submodules(model, key)
if not isinstance(target, ModulesToSaveWrapper):
new_module = ModulesToSaveWrapper(target, adapter_name)
setattr(parent, target_name, new_module)
else:
target.update(adapter_name)
_has_modules_to_save = True
continue
if not self._check_target_module_exists(peft_config, key):
continue
self.targeted_module_names.append(key)
is_target_modules_in_base_model = True
parent, target, target_name = _get_submodules(model, key)
self._create_and_replace(peft_config, adapter_name, target, target_name, parent, current_key=key)
// ......
这里是使用lora替换模型,方法用于创建适配器层并将目标模块替换为适配器层。
这里的modules_to_save这个参数很重要,会检查key_list中的每个模块中是否以modules_to_save结尾,如果是会将其包装为 ModulesToSaveWrapper 模块以保存其状态,意思是会不进行lora层的替换,该线性层不会被替换为两个低秩矩阵的,而是会以原来的结构大小进行全参训练并最终保存在lora权重当中。如果检查通过则会self._create_and_replace进行lora层的替换。
2._create_and_replace
src/peft/tuners/lora/model.py —— def _create_and_replace()
def _create_and_replace(...):
// ......
if isinstance(target, LoraLayer) and not isinstance(target, AdaLoraLayer):
target.update_layer(
adapter_name,
r,
lora_alpha=alpha,
lora_dropout=lora_config.lora_dropout,
init_lora_weights=lora_config.init_lora_weights,
use_rslora=lora_config.use_rslora,
use_dora=lora_config.use_dora,
)
else:
new_module = self._create_new_module(lora_config, adapter_name, target, **kwargs)
if adapter_name not in self.active_adapters:
# adding an additional adapter: it is not automatically trainable
new_module.requires_grad_(False)
self._replace_module(parent, target_name, new_module, target)
// ......
用于创建新模块或更新现有的 LoRA 模块,并将目标模块替换为新创建的模块,如果该层已经是LoraLayer或者是AdaLoraLayer层了,则会进行一次update,如果不是lora层则会创建新的lora层结构并对原始的线性层进行替换。
3._create_new_module
src/peft/tuners/lora/model.py —— def _create_new_module()
主要关注:
dispatchers.extend(
[
dispatch_eetq,
dispatch_aqlm,
dispatch_awq,
dispatch_gptq,
dispatch_hqq,
dispatch_megatron,
dispatch_default,
]
)
这里与
目录中的一致,其中是不同初始化方法的lora层结构,在这个函数进行lora层的创建,当然实际上都离不开对lora_A和lora_B两个低秩矩阵的初始化的创建。
二、llama2-7b lora微调实战
我们这里只关心跑通后看一下我们之前理解的lora微调流程逻辑是否正确,这里不关注最后微调结果的性能精度数据。
我们先看一下微调之前的llama2-7b结构
看一下其中线性层的结构:
from transformers import AutoModel
model_llama = AutoModel.from_pretrained('llama-2-7b-hf')
print(model_llama.state_dict())
发现其中的layers.0包含了很多,有q_proj,v-proj......等等
我们现在开始训练,再看一下训练后保存了什么。
1.构造一个简单的数据集
from datasets import Dataset
train_dataset = []
en = "good, morning"
zh = "早上好"
train_dataset.append({'text': 'Translate English to Chinese:\nInput:' + en + "\nOutput:" + zh + '</s>'})
train_dataset = Dataset.from_dict({key: [dic[key] for dic in train_dataset] for key in train_dataset[0]})
2.配置lora config
from peft import LoraConfig, get_peft_model
peft_config = LoraConfig(
r=8,
lora_alpha=8,
target_modules=['q_proj', "v_proj"],
lora_dropout=0.05,
bias='none',
task_type='CAUSAL_LM',
modules_to_save=['layers.0.self_attn.q_proj']
)
r为低秩矩阵的秩,假如原来的线性层维度是1024*1024,设置r后替换为的lora_A维度就是1024*r,lora_B为r*1024。
target_modules表示将线性层中的q_proj,v_proj替换为lora层,其他的不替换也不会保存。
modules_to_save这个参数我们在上面提到过,设置中的层不会替换但会更新权重并保存,我这里将layer.0层的q_proj设置为不替换为lora,并保存在最终的lora权重中。
3.配置训练器参数
这里直接使用全参SFT的训练器了:
from trl import SFTTrainer, SFTConfig
train_aruments = SFTConfig(
output_dir=output_dir,
per_device_train_batch_size=64,
optim='adamw_torch',
learning_rate=10e-4,
save_steps=10,
logging_steps=10,
group_by_length=False,
num_train_epochs=20,
gradient_accumulation_steps=1,
gradient_checkpointing=True,
max_grad_norm=0.3,
lr_scheduler_type='cosine'
)
4.开始训练
直接上全部的代码:
import torch
from datasets import Dataset
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer, SFTConfig
from transformers import TrainingArguments, AutoModelForCausalLM, AutoTokenizer
train_dataset = []
en = "good, morning"
zh = "早上好"
train_dataset.append({'text': 'Translate English to Chinese:\nInput:' + en + "\nOutput:" + zh + '</s>'})
train_dataset = Dataset.from_dict({key: [dic[key] for dic in train_dataset] for key in train_dataset[0]})
output_dir = r'output'
model_name = r"llama-2-7b-hf"
peft_config = LoraConfig(
r=8,
lora_alpha=8,
target_modules=['q_proj', "v_proj"],
lora_dropout=0.05,
bias='none',
task_type='CAUSAL_LM',
modules_to_save=['layers.0.self_attn.q_proj']
)
train_aruments = SFTConfig(
output_dir=output_dir,
per_device_train_batch_size=64,
optim='adamw_torch',
learning_rate=10e-4,
save_steps=10,
logging_steps=10,
group_by_length=False,
num_train_epochs=20,
gradient_accumulation_steps=1,
gradient_checkpointing=True,
max_grad_norm=0.3,
lr_scheduler_type='cosine'
)
model = AutoModelForCausalLM.from_pretrained(model_name)
model.enable_input_require_grads()
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
model.config.use_cache = False
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token_id = 0
tokenizer.padding_side = 'right'
trainer = SFTTrainer(
model=model,
train_dataset=train_dataset,
dataset_text_field='text',
peft_config=peft_config,
max_seq_length=1024,
tokenizer=tokenizer,
args=train_aruments
)
trainer.train()
trainer.model.save_pretrained(output_dir)
output_dir = r'output'
model_name = r"llama-2-7b-hf"
修改为自己的路径
看一下微调后的结果:
因为我们设置了每10次保存一次权重,所以多出了checkpoint-10和checkpoint-20这两个文件。
先看一下config:
发现都是我们刚刚配置过的lora_config参数
但有一个参数值的注意的是,base_model_name_or_path,这个参数是我们之前加载的原始的llama2-7B权重的路径,这里的路径地址的文件如果变更那么融合的时候就会报错,实际上后面在融合权重的时候不用再加载原始权重也是因为这里保存了训练前的原始权重的路径了。
再看一下adapter_model.safetensors里保存了什么权重:
import torch
from safetensors.torch import load_file, save_file
from safetensors import safe_open
model_path = 'output/adapter_model.safetensors'
tensors = {}
with safe_open(model_path, framework="pt", device='cpu') as f:
for k in f.keys():
tensors[k] = f.get_tensor(k)
print(tensors)
与我们之前的代码分析完全一致
其中没有保存原始的权重,只保存了替换后的lora的两个低秩矩阵lora_A,lora_B,我们target指定的是v_proj和q_proj,所以保存的每一层的权重当中出现了v_proj和q_proj两个lora层,我们又指定了modules_to_save,所以layers.0层的q_proj没有被替换,以原来的全参大小进行训练并保存在了lora权重中。
5.dora
peft已经支持dora微调了:
权重矩阵W可以分解为幅度m和方向V两个组件,即W = m * V,其中m是幅度向量(通过权重矩阵的列范数计算得到),V是方向矩阵。
微调福哦城中的梯度原始的预训练权重(W0)冻结,而通过DoRA方法,可以训练幅度(m)和方向(V+∆V)的更新。这些更新是通过合并低秩矩阵B和A来实现的,即∆V = BA。
CV算法工程师的LLM日志(2)PEFT训练技术——10分钟快速理解DORA【原理&&代码】_dora llm-CSDN博客
具体的原理就是从原来的权重要分解为两个权重幅度m和方向V,其中方向V还要被替换为lora的两个低秩矩阵参数训练。
只需要在peft的lora配置中配置use_dora即可使用dora微调:
peft_config = LoraConfig(
r=8,
lora_alpha=8,
target_modules=['q_proj', "v_proj"],
lora_dropout=0.05,
bias='none',
task_type='CAUSAL_LM',
modules_to_save=['layers.0.self_attn.q_proj'],
use_dora=True
)
结果的结构都一致,我们主要看adapter_model.safetensors
layers.0.self_attn.q_proj不用看,我们使用modules_to_save将它屏蔽了使用全参的方式做微调。
发现除了lora_A和lora_B还出现了lora_magnitude_vector,也就是幅度m,其中方向被替换为了lora层,实际上dora在lora的初始化方面做了修改,但流程和结构上还是沿用的lora,所以peft在架构上将dora归为到了lora包中,并仅仅使用了use_dora参数简单控制是否使用dora进行微调的原因。
6.权重融合
from peft import AutoPeftModelForCausalLM
import torch
lora_model_dir = "output"
model = AutoPeftModelForCausalLM.from_pretrained(lora_model_dir, device_map='auto', torch_dtype=torch.bfloat16)
model = model.merge_and_unload()
out_put_dir = 'output_merge'
model.save_pretrained(out_put_dir)
看一下权重融合的线性层:
这里融合使用的lora参数里没有设置modules_to_save,可以看到指定的q_proj和v_proj被进行微调,其他层都保持了原始参数没有产生变化。