Transformers 低精度训练

一、低精度训练与模型下载

1.1 大模型训练的难点

  • 计算效率:大模型的训练依赖于海量的训练数据,海量的训练数据带来了海量的计算需求
  • 显存效率:大模型主要体现在模型参数规模上,参数规模逐渐变大的模型对显存的依赖逐渐加剧
  • 模型训练的显存占用
    • 模型权重:4Bytes * 模型参数量
    • 优化器状态:8Bytes * 模型参数量,对于常用的AdamW优化器而言
    • 梯度:4Bytes * 模型参数量
    • 前向激活值:取决于序列长度、隐藏层维度、Batchsize等多个因素

1.2 如何降低训练时显存占用

  • 实战演练篇——4G显存,0.3B模型
    • 梯度累积:将Batchsize分成多个小的mini-batch进行训练,等到所有mini-batch都计算完后再进行一次反向传播
    • 梯度检查点:将中间的激活值存储到硬盘上,等到需要的时候再读取
    • 优化器配置:使用更小的优化器配置,或者使用更小的学习率
    • 输入数据长度:使用更小的输入数据长度,或者使用更小的Batchsize
    • 冻结模型参数:冻结模型的某些参数,只训练需要训练的参数
  • 参数高效微调篇——8G显存,1.4B模型
    • 参数高效微调(Prompt-Tuning、LoRA等)

1.3 模型自身的显存占用

  • 显存占用
    • 4Bytes * 模型参数量
  • 如何降低显存占用
    • 参数量不变的情况下,降低模型中每个参数占用的字节数即可降低模型的显存占用
  • 如何降低参数占用的字节数
    • 默认的数值精度为单精度fp32,32bits,4Bytes
    • 使用低精度的数据类型表示即可
    • 常用的低精度数据类型有:
      • fp16:16bits,2Bytes
      • bf16:16bits,2Bytes
      • int8:8bits,1Bytes
      • fp4:4bits,0.5Bytes
      • nf4:4bits,0.5Bytes

1.4 基于modelscope的模型下载

  • 模型下载地址
  • modelscope 环境配置
    • conda create -n modelscope python=3.9
    • conda activate modelscope
    • pip install modelscope
  • 模型下载代码
    • from modelscope.hub.snapshot_download import snapshot_download
    • snapshot_download(model_id=“xxx”, cache_dir=“xxx”, ignore_file_pattern=“xxx”)

1.5 半精度训练

  • 半精度FP16(half precision)是一种浮点数格式,它使用16bit表示一个数字(2个字节)
  • 在训练过程中,启用半精度训练可以有效节约内存,并提升计算速度
  • 半精度FP16在计算过程中可能存在溢出问题和舍入问题,可以使用bf16替代
  • 如何启用半精度训练
    • 方式一
      • 模型加载后调用half方法将单精度模型转换为半精度模型
      • model = model.half()
    • 方式二(推荐)
      • 模型加载时,指定torch_dtype参数为torch.float16/torch.half
      • XXModel.from_pretrained(model_name, torch_dtype=torch.half)

二、LlaMA2代码实战

2.1 导入相关包

1
2
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer

2.2 加载数据集

1
ds = Dataset.load_from_disk("../data/alpaca_data_zh/")

2.3 数据集预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tokenizer = AutoTokenizer.from_pretrained("D:/Pretrained_models/modelscope/Llama-2-7b-ms")

tokenizer.padding_side = "right" # 一定要设置padding_side为right,否则batch大于1时可能不收敛

def process_func(example):
MAX_LENGTH = 1024 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性
input_ids, attention_mask, labels = [], [], []
instruction = tokenizer("\n".join(["Human: " + example["instruction"], example["input"]]).strip() + "\n\nAssistant: ", add_special_tokens=False) # add_special_tokens=False表示不添加[CLS]和[SEP]等特殊token
response = tokenizer(example["output"], add_special_tokens=False)
input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.eos_token_id]
attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]
labels = [-100] * len(instruction["input_ids"]) + 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)

2.4 创建模型

1
2
3
import torch
# 多卡情况,可以去掉device_map="auto",否则会将模型拆开
model = AutoModelForCausalLM.from_pretrained("D:/Pretrained_models/modelscope/Llama-2-7b-ms", low_cpu_mem_usage=True, torch_dtype=torch.bfloat16, device_map="auto")

2.5 loRA配置

1
2
3
4
5
6
7
8
9
10
from peft import LoraConfig, get_peft_model, TaskType

# 配置文件
congfig = LoraConfig(task_type=TaskType.CAUSAL_LM,)

# 创建模型
model = get_peft_model(model, config)

# 如果要使用gradient_checkpointing,需要在模型上调用enable_input_require_grads()方法
model.enable_input_require_grads()

2.6 配置训练参数

1
2
3
4
5
6
7
8
args = TrainingArguments(
output_dir="./chatbot",
per_device_train_batch_size=1,
gradient_accumulation_steps=16,
logging_steps=10,
num_train_epochs=1,
gradient_checkpointing=True
)

2.7 创建训练器

1
2
3
4
5
6
7
trainer = Trainer(
model=model,
args=args,
tokenizer=tokenizer,
train_dataset=tokenized_ds.select(range(6000)),
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)

2.8 模型训练

1
trainer.train()

2.9 模型预测

1
2
3
4
5
model.eval()

ipt = tokenizer("Human: {}\n{}".format("你好", "").strip() + "\n\nAssistant: ", return_tensors="pt").to(model.device)

tokenizer.decode(model.generate(**ipt, max_length=512, do_sample=True, eos_token_id=tokenizer.eos_token_id)[0], skip_special_tokens=True)

2.10 LlaMA2 模型训练细节

  • LlaMA2模型分词器的padding_side要设置为right,否则当batchsize大于1时模型可能不收敛
  • LlaMA2模型的分词器表的问题,要适当调整数据的最大长度,保证数据内容的完整性
  • LlaMA2模型加载时,需要指定torch_dtype为半精度,否则模型将按照fp32进行加载
  • 当启用gradient_checkpointing时,需要在模型上调用enable_input_require_grads()方法,否则会报错
  • 当完全采用fp16半精度进行训练且采用adam优化器时,需要调整优化器中的adam_epsilon的值,否则会出现溢出和舍入问题,使得模型无法收敛
  • LlaMA2模型分词器会将非单独存在的eos_token切开,因此对于eos_token需要单独处理,否则训练后的模型在预测时不知道何时停止
    • 一种处理方式是在input_ids和labels中单独添加eos_token_id,需要注意的是attention_mask中eos_token_id的值需要设置为1
    • 另一种处理方式是将在input_ids、labels和eos_token_id之间添加一个空格
  • 半精度训练时,正确加入eos_token后,要将pad_token_id设置为eos_token_id,否则也会出现溢出问题,从而使得模型通用无法收敛
  • 尽可能使用bf16进行训练,bfloat16在训练时不会出现溢出和舍入问题

三、GLM代码实战

  • 数据集
  • 预训练模型
  • 参数高效微调方法
    • LoRA
  • GLM模型
    • 训练方式 Prefix LM
    • 数据组织
      • sentenceA with mask ([MASK]、[gMASK]) [sop] sentenceB [eop]
  • ChatGLM
    • 训练方式
      • v1 Prefix LM
      • v2 Causal LM
    • 数据格式
      • [Round N]\n\n问:Prompt\n\n答:Response
    • 数据组织
      • v1 Prompt [gMASK] [sop] Response [eop]
      • v2 [gMASK] [sop] Prompt Response [eos]
  • ChatGLM3
    • 训练层面
      • 更多样的训练数据
      • 更充分的训练步数
      • 更合理的训练策略
    • 模型层面
      • 开放了base模型
    • 应用层面
      • 支持工具调用(Function Call):支持自定义工具进行调用
      • 支持代码执行(Code Interpreter):根据指令直接生成代码解决问题
    • 新版Prompt设计
      • 整体样式
      • 对话头
        • <|role|>{metadata}
      • 角色表示
        • <|system|>,<|user|>,<|assistant|>,<|observation|>

ChatGLM3中的核心方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def chat(self, tokenizer, query: str, history: List[Dict] = 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}
inputs = tokenizer.build_chat_input(query, history=history, role=role)
inputs = inputs.to(self.device)
eos_token_id = [tokenizer.eos_token_id, tokenizer.get_command("<|user|>"),
tokenizer.get_command("<|observation|>")]
outputs = self.generate(**inputs, **gen_kwargs, eos_token_id=eos_token_id)
outputs = outputs.tolist()[0][len(inputs["input_ids"][0]):-1]
response = tokenizer.decode(outputs)
history.append({"role": role, "content": query})
# process_response后处理:用于工具调用
response, history = self.process_response(response, history)
return response, history

def build_single_message(self, role, metadata, message):
assert role in ["system", "user", "assistant", "observation"], role
role_tokens = [self.get_command(f"<|{role}|>")] + self.tokenizer.encode(f"{metadata}\n")
message_tokens = self.tokenizer.encode(message)
tokens = role_tokens + message_tokens
return tokens

def build_chat_input(self, query, history=None, role="user"):
if history is None:
history = []
input_ids = []
for item in history:
content = item["content"]
if item["role"] == "system" and "tools" in item:
content = content + "\n" + json.dumps(item["tools"], indent=4, ensure_ascii=False)
input_ids.extend(self.build_single_message(item["role"], item.get("metadata", ""), content))
input_ids.extend(self.build_single_message(role, "", query))
input_ids.extend([self.get_command("<|assistant|>")])
return self.batch_encode_plus([input_ids], return_tensors="pt", is_split_into_words=True)

示例:

1
2
3
4
5
6
7
8
9
10
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]])}

tokenizer.decode([64790, 64792, 64795, 30910, 13, 30910, 32227, 54530, 33741, 34953,
31514, 64796])

# '[gMASK] sop <|user|> \n 考试的技巧有哪些?'

3.1 导入相关包

1
2
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer

3.2 加载数据集

1
ds = Dataset.load_from_disk("../data/alpaca_data_zh/")

3.3 数据集预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
tokenizer = AutoTokenizer.from_pretrained("d:/Pretrained_models/ZhipuAI/chatglm3-6b-base/", trust_remote_code=True)

def process_func(example):
MAX_LENGTH = 256
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]
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)

3.4 创建模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch

"""
新版本中需要将modeling_chatglm源码中的613行部分进行调整,代码如下:

if not kv_caches:
kv_caches = [None for _ in range(self.num_layers)]
else:
kv_caches = kv_caches[1]

如果不进行调整,后续chat阶段会报错
"""
# 多卡情况,可以去掉device_map="auto",否则会将模型拆开
model = AutoModelForCausalLM.from_pretrained("d:/Pretrained_models/ZhipuAI/chatglm3-6b-base/", trust_remote_code=True, torch_dtype=torch.bfloat16)

3.5 loRA配置

1
2
3
4
5
6
7
from peft import LoraConfig, get_peft_model, TaskType, PeftModel

# 配置文件
congfig = LoraConfig(target_modules=["query_key_value"], modules_to_save=["post_attention_layernorm"])

# 创建模型
model = get_peft_model(model, config)

3.6 配置训练参数

1
2
3
4
5
6
7
8
9
10
args = TrainingArguments(
output_dir="./chatbot",
per_device_train_batch_size=1,
gradient_accumulation_steps=16,
logging_steps=10,
num_train_epochs=1,
learning_rate=1e-4,
remove_unused_columns=False,
save_strategy="epoch"
)

3.7 创建训练器

1
2
3
4
5
6
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_ds.select(range(6000)),
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)

3.8 模型训练

1
trainer.train()

3.9 模型预测

1
2
model.eval()
print(model.chat(tokenizer, "数学考试怎么考高分?", history=[])[0])

3.10 训练细节

  • ChatGLM3 模型训练细节(调用chat进行对话)
    • 数据格式要与chat方法的格式对齐,可通过查看chat方法对指令模板进行探索
    • special token需要单独处理,包括角色类(user、assistant等)和eos_token
    • 对于非工具类调用功能和代码执行功能的训练,要求解码的第一个token一定是"\n"
    • 对于Lora配置中未明确task_type的情况,需要指定remove_unused_columns=False,否则会报错

四、8bit模型训练

4.1 量化介绍

  • 什么是量化
    • 量化是一种模型压缩方法,使用低精度数据类型对模型权重或者激活值进行表示。简单来说,量化就是将高精度的数字通过某种手段将其转换为低精度的数据
    • 量化带来的优势
      • 降低模型加载的内存成本(FP32>FP16>INT8>INT4)
      • 减少运行计算成本,加速计算(并非所有情况都可以加速)
  • INT8量化示例(absmax)
    • 原始数据:x = [1.52, 2.64, -3.45, 4.32]
    • 量化过程:
      • x_absmax = 4.32
      • scale_factor = 127 / x_absmax = 127 / 4.32 = 29.4
      • q_x = round([1.52, 2.64, -3.45, 4.32] * scale_factor) = [45, 78, -101, 127]
    • 反量化过程:
      • x’ = q_x / scale_factor = [45, 78, -101, 127] / 29.4 = [1.53, 2.61, -3.44, 4.32]
  • INFT8量化示例2(absmax)
    • 原始数据:x = [1.42, 1.51, 1.54, 45.3]
    • 量化过程:
      • x_absmax = 45.3
      • scale_factor = 127 / x_absmax = 127 / 45.3 = 2.8
      • q_x = round([1.42, 1.51, 1.54, 45.3] * scale_factor) = [4, 4, 4, 127]
    • 反量化过程:
      • x’ = q_x / scale_factor = [4, 4, 4, 127] / 2.8 = [1.43, 1.43, 1.43, 45.36]
  • 量化的问题
    • 量化过程存在量化误差
      • 原始数据:x = [1.52, 2.64, -3.45, 4.32]
      • 反量化结果:x’ = [1.53, 2.61, -3.44, 4.32]
    • 如何降低量化误差,提高量化精度
      • 使用更多的量化参数(scale_factor)
      • 矩阵乘法A * B可以看做是A的每一行乘上B的每一列,为A的每一行和B的每一列单独设置scale_factor,这种方式被称为Vector-wise量化
    • 离群值:超出某个分布范围的值通常称为离群值
      • x = [1.42, 1.51, 1.54, 45.3]
    • 8位精度的动态范围极其有限,因此量化具有多个大值的向量会产生严重误差

4.2 混合精度分解量化

  • 混合精度分解量化是将模型的权重分解为多个低精度的权重矩阵,使用多个低精度的权重矩阵来表示一个高精度的权重矩阵
  • 步骤
    • 从输入的隐含状态中,按列提取离群值
    • 对于FP16离群值矩阵和INT8离群值矩阵分别作矩阵乘法
    • 反量化非离群值的矩阵乘结果并与离群值矩阵乘结果相加,获得最终的FP16结果


4.3 8bit量化实战

4.3.1 导入相关包

1
2
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer

4.3.2 加载数据集

1
ds = Dataset.load_from_disk("../data/alpaca_data_zh/")

4.3.3 数据集预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
tokenizer = AutoTokenizer.from_pretrained("d:/Pretrained_models/ZhipuAI/chatglm3-6b-base/", trust_remote_code=True)

def process_func(example):
MAX_LENGTH = 256
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]
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)

4.3.4 创建模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch

"""
新版本中需要将modeling_chatglm源码中的613行部分进行调整,代码如下:

if not kv_caches:
kv_caches = [None for _ in range(self.num_layers)]
else:
kv_caches = kv_caches[1]

如果不进行调整,后续chat阶段会报错
"""
# 多卡情况,可以去掉device_map="auto",否则会将模型拆开
model = AutoModelForCausalLM.from_pretrained("d:/Pretrained_models/ZhipuAI/chatglm3-6b-base/", trust_remote_code=True, low_cpu_mem_usage=True,
torch_dtype=torch.bfloat16, device_map="auto", load_in_8bit=True)

4.3.5 loRA配置

1
2
3
4
5
6
7
from peft import LoraConfig, get_peft_model, TaskType, PeftModel

# 配置文件
congfig = LoraConfig(target_modules=["query_key_value"], modules_to_save=["post_attention_layernorm"])

# 创建模型
model = get_peft_model(model, config)

4.3.6 配置训练参数

1
2
3
4
5
6
7
8
9
10
args = TrainingArguments(
output_dir="./chatbot",
per_device_train_batch_size=1,
gradient_accumulation_steps=16,
logging_steps=10,
num_train_epochs=1,
learning_rate=1e-4,
remove_unused_columns=False,
save_strategy="epoch"
)

4.3.7 创建训练器

1
2
3
4
5
6
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_ds.select(range(6000)),
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)

4.3.8 模型训练

1
trainer.train()

4.3.9 模型预测

1
2
model.eval()
print(model.chat(tokenizer, "数学考试怎么考高分?", history=[])[0])

五、4bit模型训练

5.1 QLoRA介绍

  • 量化的本质
    • 将原本一个空间的数映射到另一个空间
  • 线性量化
    • 简单线性量化的问题
      • 4bit的表示范围比8bit更小,粒度更粗
      • 不用非常大的离群值,就使得量化后的参数都集中在某数值上
      • 量化误差会非常大
    • 如何理解线性量化
      • 数据归一到[-1, 1],把[-1, 1]均匀切分成N区间(N取决于量化bit)
      • 归一化的结果归属于第几个区间,量化结果便为几,数据的分布对量化结果非常重要
      • 如果待量化的数据均匀分布,那么线性量化的结果即使是4bit也不会太差
  • 模型权重分布:正态分布
    • 更好的量化
      • 每部分的权重均匀落在N个区间中时量化效果好
    • 线性量化的问题
      • 在目标域做均匀切分,无法将近似正态分布的值均匀落在每个分区内
    • 换个思路
      • 目标域无法切分,那可以在源域上进行切分
      • 分位数量化
        模型真实权重分布
  • 分位数
    • 把顺序排列的一组数据分割为若干相等部分的分割点的数值即为相应的分位数
    • 中位数是分位数中最简单的一种
    • 示例
      • {6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49}
      • 二分位数(中位数):40
      • 四分位数:15, 40, 43
  • 分位数量化
    • 以4bit为例,表示范围为16个值,将权重从小到大排序,找到十五个分位数,将其切分为十六块,权重数值落在第几块,量化的表示就是多少,范围[0, 15]
    • 此外,由于涉及到反量化,还需要给这16个值一个浮点数的映射,这个映射可以取分位数区间两侧分位点的均值,用于反量化,这部分称之为量化值
    • 具体操作时,我们只需要把待量化的数据跟量化值进行比较,取最相近的值即可作为该数值的量化值,对应的表示可以通过分位数进行确定,存储时同时存储4bit的表示与对应量化值,计算时使用量化值反量化后进行计算

5.2 NF4量化

  • 分位数量化改进-NF4
    • 从正态分布入手
    • 大多数权重整体呈现正态分布,那么可以将其统一缩放至[-1, 1],根据标准正态分布得到16个量化值,并将量化值也缩放至[-1, 1],此时,便可利用前面提到的方法,将权重进行量化为了减少异常值的问题,采用分块量化,块大小为64,即64个值为一组进行量化
    • NF4
  • NF4量化示例
    • 原始数据:x = [1.55, 1.62, 1.83, 4.32]
    • 4bit量化过程
      • x_absmax = 4.32
      • x_normalized = [1.55, 1.62, 1.83, 4.32] / 4.32 = [0.3587, 0.375, 0.4236, 1]
      • NF4 match:q_x = [0.3379, 0.3379, 0.4407, 1.0] = [11, 11, 12, 15]
    • 反量化过程
      • x’ = [0.3379, 0.3379, 0.4407, 1.0] * x_absmax = [1.46, 1.46, 1.90, 4.32]
  • 双重量化
    • NF4量化存储空间
      • NF4分位点:16个常量值,可忽略不计
      • 量化常数(absmax):每个块需要一个FP32的量化常数,32/64 = 0.5bit,即每个参数除了4bit的值外,还会增加0.5bit的额外存储
    • 双重量化
      • 对量化常数进行二次量化,256个一组进行8bit量化,量化为fp8
      • 8/64 + 32/(64 * 256) = 0.127bit
      • 相较于原始的0.5bit,每个参数额外的资源消耗减少了0.373bit
  • 分页器优化
    • 当显存不足时,将优化器的参数转移到CPU上,在需要时再将其取回,防止显存峰值时OOM

5.3 4bit量化实战

  • 数据集
  • 预训练模型
  • 参数高效微调方法
    • LoRA
  • 4bit量化实现方式
    • 启用4bit量化时,在创建模型时传递参数load_in_4bit=True即可
    • 启用NF4量化时,在创建模型时传递参数**bnb_4bit_quant_type=“nf4”**即可
    • 启用双重量化时,在创建模型时传递参数bnb_4bit_use_double_quant=True即可
    • 启用分页器优化时,在args中设置参数**optim=“paged_adamw_32bit”**即可

5.3.1 导入相关包

1
2
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer

5.3.2 加载数据集

1
ds = Dataset.load_from_disk("../data/alpaca_data_zh/")

5.3.3 数据集预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
tokenizer = AutoTokenizer.from_pretrained("d:/Pretrained_models/ZhipuAI/chatglm3-6b-base/", trust_remote_code=True)

def process_func(example):
MAX_LENGTH = 256
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]
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)

5.3.4 创建模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch

"""
新版本中需要将modeling_chatglm源码中的613行部分进行调整,代码如下:

if not kv_caches:
kv_caches = [None for _ in range(self.num_layers)]
else:
kv_caches = kv_caches[1]

如果不进行调整,后续chat阶段会报错
"""
# 多卡情况,可以去掉device_map="auto",否则会将模型拆开,导致训练出问题
model = AutoModelForCausalLM.from_pretrained("d:/Pretrained_models/ZhipuAI/chatglm3-6b-base/", trust_remote_code=True, low_cpu_mem_usage=True,
torch_dtype=torch.bfloat16, device_map="auto", load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True)

5.3.5 loRA配置

1
2
3
4
5
6
7
8
9
from peft import LoraConfig, get_peft_model, TaskType, PeftModel

# 配置文件
congfig = LoraConfig(target_modules=["query_key_value"], modules_to_save=["post_attention_layernorm"])

# 创建模型
model = get_peft_model(model, config)

model.enable_input_require_grads()

5.3.6 配置训练参数

1
2
3
4
5
6
7
8
9
10
11
args = TrainingArguments(
output_dir="./chatbot",
per_device_train_batch_size=1,
gradient_accumulation_steps=32,
logging_steps=10,
num_train_epochs=1,
learning_rate=1e-4,
remove_unused_columns=False,
gradient_checkpointing=True,
optim="paged_adamw_32bit"
)

5.3.7 创建训练器

1
2
3
4
5
6
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_ds.select(range(6000)),
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)

5.3.8 模型训练

1
trainer.train()

5.3.9 模型预测

1
2
model.eval()
print(model.chat(tokenizer, "数学考试怎么考高分?", history=[])[0])

参考资料:
[1] 【手把手带你实战HuggingFace Transformers-入门篇】基础知识与环境安装
[2] 【Github项目地址】