解密Prompt系列6. lora指令微调扣细节-请冷静,1个小时真不够~

上一章介绍了如何基于APE+SELF自动化构建指令微调样本。这一章咱就把微调跑起来,主要介绍以Lora为首的低参数微调原理,环境配置,微调代码,以及大模型训练中显存和耗时优化的相关技术细节

标题这样写是因为上周突然收到了一周内上线一版chatbo的命令,原因无它领导们都刷到了《一个小时你也可以拥有ChatGPT》,《100美金训练ChatGPT》,《仅训练3小时超越ChatGPT》,《人人都可以拥有ChatGPT》。。。领导说人人都有了为啥我没有呀?!!真诚呼吁标题党们求手下留情,留人一命!于是这里我换个标题来Debuff!Debuff!

看到这里本文最重要的部分已经说完了,累了的小伙伴可以撤退了,五一快乐~

低参数微调原理

  • LORA:LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS
  • 原理:INTRINSIC DIMENSIONALITY EXPLAINS THE EFFECTIVENESS OF LANGUAGE MODEL FINE-TUNING
  • 前人的肩膀:Adapter: Parameter-Efficient Transfer Learning for NLP

我们之前在解密Prompt系列3. 冻结LM微调Prompt介绍过一些soft-prompt,包括P-Tunning和Prompt-Tunning也属于低参数微调。这些方案是通过参数拼接的方案引入额外参数。这里介绍另一类方案,同样是冻结LLM的参数,通过参数相加的方案引入额外参数, 相较soft-prompt最明显的优势,就是不会占用输入token的长度。

LoRA的原理比较简单,原始全量微调其实就是在原始模型参数上通过微调加入增量W=W0+ΔWW = W_0+\Delta WW=W0​+ΔW,那我们可以通过冻结原始参数W0W_0W0​,并且把增量部分通过低秩分解方式进一步降低参数量级ΔW=A∗BT\Delta W=AB^TΔW=A∗BT,原始参数的维度是d∗dddd∗d, 则低秩分解后的参数量级是2∗r∗d2rd2∗r∗d,因为这里的r<<d,因此可以起到大幅降低微调参数量级的效果,如下图

核心代码如下

python
复制代码
## 初始化低秩矩阵A和B
self.lora_A.update(nn.ModuleDict({adapter_name: nn.Linear(self.in_features, r, bias=False)}))
self.lora_B.update(nn.ModuleDict({adapter_name: nn.Linear(r, self.out_features, bias=False)}))
self.scaling[adapter_name] = lora_alpha / r

## 向前计算
result = F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)
result += (
    self.lora_B[self.active_adapter](
        self.lora_A[self.active_adapter](self.lora_dropout[self.active_adapter](x))
    )
    * self.scaling[self.active_adapter]
)

论文测试了在多数场景下适当的LORA微调和全量微调的效果不相上下。一个可能原因是INTRINSIC DIMENSIONALITY论文中提出,虽然语言模型整体参数空间很大,但具体到每个任务其实有各自的隐表征空间(intrisic dimension),这个隐表征空间的维度并不高, 因此在微调过程中加入低秩分解并不一定会影响微调效果。使用LORA微调有以下几个细节

  1. 对哪些参数进行微调:基于Transformer结构,LORA只对每层的Self-Attention的部分进行微调,有Wq,Wk,Wv,WOW_q, W_k, W_v, W_OWq​,Wk​,Wv​,WO​四个映射层参数可以进行微调。消融实验显示只微调WqW_qWq​效果略差,微调Wq,WvW_q, W_vWq​,Wv​的效果和微调Wq,Wk,Wv,WOW_q, W_k, W_v, W_OWq​,Wk​,Wv​,WO​的效果相似。需要注意不同模型参数名称不同,像chatglm对应的参数名称就是query_key_value
  2. Rank的选取:Rank的取值作者对比了1-64,效果上Rank在4-8之间最好,再高并没有效果提升。不过论文的实验是面向下游单一监督任务的,因此在指令微调上根据指令分布的广度,Rank选择还是需要在8以上的取值进行测试。
  3. alpha参数:alpha其实是个缩放参数,本质和learning rate相同,所以为了简化我默认让alpha=rank,只调整lr,这样可以简化超参
  4. 初始化:A和Linear层的权重相同Uniform初始化,B是zero初始化,这样最初的Lora权重为0。所以Lora参数是从头学起,并没有那么容易收敛。

Lora的优点很明显,低参数,适合小样本场景;可以拔插式的使用,快速针对不同下游任务训练不同的lora权重;完全没有推理延时,这个在后面代码中会提到推理时,可以预先把lora权重merge到原始权重上。

但Lora微调虽好,个人在尝试中感受到的局限性就是adapter类的微调方案可能更适合下游单一任务类型/生成风格。至于是否适合作为通用指令微调的解决方案,有个问题我也没有搞懂,就是通用的指令样本是否真的有统一的低秩空间表征?这个表征又是什么含义?因为指令微调阶段的样本其实是混合的多任务指令样本,这种情况下lora是否合适,感觉需要更全面的评估(当前出来的众多LLama们都缺少合理统一全面可比的Evaluation),当前就我们的尝试情况lora的效果并不及预期。

环境配置

我用了featurize揽睿星舟。云服务厂商的选择主要看是否有jupyter,存储够大,下载快,能连git,有高配torch环境。这两家在众多小厂里脱颖而出,4090的卡一个小时也就3块钱,来来来盆友辛苦把推广费结一下~

强调下环境配置,想跑通微调,搞定环境你就成功了80%!运气好1分钟,运气差1天都在原地打转

  1. 实例环境:TRX4090 + py38 + torch2.0 + CUDA12
  2. python环境:主要坑在transforemrs和peft,几个相关issue包括:llama tokenizer special token有问题peft adapter.bin微调不更新Bug with fan_in_fan_out。我一个不差都踩中了。。。
bash
复制代码
# 以下配置可能会随时间变化,出了问题就去issue里面刨吧
# 要相信你不是唯一一个大冤种!
accelerate
appdirs
loralib
bitsandbytes
black
black[jupyter]
datasets
fire
transformers>=4.28.0
git+https://github.com/huggingface/peft.git
sentencepiece
gradio
wandb
cpm-kernel

模型初始化

以下代码主要整合自alpaca-lora和chatglm-finetune。其实lora微调的代码本身并不复杂,相反是如何加速大模型训练,降低显存占用的一些技巧大家可能不太熟悉。模型初始化代码如下,get_peft_model会初始化PeftModel把原模型作为base模型,并在各个self-attention层加入lora层,同时改写模型forward的计算方式。

主要说下load_in_8bit和prepare_model_for_int8_training,这里涉及到2个时间换空间的大模型显存压缩技巧。

python
复制代码
from peft import get_peft_model, LoraConfig, prepare_model_for_int8_training, set_peft_model_state_dict
from transformers import AutoTokenizer, AutoModel

model = AutoModel.from_pretrained("THUDM/chatglm-6b", load_in_8bit=True, torch_dtype=torch.float16, trust_remote_code=True, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True)
model = prepare_model_for_int8_training(model)

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    inference_mode=False,
    r=8,
    lora_alpha=8,
    lora_dropout=0.05,
)
model = get_peft_model(model, lora_config)
model.config.use_cache = False

模型显存占用分成两个部分,一部分是静态显存基本由模型参数量级决定,另一部分是动态显存在向前传播的过程中每个样本的每个神经元都会计算激活值并存储,用于向后传播时的梯度计算,这部分和batchsize以及参数量级相关。以下8bit量化优化的是静态显存,而梯度检查优化的是动态显存。

1. 8bit Quantization

huggingface.co/blog/hf-bit…

from_pretrained中的load_in_8bit参数是bitsandbytes库赋予的能力,会把加载模型转化成混合8bit的量化模型,注意这里的8bit模型量化只用于模型推理,通过量化optimizer state降低训练时显存的时8bit优化器是另一个功能不要搞混哟~

模型量化本质是对浮点参数进行压缩的同时,降低压缩带来的误差。 8-bit quantization是把原始FP32(4字节)压缩到Int8(1字节)也就是1/4的显存占用。如上加载后会发现除lora层外的多数层被转化成int类型如下

当然压缩方式肯定不是直接四舍五入,那样会带来巨大的精度压缩损失。常见的量化方案有absolute-maximum和zero-point,它们的差异只是rescale的方式不同,这里简单说下absmax,如下

先寻找tensor矩阵的绝对值的最大值,并计算最大值到127的缩放因子,然后使用该缩放因子对整个tensor进行缩放后,再round到整数。这样就把浮点数映射到了INT8,逆向回到float的原理相同。

当然以上的缩放方案依旧存在精度损失,以及当矩阵中存在outlier时,这个精度损失会被放大,例如当tensor中绝大部分取值在1以下,有几个值在100+,则缩放后,所有1以下的tensor信息都会被round抹去。因此LLM.int8()的实现对outlier做了进一步的优化,把outlier和非outlier的矩阵分开计算,再把结果进行合并来降低outlier对精度的影响。

prepare_model_for_int8_training是对在Lora微调中使用LLM.int8()进行了适配用来提高训练的稳定性,主要包括

  • layer norm层保留FP32精度
  • 输出层保留FP32精度保证解码时随机sample的差异性
2. gradient checkpoint

medium.com/tensorflow/…

prepare_model_for_int8_training函数还做了一件事就是设置gradient_checkpointing=True,这是另一个时间换空间的技巧。

gradient checkpoint的实现是在向前传播的过程中使用torch.no_grad()不去存储中间激活值,降低动态显存的占用。而只是保存输入和激活函数,当进行反向传播的时候,会重新获取输入和激活函数计算激活值用于梯度计算。因此向前传播会计算两遍,所以需要更多的训练时间。

use_cache设置为False,是因为和gradient checkpoint存在冲突。因为use_cache是对解码速度的优化,在解码器解码时,存储每一步输出的hidden-state用于下一步的输入,而因为开启了gradient checkpoint,中间激活值不会存储,因此use_cahe=False。其实#21737已经加入了参数检查,这里设置只是为了不输出warning。

模型训练

训练基本和常规训练基本相同,代码如下。主要说下模型存储和加载以及混合精度训练

python
复制代码
import datasets
from transformers import Trainer, DataCollatorForSeq2Seq

if resume_from_checkpoint:
    lora_weight = torch.load(ckpt_name)
    set_peft_model_state_dict(model, lora_weight)

train_data = datasets.load_from_disk(dataset_path)

class ModifiedTrainer(Trainer):
    def save_model(self, output_dir=None, _internal_call=False):
        # 改写trainer的save_model,在checkpoint的时候只存lora权重
        from transformers.trainer import TRAINING_ARGS_NAME

        os.makedirs(output_dir, exist_ok=True)
        torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
        saved_params = {
            k: v.to("cpu") for k, v in self.model.named_parameters() if v.requires_grad
        }
        torch.save(saved_params, os.path.join(output_dir, "adapter_model.bin"))
        
trainer = ModifiedTrainer(
    model=model,
    train_dataset=train_data,
        args=transformers.TrainingArguments(
            per_device_train_batch_size=8,
            gradient_accumulation_steps=16,
            num_train_epochs=10,
            learning_rate=3e-4,
            fp16=True,
            logging_steps=10,
            save_steps=200,
            output_dir=output_dir
        ),
    data_collator=DataCollatorForSeq2Seq(
        tokenizer, pad_to_multiple_of=8, return_tensors="pt", padding=True
    ),
)
trainer.train()
model.save_pretrained(train_args.output_dir)
1. 模型的存储和加载

因为peftModel重写了原始model的save_pretrained函数,只把lora层的权重进行存储,因此model.save_pretrained只会存储lora权重。而trainer的save_model函数没有做相应的重写,因此我们重写下对应的function,避免checkpoint写入原始模型全部参数。

相应的如果你从ckpt加载lora权重去继续训练的话,也是对PeftModel中的Lora权重进行加载。

2. 混合精度训练

huggingface.co/docs/transf…

除了默认的全精度FP32,参数精度还有半精度FP16,以及BF16和TF32。最常用也是这里使用的是FP16的混合精度。

实现原理是并非所有变量都需要全精度存储,如果把部分中间变量转化成半精度,则计算效率会大幅提升,加上一些GPU对FP16计算做了优化,吞吐上比全精度会快2~5倍。

不过只使用半精度训练同样会带来量化误差,主要包括:数据溢出因为半精度比全精度的范围更小,训练到后期因为梯度越来越小可能会下溢出;舍入误差梯度变小后,因为精度有限,导致梯度更新被四舍五入,更新了个寂寞。

为了解决以上的问题引入了混合精度训练。简单来说就是向前传递时,模型权重、激活值和梯度都使用FP16进行存储,同时会拷贝一份模型权重以FP32存储,向后传播optimizer更新时会更新FP32的参数。因此混合精度训练并不会节省内存,只会提高模型训练速度。

模型推理

推理有两个方案,一个和训练相同,直接加入Lora层,不过会增加推理延时因为多了lora层的计算,适合线下测评用,如下

python
复制代码
from peft import PeftModel
from transformers import AutoModel, AutoTokenizer

model = AutoModel.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True, load_in_8bit=True, device_map='auto')
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True)
model = PeftModel.from_pretrained(model, "./lora_ckpt")
model.half().to(device)
model.eval()

另一个没有推理延时的方案,是先把lora权重和原始模型权重进行合并,把合并后的参数存储成新的bin文件,然后和加载常规模型一样加载合并后的模型参数进行推理。权重合并的代码如下

python
复制代码
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True)
# when merging disable int8
model = AutoModel.from_pretrained(
    "THUDM/chatglm-6b", load_in_8bit=False, torch_dtype=torch.float16,
    trust_remote_code=True, device_map={"": "cpu"},
)
## 用来检查权重是否合并成功,合并成功weight会改变
first_weight = model.base_model.layers[0].attention.query_key_value.weight
first_weight_old = first_weight.clone()

# 返回的不是新的模型,而是在原始模型上加了adapter层
lora_model = PeftModel.from_pretrained(
    model,
    "./lora_ckpt",
    device_map={"": "cpu"},
    torch_dtype=torch.float16,
)
# 报错:A*B shape mismatch,大概率是get_peft_model错误修改了peft_config里面的fan_in_fan_out参数,某个peft的revision有这个bug
lora_model = lora_model.merge_and_unload()
lora_model.train(False)

# 报错:大概率peft训练有问题,检查adapter.bin大小
assert not torch.allclose(first_weight_old, first_weight), 'Weight Should Change after Lora Merge'

# lora模型权重把原模型权重加了prefix,这里移除恢复原始key
deloreanized_sd = {
    k.replace("base_model.model.", ""): v
    for k, v in lora_model.state_dict().items()
    if "lora" not in k
}
# 保存合并后的模型权重
lora_model.save_pretrained(output_dir, state_dict=deloreanized_sd)

如何系统的去学习大模型LLM ?

作为一名热心肠的互联网老兵,我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。

但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的 AI大模型资料 包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来

😝有需要的小伙伴,可以V扫描下方二维码免费领取🆓

在这里插入图片描述

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

img

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

img

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

img

在这里插入图片描述

四、AI大模型商业化落地方案

img

阶段1:AI大模型时代的基础理解

  • 目标:了解AI大模型的基本概念、发展历程和核心原理。
  • 内容
    • L1.1 人工智能简述与大模型起源
    • L1.2 大模型与通用人工智能
    • L1.3 GPT模型的发展历程
    • L1.4 模型工程
      - L1.4.1 知识大模型
      - L1.4.2 生产大模型
      - L1.4.3 模型工程方法论
      - L1.4.4 模型工程实践
    • L1.5 GPT应用案例

阶段2:AI大模型API应用开发工程

  • 目标:掌握AI大模型API的使用和开发,以及相关的编程技能。
  • 内容
    • L2.1 API接口
      - L2.1.1 OpenAI API接口
      - L2.1.2 Python接口接入
      - L2.1.3 BOT工具类框架
      - L2.1.4 代码示例
    • L2.2 Prompt框架
      - L2.2.1 什么是Prompt
      - L2.2.2 Prompt框架应用现状
      - L2.2.3 基于GPTAS的Prompt框架
      - L2.2.4 Prompt框架与Thought
      - L2.2.5 Prompt框架与提示词
    • L2.3 流水线工程
      - L2.3.1 流水线工程的概念
      - L2.3.2 流水线工程的优点
      - L2.3.3 流水线工程的应用
    • L2.4 总结与展望

阶段3:AI大模型应用架构实践

  • 目标:深入理解AI大模型的应用架构,并能够进行私有化部署。
  • 内容
    • L3.1 Agent模型框架
      - L3.1.1 Agent模型框架的设计理念
      - L3.1.2 Agent模型框架的核心组件
      - L3.1.3 Agent模型框架的实现细节
    • L3.2 MetaGPT
      - L3.2.1 MetaGPT的基本概念
      - L3.2.2 MetaGPT的工作原理
      - L3.2.3 MetaGPT的应用场景
    • L3.3 ChatGLM
      - L3.3.1 ChatGLM的特点
      - L3.3.2 ChatGLM的开发环境
      - L3.3.3 ChatGLM的使用示例
    • L3.4 LLAMA
      - L3.4.1 LLAMA的特点
      - L3.4.2 LLAMA的开发环境
      - L3.4.3 LLAMA的使用示例
    • L3.5 其他大模型介绍

阶段4:AI大模型私有化部署

  • 目标:掌握多种AI大模型的私有化部署,包括多模态和特定领域模型。
  • 内容
    • L4.1 模型私有化部署概述
    • L4.2 模型私有化部署的关键技术
    • L4.3 模型私有化部署的实施步骤
    • L4.4 模型私有化部署的应用场景

学习计划:

  • 阶段1:1-2个月,建立AI大模型的基础知识体系。
  • 阶段2:2-3个月,专注于API应用开发能力的提升。
  • 阶段3:3-4个月,深入实践AI大模型的应用架构和私有化部署。
  • 阶段4:4-5个月,专注于高级模型的应用和部署。
这份完整版的大模型 LLM 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

😝有需要的小伙伴,可以Vx扫描下方二维码免费领取🆓

在这里插入图片描述

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值