1. 引言
前文微调方法概览总结了微调的各种方法,并且在更前面两篇文章Lora单卡训练
和 lora单卡二次调优中已经尝试过用Lora进行微调,本文出于好奇准备尝试下用QLora进行微调的效果。
QLoRA是一种新的微调大型语言模型(LLM)的方法,它的特点是能在节省内存的同时保持推理性能。它的出现是为了应对大型模型微调时内存需求大,成本昂贵的问题。
工作原理:首先将LLM进行4位量化,从而显著减少模型的内存占用,接着使用低阶适配器(LoRA)方法对量化的LLM进行微调,因此,QLora可以看成是量化+Lora的结合体。
采用以下核心技术:
- 4位量化,它创新性的引入了特殊的4位浮点数表示方法NF4(Normal Float 4-bit),使用非均匀量化来平衡数据范围与精度。
- 双量化,一种对量化后常数再次进行量化的方法,每个参数可平均节省约 0.37 位。
- 分页优化,使用具有 NVIDIA 统一内存的分页优化器,以避免具有长序列长度的小批量时出现内存峰值。
依赖库安装:
pip install -q -U bitsandbytes
bitsandbytes主要是针对llm和transformers模型提供了优化和量化模型的功能,专门为8位优化器、矩阵乘法和量化而设计,提供了像8位Adam/AdamW之类的函数。目标是通过8位操作实现高效的计算和内存使用从而使llm更易于访问。```
本文将用欺诈文本分类这个业务场景来测试下QLora进行量化微调的实际效果。
2. 训练过程
2.1 初始化
引入之前封装好的trainer.py脚本,定义模型路径和数据集路径,以及要使用的GPU设备。
%run trainer.py
traindata_path = '/data2/anti_fraud/dataset/train0819.jsonl'
evaldata_path = '/data2/anti_fraud/dataset/eval0819.jsonl'
model_path = '/data2/anti_fraud/models/modelscope/hub/Qwen/Qwen2-1___5B-Instruct'
output_path = '/data2/anti_fraud/models/Qwen2-1___5B-Instruct_ft_0830_2'
os.environ["CUDA_VISIBLE_DEVICES"] = "2"
device = 'cuda'
2.2 加载模型和数据集
由于QLora需要以量化的方式来加载模型,所以加载模型的方法需要作调整,这里的改动是引入BitsAndBytesConfig
类构建一个量化配置quantization_config
, 具体配置项释义:
- load_in_4bit:决定了模型参数以4位量化格式加载,加载后的模型参数占用空间会比较小;
- bnb_4bit_compute_dtype=bfloat16:决定了矩阵乘法的计算精度使用bfloat16,输入数据也会被转换成bfloat16位进行计算;
- bnb_4bit_quant_type:指定量化数据类型nf4;
- bnb_4bit_use_double_quant:是否启用双重量化;
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfigdef load_model(model_path, device='cuda'):tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False, trust_remote_code=True)model = AutoModelForCausalLM.from_pretrained(model_path,torch_dtype=torch.bfloat16,quantization_config=BitsAndBytesConfig(load_in_4bit=True,bnb_4bit_compute_dtype=torch.bfloat16,bnb_4bit_use_double_quant=False, bnb_4bit_quant_type='nf4'),)return model, tokenizer
注1:普通的量化通常是将数值分成均匀的区间,比如,将0到1之间的数值分成16个区间,每个区间的宽度相同。而NF4则根据数据的分布情况,使用不均匀的区间来表示数值,这样可以更有效地表示模型中的重要数值,特别是那些频繁出现的数值。
注2:双重量化是指在已经量化的基础上再进行量化,第二次量化并不会改变位数本身(即仍然是4位),它的目的是通过更紧凑地表示数值,使得存储和计算更加高效。由于每一次量化都会引入一些量化误差,双重量化可能会带来更大的数值误差,所以一般只用于极端内存受限的情况下。
注3:之所以模型参数加载使用4位而计算时使用16位,是因为量化本身已经带来了误差,计算时需要采用更高的精度是为了减少量化误差带来的影响。
加载模型参数和token序列化器。
%%time
model, tokenizer = load_model(model_path, device)
model.device
CPU times: user 1min 55s, sys: 3.96 s, total: 1min 59sWall time: 1min 52sdevice(type='cuda', index=0)
占用内存方面,量化加载的
1.9GB
相比于bfloat16加载的3.9GB
减少了将近一半,模型加载时的显存优化比较明显。
加载耗时方面,使用量化方式加载模型的过程是比较慢的,耗时了1分52秒,中间涉及到将模型参数从高精度的浮点数(如 FP32)转换为低精度的 NF4 格式。与之对比,不带量化时基本是秒加载。
加载数据集,复用前文的方法。
train_dataset, eval_dataset = load_dataset(traindata_path, evaldata_path, tokenizer)
2.3 构建训练参数
训练参数:引入分页内存优化器来优化训练过程中的内存分配。
train_args = build_train_arguments(output_path)
train_args.optim="paged_adamw_32bit"
paged_adamw_32bit 是一种优化器配置,它使用了分页内存管理和32位浮点数来优化训练过程,可以帮助你在训练大规模模型时更有效地管理内存和计算资源。
lora配置:同前文Lora训练一样使用大小为16的秩。
lora_config = build_loraconfig()
lora_config.lora_dropout = 0.2
lora_config.r = 16
lora_config.lora_alpha = 32
2.4 开始训练
构造训练器开始训练。
trainer = build_trainer(model, tokenizer, train_args, lora_config, train_dataset, eval_dataset)
trainer.train()
Step | Training Loss | Validation Loss |
---|---|---|
100 | 0.043300 | 0.028851 |
200 | 0.041300 | 0.045375 |
300 | 0.015600 | 0.025569 |
400 | 0.028700 | 0.023149 |
500 | 0.024400 | 0.022869 |
600 | 0.030900 | 0.021145 |
700 | 0.019600 | 0.019462 |
800 | 0.018900 | 0.023610 |
900 | 0.018800 | 0.019515 |
1000 | 0.017400 | 0.018651 |
1100 | 0.018700 | 0.018088 |
1200 | 0.012000 | 0.019770 |
1300 | 0.012400 | 0.023283 |
1400 | 0.016600 | 0.017231 |
1500 | 0.011800 | 0.020865 |
1600 | 0.012000 | 0.018183 |
1700 | 0.008000 | 0.017868 |
1800 | 0.011400 | 0.017251 |
1900 | 0.017200 | 0.017052 |
2000 | 0.015800 | 0.016834 |
2100 | 0.007400 | 0.019656 |
2200 | 0.010100 | 0.016112 |
2300 | 0.006300 | 0.016171 |
2400 | 0.007200 | 0.019825 |
2500 | 0.004600 | 0.022892 |
2600 | 0.006400 | 0.023701 |
2700 | 0.003100 | 0.025771 |
TrainOutput(global_step=2700, training_loss=0.028382157780299032, metrics={'train_runtime': 5695.8171, 'train_samples_per_second': 9.895, 'train_steps_per_second': 0.618, 'total_flos': 1.1883908308313702e+17, 'train_loss': 0.028382157780299032, 'epoch': 2.2998296422487225})
训练过程中观察内存变化:
训练时的显存占用相比非量化时并没有明显变化,基本上占满了24G显卡的显存。
推测原因可能是:QLoRA只是通过量化技术减少了模型参数加载时的显存占用,但训练时仍然会反量化为16位进行矩阵计算,尤其是前向和反向传播阶段,显存的主要消耗来自于激活值、梯度和优化器状态,模型参数仅仅是一小部分,这就导致真正训练过程中占用的显存相比非量化时并没有减少。
QLoRA 主要通过量化模型参数来减小显存占用,但在需要更大的 batch size 的场景下,其显存优化效果可能并不显著。
3. 评估测试
用验证损失最低的checkpoint-2200进行测评。
%run evaluate.py
checkpoint_path='/data2/anti_fraud/models/Qwen2-1___5B-Instruct_ft_0830_2/checkpoint-2200'
evaluate(model_path, checkpoint_path, evaldata_path, device, batch=True, debug=True)
run in batch mode, batch_size=8progress: 100%|██████████| 2348/2348 [03:19<00:00, 11.80it/s]tn:1145, fp:20, fn:167, tp:1016
precision: 0.9806949806949807, recall: 0.8588334742180896
这个训练结果和前文的召回率0.86
区别不大,说明使用量化后的模型参数进行训练确实能保持和16位精度的参数训练几乎一样的效果。
小结:本文通过实际训练来测试QLora对于显存占用和推理性能方面的效果,在我们这个验证结果里,推理性能方面几乎可以保持同先前一样的效果,但显存占用只在加载时降低了不到1/2, 而在训练过程中相比于非量化时没有明显减少。原因可能是由于我们的模型太小,而训练时的批量相对较大,模型参数加载时所优化的这部分内存,与整个训练过程所需要的内存相比较小,所以内存优化的整体效果就不太明显。
基于QLora主要是通过减少模型参数所占用的显存这个原理出发,个人理解可能在参数更大的模型并且batch_size更小的训练时效果可能会比较显著。
相关阅读
- 微调方法概览
- 欺诈文本分类微调(六):Lora单卡训练
- 欺诈文本分类微调(七):lora单卡二次调优
- QLoRA量化微调策略与实践