赞
踩
随着『GPT4多模态/Microsoft 365 Copilot/Github Copilot X/ChatGPT插件』的推出,绝大部分公司的技术 产品 服务,以及绝大部分人的工作都将被革新一遍
然ChatGPT/GPT4基本不可能开源了,而通过上篇文章《
LLaMA的解读与其微调:Alpaca-LoRA/Vicuna/BELLE/中文LLaMA/姜子牙/LLaMA 2》可知,国内外各大公司、研究者推出了很多类ChatGPT开源项目,比如LLaMA、BLOOM
在2022年上半年,当时主流的预训练框架可以分为三种:
这三种预训练模型各自称霸一方,那么问题来了,可否结合三种预训练模型,以成天下之一统?这便是2022年5月发表的这篇论文《GLM: General Language Model Pretraining with Autoregressive Blank Infilling》的出发点,它提出了GLM架构
首先,GLM框架在整体基于Transformer基础上,做了以下三点微小改动
此外,关于GLM的结构,这个视频也可以看下:从GLM-130B到ChatGLM:大模型预训练与微调
另,考虑到我讲的ChatGPT技术原理解析课群内,有同学对这块有疑问,所以再重点说下
- 本质上,一个GLMblock其实就是在一个transformer block的基础上做了下结构上的微小改动而已
至于实际模型时,这个block的数量或层数可以独立设置,比如设置24层(具体见下述代码第48行) GLM/arguments.py at 4b65bdb165ad323e28f91129a0ec053228d10566 · THUDM/GLM · GitHub
group.add_argument('--num-layers', type=int, default=24,
- 比如,基于GLM框架的类ChatGPT开源项目「ChatGLM」便用了28个GLMBlock,类似gpt2 用的12-48个decoder-transformer block,BERT用的12-24个encoder-transformer block
- 有些文章 包括我那篇Transformer笔记,为举例,便用的N=6的示例,相当于编码器模块 用的6个encoder-transformer block,解码器模块 也用的6个decoder-transformer block
其次,考虑到三类预训练模型的训练目标
为了大一统,我们必须在结构和训练目标上兼容这三种预训练模型。如何实现呢?文章给出的解决方法是结构上,只需要GLM中同时存在单向注意力和双向注意力即可
因为在原本的Transformer模型中,这两种注意力机制是通过修改attention mask实现的
类似地,GLM可以在只使用Transformer编码器的情况下,自定义attention mask来兼容三种模型结构,使得
这样综合起来实现的效果就是,将提炼信息作为条件,进行有条件地生成(有条件生成就是编解码模型)
其实,所谓编码解码,其本质就是mask的遮盖设计。举个例子,假设原始的文本序列为,采样的两个文本片段为 和 ,那么掩码后的文本序列为 (以下简称Part A),如上图所示,拆解图中的三块分别可得
需要说明的是,Part B包含所有被掩码的文本片段,但是文本片段的相对顺序是随机打乱的
训练目标上,GLM论文提出一个自回归空格填充的任务(Autoregressive Blank Infifilling),来兼容三种预训练目标
自回归填充有些类似掩码语言模型,首先采样输入文本中部分片段,将其替换为[MASK]标记,然后预测[MASK]所对应的文本片段,与掩码语言模型不同的是,预测的过程是采用自回归的方式
具体来说
最终,作者使用了两个预训练目标来优化GLM,两个目标交替进行:
尽管GLM是BERT、GPT、T5三者的结合,但是在预训练时,为了适应预训练的目标,作者还是选择掩码较长的文本片段,以确保GLM的文本生成能力,并在微调的时候将自然语言理解任务也转化为生成任务,如情感分类任务转化为填充空白的任务
输入:{Sentence},prompt:It is really ,对应的标签为good和bad
2022年8月,清华背景的智谱AI基于GLM框架,正式推出拥有1300亿参数的中英双语稠密模型 GLM-130B(论文地址、代码地址,论文解读之一,GLM-130B is trained on a cluster of 96 DGX-A100 GPU (8×40G) servers with a 60-day,可以较好的支持2048个token的上下文窗口)
其在一些任务上的表现优于GPT3-175B,是国内与2020年5月的GPT3在综合能力上差不多的模型之一(即便放到23年年初也并不多),这是它的一些重要特点
ChatGLM-6B(介绍页面、代码地址),是智谱 AI 开源、支持中英双语的对话语言模型,其
量化等级 | 最低 GPU 显存(部署/推理) | 最低 GPU 显存(高效参数微调) |
---|---|---|
FP16(无量化) | 13 GB | 14 GB |
INT8 | 8 GB | 9 GB |
INT4 | 6 GB | 7 GB |
虽尚有很多不足(比如因为6B的大小限制,导致模型的记忆能力、编码、推理能力皆有限),但在6B这个参数量级下不错了,部署也非常简单,我七月在线的同事朝阳花了一两个小时即部署好了(主要时间花在模型下载上,实际的部署操作很快)
以下是具体的部署过程
- from transformers import AutoTokenizer, AutoModel
- tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True)
- model = AutoModel.from_pretrained("/data/chatglm-6b", trust_remote_code=True).half().cuda()
- model = model.eval()
- response, history = model.chat(tokenizer, "你好", history=[])
- print(response)
- response, history = model.chat(tokenizer, "晚上睡不着应该怎么办", history=history)
- print(response)
此外,据介绍,GLM团队正在内测130B参数的ChatGLM,相信从6B到130B,效果应该能提升很多
从上文可知,Stanford Alpaca的52K数据集是通过Self Instruct方式提示GPT3对应的API产生的指令数据,然后通过这批指令数据微调Meta的LLaMA 7B
而GitHub上的这个微调ChatGLM-6B项目(作者:mymusise),则基于Stanford Alpaca的52K数据集通过LoRA(low-rank adaptation)的方式微调ChatGLM-6B
如上一篇文章所说,Huggingface公司推出的PEFT(Parameter-Efficient Fine-Tuning)库便封装了LoRA这个方法,具体而言,通过PEFT-LoRA微调ChatGLM-6B的具体步骤如下
- pip install accelerate==0.17.1
- pip install tensorboard==2.10
- pip install protobuf==3.19.5
- pip install transformers==4.27.1
- pip install icetk
- pip install cpm_kernels==1.0.11
- pip install datasets==2.10.1
- pip install git+https://github.com/huggingface/peft.git # 最新版本 >=0.3.0.dev0
遇到冲突问题:icetk 0.0.5 has requirement protobuf<3.19, but you have protobuf 3.19.5.- [
- {
- "instruction": "Give three tips for staying healthy.",
- "input": "",
- "output": "1.Eat a balanced diet and make sure to include plenty of fruits and vegetables. \n2. Exercise regularly to keep your body active and strong. \n3. Get enough sleep and maintain a consistent sleep schedule."
- },
- {
- "instruction": "What are the three primary colors?",
- "input": "",
- "output": "The three primary colors are red, blue, and yellow."
- },
- ...
- ]
-
- {"text": "### Instruction:\nGive three tips for staying healthy.\n\n### Response:\n1.Eat a balanced diet and make sure to include plenty of fruits and vegetables. \n2. Exercise regularly to keep your body active and strong. \n3. Get enough sleep and maintain a consistent sleep schedule.\nEND\n"}
- {"text": "### Instruction:\nWhat are the three primary colors?\n\n### Response:\nThe three primary colors are red, blue, and yellow.\nEND\n"}
运行 tokenize_dataset_rows.py 文件,注意:修改tokenize_dataset_rows中的model_name为自己的文件路径 :/data/chatglm-6b - python tokenize_dataset_rows.py \
- --jsonl_path data/alpaca_data.jsonl \
- --save_path data/alpaca \
- --max_seq_length 200 \
- --skip_overlength \
- python finetune.py \
- --dataset_path data/alpaca \
- --lora_rank 8 \
- --per_device_train_batch_size 6 \
- --gradient_accumulation_steps 1 \
- --max_steps 52000 \
- --save_steps 1000 \
- --save_total_limit 2 \
- --learning_rate 1e-4 \
- --fp16 \
- --remove_unused_columns false \
- --logging_steps 50 \
- --output_dir output;
这个finetune长啥样呢?- # 导入所需的库和模块
- from transformers.integrations import TensorBoardCallback
- from torch.utils.tensorboard import SummaryWriter
- from transformers import TrainingArguments
- from transformers import Trainer, HfArgumentParser
- from transformers import AutoTokenizer, AutoModel
- import torch
- import torch.nn as nn
- from peft import get_peft_model, LoraConfig, TaskType
- from dataclasses import dataclass, field
- import datasets
- import os
-
- # 从预训练模型加载tokenizer
- tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True)
-
- # 定义FinetuneArguments数据类,用于存储微调的参数
- @dataclass
- class FinetuneArguments:
- dataset_path: str = field(default="data/alpaca") # 数据集路径
- model_path: str = field(default="output") # 模型保存路径
- lora_rank: int = field(default=8) # Lora排名,用于peft模型的设置
-
- # 自定义CastOutputToFloat类,继承自nn.Sequential,用于将输出转换为float32类型
- class CastOutputToFloat(nn.Sequential):
- def forward(self, x):
- return super().forward(x).to(torch.float32)
-
- # 数据处理函数data_collator,用于将输入数据按照最长序列长度进行padding
- def data_collator(features: list) -> dict:
- len_ids = [len(feature["input_ids"]) for feature in features]
- longest = max(len_ids)
- input_ids = []
- labels_list = []
- for ids_l, feature in sorted(zip(len_ids, features), key=lambda x: -x[0]):
- ids = feature["input_ids"]
- seq_len = feature["seq_len"]
- labels = (
- [-100] * (seq_len - 1) + ids[(seq_len - 1) :] + [-100] * (longest - ids_l)
- )
- ids = ids + [tokenizer.pad_token_id] * (longest - ids_l)
- _ids = torch.LongTensor(ids)
- labels_list.append(torch.LongTensor(labels))
- input_ids.append(_ids)
- input_ids = torch.stack(input_ids)
- labels = torch.stack(labels_list)
- return {
- "input_ids": input_ids,
- "labels": labels,
- }
-
- # 自定义ModifiedTrainer类,继承自Trainer,用于微调训练,并对模型保存进行了自定义
- class ModifiedTrainer(Trainer):
- def compute_loss(self, model, inputs, return_outputs=False):
- return model(
- input_ids=inputs["input_ids"],
- labels=inputs["labels"],
- ).loss
-
- def save_model(self, output_dir=None, _internal_call=False):
- 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"))
-
- # 主函数main()
- def main():
- # 创建TensorBoard的SummaryWriter,用于记录训练过程的日志
- writer = SummaryWriter()
-
- # 使用HfArgumentParser解析命令行参数并存储为FinetuneArguments和TrainingArguments两个数据类的实例
- finetune_args, training_args = HfArgumentParser(
- (FinetuneArguments, TrainingArguments)
- ).parse_args_into_dataclasses()
-
- # 初始化模型,从预训练模型加载微调模型
- model = AutoModel.from_pretrained(
- "THUDM/chatglm-6b", load_in_8bit=True, trust_remote_code=True, device_map="auto"
- )
- model.gradient_checkpointing_enable() # 开启梯度检查点
- model.enable_input_require_grads() # 开启输入的梯度计算
- model.is_parallelizable = True # 模型可并行计算
- model.model_parallel = True # 使用模型并行计算
- model.lm_head = CastOutputToFloat(model.lm_head) # 将输出转换为float32类型
- model.config.use_cache = (
- False # 关闭缓存以减少内存占用,但在推断时需要重新开启
- )
-
- # 设置peft模型,设置LoraConfig,用于构造peft模型
- peft_config = LoraConfig(
- task_type=TaskType.CAUSAL_LM,
- inference_mode=False,
- r=finetune_args.lora_rank,
- lora_alpha=32,
- lora_dropout=0.1,
- )
- # 加载peft模型
- model = get_peft_model(model, peft_config)
-
- # 从磁盘加载数据集
- dataset = datasets.load_from_disk(finetune_args.dataset_path) print(f"\n{len(dataset)=}\n") # 打印数据集的样本数量
-
- # 开始训练
- trainer = ModifiedTrainer(
- model=model,
- train_dataset=dataset,
- args=training_args,
- callbacks=[TensorBoardCallback(writer)], # 添加TensorBoard的回调函数,用于记录训练过程的日志
- data_collator=data_collator,
- )
- trainer.train() # 执行训练
- writer.close() # 关闭TensorBoard的SummaryWriter
- # 保存模型
- model.save_pretrained(training_args.output_dir) # 保存微调后的模型
-
- # 程序入口
- if __name__ == "__main__":
- main() # 调用主函数main()
如遇Nvidia驱动报错(如没有可忽略)- export LD_LIBRARY_PATH=/usr/local/cuda-11.3/lib64:$LD_LIBRARY_PATH
- export CUDA_HOME=/usr/local/cuda-11.3:$CUDA_HOME
- export PATH=/usr/local/cuda-11.3/bin:$PATH
内存不够,考虑将per_device_train_batch_size设为1 - python finetune.py \
- --dataset_path data/alpaca \
- --lora_rank 8 \
- --per_device_train_batch_size 1 \
- --gradient_accumulation_steps 1 \
- --max_steps 52000 \
- --save_steps 1000 \
- --save_total_limit 2 \
- --learning_rate 1e-4 \
- --fp16 \
- --remove_unused_columns false \
- --logging_steps 50 \
- --output_dir output;
报错:RuntimeError: expected scalar type Half but found Float此外,ChatGLM团队自身也出了一个基于P-Tuning v2的方式微调ChatGLM-6B的项目:ChatGLM-6B 模型基于 P-Tuning v2 的微调
P-Tuning v2(代码地址,论文地址)意义在于:将需要微调的参数量减少到原来的 0.1%,再通过模型量化、Gradient Checkpoint 等方法,最低只需要 7GB 显存即可运行
那具体怎么通过P-Tuning v2微调ChatGLM-6B呢,具体步骤如下:
pip install rouge_chinese nltk jieba datasets
- PRE_SEQ_LEN=8
- LR=1e-2
-
- CUDA_VISIBLE_DEVICES=0 nohup python -u main.py \
- --do_train \
- --train_file AdvertiseGen/train.json \
- --validation_file AdvertiseGen/dev.json \
- --prompt_column content \
- --response_column summary \
- --overwrite_cache \
- --model_name_or_path /data/chatglm-6b \
- --output_dir output/adgen-chatglm-6b-pt-$PRE_SEQ_LEN-$LR \
- --overwrite_output_dir \
- --max_source_length 64 \
- --max_target_length 64 \
- --per_device_train_batch_size 1 \
- --per_device_eval_batch_size 1 \
- --gradient_accumulation_steps 16 \
- --predict_with_generate \
- --max_steps 3000 \
- --logging_steps 10 \
- --save_steps 1000 \
- --learning_rate $LR \
- --pre_seq_len $PRE_SEQ_LEN \
- >> log.out 2>&1 &
执行命令,开始微调总结:建议使用官方提供的基于P-Tuning v2微调ChatGLM-6B的方式对自己的数据进行微调
此外,此文还介绍了如何通过Cursor一步一步生成一份微调ChatGLM的示例代码
ChatGLM-6B(介绍页面),是智谱 AI 开源、支持中英双语的对话语言模型。
话不多说,直接干,虽然6B的版本相比GPT3 175B 不算大,但毕竟不是一个小工程,本文就不一一贴所有代码了,更多针对某个文件夹下或某个链接下的代码进行整体分析/说明,以帮助大家更好、更快的理解ChatGLM-6B,从而加速大家的类ChatGPT复现之路
- # 使用PyTorch的JIT编译器,将Python函数转换为Torch脚本,以便优化和加速执行
- @torch.jit.script
- # 定义名为gelu_impl的函数,接受一个参数x
- def gelu_impl(x):
- # 返回GELU激活函数的计算结果,这里使用了一种近似计算方法
- return 0.5 * x * (1.0 + torch.tanh(0.7978845608028654 * x *
- (1.0 + 0.044715 * x * x)))
-
- # 定义名为gelu的函数,接受一个参数x
- def gelu(x):
- # 调用gelu_impl函数并返回结果
- return gelu_impl(x)
- # 类的前向传播方法,接收三个参数
- def forward(self, x, seq_dim=1, seq_len=None):
- # 如果没有提供序列长度,则从输入张量的形状中获取序列长度
- if seq_len is None:
- seq_len = x.shape[seq_dim]
-
- # 如果缓存的最大序列长度不存在,或者提供的序列长度大于缓存的最大序列长度
- if self.max_seq_len_cached is None or (seq_len > self.max_seq_len_cached):
- # 更新缓存的最大序列长度
- self.max_seq_len_cached = None if self.learnable else seq_len
- # 创建等差序列
- t = torch.arange(seq_len, device=x.device, dtype=self.inv_freq.dtype)
-
- # 计算频率张量
- freqs = torch.einsum('i,j->ij', t, self.inv_freq)
-
- # 将频率张量沿最后一个维度进行拼接,形成旋转嵌入
- emb = torch.cat((freqs, freqs), dim=-1).to(x.device)
- # 如果精度为bfloat16,将旋转嵌入转换为float类型
- if self.precision == torch.bfloat16:
- emb = emb.float()
-
- # 计算旋转嵌入的余弦值和正弦值,形状为 [sx, 1 (b * np), hn]
- cos_cached = emb.cos()[:, None, :]
- sin_cached = emb.sin()[:, None, :]
- if self.precision == torch.bfloat16:
- # 如果精度为bfloat16,将余弦值转换为bfloat16类型
- cos_cached = cos_cached.bfloat16()
- # 如果精度为bfloat16,将正弦值转换为bfloat16类型
- sin_cached = sin_cached.bfloat16()
-
- # 如果旋转嵌入是可学习的
- if self.learnable:
- # 返回余弦值和正弦值
- return cos_cached, sin_cached
- # 更新缓存的余弦值和正弦值
- self.cos_cached, self.sin_cached = cos_cached, sin_cached
- # 返回截取后的余弦值和正弦值,以匹配输入序列的长度
- return self.cos_cached[:seq_len, ...], self.sin_cached[:seq_len, ...]
_apply方法:这个方法应用给定的函数(fn)到缓存的余弦和正弦值上,并调用父类的_apply方法- # 使用PyTorch的JIT编译器,将Python函数转换为Torch脚本,以便优化和加速执行
- @torch.jit.script
- # 定义一个名为apply_rotary_pos_emb_index的函数,接收五个参数
- def apply_rotary_pos_emb_index(q, k, cos, sin, position_id):
- # 通过position_id获取cos和sin的嵌入表示
- # cos.squeeze(1)和sin.squeeze(1)用于去除多余的维度
- # 而unsqueeze(2)则用于重新添加所需的维度
- # 从而将cos和sin的形状从[sq, 1, hn]变为[sq, b, np, hn],以便后续q和k进行运算
- cos, sin = F.embedding(position_id, cos.squeeze(1)).unsqueeze(2), \
- F.embedding(position_id, sin.squeeze(1)).unsqueeze(2)
-
- # 计算旋转位置编码后的q和k,将q和k与cos和sin进行点积运算
- q, k = (q * cos) + (rotate_half(q) * sin), (k * cos) + (rotate_half(k) * sin)
- # 返回旋转位置编码后的q和k
- return q, k
定义了一个名为SelfAttention的PyTorch模块,它实现了自注意力机制。这个模块在许多自然语言处理任务中都被用作基本构建块。以下是代码中的关键部分:
- # 定义attention函数
- def attention_fn(
- self,
- query_layer, # 查询层张量
- key_layer, # 键层张量
- value_layer, # 值层张量
- attention_mask, # 注意力掩码张量
- hidden_size_per_partition, # 每个分区的隐藏层大小,每个分区可能包含2或4或8个头
- layer_id, # 当前层的ID
- layer_past=None, # 保存过去的键和值的张量,用于解码器的自回归任务
- scaling_attention_score=True, # 是否缩放注意力分数,默认为True
- use_cache=False, # 是否使用缓存,默认为False
- ):
-
- # 如果layer_past不为空,则获取然后拼接过去的key和value
- if layer_past is not None:
- past_key, past_value = layer_past[0], layer_past[1]
- key_layer = torch.cat((past_key, key_layer), dim=0)
- value_layer = torch.cat((past_value, value_layer), dim=0)
-
- # 获取key_layer的形状信息[sk、b、np、hn],『顺带,query_layer便应为[sq、b、np、hn]』
- # 包括序列长度sq、批大小b、注意力头数np(原代码为nh,应为笔误)、每个注意力头的隐藏层大小hn
- seq_len, b, nh, hidden_size = key_layer.shape
-
- # 如果使用缓存,则设置present为key和value的元组,否则为None
- if use_cache:
- present = (key_layer, value_layer)
- else:
- present = None
-
- # 计算查询-键层缩放系数
- query_key_layer_scaling_coeff = float(layer_id + 1)
-
- # 如果需要缩放注意力分数,对查询层进行缩放
- if scaling_attention_score:
- query_layer = query_layer / (math.sqrt(hidden_size) * query_key_layer_scaling_coeff)
-
- """
- 注意:如上可得query_layer 的原始形状为
- [seqlen, batch,num_attention_heads,hidden_size_per_attention_head],简写为[sq,b,np,hn]
- 故query_layer.size(1)对应b, query_layer.size(2)对应np, query_layer.size(0)对应sq
- key_layer 的原始形状为
- [seklen,batch,num_attention_heads,hidden_size_per_attention_head],简写为[sk,b,np,hn]
- 所以key_layer.size(0)对应sk
- """
- # 设置输出张量的大小,即下面第48行所得到的原始注意力分数output_size:[b, np, sq, sk]
- output_size = (query_layer.size(1), query_layer.size(2), query_layer.size(0), key_layer.size(0))
-
- # 基于上面第48行所得到的结果output_size:[b, np, sq, sk],重塑查询层和键层张量,好进行矩阵相乘
- # 相当于[sq, b, np, hn] =》 [b, np, sq, sk] =》[sq, b * np, hn]
- query_layer = query_layer.view(output_size[2], output_size[0] * output_size[1], -1)
- # 相当于[sk, b, np, hn] =》 [b, np, sq, sk] =》[sk, b * np, hn]
- key_layer = key_layer.view(output_size[3], output_size[0] * output_size[1], -1)
-
- """
- 上面那两行再解释下,因为需要计算每个批次中每个注意力头的注意力分数,为此
- 将批次大小(batch)和注意力头数量(num_attention_heads)合并到一个维度中以便于执行矩阵乘法
- 因此,我们将 query_layer 从[sq,b,np,hn]调整为 [sq, b * np, hn]
- 同理,对于 key_layer,将 key_layer 从[sk,b,np,hn]调整为 [sk, b * np, hn]
- """
-
- # 初始化乘法结果张量
- matmul_result = torch.zeros(
- 1, 1, 1,
- dtype=query_layer.dtype,
- device=query_layer.device,
- )
-
- # 计算查询层和键层的乘积
- matmul_result = torch.baddbmm(
- matmul_result,
- # 将 query_layer 的形状从 [sq, b * np, hn] 转换为 [b * np, sq, hn]
- query_layer.transpose(0, 1),
-
- # 将 key_layer 的形状从 [sk, b * np, hn] 转换为 [b * np, hn, sk]
- # 相当于进行了两次转置操作:[sk, b * np, hn] =》[b * np, sk, hn] =》[b * np, hn, sk]
- key_layer.transpose(0, 1).transpose(1, 2),
-
- beta=0.0,
- alpha=1.0,
- )
-
- # 上面最终query_layer为[b * np, sq, hn]
- # 上面最终key_layer 为[b * np, hn, sk]
- # 现在,沿用之前第48行的output_size的注意力分数张量[b, np, sq, sk]
- attention_scores = matmul_result.view(*output_size)
-
- # 使用缩放掩码Softmax计算注意力概率
- if self.scale_mask_softmax:
- self.scale_mask_softmax.scale = query_key_layer_scaling_coeff
- attention_probs = self.scale_mask_softmax(attention_scores, attention_mask.contiguous())
- else:
- # 如果掩码不全为0,应用注意力掩码
- if not (attention_mask == 0).all():
- attention_scores.masked_fill_(attention_mask, -10000.0)
-
- # 转换注意力分数张量的数据类型为浮点数
- dtype = attention_scores.dtype
- attention_scores = attention_scores.float()
-
- # 缩放注意力分数
- attention_scores = attention_scores * query_key_layer_scaling_coeff
-
- # 对注意力分数执行Softmax操作以获取注意力概率
- attention_probs = F.softmax(attention_scores, dim=-1)
-
- # 将注意力概率张量的数据类型恢复为原始数据类型
- attention_probs = attention_probs.type(dtype)
-
- """
- 计算上下文层[sq, b, hp]
- """
- # 对原始value_layer做下转换得到新的output_size:[sk, b, np, hn] --> [b, np, sq, hn]
- # query_layer最早的时候即是[sq, b, np, hn]
- output_size = (value_layer.size(1), value_layer.size(2), query_layer.size(0), value_layer.size(3))
-
- # 对原始value_layer的中间两个维度做下合并 [sk, b, np, hn] -> [sk, b * np, hn]
- # 且基于这个前提条件,即新的output_size:[b, np, sq, hn],顺带再提下 第48行旧的output_size是[b, np, sq, sk]
- value_layer = value_layer.view(value_layer.size(0), output_size[0] * output_size[1], -1)
-
- # 调整注意力概率:对之前第48行得到的前两个维度做下合并:[b, np, sq, sk] =》[b * np, sq, sk]
- attention_probs = attention_probs.view(output_size[0] * output_size[1], output_size[2], -1)
-
- # 对上一行得到的attention_probs[b * np, sq, sk]
- # 乘以『原始value_layer但中间两个维度合并了的:[sk, b * np, hn],做两次转置』,即[b * np, hn, sk]
- # 相当于[b * np, sq, sk] x [b * np, hn, sk],最终得到[b * np, sq, hn]
- context_layer = torch.bmm(attention_probs, value_layer.transpose(0, 1))
-
- # 上行得到context_layer的[b * np, sq, hn]通过上面第117行的新output_size[b, np, sq, hn]调整为
- # 4个维度的[b, np, sq, hn]
- # 使其更直观地表示批量大小b、注意力头数np、查询序列长度sq以及每个注意力头的隐藏层大小hn
- context_layer = context_layer.view(*output_size)
-
- # [b, np, sq, hn] --> [sq, b, np, hn],使其与查询层(query_layer)的形状一致
- context_layer = context_layer.permute(2, 0, 1, 3).contiguous()
-
- # [sq, b, np, hn] --> [sq, b, hp],此举的作用在于前两个维度(sq 和 b)不变
- # 同时将后两个维度(np 和 hn)合并成单个维度,即每个分区的隐藏层大小(hp)
- new_context_layer_shape = context_layer.size()[:-2] + (hidden_size_per_partition,)
- context_layer = context_layer.view(*new_context_layer_shape)
-
- # 将上下文层、当前的键值对(present)以及注意力概率(attention_probs)打包成一个元组
- outputs = (context_layer, present, attention_probs)
-
- return outputs
SelfAttention类定义:这个类实现了自注意力机制,包括定义类的初始化方法和成员变量。类的初始化方法包括设置各种属性,如hidden_size,num_attention_heads,layer_id等。类还包含一个名为rotary_emb的RotaryEmbedding实例,用于处理位置编码。此外,query_key_value和dense是用于计算查询、键和值的线性层。
- @staticmethod
- def attention_mask_func(attention_scores, attention_mask):
- # 使用掩码 (attention_mask) 更新注意力得分 (attention_scores)
- # 对于掩码值为0的位置,将注意力得分设置为-10000.0
- attention_scores.masked_fill_(attention_mask, -10000.0)
-
- # 返回更新后的注意力得分张量
- return attention_scores
GLMBlock 类:这是一个包含多个子模块的Transformer层,如层归一化 (LayerNorm)、自注意力 (SelfAttention) 和门控线性单元 (GLU)
- // 第554到第569行
- class GLMBlock(torch.nn.Module):
- def __init__(
- self,
- hidden_size,
- num_attention_heads,
- layernorm_epsilon,
- layer_id,
- inner_hidden_size=None,
- hidden_size_per_attention_head=None,
- layernorm=LayerNorm,
- use_bias=True,
- params_dtype=torch.float,
-
- //相当于有28层或28个GLMBlock
- num_layers=28,
- position_encoding_2d=True,
- empty_init=True
- ):
GLMBlock 类的 forward 方法接收与SelfAttention的forward方法类似的参数,如输入序列的隐藏状态、位置编码、注意力掩码等。在这个方法中
接下来第661-729行,定义了一个名为 ChatGLMPreTrainedModel 的类,它继承自 PreTrainedModel。这个类是用于处理权重初始化以及简化下载和加载预训练模型的接口。
此外,还定义了一个名为 CHATGLM_6B_START_DOCSTRING 的变量,包含有关 ChatGLM6BConfig 的文档字符串,描述了如何使用这个 PyTorch 模型
定义了一个名为ChatGLMModel的类,它继承自ChatGLMPreTrainedModel。这是一个基于transformer的模型,能够作为编码器(仅使用自注意力机制)或解码器。解码器的情况下,会在自注意力层之间添加一个跨注意力层。模型的结构遵循论文Attention is all you need中描述的结构。
ChatGLMModel类的forward方法负责执行模型的前向传播。这个方法接收一系列输入参数,如input_ids、attention_mask、past_key_values等。根据这些输入,方法将执行以下操作:
这个模型的设计可以在序列到序列(Seq2Seq)任务中使用,这时需要将is_decoder和add_cross_attention参数设置为True,并在前向传播时提供encoder_hidden_states。
定义了一个名为ChatGLMForConditionalGeneration的类,,它用于条件生成任务,如文本生成。这个类继承自ChatGLMPreTrainedModel,主要包括初始化方法、模型的前向传播逻辑以及生成过程中需要的输入预处理方法。
主要部分的解释如下:
该类中还包括一些辅助方法,例如 _get_logits_processor, _get_stopping_criteria, _get_logits_warper, prepare_inputs_for_generation, 和 _update_model_kwargs_for_generation,这些方法用于处理生成过程中的各种设置和参数。
TextTokenizer:
处理文本和词条之间的转换TextTokenizer
:这个类主要处理文本和词条之间的转换,包括将文本转化为词条列表的分词(tokenize
),将词条列表转化为文本的解码(decode
),以及获取词条的ID和从ID获取词条(convert_tokens_to_ids
, convert_ids_to_tokens
)等操作。此外,它还包含了处理特殊词条和填充的功能
这些处理是许多自然语言处理(NLP)任务,如文本分类、命名实体识别、问答系统、机器翻译等的基础步骤
当然,TextTokenizer
还依赖下面的SPTokenizer
进行文本的分词和解码操作,而将复杂的操作封装在了自己的接口之下,同时添加了对特殊词条和填充的处理。
- # 导入相关库和模块
- from typing import List, Optional, Union
- import os
-
- from transformers.tokenization_utils import PreTrainedTokenizer # 从 transformers 包导入预训练的词条化工具类
- from transformers.utils import logging, PaddingStrategy # 导入 transformers 的日志和填充策略工具类
- from transformers.tokenization_utils_base import EncodedInput, BatchEncoding # 导入词条化相关工具类
- from typing import Dict # 导入字典类型
- import sentencepiece as spm # 导入 sentencepiece,一个开源的词条化工具
- import numpy as np # 导入 numpy,用于科学计算
-
- logger = logging.get_logger(__name__) # 创建一个日志记录器
-
- # 定义预训练位置嵌入大小的常量
- PRETRAINED_POSITIONAL_EMBEDDINGS_SIZES = {
- "THUDM/chatglm-6b": 2048,
- }
-
-
- class TextTokenizer:
- def __init__(self, model_path):
- self.sp = spm.SentencePieceProcessor() # 创建一个 SentencePieceProcessor 实例
- self.sp.Load(model_path) # 加载模型
- self.num_tokens = self.sp.vocab_size() # 获取模型的词汇表大小
-
- def encode(self, text):
- return self.sp.EncodeAsIds(text) # 将文本编码为ID序列
-
- def decode(self, ids: List[int]):
- return self.sp.DecodeIds(ids) # 将ID序列解码为文本
-
- def tokenize(self, text):
- return self.sp.EncodeAsPieces(text) # 将文本分割为词条序列
-
- def convert_tokens_to_string(self, tokens):
- return self.sp.DecodePieces(tokens) # 将词条序列解码为文本
-
- def convert_tokens_to_ids(self, tokens):
- return [self.sp.PieceToId(token) for token in tokens] # 将词条序列转换为ID序列
-
- def convert_token_to_id(self, token):
- return self.sp.PieceToId(token) # 将单个词条转换为ID
-
- def convert_id_to_token(self, idx):
- return self.sp.IdToPiece(idx) # 将ID转换为词条
-
- def __len__(self):
- return self.num_tokens # 返回词汇表大小
SPTokenizer:
包装了SentencePiece库的分词器SPTokenizer
:这个类是一个包装了SentencePiece库的分词器。SentencePiece是一个开源的自然语言处理库,用于神经网络模型的不规则文本分词,这个类主要提供了一些接口来利用SentencePiece库进行分词、解码等操作
- class SPTokenizer:
- def __init__(
- self,
- vocab_file,
- num_image_tokens=20000,
- max_blank_length=80,
- byte_fallback=True,
- ):
- assert vocab_file is not None # 检查词汇表文件是否存在
- self.vocab_file = vocab_file # 保存词汇表文件路径
- self.num_image_tokens = num_image_tokens # 保存图像词条数量
- self.special_tokens = ["[MASK]", "[gMASK]", "[sMASK]", "<unused_0>", "<sop>", "<eop>", "<ENC>", "<dBLOCK>"] # 定义特殊词条
- self.max_blank_length = max_blank_length # 定义最大空白长度
- self.byte_fallback = byte_fallback # 设置字节回退标记
- self.text_tokenizer = TextTokenizer(vocab_file) # 创建文本词条化工具
-
- def _get_text_tokenizer(self):
- return self.text_tokenizer # 获取文本词条化工具
-
- @staticmethod
- def get_blank_token(length: int):
- assert length >= 2
- return f"<|blank_{length}|>" # 获取空白词条
-
- @staticmethod
- def get_tab_token():
- return f"" # 获取制表符词条
-
- @property
- def num_text_tokens(self):
- return self.text_tokenizer.num_tokens # 获取文本词条数量
-
- @property
- def num_tokens(self):
- return self.num_image_tokens + self.num_text_tokens # 获取总词条数量
-
- @staticmethod
- def _encode_whitespaces(text: str, max_len: int = 80):
- text = text.replace("\t", SPTokenizer.get_tab_token()) # 替换制表符
- for i in range(max_len, 1, -1):
- text = text.replace(" " * i, SPTokenizer.get_blank_token(i)) # 替换多个连续空格
- return text
-
- def _preprocess(self, text: str, linebreak=True, whitespaces=True):
- if linebreak:
- text = text.replace("\n", "<n>") # 替换换行符
- if whitespaces:
- text = self._encode_whitespaces(text, max_len=self.max_blank_length) # 编码空白字符
- return text
-
- def encode(
- self, text: str, linebreak=True, whitespaces=True, add_dummy_prefix=True
- ) -> List[int]:
- """
- 文本编码方法
- """
- text = self._preprocess(text, linebreak, whitespaces) # 预处理文本
- if not add_dummy_prefix:
- text = "<n>" + text
- tmp = self._get_text_tokenizer().encode(text) # 编码文本
- tokens = [x + self.num_image_tokens for x in tmp] # 将文本词条ID转换为包含图像词条ID的序列
- return tokens if add_dummy_prefix else tokens[2:]
-
- def postprocess(self, text):
- text = text.replace("<n>", "\n") # 替换换行词条
- text = text.replace(SPTokenizer.get_tab_token(), "\t") # 替换制表符词条
- for i in range(2, self.max_blank_length + 1):
- text = text.replace(self.get_blank_token(i), " " * i) # 替换空白词条
- return text
-
- def decode(self, text_ids: List[int]) -> str:
- ids = [int(_id) - self.num_image_tokens for _id in text_ids] # 将包含图像词条的ID序列转换为文本词条ID序列
- ids = [_id for _id in ids if _id >= 0] # 删除非文本词条ID
- text = self._get_text_tokenizer().decode(ids) # 解码ID序列为文本
- text = self.postprocess(text) # 对文本进行后处理
- return text
-
- def decode_tokens(self, tokens: List[str]) -> str:
- text = self._get_text_tokenizer().convert_tokens_to_string(tokens) # 将词条序列解码为文本
- text = self.postprocess(text) # 对文本进行后处理
- return text
-
- def tokenize(
- self, text: str, linebreak=True, whitespaces=True, add_dummy_prefix=True
- ) -> List[str]:
- """
- 文本分词方法
- """
- text = self._preprocess(text, linebreak, whitespaces) # 预处理文本
- if not add_dummy_prefix:
- text = "<n>" + text
- tokens = self._get_text_tokenizer().tokenize(text) # 分词
- return tokens if add_dummy_prefix else tokens[2:]
-
- def __getitem__(self, x: Union[int, str]):
- if isinstance(x, int):
- if x < self.num_image_tokens:
- return "<image_{}>".format(x) # 如果是图像词条,返回词条形式
- else:
- return self.text_tokenizer.convert_id_to_token(x - self.num_image_tokens) # 如果是文本词条,返回文本词条
- elif isinstance(x, str):
- if x.startswith("<image_") and x.endswith(">") and x[7:-1].isdigit():
- return int(x[7:-1]) # 如果是图像词条形式,返回词条ID
- else:
- return self.text_tokenizer.convert_token_to_id(x) + self.num_image_tokens # 如果是文本词条,返回包含图像词条的ID
- else:
- raise ValueError("The key should be str or int.") # 如果不是整数或字符串,抛出异常
下面这段代码定义了一个ChatGLM的字节级字节对编码(Byte-Pair Encoding,BPE)分词器类,包含了一些分词器的基础操作,例如文本预处理、分词、词条解码、填充等
具体而言,以下的代码包括了对输入文本的预处理,将文本转化为词条序列的分词,以及将词条序列转化为文本的解码,等一系列分词器常用的操作。同时,这个分词器还支持添加特殊词条,以及在分词器的左边或右边进行填充,以满足模型输入的需要
- class ChatGLMTokenizer(PreTrainedTokenizer): # 基于PreTrainedTokenizer定义一个新的分词器类
- """
- Construct a ChatGLM tokenizer. Based on byte-level Byte-Pair-Encoding.
- Args:
- vocab_file (`str`):
- Path to the vocabulary file.
- """
-
- vocab_files_names = {"vocab_file": "ice_text.model"} # 设定词汇表文件名称
- max_model_input_sizes = PRETRAINED_POSITIONAL_EMBEDDINGS_SIZES # 预设模型输入的最大尺寸
- model_input_names = ["input_ids", "attention_mask", "position_ids"] # 预设模型输入的名称列表
-
- def __init__( # 定义初始化函数
- self,
- vocab_file, # 词汇表文件路径
- do_lower_case=False, # 是否对文本做小写转换
- remove_space=False, # 是否移除文本中的空格
- bos_token='<sop>', # 文本开头的特殊词条
- eos_token='<eop>', # 文本结尾的特殊词条
- end_token='</s>', # 文本结束的特殊词条
- mask_token='[MASK]', # 遮蔽词条
- gmask_token='[gMASK]', # gMASK词条
- padding_side="left", # 填充侧(左侧填充或右侧填充)
- pad_token="<pad>", # 填充词条
- unk_token="<unk>", # 未知词条
- num_image_tokens=20000, # 图像词条的数量
- **kwargs # 其他参数
- ) -> None:
- super().__init__( # 调用父类的初始化函数
- do_lower_case=do_lower_case,
- remove_space=remove_space,
- padding_side=padding_side,
- bos_token=bos_token,
- eos_token=eos_token,
- end_token=end_token,
- mask_token=mask_token,
- gmask_token=gmask_token,
- pad_token=pad_token,
- unk_token=unk_token,
- num_image_tokens=num_image_tokens,
- **kwargs
- )
-
- self.do_lower_case = do_lower_case # 是否进行小写转换
- self.remove_space = remove_space # 是否移除空格
- self.vocab_file = vocab_file # 词汇表文件
-
- self.bos_token = bos_token # 文本开头的特殊词条
- self.eos_token = eos_token # 文本结尾的特殊词条
- self.end_token = end_token # 文本结束的特殊词条
- self.mask_token = mask_token # 遮蔽词条
- self.gmask_token = gmask_token # gMASK词条
-
- self.sp_tokenizer = SPTokenizer(vocab_file, num_image_tokens=num_image_tokens) # 初始化SPTokenizer
-
- # 以下部分是定义了一些属性和方法
- @property
- def gmask_token_id(self) -> Optional[int]: # 获取gmask词条的id
- if self.gmask_token is None: # 若不存在,则返回None
- return None
- return self.convert_tokens_to_ids(self.gmask_token) # 返回gmask词条对应的id
-
- @property
- def end_token_id(self) -> Optional[int]: # 获取end词条的id
- if self.end_token is None: # 若不存在,则返回None
- return None
- return self.convert_tokens_to_ids(self.end_token) # 返回end词条对应的id
-
- @property
- def vocab_size(self): # 获取词汇表的大小
- return self.sp_tokenizer.num_tokens # 返回词汇表的大小
-
- def get_vocab(self): # 获取词汇表
- vocab = {self._convert_id_to_token(i): i for i in range(self.vocab_size)} # 将词汇表转化为字典形式
- vocab.update(self.added_tokens_encoder) # 更新添加的词条编码器
- return vocab # 返回词汇表
-
- def preprocess_text(self, inputs): # 文本预处理函数
- if self.remove_space: # 若需要移除空格
- outputs = " ".join(inputs.strip().split()) # 则移除多余的空格
- else:
- outputs = inputs # 否则保持不变
-
- if self.do_lower_case: # 若需要进行小写转换
- outputs = outputs.lower() # 则转换为小写
-
- return outputs # 返回预处理后的文本
-
- def _tokenize(self, text, **kwargs): # 分词函数
- text = self.preprocess_text(text) # 对文本进行预处理
-
- seq = self.sp_tokenizer.tokenize(text) # 对文本进行分词
-
- return seq # 返回分词结果
-
- def convert_tokens_to_string(self, tokens: List[str]) -> str: # 将词条转化为字符串
- return self.sp_tokenizer.decode_tokens(tokens) # 解码词条
-
- def _decode(
- self,
- token_ids: Union[int, List[int]],
- **kwargs
- ) -> str:
- # 对id进行解码
- if isinstance(token_ids, int): # 如果输入是单个id
- token_ids = [token_ids] # 则将其转化为列表
- if len(token_ids) == 0: # 如果输入为空
- return "" # 则返回空字符串
- if self.pad_token_id in token_ids: # 如果填充id在输入中
- token_ids = list(filter((self.pad_token_id).__ne__, token_ids)) # 则移除填充id
- return super()._decode(token_ids, **kwargs) # 返回父类的解码函数
-
- def _convert_token_to_id(self, token): # 将词条转化为id
- return self.sp_tokenizer[token] # 使用sp_tokenizer进行转换
-
- def _convert_id_to_token(self, index): # 将id转化为词条
- return self.sp_tokenizer[index] # 使用sp_tokenizer进行转换
-
- def save_vocabulary(self, save_directory, filename_prefix=None): # 保存词汇表到指定目录
- # 将词汇表及特殊词条文件保存到目录
- if os.path.isdir(save_directory): # 如果保存目录存在
- vocab_file = os.path.join(
- save_directory, self.vocab_files_names["vocab_file"]
- ) # 则构建vocab文件路径
- else:
- vocab_file = save_directory # 否则vocab文件就是保存目录
-
- with open(self.vocab_file, 'rb') as fin: # 打开vocab文件
- proto_str = fin.read() # 读取文件内容
-
- with open(vocab_file, "wb") as writer: # 打开待写入的文件
- writer.write(proto_str) # 写入内容
-
- return (vocab_file,) # 返回保存的文件路径
-
- # 以下是与特殊词条有关的方法
- def build_inputs_with_special_tokens(
- self, token_ids_0: List[int], token_ids_1: Optional[List[int]] = None
- ) -> List[int]:
- # 构建带有特殊词条的输入
- gmask_id = self.sp_tokenizer[self.gmask_token] # 获取gmask的id
- eos_id = self.sp_tokenizer[self.eos_token] # 获取eos的id
- token_ids_0 = token_ids_0 + [gmask_id, self.sp_tokenizer[self.bos_token]] # 添加gmask和bos到第一部分的尾部
- if token_ids_1 is not None: # 如果存在第二部分
- token_ids_0 = token_ids_0 + token_ids_1 + [eos_id] # 则将第二部分及eos添加到token_ids_0的尾部
- return token_ids_0 # 返回结果
-
- # 以下是与填充有关的方法
- def _pad(
- self,
- encoded_inputs: Union[Dict[str, EncodedInput], BatchEncoding],
- max_length: Optional[int] = None,
- padding_strategy: PaddingStrategy = PaddingStrategy.DO_NOT_PAD,
- pad_to_multiple_of: Optional[int] = None,
- return_attention_mask: Optional[bool] = None,
- ) -> dict:
- # 对编码后的输入进行填充
- bos_token_id = self.sp_tokenizer[self.bos_token] # 获取bos的id
- mask_token_id = self.sp_tokenizer[self.mask_token] # 获取mask的id
- gmask_token_id = self.sp_tokenizer[self.gmask_token] # 获取gmask的id
- assert self.padding_side == "left" # 断言填充在左边
-
- required_input = encoded_inputs[self.model_input_names[0]] # 获取所需的输入
- seq_length = len(required_input) # 获取序列长度
-
- if padding_strategy == PaddingStrategy.LONGEST: # 如果填充策略是最长的
- max_length = len(required_input) # 则最大长度为输入的长度
-
- if max_length is not None and pad_to_multiple_of is not None and (max_length % pad_to_multiple_of != 0):
- max_length = ((max_length // pad_to_multiple_of) + 1) * pad_to_multiple_of # 如果最大长度不是pad_to_multiple_of的倍数,则进行相应的调整
-
- if max_length is not None and seq_length < max_length: # 如果最大长度存在且序列长度小于最大长度
- difference = max_length - seq_length # 计算差值
- if self.padding_side == "right": # 如果填充在右边
- if return_attention_mask: # 如果需要返回注意力掩码
- encoded_inputs["attention_mask"] = [1] * seq_length + [0] * difference # 则构建注意力掩码
- encoded_inputs[self.model_input_names[0]] = (
- [bos_token_id] + [mask_token_id] * difference + required_input + [gmask_token_id]
- ) # 构建输入
- else:
- if return_attention_mask: # 如果需要返回注意力掩码
- encoded_inputs["attention_mask"] = [0] * difference + [1] * seq_length # 则构建注意力掩码
- encoded_inputs[self.model_input_names[0]] = (
- [gmask_token_id] + required_input + [mask_token_id] * difference + [bos_token_id]
- ) # 构建输入
-
- return encoded_inputs # 返回编码后的输入
跟分词相关的还有一个tokenizer_config.json
:这个文件通常包含分词器的配置信息,例如预训练模型使用的特殊令牌(如[CLS],[SEP]等)
- {
- "name_or_path": "THUDM/chatglm-6b",
- "bos_token": "<sop>",
- "eos_token": "<eop>",
- "end_token": "</s>",
- "gmask_token": "[gMASK]",
- "mask_token": "[MASK]",
- "pad_token": "<pad>",
- "unk_token": "<unk>",
- "remove_space": false,
- "do_lower_case": false,
- "tokenizer_class": "ChatGLMTokenizer",
- "num_image_tokens": 0,
- "auto_map": {
- "AutoTokenizer": [
- "tokenization_chatglm.ChatGLMTokenizer",
- null
- ]
- }
- }
quantization.py: 这是一个Python脚本,可能包含了对模型进行量化的代码,量化是一种减小模型大小和推理时间的技术
- from torch.nn import Linear # 从torch.nn模块导入Linear线性模块
- from torch.nn.parameter import Parameter # 从torch.nn.parameter模块导入Parameter参数模块
-
- import bz2 # 导入bz2模块,该模块支持bzip2压缩和解压缩
- import torch # 导入torch模块,这是一个深度学习框架
- import base64 # 导入base64模块,该模块提供了将二进制数据转换为ASCII字符的方法
- import ctypes # 导入ctypes模块,该模块提供了一种强大的工具来创建、访问和操纵C数据类型
- from transformers.utils import logging # 从transformers.utils模块导入logging日志模块
-
- from typing import List # 从typing模块导入List,可以用于注解变量的类型
- from functools import partial # 从functools模块导入partial,可以用来固定函数的部分参数,返回新的partial对象
-
- logger = logging.get_logger(__name__) # 创建一个logger,名字为当前模块的名称
-
- try:
- # 从cpm_kernels.kernels.base模块导入LazyKernelCModule,KernelFunction和round_up
- from cpm_kernels.kernels.base import LazyKernelCModule, KernelFunction, round_up
-
- class Kernel: # 定义一个名为Kernel的类
- def __init__(self, code: bytes, function_names: List[str]): # 定义类的初始化函数,接收一个字节类型的code和一个字符串列表类型的function_names作为参数
- self.code = code # 将传入的code参数赋值给self.code
- self._function_names = function_names # 将传入的function_names参数赋值给self._function_names
- self._cmodule = LazyKernelCModule(self.code) # 使用传入的code创建一个LazyKernelCModule对象,并赋值给self._cmodule
-
- for name in self._function_names: # 遍历_function_names列表
- setattr(self, name, KernelFunction(self._cmodule, name)) # 为self设置一个属性,属性名为name,值为KernelFunction对象
-
- quantization_code = "$QlpoOTFBWSZTWU9yuJUAQHN......"
-
- # 尝试加载一组用于权重压缩和解压的 CUDA kernels
- # 其中,kernels 中包括四种不同的操作:
- # "int4WeightCompression","int4WeightExtractionFloat",
- # "int4WeightExtractionHalf","int8WeightExtractionFloat",
- # "int8WeightExtractionHalf"
- kernels = Kernel(
- bz2.decompress(base64.b64decode(quantization_code)),
- [
- "int4WeightCompression",
- "int4WeightExtractionFloat",
- "int4WeightExtractionHalf",
- "int8WeightExtractionFloat",
- "int8WeightExtractionHalf",
- ],
- )
- # 如果在加载过程中出现任何异常,kernels 设为 None,并记录警告信息
- except Exception as exception:
- kernels = None
- logger.warning("Failed to load cpm_kernels:" + str(exception))
-
- # 定义一个自定义的 PyTorch autograd 函数,表示一种线性操作
- # 这种操作在前向传播过程中使用的是量化后的权重,而在反向传播过程中则使用的是半精度浮点数的权重
- class W8A16Linear(torch.autograd.Function):
- @staticmethod
- def forward(ctx, inp: torch.Tensor, quant_w: torch.Tensor, scale_w: torch.Tensor, weight_bit_width):
- # 保存输入的形状、权重的位宽以及权重的量化值和量化尺度等信息,供后向传播时使用
- ctx.inp_shape = inp.size()
- ctx.weight_bit_width = weight_bit_width
- out_features = quant_w.size(0)
- inp = inp.contiguous().view(-1, inp.size(-1))
- # 提取权重的半精度浮点数表示
- weight = extract_weight_to_half(quant_w, scale_w, weight_bit_width)
- ctx.weight_shape = weight.size()
- # 计算输出
- output = inp.mm(weight.t())
- # 保存必要的信息,供后向传播时使用
- ctx.save_for_backward(inp, quant_w, scale_w)
- return output.view(*(ctx.inp_shape[:-1] + (out_features,)))
-
- @staticmethod
- def backward(ctx, grad_output: torch.Tensor):
- # 提取前向传播时保存的信息
- inp, quant_w, scale_w = ctx.saved_tensors
- # 提取权重的半精度浮点数表示
- weight = extract_weight_to_half(quant_w, scale_w, ctx.weight_bit_width)
- grad_output = grad_output.contiguous().view(-1, weight.size(0))
- # 计算输入和权重的梯度
- grad_input = grad_output.mm(weight)
- grad_weight = grad_output.t().mm(inp)
- return grad_input.view(ctx.inp_shape), grad_weight.view(ctx.weight_shape), None, None
-
- # 定义一个函数,用于将权重压缩为 int4 格式
- def compress_int4_weight(weight: torch.Tensor): # (n, m)
- with torch.cuda.device(weight.device):
- n, m = weight.size(0), weight.size(1)
- assert m % 2 == 0
- m = m // 2
- out = torch.empty(n, m, dtype=torch.int8, device="cuda")
- stream = torch.cuda.current_stream()
-
- gridDim = (n, 1, 1)
- blockDim = (min(round_up(m, 32), 1024), 1, 1)
-
- # 调用 CUDA kernels 进行权重压缩
- kernels.int4WeightCompression(
- gridDim,
- blockDim,
- 0,
- stream,
- [ctypes.c_void_p(weight.data_ptr()), ctypes.c_void_p(out.data_ptr()), ctypes.c_int32(n), ctypes.c_int32(m)],
- )
- return out
-
- # 定义一个函数,用于将量化的权重转换为半精度浮点数格式
- def extract_weight_to_half(weight: torch.Tensor, scale_list: torch.Tensor, source_bit_width: int):
- if source_bit_width == 8:
- func = kernels.int8WeightExtractionHalf
- elif source_bit_width == 4:
- func = kernels.int4WeightExtractionHalf
- else:
- assert False, "Unsupported bit-width"
-
- with torch.cuda.device(weight.device):
- n, m = weight.size(0), weight.size(1)
- out = torch.empty(n, m * (8 // source_bit_width), dtype=torch.half, device="cuda")
- stream = torch.cuda.current_stream()
-
- gridDim = (n, 1, 1)
- blockDim = (min(round_up(m, 32), 1024), 1, 1)
-
- # 调用 CUDA kernels 提取权重
- func(
- gridDim,
- blockDim,
- 0,
- stream,
- [
- ctypes.c_void_p(weight.data_ptr()),
- ctypes.c_void_p(scale_list.data_ptr()),
- ctypes.c_void_p(out.data_ptr()),
- ctypes.c_int32(n),
- ctypes.c_int32(m),
- ],
- )
- return out
- # 定义一个名为 QuantizedLinear 的类,该类继承自 PyTorch 中的 Linear 类
- class QuantizedLinear(Linear):
- # 初始化函数,接受一些参数,包括权重的位宽、权重张量、偏置张量等
- def __init__(self, weight_bit_width: int, weight_tensor=None, bias_tensor=None, empty_init=False, *args, **kwargs):
- # 调用父类的初始化函数
- super(QuantizedLinear, self).__init__(*args, **kwargs)
- # 保存权重的位宽
- self.weight_bit_width = weight_bit_width
-
- # 获取权重的形状,并删除父类中的权重
- shape = self.weight.shape
- del self.weight
-
- # 如果未指定权重张量,或者指定了空初始化,则初始化权重和权重的量化尺度
- if weight_tensor is None or empty_init:
- self.weight = torch.empty(
- shape[0], shape[1] * weight_bit_width // 8, dtype=torch.int8, device=kwargs["device"]
- )
- self.weight_scale = torch.empty(shape[0], dtype=kwargs["dtype"], device=kwargs["device"])
- else: # 否则,计算权重的量化值和量化尺度
- self.weight_scale = (weight_tensor.abs().max(dim=-1).values / ((2 ** (weight_bit_width - 1)) - 1)).half()
- self.weight = torch.round(weight_tensor / self.weight_scale[:, None]).to(torch.int8)
- # 如果权重的位宽为 4,压缩权重
- if weight_bit_width == 4:
- self.weight = compress_int4_weight(self.weight)
-
- # 将权重和权重的量化尺度设置为参数,并指定它们不需要梯度
- self.weight = Parameter(self.weight.to(kwargs["device"]), requires_grad=False)
- self.weight_scale = Parameter(self.weight_scale.to(kwargs["device"]), requires_grad=False)
- # 如果指定了偏置张量,将偏置设置为参数,并指定它不需要梯度
- if bias_tensor is not None:
- self.bias = Parameter(bias_tensor.to(kwargs["device"]), requires_grad=False)
- else: # 否则,偏置设为 None
- self.bias = None
-
- # 定义前向传播函数
- def forward(self, input):
- # 应用 W8A16Linear 函数计算输出
- output = W8A16Linear.apply(input, self.weight, self.weight_scale, self.weight_bit_width)
- # 如果存在偏置,将偏置加到输出上
- if self.bias is not None:
- output = output + self.bias
- return output
-
-
- # 定义一个函数,用于将模型中的线性层替换为量化的线性层
- def quantize(model, weight_bit_width, empty_init=False, **kwargs):
- """Replace fp16 linear with quantized linear"""
- # 遍历模型中的每一层
- for layer in model.layers:
- # 将每一层中的 query_key_value 替换为量化的线性层
- layer.attention.query_key_value = QuantizedLinear(
- weight_bit_width=weight_bit_width,
- weight_tensor=layer.attention.query_key_value.weight.to(torch.cuda.current_device()),
- bias_tensor=layer.attention.query_key_value.bias,
- in_features=layer.attention.query_key_value.in_features,
- out_features=layer.attention.query_key_value.out_features,
- bias=True,
- dtype=torch.half,
- device=layer.attention.query_key_value.weight.device,
- empty_init=empty_init
- )
- # 将每一层中的 dense 替换为量化的线性层
- layer.attention.dense = QuantizedLinear(
- weight_bit_width=weight_bit_width,
- weight_tensor=layer.attention.dense.weight.to(torch.cuda.current_device()),
- bias_tensor=layer.attention.dense.bias,
- in_features=layer.attention.dense.in_features,
- out_features=layer.attention.dense.out_features,
- bias=True,
- dtype=torch.half,
- device=layer.attention.dense.weight.device,
- empty_init=empty_init
- )
- # 将每一层中的 dense_h_to_4h 替换为量化的线性层
- layer.mlp.dense_h_to_4h = QuantizedLinear(
- weight_bit_width=weight_bit_width,
- weight_tensor=layer.mlp.dense_h_to_4h.weight.to(torch.cuda.current_device()),
- bias_tensor=layer.mlp.dense_h_to_4h.bias,
- in_features=layer.mlp.dense_h_to_4h.in_features,
- out_features=layer.mlp.dense_h_to_4h.out_features,
- bias=True,
- dtype=torch.half,
- device=layer.mlp.dense_h_to_4h.weight.device,
- empty_init=empty_init
- )
- # 将每一层中的 dense_4h_to_h 替换为量化的线性层
- layer.mlp.dense_4h_to_h = QuantizedLinear(
- weight_bit_width=weight_bit_width,
- weight_tensor=layer.mlp.dense_4h_to_h.weight.to(torch.cuda.current_device()),
- bias_tensor=layer.mlp.dense_4h_to_h.bias,
- in_features=layer.mlp.dense_4h_to_h.in_features,
- out_features=layer.mlp.dense_4h_to_h.out_features,
- bias=True,
- dtype=torch.half,
- device=layer.mlp.dense_4h_to_h.weight.device,
- empty_init=empty_init
- )
- return model
// 待更..
ChatGLM-6B-PT仓库实现了对于 ChatGLM-6B 模型基于 P-Tuning v2 的微调。P-Tuning v2 将需要微调的参数量减少到原来的 0.1%,再通过模型量化、Gradient Checkpoint 等方法,最低只需要 7GB 显存即可运行
运行微调需要4.27.1版本的transformers
,且除 ChatGLM-6B 的依赖之外,还需要安装以下依赖
pip install rouge_chinese nltk jieba datasets
下面以 ADGEN (广告生成) 数据集为例介绍代码的使用方法
ADGEN 数据集任务为根据输入(content)生成一段广告词(summary)
{
"content": "类型#上衣*版型#宽松*版型#显瘦*图案#线条*衣样式#衬衫*衣袖型#泡泡袖*衣款式#抽绳",
"summary": "这件衬衫的款式非常的宽松,利落的线条可以很好的隐藏身材上的小缺点,穿在身上有着很好的显瘦效果。领口装饰了一个可爱的抽绳,漂亮的绳结展现出了十足的个性,配合时尚的泡泡袖型,尽显女性甜美可爱的气息。"
}
从 Google Drive 或者 Tsinghua Cloud 下载处理好的 ADGEN 数据集,将解压后的 AdvertiseGen
目录放到本目录下
运行以下指令进行训练:bash train.sh
- PRE_SEQ_LEN=128
- LR=2e-2
-
- CUDA_VISIBLE_DEVICES=0 python3 main.py \
- --do_train \
- --train_file AdvertiseGen/train.json \
- --validation_file AdvertiseGen/dev.json \
- --prompt_column content \
- --response_column summary \
- --overwrite_cache \
- --model_name_or_path THUDM/chatglm-6b \
- --output_dir output/adgen-chatglm-6b-pt-$PRE_SEQ_LEN-$LR \
- --overwrite_output_dir \
- --max_source_length 64 \
- --max_target_length 64 \
- --per_device_train_batch_size 1 \
- --per_device_eval_batch_size 1 \
- --gradient_accumulation_steps 16 \
- --predict_with_generate \
- --max_steps 3000 \
- --logging_steps 10 \
- --save_steps 1000 \
- --learning_rate $LR \
- --pre_seq_len $PRE_SEQ_LEN \
- --quantization_bit 4
如果需要进行全参数的 Finetune,需要安装 Deepspeed,然后运行以下指令:bash ds_train_finetune.sh
- LR=1e-4
-
- MASTER_PORT=$(shuf -n 1 -i 10000-65535)
-
- deepspeed --num_gpus=4 --master_port $MASTER_PORT main.py \
- --deepspeed deepspeed.json \
- --do_train \
- --train_file AdvertiseGen/train.json \
- --test_file AdvertiseGen/dev.json \
- --prompt_column content \
- --response_column summary \
- --overwrite_cache \
- --model_name_or_path THUDM/chatglm-6b \
- --output_dir ./output/adgen-chatglm-6b-ft-$LR \
- --overwrite_output_dir \
- --max_source_length 64 \
- --max_target_length 64 \
- --per_device_train_batch_size 4 \
- --per_device_eval_batch_size 1 \
- --gradient_accumulation_steps 1 \
- --predict_with_generate \
- --max_steps 5000 \
- --logging_steps 10 \
- --save_steps 1000 \
- --learning_rate $LR \
- --fp16
其中的main.py引入了from trainer_seq2seq import Seq2SeqTrainer,而trainer_seq2seq.py又引入了from trainer import Trainer,而Trainer.py则来自https://github.com/huggingface/transformers/blob/main/src/transformers/trainer.py
关于这个有着3800多行的traner.py的分析详见此文《从零实现Transformer的简易版与强大版:从300多行到3000多行》4.1节逐行解读:3858行的transformers/src/transformers/trainer.py
在 P-tuning v2 训练时模型只保存 PrefixEncoder 部分的参数,所以在推理时需要同时加载原 ChatGLM-6B 模型以及 PrefixEncoder 的权重,因此需要指定 evaluate.sh
中的参数:
- --model_name_or_path THUDM/chatglm-6b
- --ptuning_checkpoint $CHECKPOINT_PATH
仍然兼容旧版全参保存的 Checkpoint,只需要跟之前一样设定 model_name_or_path:
--model_name_or_path $CHECKPOINT_PATH
评测指标为中文 Rouge score 和 BLEU-4。生成的结果保存在 ./output/adgen-chatglm-6b-pt-8-1e-2/generated_predictions.txt
修改 train.sh 和 evaluate.sh 中的 train_file、validation_file和test_file为你自己的 JSON 格式数据集路径,并将 prompt_column 和 response_column 改为 JSON 文件中输入文本和输出文本对应的 KEY。可能还需要增大 max_source_length 和 max_target_length 来匹配你自己的数据集中的最大输入输出长度
如需要使用多轮对话数据对模型进行微调,可以提供聊天历史,例如以下是一个三轮对话的训练数据:
- {"prompt": "长城h3风扇不转。继电器好的。保险丝好的传感器新的风扇也新的这是为什么。就是继电器缺一个信号线", "response": "用电脑能读数据流吗?水温多少", "history": []}
- {"prompt": "95", "response": "上下水管温差怎么样啊?空气是不是都排干净了呢?", "history": [["长城h3风扇不转。继电器好的。保险丝好的传感器新的风扇也新的这是为什么。就是继电器缺一个信号线", "用电脑能读数据流吗?水温多少"]]}
- {"prompt": "是的。上下水管都好的", "response": "那就要检查线路了,一般风扇继电器是由电脑控制吸合的,如果电路存在断路,或者电脑坏了的话会出现继电器不吸合的情况!", "history": [["长城h3风扇不转。继电器好的。保险丝好的传感器新的风扇也新的这是为什么。就是继电器缺一个信号线", "用电脑能读数据流吗?水温多少"], ["95", "上下水管温差怎么样啊?空气是不是都排干净了呢?"]]}
训练时需要指定 --history_column 为数据中聊天历史的 key(在此例子中是 history),将自动把聊天历史拼接。要注意超过输入长度 max_source_length 的内容会被截断。
可以参考以下指令:
bash train_chat.sh
至于模型的部署请见:https://github.com/THUDM/ChatGLM-6B/blob/main/ptuning/README.md
这个示例数据集总计有100条,以下是前三条的示例
- {"prompt": "请根据以下标签为商品编写一段广告\n类型#裤*版型#宽松*风格#性感*图案#线条*裤型#阔腿裤", "response": "宽松的阔腿裤这两年真的吸粉不少,明星时尚达人的心头爱。毕竟好穿时尚,谁都能穿出腿长2米的效果宽松的裤腿,当然是遮肉小能手啊。上身随性自然不拘束,面料亲肤舒适贴身体验感棒棒哒。系带部分增加设计看点,还让单品的设计感更强。腿部线条若隐若现的,性感撩人。颜色敲温柔的,与裤子本身所呈现的风格有点反差萌。"}
- {"prompt": "请根据以下标签为商品编写一段广告\n类型#裙*风格#简约*图案#条纹*图案#线条*图案#撞色*裙型#鱼尾裙*裙袖长#无袖", "response": "圆形领口修饰脖颈线条,适合各种脸型,耐看有气质。无袖设计,尤显清凉,简约横条纹装饰,使得整身人鱼造型更为生动立体。加之撞色的鱼尾下摆,深邃富有诗意。收腰包臀,修饰女性身体曲线,结合别出心裁的鱼尾裙摆设计,勾勒出自然流畅的身体轮廓,展现了婀娜多姿的迷人姿态。"}
- {"prompt": "请根据以下标签为商品编写一段广告\n类型#上衣*版型#宽松*颜色#粉红色*图案#字母*图案#文字*图案#线条*衣样式#卫衣*衣款式#不规则", "response": "宽松的卫衣版型包裹着整个身材,宽大的衣身与身材形成鲜明的对比描绘出纤瘦的身形。下摆与袖口的不规则剪裁设计,彰显出时尚前卫的形态。被剪裁过的样式呈现出布条状自然地垂坠下来,别具有一番设计感。线条分明的字母样式有着花式的外观,棱角分明加上具有少女元气的枣红色十分有年轻活力感。粉红色的衣身把肌肤衬托得很白嫩又健康。"}
// 待更..
ChatGLM2-6B(GitHub项目地址、HuggingFace地址)是开源中英双语对话模型 ChatGLM-6B 的第二代版本,相比第一代,第二点引入了如下新特性:
context_layer 这个函数实现了attention机制的计算,入参 is_causal=True 表示遮后看前的mask(这种类型的注意力通常用在transformer的decoder部分,以确保当前位置只能关注到之前的位置,俗称“看不见未来”,从而使模型可以进行自回归预测 )
请参见《通透理解FlashAttention与FlashAttention2:让大模型上下文长度突破32K的技术之一》
多查询注意力(Muti Query Attention)是 19 年Google一研究者提出的一种新的 Attention 机制(对应论文为:Fast Transformer Decoding: One Write-Head is All You Need、这是其解读之一),其能够在保证模型效果的同时加快 decoder 生成 token 的速度
那其与17年 Google提出的transformer中多头注意力机制(简称MHA)有啥本质区别呢?有意思的是,区别在于:
下图对比了多头注意力(Multi-Head Attention)、LLaMA2中分组查询注意力(Grouped-Query Attention)、多查询注意力(Muti Query Attention)的差别
总之,MHA 和 MQA 之间的区别只在于建立 Wqkv Layer 上
更多请参见:《一文通透各种注意力:从多头注意力MHA到分组查询注意力GQA、多查询注意力MQA》
- git clone https://github.com/THUDM/ChatGLM2-6B
- cd ChatGLM2-6B
pip install -r requirements.txt
其中 transformers 库版本推荐为 4.30.2
,torch
推荐使用 2.0 及以上的版本,以获得最佳的推理性能- >>> from transformers import AutoTokenizer, AutoModel
- >>> tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm2-6b", trust_remote_code=True)
- >>> model = AutoModel.from_pretrained("THUDM/chatglm2-6b", trust_remote_code=True, device='cuda')
- >>> model = model.eval()
- >>> response, history = model.chat(tokenizer, "你好", history=[])
- >>> print(response)
你好 本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。