AI大模型应用(2)ChatGLM3本地部署及其在alpaca_zh数据集上的低精度微调

AI大模型应用(2)ChatGLM3部署及其在alpaca_zh数据集上的低精度微调

  • 我们之前已经了解了HuggingFace中peft库的几种高效微调方法。

参数高效微调PEFT(一)快速入门BitFit、Prompt Tuning、Prefix Tuning

参数高效微调PEFT(二)快速入门P-Tuning、P-Tuning V2

参数高效微调PEFT(三)快速入门LoRA、AdaLoRA

参数高效微调PEFT(四)快速入门(IA)3_ia3微调

  • 之前我们都是以单精度FP32加载模型,因此在训练过程中,模型本身占用的显存大小并没有改变。
  • 今天我们了解下ChatGLM3模型,以及低精度微调。

1 ChatGLM3 API快速入门

ChatGLM3 是智谱AI和清华大学 KEG 实验室联合发布的对话预训练模型。ChatGLM3-6B 是 ChatGLM3 系列中的开源模型,在保留了前两代模型对话流畅、部署门槛低等众多优秀特性的基础上,ChatGLM3-6B 引入了如下特性:

  1. 更强大的基础模型: ChatGLM3-6B 的基础模型 ChatGLM3-6B-Base 采用了更多样的训练数据、更充分的训练步数和更合理的训练策略。
  2. 更完整的功能支持: ChatGLM3-6B 采用了全新设计的 Prompt 格式 ,除正常的多轮对话外。同时原生支持工具调用(Function Call)、代码执行(Code Interpreter)和 Agent 任务等复杂场景。
  3. 更全面的开源序列: 除了对话模型 ChatGLM3-6B 外,还开源了基础模型 ChatGLM3-6B-Base 、长文本对话模型 ChatGLM3-6B-32K 和进一步强化了对于长文本理解能力的 ChatGLM3-6B-128K

1.1 本地GPU部署

首先需要下载本仓库:

git clone https://github.com/THUDM/ChatGLM3
cd ChatGLM3

然后使用 pip 安装依赖:

pip install -r requirements.txt
  • 为了保证 torch 的版本正确,请严格按照 官方文档 的说明安装。
  • 默认情况下,模型以 FP16 精度加载,需要大概 13GB 显存。
  • 如果本地没有GPU,可以租借云GPU:AutoDL平台租借GPU详解
  • 官方提供了很多demo示例,例如:模型微调、网页版对话 Demo、命令行对话 Demo、LangChain Demo、OpenAI API / Zhipu API Demo等,可以参考官方文档进行使用。

1.2 API快速调用

1.3.1 HuggingFace风格API调用

# 注意需要安装transformers库
# 可以参考:https://blog.csdn.net/qq_44665283/article/details/133823613
from transformers import AutoTokenizer, AutoModelForCausalLM

# 本地模型路径
# 可以利用魔搭社区下载:https://blog.csdn.net/qq_44665283/article/details/139244306
model_path = r'/root/autodl-tmp/models/ZhipuAI/chatglm3-6b'

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)


# 1、非量化(默认是FP16精度加载,大概需要13GB显存)
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, device='cuda')

# 2、量化为int8
# model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True).quantize(8).cuda()


# 3、量化为int4
# model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True).quantize(4).cuda()

for name, param in model.named_parameters():
    print(name, param.dtype)


model.eval()
response, history = model.chat(tokenizer, "你好", history=[])

print(f'response = {response}')
print(f'history = {history}')

1.3.2 OpenAI风格的API调用

作者团队已经推出了 OpenAI / ZhipuAI 格式的 开源模型 API 部署代码,可以作为任意基于 ChatGPT 的应用的后端。
目前,可以通过运行仓库中的 api_server.py 进行部署

root@autodl-container-c4c240bdcc-422ab4ba:~/autodl-tmp/llm/ChatGLM3# cd openai_api_demo


# 1、设置从本地路径加载
# 因此需要修改api_server.py中的模型路径
# 还需要设置EMBEDDING_PATH,我这里使用的是text2vec-large-chinese模型
root@autodl-container-c4c240bdcc-422ab4ba:~/autodl-tmp/llm/ChatGLM3/openai_api_demo# vim api_server.py


# MODEL_PATH = os.environ.get('MODEL_PATH', 'THUDM/chatglm3-6b')
# TOKENIZER_PATH = os.environ.get("TOKENIZER_PATH", MODEL_PATH)
# set Embedding Model path
# EMBEDDING_PATH = os.environ.get('EMBEDDING_PATH', 'BAAI/bge-m3')

# set LLM path
MODEL_PATH = r'/root/autodl-tmp/models/ZhipuAI/chatglm3-6b'
TOKENIZER_PATH = r'/root/autodl-tmp/models/ZhipuAI/chatglm3-6b'
EMBEDDING_PATH = r'/root/autodl-tmp/models/ZhipuAI/text2vec-large-chinese'


# 2、启动API服务,默认端口8000
root@autodl-container-c4c240bdcc-422ab4ba:~/autodl-tmp/llm/ChatGLM3/openai_api_demo# python api_server.py
  • 启动服务后,我们就可以使用OpenAI风格的API进行调用了:
from openai import OpenAI

base_url = "http://127.0.0.1:8000/v1/"
client = OpenAI(api_key="EMPTY", base_url=base_url)


def simple_chat(use_stream=True):
    messages = [
        {
            "role": "system",
            "content": "You are ChatGLM3, a large language model trained by Zhipu.AI. Follow the user's instructions carefully. "
        },
        {
            "role": "user",
            "content": "你是谁"
        }
    ]

    response = client.chat.completions.create(
        model="chatglm3-6b",    # 模型名称
        messages=messages,      # 会话历史
        stream=use_stream,      # 指定是否使用流式传输模式,如果设置为True,则返回一个生成器对象,可以逐个获取生成的文本片段;如果设置为False,则一次性返回完整的生成结果。
        max_tokens=256,         # 最多生成字数
        temperature=0.8,        # 温度
        presence_penalty=1.1,   # 控制生成回答时对已出现词汇的惩罚强度,较高的值会减少重复词汇的出现
        top_p=0.8)              # 采样概率
    print(response)
    if response:
        if use_stream:
            for chunk in response:
                print(chunk.choices[0].delta.content)
        else:
            content = response.choices[0].message.content
            print(content)
    else:
        print("Error:", response.status_code)
if __name__ == "__main__":
    simple_chat(use_stream=False)
    # simple_chat(use_stream=True)

输出:

ChatCompletion(id=''
    , choices=[
        Choice(finish_reason='stop'
                , index=0
                , logprobs=None
                , message=ChatCompletionMessage(
                        content='我是一个名为 ChatGLM3 的大型语言模型,由 Zhipu.AI 训练。我的目的是通过回答用户的问题来帮助他们解决问题。'
                        , role='assistant'
                        , function_call=None
                        , tool_calls=None
                        , name=None
                        )
        )
    ]
    , created=1721811856
    , model='chatglm3-6b'
    , object='chat.completion'
    , service_tier=None
    , system_fingerprint=None
    , usage=CompletionUsage(completion_tokens=34, prompt_tokens=44, total_tokens=78)
)
  • 启动API服务后,我们也可以对文本进行embedding了
from openai import OpenAI

base_url = "http://127.0.0.1:8000/v1/"
client = OpenAI(api_key="EMPTY", base_url=base_url)

def embedding():
    response = client.embeddings.create(
        # model="bge-large-zh-1.5",
        model="text2vec-large-chinese",
        input=["你好,给我讲一个故事,大概100字"],
    )
    embeddings = response.data[0].embedding
    print("嵌入完成,维度:", len(embeddings))
    return embeddings


if __name__ == "__main__":
    # 嵌入完成,维度: 1024
    embedding()
  • 当然,我们也可以使用function_call了
  • 更详细的使用案例,可以参考官方tools_using_demo文件夹下内容
from openai import OpenAI

base_url = "http://127.0.0.1:8000/v1/"
client = OpenAI(api_key="EMPTY", base_url=base_url)


tools = [
        {
            "type": "function",
            "function": {
                "name": "get_current_weather",
                "description": "Get the current weather in a given location",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "The city and state, e.g. San Francisco, CA",
                        },
                        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                    },
                    "required": ["location"],
                },
            },
        }
    ]

messages = [{"role": "user", "content": "What's the weather like in BeiJing?"}]
response = client.chat.completions.create(
        model="chatglm3-6b",
        messages=messages,
        tools=tools,
        tool_choice="auto",
    )

print(response)
content = response.choices[0].message.content


print(f'content:\n{content}')
"""
content:
get_current_weather
 ```python
tool_call(location='Beijing', unit='celsius')
​```
"""

输出:

ChatCompletion(id=''
    , choices=[
        Choice(finish_reason='function_call'
            , index=0
            , logprobs=None
            , message=ChatCompletionMessage(
                      content="get_current_weather\n ```python\ntool_call(location='Beijing', unit='celsius')\n```"
                    , role='assistant'
                    # 模型识别到了function_call
                    , function_call=FunctionCall(arguments='{"location": "Beijing", "unit": "celsius"}', name='get_current_weather')
                    , tool_calls=None
                    , name=None
                )
        )
    ]
    , created=1721811783
    , model='chatglm3-6b'
    , object='chat.completion'
    , service_tier=None
    , system_fingerprint=None
    , usage=CompletionUsage(completion_tokens=28, prompt_tokens=222, total_tokens=250))

2 ChatGLM3的低精度微调

具体微调可以参考官方源码finetune_demo,我们今天利用transformer库来了解下低精度微调的过程。

  • 我们知道,模型训练时候的显存占用主要如下:

    • 模型权重:4Bytes*模型参数量
    • 优化器状态(AdamW优化器):8Bytes*模型参数量
    • 梯度:4Bytes*模型参数量
    • 当然也受批次大小,序列长度等多个因素的影响。
  • 我们以ChatGLM3-6B为例,如果以FP32(4Bytes)进行加载,大概需要24GB的显存。

    • FP32也叫做 float32,两种叫法是完全一样的,全称是Single-precision floating-point(单精度浮点数)
    • 如下图所示,FP32是用32位二进制来表示的浮点数:
      • Sign(符号位): 1位,0表示整数,1表示负数
      • Exponent(指数位):8位,表示整数部分,偏置值是127
      • Fraction(尾数位):23位,表示小数部分,隐含了首位的1,实际的尾数精度为24位

在这里插入图片描述

  • 而利用transformers库加载时,默认是FP16(2Bytes)精度加载,大概需要13GB显存。我们发现,在参数量不变的情况下,降低模型中每个参数占用的字节数,能够降低模型的显存占用。
  • 常见的低精度数据类型有:float16(半精度)、bfloat16、int8、fp4、nf4等

2.1 半精度训练

2.1.1 FP16简介

  • FP16也叫做 float16,两种叫法是完全一样的,全称是Half-precision floating-point(半精度浮点数)

  • 如下图所示,是用16位二进制来表示的浮点数:

    • Sign(符号位): 1位,0表示整数;1表示负数。

    • Exponent(指数位):5位,简单地来说就是表示整数部分。

      • 范围为00001(1)到11110(30),正常来说整数范围就是 2 1 − 2 30 2^{1}-2^{30} 21230,注:当指数位都为00000和11111时,表示的是特殊情况。
      • 但为了指数位能够表示负数,引入了一个偏置值,偏置值是一个固定的数,它被加到实际的指数上,在二进制16位浮点数中,偏置值是15。
      • 这个偏置值确保了指数位可以表示从-14到+15的范围,即 2 − 14 − 2 15 2^{-14}-2^{15} 214215
    • Fraction(尾数位):10位,简单地来说就是表示小数部分。

      • 存储的尾数位数为10位,但其隐含了首位的1,因此实际的尾数精度为11位。

      • 这里的隐含位简单通俗来说,假设尾数部分为1001000000,为默认在其前面加一个1,最后变成1.1001000000然后换成10进制就是:

        1.1001000000 = 1 + 576(1001000000转换为10进制)/1024 = 1.5625
        

在这里插入图片描述

  • 因此,FP16所表示10进制数的计算公式为:

( − 1 ) s i g n × 2 e x p o n e n t − 15 × ( 1 + f r a c t i o n ( 转为 10 进制 ) 1024 ) (-1)^{sign}×2^{exponent-15}×(1+\frac{fraction(转为10进制)}{1024}) (1)sign×2exponent15×(1+1024fraction(转为10进制))

我们可以计算下FP16表示的范围:
在这里插入图片描述

我们用pytorch框架进行验证:

>>> import torch

>>> torch.finfo(torch.float16)
finfo(
    resolution=0.001   # 在十进制上的分辨率,表示两个不同值之间的最小间隔。
    , min=-65504       # 最小值
    , max=65504        # 最大值
    , eps=0.000976562  # 表示在给定数据类型下,比1大的最小浮点数
    , smallest_normal=6.10352e-05  # 最小正规数,大于零的最小浮点数
    , tiny=6.10352e-05 # 最小非零数,大于零的最小浮点数
    , dtype=float16
)


# 这里重点解释下resolution,这个是我们以十进制来说的两个数之间的最小间隔
# float16下,resolution为0.001,而3.141和3.1415间隔只有0.0005,所以在float16下结果是一样的

# 把10进制数转化为 torch.float16
>>> num = 3.141
>>> num_fp16 = torch.tensor(num).half()
>>> print(num_fp16)
tensor(3.1406, dtype=torch.float16)


>>> num = 3.1415
>>> num_fp16 = torch.tensor(num).half()
>>> print(num_fp16)
tensor(3.1406, dtype=torch.float16)
# 可以看到3.141和3.1415间隔只有0.0005,所以在float16下结果是一样的



# 我们看下float32的信息,可以看到表示范围远远大于float16
>>> torch.finfo(torch.float32)
finfo(resolution=1e-06    # 在十进制上的分辨率,表示两个不同值之间的最小间隔。
      , min=-3.40282e+38  # 最小值
      , max=3.40282e+38   # 最大值
      , eps=1.19209e-07
      , smallest_normal=1.17549e-38
      , tiny=1.17549e-38 
      , dtype=float32
)
  • 如下图所示,使用半精度进行计算会导致的两个问题的处理:舍入误差(Rounding Error)和溢出错误(Grad Overflow / Underflow)。

    • 舍入误差:舍入误差指的是当梯度过小时,该次梯度更新可能会失败。
    >>> num1 =  2**(-3)
    >>> num2 =  2**(-14)
    
    >>> num_fp16_1 = torch.tensor(num1).half()
    >>> num_fp16_2 = torch.tensor(num2).half()
    
    >>> print(num_fp16_1)
    0.125
    >>> print(num_fp16_2)
    6.103515625e-05
    >>> print(num_fp16_1 + num_fp16_2)
    tensor(0.1250, dtype=torch.float16)
    
    • 溢出错误:

      • float16的有效的动态范围约为 ( 5.96 × 1 0 − 8 至 6.55 × 1 0 4 5.96×10^{-8}至6.55×10^4 5.96×1086.55×104)
        最大正值上面已经计算过: 0 _ 11110 _ 1111111111 = ( − 1 ) 0 × 2 30 − 15 × ( 1 + 1023 1024 ) = 65504 计算最小正值时,指数位全为 0 ,也就是所谓的非规格数,此时指数位置固定为 ( 1 − 偏置项 ) 另外小数位的隐藏也不再是 1 ,而是 0 ,因此最小正值为: 0 _ 00000 _ 0000000001 = ( − 1 ) 0 × 2 1 − 15 × ( 0 + 1 1024 ) = 2 − 24 = 5.96 × 1 0 − 8 最大正值上面已经计算过:\\ 0\_11110\_1111111111=(-1)^0×2^{30-15}×(1+\frac{1023}{1024})=65504\\ 计算最小正值时,指数位全为0,也就是所谓的非规格数,此时指数位置固定为(1-偏置项)\\ 另外小数位的隐藏也不再是1,而是0,因此最小正值为:\\ 0\_00000\_0000000001=(-1)^0×2^{1-15}×(0+\frac{1}{1024})=2^{-24}=5.96×10^{-8} 最大正值上面已经计算过:0_11110_1111111111=(1)0×23015×(1+10241023)=65504计算最小正值时,指数位全为0,也就是所谓的非规格数,此时指数位置固定为(1偏置项)另外小数位的隐藏也不再是1,而是0,因此最小正值为:0_00000_0000000001=(1)0×2115×(0+10241)=224=5.96×108

      • 比单精度的float32( 1.4 × 1 0 − 45 至 3.4 × 1 0 38 1.4×10^{-45}至3.4×10^{38} 1.4×10453.4×1038)要狭窄很多,注意:这里不是从最小值到最大值, 而是说的正数的部分;
        计算最小正值时,指数位全为 0 ,也就是所谓的非规格数,此时指数位置固定为 ( 1 − 偏置项 ) 另外小数位的隐藏也不再是 1 ,而是 0 ,因此最小正值为: ( − 1 ) 0 × 2 1 − 127 × ( 0 + 1 2 23 ) = 2 − 149 = 1.4 × 1 0 − 45 计算最小正值时,指数位全为0,也就是所谓的非规格数,此时指数位置固定为(1-偏置项)\\ 另外小数位的隐藏也不再是1,而是0,因此最小正值为:\\ (-1)^0×2^{1-127}×(0+\frac{1}{2^{23}})=2^{-149}=1.4×10^{-45} 计算最小正值时,指数位全为0,也就是所谓的非规格数,此时指数位置固定为(1偏置项)另外小数位的隐藏也不再是1,而是0,因此最小正值为:(1)0×21127×(0+2231)=2149=1.4×1045

      • 精度下降(小数点后16相比较小数点后8位要精确的多)会导致得到的值大于或者小于fp16的有效动态范围,也就是上溢出或者下溢出。

      • 因此,使用FP16会损失掉梯度更新小于 2 − 24 2^{-24} 224的值,有研究表明大约占网络所有梯度更新的5%。因此,现在大型模型在训练时候,一般会使用混合精度训练(Mixed Precision,而且更常用是BF16的数据类型。

在这里插入图片描述

  • BF16也叫做bfloat16(这是最常叫法),由Google Brain开发的。如下图所示,其指数位数与FP32相同,都是8位,因此表示的数据范围更广,但是精度比FP16要差。

  • BF16的指数域位数(8位)和float32一样多,能表示的大小范围类似,只是精度降低了(也就是相邻数之间的间隔略微变大,大多数情况下对神经网络的表现影响不显著),而float16的指数域位数只有5,可以表达的大数上限降低,接近0的小数下限升高,比BF16更容易发生上溢和下溢等数值问题,因此大模型的训练和推理更常用BF16。
    B F 16 最小正值为: ( − 1 ) 0 × 2 1 − 127 × ( 0 + 1 2 7 ) = 2 − 133 = 9.2 × 1 0 − 41 B F 16 最大正值范围和 F P 32 类似,可以看到 B F 16 有效动态范围比 F P 16 大很多 BF16最小正值为:\\ (-1)^0×2^{1-127}×(0+\frac{1}{2^{7}})=2^{-133}=9.2×10^{-41}\\ BF16最大正值范围和FP32类似,可以看到BF16有效动态范围比FP16大很多 BF16最小正值为:(1)0×21127×(0+271)=2133=9.2×1041BF16最大正值范围和FP32类似,可以看到BF16有效动态范围比FP16大很多

>>> import torch
>>> torch.finfo(torch.bfloat16)
# 结果
finfo(resolution=0.01    # 在十进制上的分辨率,表示两个不同值之间的最小间隔。
      , min=-3.38953e+38 # 最小值
      , max=3.38953e+38  # 最大值
      , eps=0.0078125
      , smallest_normal=1.17549e-38
      , tiny=1.17549e-38
      , dtype=bfloat16
)

在这里插入图片描述

  • 这里要注意一下,并不是所有的硬件都支持bfloat16,可以用下面代码验证是否支持bfloat16

    import transformers
    
    transformers.utils.import_utils.is_torch_bf16_gpu_available()
    # 结果为True就是支持
    

2.1.2 使用半精度微调ChatGLM3-6B模型

开启半精度训练很简单:

# 推荐做法如下
# 在模型加载时,指定torch_dtype为torch.half或者torch.float16
# 如果,你的机器支持torch.bfloat16,推荐使用
model = AutoModelForCausalLM.from_pretrained(model_path
                                              , trust_remote_code=True
                                              , torch_dtype=torch.bfloat16
                                              , device_map="cuda:0")

我们使用transformers库进行低精度微调:

1、加载数据集
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer

# 1、加载数据集
ds = Dataset.load_from_disk("./data/alpaca_data_zh/")
ds

输出:

Dataset({
    features: ['output', 'input', 'instruction'],
    num_rows: 26858
})
# 查看一些数据
ds[:3]

输出:

{'output': ['以下是保持健康的三个提示:\n\n1. 保持身体活动。每天做适当的身体运动,如散步、跑步或游泳,能促进心血管健康,增强肌肉力量,并有助于减少体重。\n\n2. 均衡饮食。每天食用新鲜的蔬菜、水果、全谷物和脂肪含量低的蛋白质食物,避免高糖、高脂肪和加工食品,以保持健康的饮食习惯。\n\n3. 睡眠充足。睡眠对人体健康至关重要,成年人每天应保证 7-8 小时的睡眠。良好的睡眠有助于减轻压力,促进身体恢复,并提高注意力和记忆力。',
  '4/16等于1/4是因为我们可以约分分子分母都除以他们的最大公约数4,得到(4÷4)/ (16÷4)=1/4。分数的约分是用分子和分母除以相同的非零整数,来表示分数的一个相同的值,这因为分数实际上表示了分子除以分母,所以即使两个数同时除以同一个非零整数,分数的值也不会改变。所以4/16 和1/4是两种不同的书写形式,但它们的值相等。',
  '朱利叶斯·凯撒,又称尤利乌斯·恺撒(Julius Caesar)是古罗马的政治家、军事家和作家。他于公元前44年3月15日被刺杀。 \n\n根据历史记载,当时罗马元老院里一些参议员联合起来策划了对恺撒的刺杀行动,因为他们担心恺撒的统治将给罗马共和制带来威胁。在公元前44年3月15日(又称“3月的艾达之日”),恺撒去参加元老院会议时,被一群参议员包围并被攻击致死。据记载,他身中23刀,其中一刀最终致命。'],
 'input': ['', '输入:4/16', ''],
 'instruction': ['保持健康的三个提示。', '解释为什么以下分数等同于1/4', '朱利叶斯·凯撒是如何死亡的?']}
# 2、加载tokenizer
model_path = r'/root/autodl-tmp/models/ZhipuAI/chatglm3-6b'

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
2、获取ChatGLM3模型需要的数据格式

ChatGLM3模型需要的数据格式呢?

  • 我们看下model.chat(tokenizer, "你好", history=[])这行代码中的chat方法:
	# modeling_chatglm.py
    @torch.inference_mode()
    def chat(self, tokenizer, query: str, history: List[Tuple[str, str]] = None, role: str = "user",
             max_length: int = 8192, num_beams=1, do_sample=True, top_p=0.8, temperature=0.8, logits_processor=None,
             **kwargs):
        if history is None:
            history = []
        if logits_processor is None:
            logits_processor = LogitsProcessorList()
        logits_processor.append(InvalidScoreLogitsProcessor())
        gen_kwargs = {"max_length": max_length, "num_beams": num_beams, "do_sample": do_sample, "top_p": top_p,
                      "temperature": temperature, "logits_processor": logits_processor, **kwargs}
        # 1、调用tokenizer.build_chat_input方法对user的输入query进行处理
        inputs = tokenizer.build_chat_input(query, history=history, role=role)
        inputs = inputs.to(self.device)
        # 注意:这里是调用 tokenizer.get_command("<|user|>")获得token_id
        eos_token_id = [tokenizer.eos_token_id, tokenizer.get_command("<|user|>"),
                        tokenizer.get_command("<|observation|>")]
        
        # 2、generate方法
        outputs = self.generate(**inputs, **gen_kwargs, eos_token_id=eos_token_id)
        
        # 3、解码
        outputs = outputs.tolist()[0][len(inputs["input_ids"][0]):-1]
        response = tokenizer.decode(outputs)
        history.append({"role": role, "content": query})
        # 后处理阶段
        response, history = self.process_response(response, history)
        return response, history

我们调用用build_chat_input方法:

>>> from transformers import AutoTokenizer, AutoModelForCausalLM

>>> model_path = r'/root/autodl-tmp/models/ZhipuAI/chatglm3-6b'

>>> tokenizer.build_chat_input("考试的技巧有哪些?", history=[], role="user")
{
    'input_ids': tensor([[64790, 64792, 64795, 30910, 13, 30910, 32227, 54530, 33741, 34953, 31514, 64796]])
 , 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])
 , 'position_ids': tensor([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11]])
}

# 我们对input_ids进行解码
>>> tokenizer.decode([64790, 64792, 64795, 30910, 13, 30910, 32227, 54530, 33741, 34953,
         31514, 64796])
'[gMASK]sop<|user|> \n 考试的技巧有哪些?<|assistant|>'

# 需要注意的是,由于chatglm3支持工具调用等功能,因此我们还需关注下后处理
    # modeling_chatglm.py  后处理
    def process_response(self, output, history):
        content = ""
        history = deepcopy(history)
        for response in output.split("<|assistant|>"):
            # 注意:又利用"\n"进行了split
            # 前面为metadata,工具调用时使用,比如为:get_current_weather
            # 我们这里虽然没有metadata,但是需要手动拼接"\n",否则会把conent当作metadata
            metadata, content = response.split("\n", maxsplit=1)
            if not metadata.strip():
                # 没有metadata,只返回content
                content = content.strip()
                history.append({"role": "assistant", "metadata": metadata, "content": content})
                content = content.replace("[[训练时间]]", "2023年")
            else:
                history.append({"role": "assistant", "metadata": metadata, "content": content})
                if history[0]["role"] == "system" and "tools" in history[0]:
                    content = "\n".join(content.split("\n")[1:-1])
                    def tool_call(**kwargs):
                        return kwargs
                    parameters = eval(content)
                    content = {"name": metadata.strip(), "parameters": parameters}
                else:
                    content = {"name": metadata.strip(), "content": content}
        return content, history
# 因此,我们得到最终的数据格式为:
# '[gMASK]sop<|user|> \n Prompt<|assistant|> \n Response eos_token'
  • 根据以上步骤,我们得到了ChatGLM3最终的数据格式,不过这样有些麻烦

  • 目前,一些框架支持多种模型的微调,那么不同的模型肯定数据预处理是不一样的,这些框架是如何处理的呢?

    • 这里介绍一个比较火的训练框架LLaMA-Factoryhttps://github.com/hiyouga/LLaMA-Factory

    • 通过LLaMA-Factory的WebUI,小白也可以快速的训练出自己需要的模型(不仅支持SFT,还支持PPO、DPO等方法)。

    • LLaMA-Factory中有一个template.py,提供了大量模型的数据预处理,我们找到chatglm3的模板如下。

    # LLaMA-Factory\src\llamafactory\data\template.py
    
    # 这里我们关注format_prefix + format_user + format_assistant
    # format_prefix=[gMASK]sop
    # format_user=<|user|>\n{{content}}<|assistant|>
    # format_assistant=\n{{content}}
    
    # 因此,我们可以得到模板为:
    # [gMASK]sop<|user|> \n Prompt<|assistant|> \n Response eos_token
    _register_template(
        name="chatglm3",
        format_user=StringFormatter(slots=[{"token": "<|user|>"}, "\n", "{{content}}", {"token": "<|assistant|>"}]),
        format_assistant=StringFormatter(slots=["\n", "{{content}}"]),
        format_system=StringFormatter(slots=[{"token": "<|system|>"}, "\n", "{{content}}"]),
        format_function=FunctionFormatter(slots=[], tool_format="glm4"),
        format_observation=StringFormatter(
            slots=[{"token": "<|observation|>"}, "\n", "{{content}}", {"token": "<|assistant|>"}]
        ),
        format_tools=ToolFormatter(tool_format="glm4"),
        format_prefix=EmptyFormatter(slots=[{"token": "[gMASK]"}, {"token": "sop"}]),
        stop_words=["<|user|>", "<|observation|>"],
        efficient_eos=True,
    )
    
    # 假如我们现在需要微调qwen模型,我们可以找到qwen的模板
    # 这里我们关注format_system + format_user
    # format_system=<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n
    # format_user=<|im_start|>user\n{{content}}<|im_end|>\n<|im_start|>assistant\n
    # 最终的模板为:
    """
    <|im_start|>system
    You are a helpful assistant.<|im_end|>
    <|im_start|>user
    Prompt<|im_end|>
    <|im_start|>assistant
    Response eos_token
    """
    _register_template(
        name="qwen",
        format_user=StringFormatter(slots=["<|im_start|>user\n{{content}}<|im_end|>\n<|im_start|>assistant\n"]),
        format_system=StringFormatter(slots=["<|im_start|>system\n{{content}}<|im_end|>\n"]),
        format_observation=StringFormatter(slots=["<|im_start|>tool\n{{content}}<|im_end|>\n<|im_start|>assistant\n"]),
        format_separator=EmptyFormatter(slots=["\n"]),
        default_system="You are a helpful assistant.",
        stop_words=["<|im_end|>"],
        replace_eos=True,
    )
    
3、数据预处理
def process_func(example):
    MAX_LENGTH = 384
    input_ids, attention_mask, labels = [], [], []
    instruction = "\n".join([example["instruction"], example["input"]]).strip()     # query
    instruction = tokenizer.build_chat_input(instruction, history=[], role="user")  # [gMASK]sop<|user|> \n query<|assistant|>
    response = tokenizer("\n" + example["output"], add_special_tokens=False)        # \n response, 缺少eos token
    input_ids = instruction["input_ids"][0].numpy().tolist() + response["input_ids"] + [tokenizer.eos_token_id]
    attention_mask = instruction["attention_mask"][0].numpy().tolist() + response["attention_mask"] + [1]
    # 这里instruction部分设置为-100,不计算Loss
    labels = [-100] * len(instruction["input_ids"][0].numpy().tolist()) + response["input_ids"] + [tokenizer.eos_token_id]
    if len(input_ids) > MAX_LENGTH:
        input_ids = input_ids[:MAX_LENGTH]
        attention_mask = attention_mask[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]
    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels
    }

tokenized_ds = ds.map(process_func, remove_columns=ds.column_names)
tokenized_ds

输出:

在这里插入图片描述

4、加载模型进行训练
import torch

# 1、单精度进行加载(此时加载模型需要约24GB显存,因此官方默认是半精度加载)
# model = AutoModelForCausalLM.from_pretrained(model_path
#                                              , trust_remote_code=True
#                                              , torch_dtype=torch.float32
#                                              , device_map="cuda:0")


# 2、半精度进行加载(此时加载模型需要13GB显存)
model = AutoModelForCausalLM.from_pretrained(model_path
                                              , trust_remote_code=True
                                              , torch_dtype=torch.bfloat16
                                              , device_map="cuda:0")
# 利用Lora进行微调
from peft import LoraConfig, TaskType, get_peft_model, PeftModel

config = LoraConfig(task_type=TaskType.CAUSAL_LM, target_modules=["query_key_value"])
model = get_peft_model(model, config)

# 配置训练参数
args = TrainingArguments(
    output_dir="./chatbot-chatglm3",
    per_device_train_batch_size=2,
    gradient_accumulation_steps=16,
    logging_steps=10,
    num_train_epochs=1,
    learning_rate=1e-4,
    remove_unused_columns=False,
    save_strategy="epoch"
)

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_ds.select(range(6000)),
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)

trainer.train()

# 模型预测
model.eval()
print(model.chat(tokenizer, "数学考试怎么考高分?", history=[])[0])
# 训练过程中,显存消耗如下
(llm) root@autodl-container-c4c240bdcc-422ab4ba:~# nvidia-smi    
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.78                 Driver Version: 550.78         CUDA Version: 12.4     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 3090        On  |   00000000:0E:00.0 Off |                  N/A |
| 49%   58C    P2            310W /  350W |   15518MiB /  24576MiB |     95%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+
                                                                                         
+-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI        PID   Type   Process name                              GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
+-----------------------------------------------------------------------------------------+

2.2 8bit量化模型微调

2.2.1 为什么需要进行模型的量化

把Float类型 (FP32,FP16) 的模型参数和激活值,用整数 (Int8,Int4)来代替,同时尽可能减少量化后模型推理的误差。
模型量化带来的好处:

  • 减少模型的存储空间和显存的占用
  • 减少显存和TensorCore之间的数据传输量,从而加快模型推理时间。
  • 显卡对整数运算速度快于浮点型数据,从而加快模型推理时间。

2.2.2 如何进行量化和反量化

下图就是非对称量化和反量化的过程:

在这里插入图片描述

但是上面的量化存在下面的问题:

  • 不能处理离群值问题

    假设hidden state中有一个向量A:
    A=[-0.10, -0.23, 0.08, -0.38, -0.28, -0.29, -2.11, 0.34, -0.53, -67.0]。
    向量A有一个emergent feature -67.0。
    
    如果我们去掉emergent feature -67.0对向量A做量化和反量化,
    处理后的结果是:[-0.10, -0.23, 0.08, -0.38, -0.28, -0.28, -2.11, 0.33, -0.53]。
    出现的误差只有-0.29 -> -0.28。
    
    但是如果我们在保留emergent feature -67.0的情况下对该向量做量化和反量化,
    处理后的结果是:[ -0.00, -0.00, 0.00, -0.53, -0.53, -0.53, -2.11, 0.53, -0.53, -67.00]。大部分信息在处理后都丢失了。
    
  • 8位精度表示的动态范围有限,因此量化具有多个大值得向量会产生严重的误差

  • 误差在累计过程中会导致模型最终性能的大幅度下降

对此,可以采用混合精度分解量化(LLM.int8()):
在这里插入图片描述

  • 将包含了Emergent Features的几个维度从矩阵中分离出来,对其做高精度的矩阵乘法(按列提取离群值 ,即大于某个阈值的值),其余部分进行量化;

  • 对 FP16 离群值矩阵和INT8 非离群值矩阵分别作矩阵乘法。

  • 反量化非离群值的矩阵乘结果与离群值矩阵乘结果相加,获得最终的 FP16 结果。

在这里插入图片描述

2.2.3 8bit量化模型微调

# 3、量化为int8
# 通过transformers库和bitsandbytes库,如下所示很容易进行8bit的量化
model = AutoModelForCausalLM.from_pretrained(model_path
                                              , trust_remote_code=True
                                              , torch_dtype=torch.half
                                              , device_map="cuda:0"
                                              , load_in_8bit=True 
                                            )
  • 此时显存消耗情况如下,可以看到显存降低了不少
# 训练过程中,显存消耗如下,可以看到显存降低了不少
(llm) root@autodl-container-c4c240bdcc-422ab4ba:~# nvidia-smi        
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.78                 Driver Version: 550.78         CUDA Version: 12.4     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 3090        On  |   00000000:0E:00.0 Off |                  N/A |
| 38%   53C    P2            271W /  350W |   11308MiB /  24576MiB |     71%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+
                                                                                         
+-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI        PID   Type   Process name                              GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
+-----------------------------------------------------------------------------------------+

2.3 4bit量化模型微调(QLora)

QLoRA的解决方案主要包括三个部分:

(1)NF4 Quantization(4-bit量化):经NF4量化的权重信息损失较小,从而保证模型整体精度的最小损失。

(2)Double Quantization(双重量化):对初次完成量化的常量进行二次量化,进一步缩减模型存储体积。

(3)Paged Optimizers(分页优化器):利用NVIDIA的统一内存管理功能,该技术可以在CPU和GPU之间自动进行页对页的传输,使得即便在GPU偶发地内存溢出(OOM)时仍能够继续进行训练。

2.3.1 NF4量化

  • int8量化是一种常见的线性量化过程,其计算公式是线性的: q = ( w / s c a l e ) + z e r o _ p o i n t q = (w / scale) + zero\_point q=(w/scale)+zero_point

    • 但这存在一个问题:若数据分布不均匀,量化后的值有可能“粘连”堆叠在一起

    • 例如, [ 0.001 , 0.0015 , 0.0016 , 0.002 , 55.0 ] [0.001, 0.0015, 0.0016, 0.002, 55.0] [0.001,0.0015,0.0016,0.002,55.0]在经量化处理后,变为: [ − 128 , − 128 , − 128 , − 128 , 127 ] [-128,-128,-128,-128,127] [128,128,128,128,127]

    • 这四个原本不同的权重经量化后全数转化为相同的数值,导致模型出现较大误差。

  • 一般的模型参数通常呈正态分布,而非均匀分布。

    • 若依照线性方式进行量化,极可能导致多个不同的值被量化到相同的数值上。
    • 如参数符合标准正态分布,(0,1)区间内的值差异性将远大于(10,11),造成相同值概率的不均衡。
  • nf4量化则采取一种非对称量化方式,它基于分位数来执行量化映射。在标准正态分布里,由于靠近中心0点的取值较多,非对称量化能为这些取值提供更多的“格子”,以维持数据的精细度

    • 以4bit为例,表示范围为16个值,将权重从小到大排序,找到十五个分位数,将其切分为十六块,权重数值落在第几块,量化的表示就是多少,范围[0-15];
    • 此外,由于涉及到反量化,还需要给这16个值一个浮点数的映射,这个映射可以取分位区间两侧分位点的均值,用于反量化,这部分称之为量化值;
    • 具体操作时,我们只需要把待量化的数跟量化值进行比较,取最相近的值即可作为该数值的量化值,对应的表示可以通过分位数进行确定,存储时同时存储4bit的表示与对应量化值,反量化后进行计算
  • 大多数权重整体呈现正态分布,那么可以将其统一缩放至[-1,1],根据标准正态分布得到16个量化值,并将量化值也缩放至[-1,1],此时,便可利用前面提到的方法,将权重进行量化

  • 为了减少异常值的问题,采用分块量化,块大小为64,即64个值为一组进行量化

在这里插入图片描述

2.3.2 双重量化

  • 在QLoRA框架中,采用64个参数构成一个block进行量化,即block_size=64,每个块计算出一个对称量化中用到的Scale值。

  • 如果以32位浮点数存储Scale,那么每个block将会额外存储一个32位数字,这意味着每个参数实际上需要额外的 32 / 64 = 0.5 b i t 32/64=0.5bit 32/64=0.5bit存储空间。因此,每个参数实际占用的存储空间变成了 4 + 0.5 = 4.5 b i t s 4+0.5=4.5bits 4+0.5=4.5bits

为了优化这一存储需求,研究人员提出了Double Quant策略,即对Scale本身再进行一次量化;不过这里使用的是线性量化方法,量化后的格式为FP8,其中block_size=256。

  • Double Quant 后,每个参数做量化只需要额外的 8/64 + 32 / (64*256) = 0.127 bits 显存。

  • Double Quant策略通过对量化系数Scale再次进行量化,有效地降低了每个参数所需的额外存储开销。在这一策略的应用之下,每个参数的总量化开销降至大约0.127 bits的额外显存,极大程度上节约了资源。

2.3.3 4bit量化模型微调

# 4、量化为int4
model = AutoModelForCausalLM.from_pretrained(model_path
                                    , trust_remote_code=True
                                    , torch_dtype=torch.half
                                    , device_map="cuda:0"
                                    , load_in_4bit=True                        
                                    , bnb_4bit_compute_dtype=torch.half  
                                    , bnb_4bit_quant_type="nf4"         
                                    , bnb_4bit_use_double_quant=True    
)
"""
参数解释:
load_in_4bit:替换Linear层为FP4/NF4层,启用4位量化
bnb_4bit_compute_dtype:设置计算类型,它可能与输入时的类型不同。例如,输入可能是fp32,但计算可以设置为bf16以获得速度提升。
bnb_4bit_use_double_quant:是否开启double_quant(双重量化)
bnb_4bit_quant_type:有两个参数可以选fp4和nf4,默认是fp4
"""
  • 训练过程中,显存消耗如下,现在显存降到了一块2080Ti就能对其进行微调了
# 训练过程中,显存消耗如下,现在显存降到了一块2080Ti就能对其进行微调了
(llm) root@autodl-container-c4c240bdcc-422ab4ba:~# nvidia-smi    
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.78                 Driver Version: 550.78         CUDA Version: 12.4     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 3090        On  |   00000000:0E:00.0 Off |                  N/A |
| 48%   58C    P2            305W /  350W |    7260MiB /  24576MiB |    100%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+
                                                                                         
+-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI        PID   Type   Process name                              GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
+-----------------------------------------------------------------------------------------+
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值