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

news2024/11/14 5:29:09

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模型,以及低精度微调。
    • https://github.com/THUDM/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库进行低精度微调:

  • 数据集:https://huggingface.co/datasets/shibing624/alpaca-zh

  • 微调模型:https://modelscope.cn/models/ZhipuAI/chatglm3-6b

  • 代码和之前PEFT微调代码相似,最主要的区别有两点:

    • 我们这次使用半精度进行微调,加载的模型是ChatGLM3-6B
    • 数据预处理逻辑和之前不同(ChatGLM模型新设计的prompt模板)
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-Factory:https://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      |
|=========================================================================================|
+-----------------------------------------------------------------------------------------+

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1966287.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++第三十弹---C++继承机制深度剖析(中)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1、派生类的默认成员函数 1.1、派生类的构造函数 1.2、派生类的拷贝构造函数 1.3、派生类的赋值重载 1.4、派生类的析构函数 2、继承与友元 …

TL3568编译Kernel内核 make tl3568-evm.img -j16报错 ‘arch/arm64/boot/Image.lz4‘ failed

在编译Kernel时&#xff0c;遇到报错内容&#xff1a; /bin/sh: lz4c: command not found arch/arm64/boot/Makefile:31: recipe for target arch/arm64/boot/Image.lz4 failed make[1]: *** [arch/arm64/boot/Image.lz4] Error 1 arch/arm64/Makefile:139: recipe for target …

科普文:【支持信创、宣传国产】Alibaba Dragonwell JVM性能提升50%

4月5日&#xff0c;阿里云开放了新一代ECS实例的邀测[1]&#xff0c;Alibaba Dragonwell也在新ECS上进行了极致的优化。相比于之前的dragonwell_11.0.8.3版本&#xff0c;即将发布的dragonwell_11.0.11.6在SPECjbb2015[2] composite模式测试中&#xff0c;系统吞吐量max-jOPS提…

【算法】插值查找(对二分查找的优化)

引言 在二分查找中&#xff0c;对于相对较大的数或较小的数来说&#xff0c;查询效率是很低的&#xff0c;我们希望程序可以自适应待查询的数&#xff0c;使用插值算法 插值查找原理 1.插值查找算法类似于二分查找&#xff0c;不同的是插值查找每次从自适应 mid 处开始查找 2…

Airtest封装的Tidevice接口有多好用(二)

一、前言 上节课我们分享了一批Airtest封装的Tidevice接口&#xff0c;是有关获取设备信息的&#xff0c;还没看到的同学可以戳这里复习一下。那么本周我们继续来看一下Airtest还封装了哪些Tidevice的接口吧~ 二、Airtest封装的Tidevice接口 2.1 list_app(udid ,app_typeuse…

早得农元早享“富”!农元又双叒叕涨了!

农元升值设定的唯一途径&#xff0c;仅随着用户在平台每次的购物而升值&#xff0c;未来农元的价值升值甚至会达到几千、上万元人民币&#xff0c;真正实现了购物乐趣与财富增长的双重盛宴&#xff0c;让每一位平台用户都能享受到数字经济时代带来的红利。 快从消费者变为经营者…

警惕!六西格玛培训中不可不知的六大陷阱

近年来&#xff0c;随着六西格玛的普及&#xff0c;一些常见的培训陷阱也逐渐浮出水面&#xff0c;让不少求学者误入歧途。本文&#xff0c;深圳天行健企业管理咨询公司旨在为大家揭示六西格玛培训中的六大常见陷阱&#xff0c;真正掌握六西格玛的精髓。 陷阱一&#xff1a;速成…

KMP入门与算法题实践

基础知识 参考视频 下面是两个b站上个人借鉴学习的视频 第一个视频用来快速理解KMP&#xff1a; 【最浅显易懂的 KMP 算法讲解】 https://www.bilibili.com/video/BV1AY4y157yL/?share_sourcecopy_web&vd_sourced124eda224bf54d0e3ab795c0b89dbb0 第二、三个视频用来理…

vue3学习day01-vue3的优势、新的脚手架工具create-vue、创建vue3项目、vue3的项目文件内容、插件变化

1、vue3的优势 &#xff08;1&#xff09;更易维护&#xff1a;组合式api&#xff0c;更好的TypeScript支持 &#xff08;2&#xff09;更快的速度&#xff1a;重写diff算法&#xff0c;模版编译优化&#xff0c;更高效的组件化 &#xff08;3&#xff09;更小的体积&#x…

MES系统:生产实时监控与智能反馈,驱动制造业智能化升级

MES系统&#xff08;Manufacturing Execution System&#xff0c;制造执行系统&#xff09;通过集成多种技术手段和管理模块&#xff0c;实现了生产过程的实时监控与反馈。以下是实时监控与反馈具体实现的详细分析&#xff1a; 一、实时监控 1. 数据采集 传感器与设备集成&am…

nrm: npm 镜像源管理工具

nrm 是 “npm registry manager” 的缩写&#xff0c;是一个 npm 镜像源管理工具&#xff0c;用于在不同的 npm 镜像源之间快速切换&#xff0c;帮助开发者根据需要选择不同的源来加速包的下载或解决网络问题。 常用命令 详细介绍 以下是 nrm 的一些主要特性和用法&#xff1…

精美UI三方用户中心 新版QRuser用户中心主题 | 魔方财务模板

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 新版QRuser用户中心主题 | 魔方财务模板 本主题支持魔方财务3.5.7版本&#xff01;可自由切换魔方财务3.5.7版本与其他版本。 本主题基于官方default开发&#xff0c;主要面向企业&…

Java语言程序设计——篇十一(1)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 欢迎大家&#xff1a;这里是CSDN&#xff0c;我的学习笔记、总结知识的地方&#xff0c;喜欢的话请三连&#xff0c;有问题可以私信&#x1f33…

【数据结构-前缀哈希同余运算】力扣974. 和可被 K 整除的子数组

给定一个整数数组 nums 和一个整数 k &#xff0c;返回其中元素之和可被 k 整除的非空 子数组 的数目。 子数组 是数组中 连续 的部分。 示例 1&#xff1a; 输入&#xff1a;nums [4,5,0,-2,-3,1], k 5 输出&#xff1a;7 解释&#xff1a; 有 7 个子数组满足其元素之和可…

仿真入门!Hypermesh焊接处理方法

某抗硫球阀为对焊端结构&#xff0c;需要焊接袖管。焊接过程产生的高温会造成热影响区域&#xff0c;尤其接头区域产生变形及残余应力&#xff0c;同时O型密封圈处的温度也是焊接生产比较关心的问题。因此&#xff0c;为弄清袖管焊接过程中温度场规律&#xff0c;给实际焊接生产…

RAID 级别:0、1、5、6、10 和 50 傻傻分不清?那是你没看过这篇

号主&#xff1a;老杨丨11年资深网络工程师&#xff0c;更多网工提升干货&#xff0c;请关注公众号&#xff1a;网络工程师俱乐部 早上好&#xff0c;我的网工朋友。 大家都知道&#xff0c;无论是企业级服务器还是个人计算机&#xff0c;数据的安全性和可用性都是至关重要的。…

通配符HTTPS证书快速申请流程

通配符HTTPS证书是一种特别类型的HTTPS证书&#xff0c;它可以为一个主域名及其下级所有子域名提供安全加密。这对于拥有多个子域名的企业来说非常有用&#xff0c;因为它简化了管理流程&#xff0c;并降低了成本。具体申请流程&#xff1a; 注册账号填写230919注册码即可获得…

【C++】实验十四

题目&#xff1a; 1、编写程序&#xff0c;输入a&#xff0c;b&#xff0c;c&#xff0c;检查a&#xff0c;b&#xff0c;c是否满足以下条件&#xff0c;如不满足&#xff0c;由cerr输出有关错误信息。 2、从键盘输入一批数值。要求保留3位小数&#xff0c;在输出时上下行小数…

成都夏光汝网络科技有限公司抖音小店品质与创新的完美结合

在数字经济蓬勃发展的今天&#xff0c;电商行业以其独特的魅力和无限的可能性&#xff0c;正深刻改变着我们的消费习惯与生活方式。其中&#xff0c;抖音小店作为短视频与电商结合的典范&#xff0c;更是以其独特的优势迅速崛起&#xff0c;成为广大消费者喜爱的购物渠道。成都…

企业版邮箱如何确保全球畅邮

企业版邮箱如何确保全球畅邮呢&#xff1f;一邮箱通过多项国际隐私认证&#xff0c;加密技术保障数据安全。二是全球网络部署确保邮件畅通。三提供灵活价格方案&#xff0c;支持用户定制化和跨平台。四是第三方工具集成&#xff0c;提升效率。 一、Zoho邮箱的安全保障 1.1 高…