【导读】:本文是LLM模型微调第六篇,分享论文QLoRA: Efficient Finetuning of Quantized LLMs的解读。主要内容有论文解读(提出背景、技术原理,细节补充...),实验效果,细节解析 和代码详解。
QLoRA相关
【#】LoRA相关论文及文档
QLoRA论文:QLoRA: Efficient Finetuning of Quantized LLMs
论文地址:https://arxiv.org/abs/2305.14314
Github地址:https://github.com/artidoro/qlora
HF地址:https://huggingface.co/timdettmers
QLoRA oral presentation at NeurIPS 2023:
https://neurips.cc/media/neurips-2023/Slides/73855.pdf
QLora训练更大的GPT文档:
https://readpaper.feishu.cn/docx/CrMGdSVPKow5d1x1XQMcJioRnQe
8bit量化:https://huggingface.co/blog/zh/hf-bitsandbytes-integration
4bit量化:https://huggingface.co/blog/zh/4bit-transformers-bitsandbytes
PEFT: https://https://github.com/huggingface/peft
【#】QLoRA-文章目录
QLoRA-论文解读
【1】QLoRA的提出背景~ 【2】QLoRA的技术原理
【3】QLoRA的细节补充-4-bit NormalFloat(NF4)
【4】QLoRA的细节补充-Double Quantization -双重量化
QLoRA-实验效果
【1】QLoRA的实验结果QLoRA vs. Standard Finetuning
【2】QLoRA的实验结果Pushing the Chatbot State-of-the-art with QLoRA
【3】QLoRA vs. Standard Finetuning实验细节
QLoRA-细节解析
【1】QLoRA微调经验碎碎念
【2】💡LoRA微调大模型的实践经验总结
QLoRA-代码详解
【1】BitsAndBytesConfig参数详解
【2】QLoRA-bnb-4bit-training.ipynb
【3】QLoRA-权重合并推理
【4】量化分位数如何计算的
QLoRA-论文解读
【1】QLoRA的提出背景~
【2】QLoRA的技术原理
4bit NormalFloat(NF4):4-bit NormlFLoat量化是对Quantile Quantization(分位量化)进行了改进,并结合Block-wise Quantization,降低计算复杂度和误差。即提出基于分块(Block-wise)的分位数量化(Quantile Quantization)的量化策略; 双重量化(Double Quantization):是针对量化常数的二次量化。由于BnB的量化是块量化(block-wise),因此块级别的常数存储也会占用GPU memory。对第一次量化后的那些常量再进行一次量化,减少存储空间。 分页优化器(Paged Optimizers):为防止梯度检查点所引起的内存波动,导致的内存不足错误。使用NVIDIA统一内存特性,该特性可以在在GPU偶尔OOM的情况下,进行CPU和GPU之间自动的页面切换,以实现无错误的 GPU 处理。使用此功能为优化器状态(Optimizer)分配分页内存,然后在 GPU 内存不足时将其自动卸载到 CPU 内存,并在优化器更新步骤需要时将其加载回 GPU 内存。
【3】QLoRA的细节补充-4-bit NormalFloat(NF4)
QLORA引入多项创新来节省内存而不牺牲性能:(a) 4位NormalFloat (NF4) 数据类型,理论上最优用于正态分布权重 (b) 双重量化,通过量化量化常数来减少平均内存占用 (c) 分页优化器来管理内存峰值。
分块k位量化的量化和反量化的过程
作者提出的4-bit NormlFLoat量化是对Quantile Quantization(分位量化)进行了改进,并结合Block-wise Quantization,降低计算复杂度和误差。
【1】NF4量化
【2】Quantile Quantization 分位量化
【3】NormFloat-NFK
(NormFloat在信息论上对于以零为中心的正态分布数据是最优的。)
【4】QLoRA的细节补充-Double Quantization -双重量化
Double Quant 前,每个参数做量化会需要额外的32/64=0.5 bits 显存;
Double Quant后,每个参数做量化只需要额外的8/64+32/(64*256)= 0.127 bits 显存
QLoRA-实验效果
【1】QLoRA的实验结果QLoRA vs. Standard Finetuning
【4位NormalFloat比4位浮点数表现更好】在语言模型和零样本任务的评估中,4-bit NormalFloat数据类型(NF4)表现出更好的性能,优于4-bit浮点数(FP4)。如上表所示:研究采用了不同类型的量化LLMs(OPT,BLOOM,Pythia,LLaMA)以及不同大小(125M到65B),比较NF4、FP4和Int4的性能,并发现NF4能够显著提升性能,双重量化可以减少内存占用同时不影响性能。
【k位QLoRA匹配16位全微调和16位LoRA性能】比较RoBERTA和T5模型在GLUE和Super-NaturalInstructions数据集上结果表明:无论是使用16位、8位还是4位的适配器方法,都能够复制全16位微调的基准的性能。这说明,尽管量化过程中会存在性能损失,但通过适配器微调,完全可以恢复由于量化不准确而丢失这些性能。
【具有双重量化的NF4】测试4位QLoRA是否可以在7B到65B参数范围内匹配16位LoRA。为此,在两个指令跟随数据集Alpaca和FLAN v2上对LLaMA 7B至65B进行了微调,并在MMLU基准测试上通过5点精度进行了评估。结果上图所示,其中看到具有双重量化的NF4完全恢复了16位LoRA在MMLU性能。与FP4相比,QLoRA性能落后于16位Brain Float LoRA基准约1个百分点。这证实我们的发现:(1)QLoRA与NF4能够复制16位全微调和16位LoRA微调的性能,以及(2)NF4在量化精度方面优于FP4。
【2】QLoRA的实验结果Pushing the Chatbot State-of-the-art with QLoRA
使用MMLU(Massively Multitask Language Understanding)基准 来测量模型在一系列语言理解任务上的性能。报告5-shot测试准确率。
还通过自动化和人工评估来测试生成语言能力。使用nucleus采样(p=0.9)和温度0.7进行所有测试。
基于QLoRA调优方法,作者对OASST1进行调优得到系列模型Guanaco。并基于自动化和人工评估,发现QLoRA优化的顶级模型Guanaco 65B是性能最好的开源聊天机器人模型,其性能与ChatGPT相比具有竞争力。与GPT-4相比,Guanaco 65B和33B的预期获胜概率为30%。在Vicuna基准相对于ChatGPT的结果如下图所示。我们发现,Guanaco 65B是继GPT-4之后性能最好的型号,相对于ChatGPT实现了99.3%的性能。Guanaco 33B比Vicuna 13B型号有更多的参数,但其权重仅使用4位精度,因此在21 GB和26 GB时的存储效率要高得多,比Vicuna13B提高了三个百分点。此外,Guanaco 7B以5GB的容量轻松安装在手机上,同时仍比Alpaca 13B高出近20个百分点。
【#】QLoRA vs. Standard Finetuning实验细节
QLoRA-细节解析
【1】QLoRA微调经验碎碎念
介绍一下lora的原理和ptuning的原理。P-tuning与Lora有什么区别;
Lora方法的核心是在大型语言模型上对指定参数增加额外的低秩矩阵,也就是在原始PLM旁边增加一个旁路,做一个降维再升维的操作。并在模型训练过程中,固定PLM的参数,只训练降维矩阵A与升维矩阵B。
ptuning方法的核心是使用可微的virtual token替换了原来的discrete tokens,且仅加入到输入层,并使用prompt encoder(BiLSTM+MLP)对virtual token进行编码学习。
LoRA和全参数训练在计算量和显存上相比如何?为什么LoRA能提升大模型训练效率?
1、计算量上:LoRA训练时,在主干模型的(部分)全连接层增加了LoRA旁路,前向和后向的计算量都在主干模型的基础上,增加了旁路部分的计算,因此相比全参数训练,略有增加。
2、显存上:训练时,显存主要有①模型参数②梯度③中间激活值④优化器参数四个部分。模型参数/梯度/激活值相 比全参数训练也略微增加;而优化器则不需要再存储原模型参数的部分,只需要存储LoRA旁路部分,这部分节省较多显存。
3、使用LoRA能提升训练效率主要是因为(1)优化器部分的显存需要减少了,可以增大batch(2)优化器参数减少了,分布式训练中多卡之间的通信量减少了(3)(optional)主干模型由于不用更新,可以进一步量化到int8/int4等。
FP4 量化有硬件要求吗?(qlora使用NF4)FP4量化方法仅与 GPU 兼容,目前尚无法在 CPU 上对模型进行 4 比特量化。在 GPU 中,此方法没有任何硬件要求,只要安装了 CUDA>=11.2,任何 GPU 都可以用于运行 4 比特量化。另请记住,计算不是以 4 比特完成的,仅仅是权重和激活被压缩为该格式,而计算仍在指定的或者原始数据类型上进行。
可以训练 4 比特 / 8 比特模型吗?对这些模型进行全模型 4 比特训练是不可能的。但是,可以利用参数高效微调 (PEFT) 来训练这些模型,即在基础模型之上训练新增部分如适配器。QLoRA 论文就是这么做的,Hugging Face 的 PEFT 库也正式支持了该方法。
QLoRA 量化调用流程
PreTrainedModel.from_pretrained 调用了 bitsandbytes.py 的 replace_with_bnb_linear,将 nn.Linear 层替换成量化层。
随后需要加载模型权重,调用函数 _load_pretrained_model -> _load_state_dict_into_meta_model -> set_module_quantized_tensor_to_device
set_module_quantized_tensor_to_device 在 bitsandbytes.py 中,用于将权重转化为 int4 量化权重,并更新量化层信息。
new_value = bnb.nn.Params4bit(new_value, requires_grad=False, **kwargs).to(device)
转化过程在 Params4bit.cuda 中实现,它调用了 bnb.functional.quantize_4bit 函数,这个函数再调用 lib 的一个函数 lib.cquantize_blockwise_fp16_nf4,所以核心计算过程都在 cuda 上完成。
在 kernel.cu 找到对应的函数 kQuantizeBlockwise,它用了 cub 做 block 内部的通信。
最终,每个参数的量化交给 dQuantizeNF4 这个函数完成,而这个函数就是依照输入数值的区间赋值的,简单粗暴。
https://zhuanlan.zhihu.com/p/646235855
LoRA与其他微调大模型对比优点
Adapter Tuning 增加了模型层数,Adapter引入了额外的推理延迟(只能串行)。
Prefix-Tuning 难于训练,且预留给 Prompt 的序列挤占了下游任务的输入序列空间,影响模型性能。
P-tuning v2 很容易导致旧知识遗忘,微调之后的模型,在之前的问题上表现明显变差。要占用context length,变相的降低模型能力。
【2】💡LoRA微调大模型的实践经验总结
LoRA的一致性:尽管训练模型通常具有随机性,但是多次进行LoRA微调的实验结果在测试集上的效果十分稳定。
QLoRA的计算与内存权衡:QLoRA是一种在微调时进一步降低内存使用的技术,通过将预训练的权重量化为4位精度,并使用分页优化器来处理内存峰值。QLoRA可以节省33%的GPU内存,但增加了39%的训练时间。
默认的16位浮点LoRA:训练时间:1.85小时,内存使用:21.33GB
这里的4位浮点QLoRA:训练时间:2.79小时,内存使用:14.18GB
学习率调度器:文章讨论了余弦退火学习率调度器如何调整学习率,模仿余弦曲线逐渐减少学习率以优化收敛并避免过度拟合。实验中引入这种调度器显著改善了SGD性能,但对Adam和AdamW优化器影响较小。
Adam与SGD的比较:在7B参数的Llama 2模型训练中,使用AdamW和LoRA的默认设置(r=8)需要14.18GB的GPU内存,而使用SGD需要14.15GB的内存,节省非常有限。
多次训练epoch:对于50k样本的Alpaca指令微调数据集,增加训练迭代次数后,模型性能出现下降,这表明多轮训练可能不适用于指令微调,因为可能会导致过拟合。
为更多层启用LoRA:实验显示,如果为更多层启用LoRA,虽然内存需求从14.18GB增加到16.62GB,但模型性能有显著提升。
平衡LoRA超参数R和Alpha:文章讨论了LoRA权重的缩放系数,发现虽然一般来说是较好的选择,但在某些情况下,不同的组合可能会产生更好的性能。
在单GPU上训练7B参数模型:LoRA技术使得在单个GPU上微调7B参数的模型成为可能。使用QLoRA最优设置(r=256和alpha=512)时,训练一个7B参数模型在A100 GPU上需要大约3小时。
【1】BitsAndBytesConfig参数详解
使用bnb加载nf4量化的模型
from transformers import BitsAndBytesConfig
nf4_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16
)
model_nf4 = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=nf4_config
)
【2】QLoRA-bnb-4bit-training.ipynb
在消费级GPU上微调大型语言模型
!pip install -q -U bitsandbytes
!pip install -q -U git+https://github.com/huggingface/transformers.git
!pip install -q -U git+https://github.com/huggingface/peft.git
!pip install -q -U git+https://github.com/huggingface/accelerate.git
!pip install -q datasets
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
model_id = "EleutherAI/gpt-neox-20b"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
#加载model 使用qlora
model = AutoModelForCausalLM.from_pretrained(model_id,
quantization_config=bnb_config, device_map={"":0})
from peft import prepare_model_for_kbit_training
model.gradient_checkpointing_enable()
model = prepare_model_for_kbit_training(model)
def print_trainable_parameters(model):
"""
Prints the number of trainable parameters in the model.
"""
trainable_params = 0
all_param = 0
for _, param in model.named_parameters():
all_param += param.numel()
if param.requires_grad:
trainable_params += param.numel()
print(
f"trainable params: {trainable_params} || all params: {all_param} || trainable%: {100 * trainable_params / all_param}"
)
from peft import LoraConfig, get_peft_model
config = LoraConfig(
r=8,
lora_alpha=32,
target_modules=["query_key_value"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, config)
print_trainable_parameters(model)
from datasets import load_dataset
data = load_dataset("Abirate/english_quotes")
data = data.map(lambda samples: tokenizer(samples["quote"]), batched=True)
import transformers
# needed for gpt-neo-x tokenizer
tokenizer.pad_token = tokenizer.eos_token
trainer = transformers.Trainer(
model=model,
train_dataset=data["train"],
args=transformers.TrainingArguments(
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
warmup_steps=2,
max_steps=10,
learning_rate=2e-4,
fp16=True,
logging_steps=1,
output_dir="outputs",
optim="paged_adamw_8bit"
),
data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
model.config.use_cache = False # silence the warnings. Please re-enable for inference!
trainer.train()
基于QLoRa+Transformer的推理
text = "Ask not what your country"
device = "cuda:0"
inputs = tokenizer(text, return_tensors="pt").to(device)
outputs = model.generate(**inputs, max_new_tokens=20)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
【推荐阅读】QLoRa:在消费级GPU上微调大型语言模型
https://colab.research.google.com/drive/1VoYNfYDKcKRQRor98Zbf2-9VQTtGJ24k?usp=sharing
【3】QLoRA-权重合并推理
模型权重合并脚本:export_hf_checkpoint.py,将lora权重合并回原始权重。
import os
import torch
import transformers
from peft import PeftModel
from transformers import LlamaForCausalLM, LlamaTokenizer # noqa: F402
BASE_MODEL = os.environ.get("BASE_MODEL", None)
LORA_MODEL = os.environ.get("LORA_MODEL", "tloen/alpaca-lora-7b")
HF_CHECKPOINT = os.environ.get("HF_CHECKPOINT", "./hf_ckpt")
assert (
BASE_MODEL
), "Please specify a value for BASE_MODEL environment variable, e.g. `export BASE_MODEL=decapoda-research/llama-7b-hf`" # noqa: E501
tokenizer = LlamaTokenizer.from_pretrained(BASE_MODEL)
base_model = LlamaForCausalLM.from_pretrained(
BASE_MODEL,
#load_in_8bit=False,
torch_dtype=torch.bfloat16,
device_map={"": "cpu"},
)
first_weight = base_model.model.layers[0].self_attn.q_proj.weight
first_weight_old = first_weight.clone()
lora_model = PeftModel.from_pretrained(
base_model,
# TODO
# "tloen/alpaca-lora-7b",
LORA_MODEL,
#device_map={"": "cpu"},
#torch_dtype=torch.float16,
)
lora_weight = lora_model.base_model.model.model.layers[0].self_attn.q_proj.weight
assert torch.allclose(first_weight_old, first_weight)
# merge weights
for layer in lora_model.base_model.model.model.layers:
layer.self_attn.q_proj.merge_weights = True
layer.self_attn.v_proj.merge_weights = True
lora_model.train(False)
# did we do anything?
#assert not torch.allclose(first_weight_old, first_weight)
lora_model_sd = lora_model.state_dict()
deloreanized_sd = {
k.replace("base_model.model.", ""): v
for k, v in lora_model_sd.items()
if "lora" not in k
}
LlamaForCausalLM.save_pretrained(
base_model, HF_CHECKPOINT , state_dict=deloreanized_sd, max_shard_size="400MB"
推理脚本:inference.py
from transformers import AutoModelForCausalLM, LlamaTokenizer
import torch
model_id = "/data/nfs/guodong.li/pretrain/hf-llama-model/llama-7b"
merge_model_id = "/home/guodong.li/output/llama-7b-merge"
#model = AutoModelForCausalLM.from_pretrained(model_id, load_in_4bit=True)
model = AutoModelForCausalLM.from_pretrained(merge_model_id, load_in_4bit=True, device_map="auto")
tokenizer = LlamaTokenizer.from_pretrained(model_id)
#print(model)
device = torch.device("cuda:0")
#model = model.to(device)
text = "Hello, my name is "
inputs = tokenizer(text, return_tensors="pt").to(device)
outputs = model.generate(**inputs, max_new_tokens=20, do_sample=True, top_k=30, top_p=0.85)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
print("\n------------------------------------------------\nInput: ")
line = input()
while line:
inputs = tokenizer(line, return_tensors="pt").to(device)
outputs = model.generate(**inputs, max_new_tokens=20, do_sample=True, top_k=30, top_p=0.85)
print("Output: ",tokenizer.decode(outputs[0], skip_special_tokens=True))
print("\n------------------------------------------------\nInput: ")
line = input()
【4】量化分位数如何计算的
Quantile Quantization 分位量化的量化分位数如何计算的。其中核心代码片段摘抄如下。
https://github.com/TimDettmers/bitsandbytes/blob/main/bitsandbytes/functional.py#L236
from scipy.stats import norm
import torch
def create_normal_map(offset=0.9677083, use_extra_value=True):
if use_extra_value:
# one more positive value, this is an asymmetric type
v1 = norm.ppf(torch.linspace(offset, 0.5, 9)[:-1]).tolist() # 正数部分
v2 = [0]*(256-15) ## we have 15 non-zero values in this data type
v3 = (-norm.ppf(torch.linspace(offset, 0.5, 8)[:-1])).tolist() #负数部分
v = v1 + v2 + v3
else:
v1 = norm.ppf(torch.linspace(offset, 0.5, 8)[:-1]).tolist()
v2 = [0]*(256-14) ## we have 14 non-zero values in this data type
v3 = (-norm.ppf(torch.linspace(offset, 0.5, 8)[:-1])).tolist()
v = v1 + v2 + v3
values = torch.Tensor(v)
values = values.sort().values
values /= values.max()
assert values.numel() == 256
return values
Q = create_normal_map()
函数create_normal_map有两个入参:offset和use_extra_value。其中offset的作用是确定分位数的始末值。use_extra_value用来控制是使用对称量化还是非对称量化。
函数体内部有两个核心功能,其中if...else...部分是用来计算分位数。其中v1计算正数部分,v3计算负数部分。v2直接将0映射到0,并且根据要量化的单位计算0的个数。
源码是使用NF4来表示8比特的量化,如果是使用4比特的量化,我们将计算v2的256改成16就行。接下来最后几行用来将量化值归一化到[
这里有个疑问,offset的默认值为什么是0.9677083,作者解释,具体可以看[6]给出的解释。https://readpaper.feishu.cn/docx/CrMGdSVPKow5d1x1XQMcJioRnQe
进技术交流群请添加AINLP小助手微信(id: ainlp2)
请备注具体方向+所用到的相关技术点 关于AINLP
AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括LLM、预训练模型、自动生成、文本摘要、智能问答、聊天机器人、机器翻译、知识图谱、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLP小助手微信(id:ainlp2),备注工作/研究方向+加群目的。