Ref: 微调llama 3 — PEFT微调和全量微调_llama3 微调原理-CSDN博客
2. PEFT方法微调llama 3
1、QLoRA 是量化的 LoRA 与 LLMs 的结合。要使用这种方法对 Llama 3 8B 进行微调,我们需要安装
pip install --upgrade bitsandbytes transformers peft accelerate datasets trl
2、然后导入需要的pkgs
import torch, os
from datasets import load_dataset
from peft import LoraConfig, prepare_model_for_kbit_training
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
)
from trl import SFTTrainer
3、如果你拥有较新的GPU,就可以使用bfloat16
数据类型以获得更好的训练稳定性,并使用FlashAttention
来减少处理长序列时的内存消耗。下面的代码会自动检测GPU是否兼容bfloat16
、FlashAttention
:
#use bf16 and FlashAttention if supported if torch.cuda.is_bf16_supported(): os.system('pip install flash_attn') compute_dtype = torch.bfloat16 attn_implementation = 'flash_attention_2' # 传统的注意力计算在处理长序列时可能会非常耗时和占用大量内存。 FlashAttention 技术通过优化计算过程和内存使用,使得注意力机制能够更快、更高效地运行 else: compute_dtype = torch.float16 attn_implementation = 'sdpa'动态范围较小,可能在处理非常大或非常小的数值时遇到数值稳定性问题
float16
具有5位指数和10位尾数,动态范围小(可表示的数值范围)、精度高;
bfloat16
具有8位指数和7位尾数,动态范围大(数值稳定性高)、精度略低(但对于深度学习也足够了)
float32
具有1位符号位,8位指数位和23位尾数位,动态范围大,精度更高。# 精度上 b
float16
<float16
<float32
#占用内存上float16 =
bfloat16
= 0.5 *float32
4、然后,我们需要初始化并配置Tokenizer。通常,LLMs在预训练时不包含pad_token。然而,在微调过程中,由于我们的训练示例长度不相同,我们需要将其填充到batch中。我们可以创建并添加一个pad_token到词汇表中,但更简单的选择是将eos_token指定为pad_token。
model_name = "meta-llama/Meta-Llama-3-8B"
#Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, add_eos_token=True, use_fast=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.pad_token_id = tokenizer.eos_token_id
tokenizer.padding_side = 'left'# 注意,我们使用的是左边填充。如果想使用flash_attention,右填充是不兼容的。
#
pad_token
:这是分词器用来填充序列的token,pad_token
的ID在模型的词汇表中通常是0。#
eos_token
:这是分词器用来表示序列结束的token,在词汇表中,eos_token
的ID通常是1或2。
5、至于微调数据集,可以选择了 timdettmers/openassistant-guanaco,因为这个数据集足够小。
OpenAssistant Guanaco数据集是一个用于大模型微调的高质量且开源的数据集。该数据集由13500名全球志愿者共同标注完成,包含了35种语言在内的66497个完整的对话树,共计161443条消息,以及461292条优质的人类评分。
6、然后,我们创建bnb_config并加载模型:
bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=compute_dtype, bnb_4bit_use_double_quant=True, ) # 这段代码配置了一个 BitsAndBytes 量化方案,该方案旨在通过减少模型权重和中间计算的精度来加速训练和推理,同时尽量减少精度损失。 model = AutoModelForCausalLM.from_pretrained(model_name, quantization_config=bnb_config, device_map={"": 0}, attn_implementation=attn_implementation) # BitsAndBytes 量化通常不是在加载预训练模型时进行的,而是在模型加载完成后,在准备进行推理或进一步训练之前进行的。
7、bnb_config定义了在4位精度下加载模型,并对量化常数进行量化(即双重量化)。在前向传递过程中,如果你的GPU支持bfloat16数据类型,则将创建bfloat16张量。请注意:如果你的GPU不支持bfloat16,则笔记本将使用float16。然而,这可能会导致训练不稳定。如果你发现训练损失降至0或NaN,请将compute_dtype更改为torch.float32。
8、为了减少激活的内存消耗,我们还需要启用梯度检查点,这是通过
model = prepare_model_for_kbit_training(model) # 梯度检查点技术通过在前向传播期间临时丢弃一些中间结果,仅保留必要的信息(即检查点),来减少内存使用量。在反向传播过程中,当需要这些被丢弃的中间结果时,再重新计算它们,而不是存储所有的中间结果。这样,可以显著减少内存消耗,使得训练更深的网络或更大的批量大小成为可能。
9、对于 LoRA 的配置,可以使用:
peft_config = LoraConfig( lora_alpha=16, lora_dropout=0.05, # 在LoRA的参数上应用5%的dropout r=16, bias="none", # 偏置项保持预训练模型中的值,并在微调过程中不会更新 task_type="CAUSAL_LM", # 模型的类型或用于的任务类型 “因果语言模型” target_modules= ['k_proj', 'q_proj', 'v_proj', 'o_proj', "gate_proj", "down_proj", "up_proj"] ) # 如果原始权重矩阵的维度是(m, n),那么LoRA的秩(即增加的矩阵的维度)通常是(m, lora_alpha * r)和(lora_alpha * r, n)。在您提供的配置中,lora_alpha=16意味着LoRA将使用比r大16倍的秩。r即为低秩矩阵的维度。 #target_modules这是一个列表,指定了将应用LoRA的目标模块的名称。在Transformer模型中,这些模块通常是注意力机制中的投影矩阵(key、query、value投影)和输出投影矩阵。在您的配置中,target_modules包含了Transformer的多个投影矩阵("k_proj", "q_proj", "v_proj", "o_proj")以及其他可能的模块("gate_proj", "down_proj", "up_proj")。这些额外的模块可能是特定模型架构或自定义层的一部分。
可以增加rank来获得更好的结果。增加rank也会增加内存消耗,因为rank增大,适配器的参数也会增加。
10、接下来,定义训练参数和超参数:
training_arguments = TrainingArguments( output_dir="./Llama3_8b_QLoRA", #指定模型输出和检查点(checkpoints)的保存目录 evaluation_strategy="steps", # 定义评估策略。"steps"意味着每隔一定数量的训练步骤就会进行一次评估。 do_eval=True,#是否在训练过程中进行评估 optim="paged_adamw_8bit",#优化器 per_device_train_batch_size=8, #每个GPU上每个批次包含8个样本 gradient_accumulation_steps=4, #没4步(批次)进行一次参数更新,那么就是8*4个样本更新一次参数 per_device_eval_batch_size=8,#在每个设备上用于评估的批次大小 log_level="debug", save_strategy="epoch",#"epoch"意味着在每个训练周期结束时保存检查点 logging_steps=100,#每多少步记录一次日志 learning_rate=1e-4, fp16 = not torch.cuda.is_bf16_supported(), #是否使用半精度浮点数(FP16)进行训练 bf16 = torch.cuda.is_bf16_supported(), #是否使用BF16(半字节浮点数)进行训练 eval_steps=100,# 每多少步进行一次评估 num_train_epochs=3, #训练周期的数量。在这里,它被设置为3,意味着整个训练集将被遍历3次。 warmup_ratio=0.1, #学习率预热(warmup)的比例。"0.1" 意味着在前10%的训练步骤中,学习率会逐渐从初始值增加到设置的学习率。 lr_scheduler_type="linear", #学习率调度器的类型。"linear"意味着学习率将随着训练的进行而线性地减小。 )
11、使用"paged_adamw_8bit",会在需要时将一些优化器状态存储到CPU RAM中,以进一步减少GPU内存消耗。
补充:QLoRA其实是核心就是在LoRA的技术加上深度的量化过程。核心优化思想包括以下三点:
(1)4bit NoramlFloat Quantization:一种新的数据类型,只用4字节表征参数并且保证整个模型的精度损失极小.(和我们之前的Int8,int4量化方式不同, 原理这篇先不展开了)
(2)Double Quantization:对第一次量化后的那些常量再进行一次量化,减少存储空间。
(3)Paged optimizers:使用NVIDIA统一内存功能,该功能在CPU和GPU之间进行自动page对page传输,以便在GPU偶尔OOM的情况下进行。可以从现象上理解成出现训练过程中偶发OOM时能够自动处理,保证训练正常训练下去。
对于批量大小,随机选择了一个批量大小为32(每个设备的批量大小为8,梯度累积步骤为4(8x4=32)的配置)。该配置消耗了16.6 GB的GPU内存。如果你只有16 GB的GPU,请将每个设备的批量大小减少到4。
12、最后,开始微调时,运行以下命令:
trainer = SFTTrainer( model=model, train_dataset=ds['train'], eval_dataset=ds['test'], peft_config=peft_config, dataset_text_field="text", max_seq_length=512, tokenizer=tokenizer, args=training_arguments, ) trainer.train()
13、使用Google Colab的L4实例完成这3个epoch大约需要10个小时。
3. 将微调后的adapter集成到Llama 3中
为了避免每次使用时都加载adapter(可能是微调的低秩矩阵),你可以将其合并到 Llama 3 中。当适配器已经使用 QLoRA 进行微调时,必须小心进行合并,以保持adapter的大部分准确性。我们必须遵循以下步骤:
- 加载并量化Llama 3
- Dequantize Llama 3 to the compute dtype used during QLoRA fine-tuning
- Merge the adapter into the dequantized model
- Save the resulting model
最后得到一个没有量化的模型。我们不能像微调那样用bitsandbytes量化它,否则会严重降低模型的性能。使用AWQ或GPTQ来代替即可。
微调之后再次加载模型需要单独加载预训练模型参数和微调低秩矩阵模型参数,如果将两者合并就不需要单独加载微调的模型参数了。但是在合并时需要谨慎,以确保适配器的性能不会受到损害。这可能涉及到量化误差的管理,以及确保合并后的模型能够在保持适配器性能的同时,有效执行其在训练任务上的推理。
4. 使用AWQ对llama 3进行4位量化
AWQ是一种量化方案,它保留了模型的重要权重。AWQ很准确,也受到高效的推理核的支持。首先需要安装AutoAWQ: pip install autoawq
然后,用几行代码执行量化,例如,要量化前一节合并后得到的模型:
from awq import AutoAWQForCausalLM from transformers import AutoTokenizer tokenizer_path = "meta-llama/Meta-Llama-3-8B" #分词器的路径 model_path = './dqz_merge/' #量化前模型的存储路径 quant_path = 'llama-3-oasstguanaco3e-awq-4bit' #量化后模型的保存路径 quant_config = { "zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM" } #量化配置的参数 # Load model and tokenizer model = AutoAWQForCausalLM.from_pretrained(model_path, safetensors=True) tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, use_fast=True)# Quantize model.quantize(tokenizer, quant_config=quant_config) # 调用模型的 quantize方法进行量化,传递了分词器和量化配置quant_config作为参数。量化过程会将模型的权重转换为更低位数的表示,以减少模型的存储大小和加速推理。 # Save quantized model with safetensors model.save_quantized("./"+quant_path, safetensors=True) #使用model.save_quantized方法将量化后的模型保存到指定的路径 tokenizer.save_pretrained("./"+quant_path) #使用分词器的save_pretrained方法将分词器对象保存到量化模型相同的路径下,这样量化模型和分词器就可以一起被加载和使用了。
这将把量化模型保存到一个名为“llama-3-oasstguanaco3e-awq-4bit”的目录中。
5. 完全微调模型
QLoRA和LoRA只是微调适配器。如果你真的想微调整个模型,你可以尝试GaLore。GaLore将梯度投影到低秩子空间,以显著减少它们的维数,从而减少它们的内存消耗。虽然GaLore大大降低了优化器状态的内存需求,但你仍然需要48GB的GPU RAM。
CODE
具体的notebook代码可以在github仓库中拿到。
notebook中包含了4个部分:
- QLoRA fine-tuning
- Merging the fine-tuned adapter into the base model
- Quantization the Llama 3 with AWQ
- Appendices: LoRA and GaLore fine-tuning