第一部分 复旦MOSS
MOSS是复旦大学邱锡鹏团队推出的一个支持中英双语和多种插件的开源对话语言模型,moss-moon系列模型具有160亿参数,在FP16精度下可在单张A100/A800或两张3090显卡运行,在INT4/8精度下可在单张3090显卡运行
其基座语言模型在约七千亿中英文以及代码单词上预训练得到,后续经过对话指令微调、插件增强学习和人类偏好训练具备多轮对话能力及使用多种插件的能力
5.1 已开源的模型/数据
5.1.1 已开源的模型
- moss-moon-003-base: MOSS-003基座模型,在高质量中英文语料上自监督预训练得到,预训练语料包含约700B单词,计算量约6.67x1022次浮点数运算。
- moss-moon-003-sft: 基座模型在约110万多轮对话数据上微调得到,具有指令遵循能力、多轮对话能力、规避有害请求能力。
- moss-moon-003-sft-plugin: 基座模型在约110万多轮对话数据和约30万插件增强的多轮对话数据上微调得到,在
moss-moon-003-sft
基础上还具备使用搜索引擎、文生图、计算器、解方程等四种插件的能力。 - moss-moon-003-sft-int4: 4bit量化版本的
moss-moon-003-sft
模型,约占用12GB显存即可进行推理。 - moss-moon-003-sft-int8: 8bit量化版本的
moss-moon-003-sft
模型,约占用24GB显存即可进行推理。 - moss-moon-003-sft-plugin-int4: 4bit量化版本的
moss-moon-003-sft-plugin
模型,约占用12GB显存即可进行推理。 - moss-moon-003-sft-plugin-int8: 8bit量化版本的
moss-moon-003-sft-plugin
模型,约占用24GB显存即可进行推理。 - moss-moon-003-pm: 在基于
moss-moon-003-sft
收集到的偏好反馈数据上训练得到的偏好模型,将在近期开源。 - moss-moon-003: 在
moss-moon-003-sft
基础上经过偏好模型moss-moon-003-pm
训练得到的最终模型,具备更好的事实性和安全性以及更稳定的回复质量,将在近期开源。 - moss-moon-003-plugin: 在
moss-moon-003-sft-plugin
基础上经过偏好模型moss-moon-003-pm
训练得到的最终模型,具备更强的意图理解能力和插件使用能力,将在近期开源。
5.1.2 已开源的数据
- moss-002-sft-data: MOSS-002所使用的多轮对话数据,覆盖有用性、忠实性、无害性三个层面,包含由
text-davinci-003
生成的约57万条英文对话和59万条中文对话 - moss-003-sft-data:
moss-moon-003-sft
所使用的多轮对话数据,基于MOSS-002内测阶段采集的约10万用户输入数据和gpt-3.5-turbo
构造而成,相比moss-002-sft-data
,moss-003-sft-data
更加符合真实用户意图分布,包含更细粒度的有用性类别标记、更广泛的无害性数据和更长对话轮数,约含110万条对话数据。目前仅开源少量示例数据,完整数据将在近期开源 - moss-003-sft-plugin-data:
moss-moon-003-sft-plugin
所使用的插件增强的多轮对话数据,包含支持搜索引擎、文生图、计算器、解方程等四个插件在内的约30万条多轮对话数据。目前仅开源少量示例数据,完整数据将在近期开源 - moss-003-pm-data:
moss-moon-003-pm
所使用的偏好数据,包含在约18万额外对话上下文数据及使用moss-moon-003-sft
所产生的回复数据上构造得到的偏好对比数据,将在近期开源
5.2 MOSS模型量化版部署过程
我司七月杜助教写了一篇部署MOSS的教程,详情请点击:MOSS模型量化版部署过程
- 项目背景
- 配置环境与准备
- 部署推理
-
- 命令行部署
-
- 报错1
- 报错2:
- 使用免费试用的阿里云GPU部署
- 在AutoDL平台上部署
第二部分 baichuan-7B/13B:与LLaMA的结构相同且表现优秀可商用
2.1 baichuan-7B
2.1.1 基于Transformer/RoPE/RMSNorm/SwiGLU + 1.2万亿训练数据/上下文窗口4096
baichuan-7B 是由百川智能(CEO为原搜狗创始人王小川)开发的一个开源可商用的大规模预训练语言模型
- 基于 Transformer 结构,采用了和 LLaMA 一样的模型设计,比如
位置编码:用的现阶段被大多模型采用的 rotary-embedding 方案,具有更好的外延效果
激活层:SwiGLU, Feedforward 变化为 8/3 倍的隐含层大小,即 11,008
Layer-Normalization: 基于 RMSNorm 的 Pre-Normalization
关于LLaMA结构的解读,请参见:类ChatGPT模型LLaMA的解读与其微调:Alpaca-LoRA/Vicuna/BELLE - 在大约 1.2 万亿 tokens 上训练的 70 亿参数模型,支持中英双语
- 上下文窗口长度为 4096
- 在标准的中文和英文权威 benchmark(C-EVAL/MMLU)上均取得同尺寸最好的效果
具体而言,C-Eval 数据集是一个全面的中文基础模型评测数据集,涵盖了 52 个学科和四个难度的级别
我们使用该数据集的 dev 集作为 few-shot 的来源,在 test 集上进行了 5-shot 测试
除了中文之外,作者团队也测试了模型在英文上的效果,MMLU 是包含 57 个多选任务的英文评测数据集,涵盖了初等数学、美国历史、计算机科学、法律等,难度覆盖高中水平到专家水平,是目前主流的LLM评测数据集
一句话总结,即是在C-EVAL/MMLU等数据集上的表现好于ChatGLM-6B (当然,ChatGLM2-6B又变更强了)
2.1.2 baichuan-7B相比LLaMA-7B的优势
虽然baichuan-7B采用了和LLaMA一样的模型设计,但他们在原本的 LLaMA 框架上进行诸多修改
比如为提升模型的效果以及解码效率,做了
- 分词改进
词表大小为64K ,而LLaMA词表大小为32K
具体而言,参考学术界方案使用 SentencePiece 中的 Byte-Pair Encoding (BPE) 作为分词算法,并且进行了以下的优化:
目前大部分开源模型主要基于英文优化,因此对中文语料存在效率较低的问题,使用 2000 万条以中英为主的多语言语料训练分词模型,显著提升对于中文的压缩率
对于数学领域,我们参考了 LLaMA 和 Galactica 中的方案,对数字的每一位单独分开,避免出现数字不一致的问题,对于提升数学能力有重要帮助
对于罕见字词(如特殊符号等),支持 UTF-8 characters 的 byte 编码,因此做到未知字词的全覆盖 - 数据集改进
使用了大约 1.2T 中英 tokens 进行训练(基于开源的中英文数据和自行抓取的中文互联网数据以及部分高质量知识性数据进行的数据清洗),而 LLaMA 7B 使用 1T 英文 tokens 进行训练
比如为提升训练时的吞吐,做了以下优化
- 算子优化技术:采用更高效算子,如 Flash-Attention,NVIDIA apex 的 RMSNorm 等。
- 算子切分技术:将部分计算算子进行切分,减小内存峰值。
- 混合精度技术:降低在不损失模型精度的情况下加速计算过程。
- 训练容灾技术:训练平台和训练框架联合优化,IaaS + PaaS 实现分钟级的故障定位和任务恢复。
- 通信优化技术,具体包括:
采用拓扑感知的集合通信算法,避免网络拥塞问题,提高通信效率
根据卡数自适应设置 bucket size,提高带宽利用率
根据模型和集群环境,调优通信原语的触发时机,从而将计算和通信重叠
基于上述的几个优化技术,使得在千卡 A800 显卡上达到了 7B 模型 182 TFLOPS 的吞吐,GPU 峰值算力利用率高达 58.3%
2.1.3 baichuan-7B的微调
本次微调参考项目:https://github.com/wp931120/baichuan_sft_lora
由于baichuan没有 supervised finetune 这一步,没有和人类意图进行对齐,经常听不懂你下达的指令。该项目遂利用belle 0.5M 指令微调数据,采用qlora的量化微调的方式对百川大模型进行人类意图对齐训练
训练前置条件,先从huggingface 中将baichuan7b 大模型权重 ,然后,最后运行sft_lora.py 脚本
先将百川LLM 采用qlora的 nf4 和双重量化方式进行量化
在采用lora进行指令微调
本次微调baichuan-7B的步骤如下
- 微调之前的准备
下载项目仓库
配置环境git clone https://github.com/wp931120/baichuan_sft_lora.git cd baichuan_sft_lora
数据集下载conda create -n baichuan-7b python=3.9 conda activate baichuan-7b pip install -r requirements.txt
sft 数据集采用的是belle 0.5M
下载地址:https://huggingface.co/datasets/BelleGroup/train_0.5M_CN/tree/main
将 belle 数据集 train_0.5M_CN 下载到本地放到项目目录下的dataset文件夹下 - 将百川LLM 采用qlora的 nf4 和双重量化方式进行量化
- 再采用lora进行指令微调
wp931120x/baichuan_4bit_lora · Hugging Face - 修改并运行sft_lora.py文件
将sft_lora.py中的模型路径设置为自己的模型路径
执行python sft_lora.py运行代码
最终,显存占用为7G左右import os # 导入os模块,这个模块提供了一种方便的使用操作系统依赖功能的方式 os.environ['CUDA_VISIBLE_DEVICES'] = '0' # 设置CUDA可见设备,'0'表示仅使用第一块GPUfrom datasets import load_dataset # 导入load_dataset函数,用于加载数据集 import transformers # 导入transformers库,这是一个常用的NLP库# 导入Trainer和TrainingArguments,分别用于模型的训练和训练参数的设置 from transformers import Trainer, TrainingArguments # 导入AutoTokenizer和AutoModelForCausalLM,分别用于自动化地从预训练模型中获取Tokenizer和模型 from transformers import AutoTokenizer, AutoModelForCausalLM # 导入BitsAndBytesConfig,用于设置模型的量化配置 from transformers import BitsAndBytesConfig# 导入一些特定的函数和配置类 from peft import (LoraConfig,get_peft_model,prepare_model_for_kbit_training,set_peft_model_state_dict, ) import torch # 导入PyTorch库,这是一个常用的深度学习库# 定义一些配置信息 CUTOFF_LEN = 1024 VAL_SET_SIZE = 2000 DATA_PATH = "./dataset/Belle_open_source_0.5M.json" OUTPUT_DIR = "baichuansft" resume_from_checkpoint = "baichuansft"# 设置设备映射,""表示默认设备,0表示设备编号 device_map = {"": 0} # 使用AutoTokenizer从预训练模型中获取Tokenizer tokenizer = AutoTokenizer.from_pretrained("./baichuan-7B",trust_remote_code=True) # 使用AutoModelForCausalLM从预训练模型中获取模型,并设置量化配置 model = AutoModelForCausalLM.from_pretrained("./baichuan-7B",trust_remote_code=True,quantization_config=BitsAndBytesConfig(load_in_4bit=True,bnb_4bit_compute_dtype=torch.bfloat16,bnb_4bit_use_double_quant=True,bnb_4bit_quant_type='nf4'),device_map=device_map)model = prepare_model_for_kbit_training(model) # 准备模型进行kbit训练# 导入bitsandbytes模块 import bitsandbytes as bnb# 定义一个函数,用于找到模型中所有的线性层的名称 def find_all_linear_names(model):cls = bnb.nn.Linear4bit lora_module_names = set()for name, module in model.named_modules(): # 遍历模型中的所有模块if isinstance(module, cls): # 如果模块是线性层names = name.split('.')lora_module_names.add(names[0] if len(names) == 1 else names[-1]) # 添加到线性层名称集合中if 'lm_head' in lora_module_names: # 如果'lm_head'在名称集合中,需要移除lora_module_names.remove('lm_head')return list(lora_module_names) # 返回线性层名称列表# 获取所有的线性层的名称 modules = find_all_linear_names(model)# 设置LoRA配置 config = LoraConfig(r=8,lora_alpha=16,lora_dropout=0.05,bias="none",target_modules=modules,task_type="CAUSAL_LM", )# 获取用于训练的模型 model = get_peft_model(model, config) tokenizer.pad_token_id = 0 # 设置tokenizer的pad_token_id为0# 如果有设置从检查点恢复 if resume_from_checkpoint:# 检查可用的权重并加载checkpoint_name = os.path.join(resume_from_checkpoint, "pytorch_model.bin") # 完整的检查点# 如果完整的检查点不存在,则加载LoRA模型的检查点if not os.path.exists(checkpoint_name):checkpoint_name = os.path.join(resume_from_checkpoint, "adapter_model.bin") # 仅LoRA模型 - 上面的LoRA配置必须匹配resume_from_checkpoint = (False # 所以训练器不会尝试加载状态)if os.path.exists(checkpoint_name):print(f"Restarting from {checkpoint_name}")adapters_weights = torch.load(checkpoint_name)set_peft_model_state_dict(model, adapters_weights) # 设置模型的状态字典else:print(f"Checkpoint {checkpoint_name} not found")# 加载数据集 data = load_dataset("json", data_files=DATA_PATH)# 定义tokenize函数,用于将输入进行tokenize def tokenize(prompt, add_eos_token=True):# 这里是tokenize的具体操作result = tokenizer(prompt,truncation=True,max_length=CUTOFF_LEN,padding=False,return_tensors=None,)# 添加EOS tokenif (result["input_ids"][-1] != tokenizer.eos_token_idand len(result["input_ids"]) < CUTOFF_LENand add_eos_token):result["input_ids"].append(tokenizer.eos_token_id)result["attention_mask"].append(1)if add_eos_token and len(result["input_ids"]) >= CUTOFF_LEN:result["input_ids"][CUTOFF_LEN - 1] = tokenizer.eos_token_idresult["attention_mask"][CUTOFF_LEN - 1] = 1# 输入和标签都是input_idsresult["labels"] = result["input_ids"].copy()return result# 定义generate_and_tokenize_prompt函数,用于生成并tokenize输入 def generate_and_tokenize_prompt(data_point):instruction = data_point['instruction']input_text = data_point["input"]input_text = "Human: " + instruction + input_text + "\n\nAssistant: "input_text = tokenizer.bos_token + input_text if tokenizer.bos_token != None else input_texttarget_text = data_point["output"] + tokenizer.eos_tokenfull_prompt = input_text + target_texttokenized_full_prompt = tokenize(full_prompt)return tokenized_full_prompt# 划分训练集和验证集,并进行shuffle和map操作 if VAL_SET_SIZE > 0:train_val = data["train"].train_test_split(test_size=VAL_SET_SIZE, shuffle=True, seed=42)train_data = train_val["train"].shuffle().map(generate_and_tokenize_prompt)val_data = train_val["test"].shuffle().map(generate_and_tokenize_prompt) else:train_data = data['train'].shuffle().map(generate_and_tokenize_prompt)val_data = None# 创建Trainer对象,用于进行训练 trainer = Trainer(model=model,train_dataset=train_data,eval_dataset=val_data,args=TrainingArguments(num_train_epochs=1,per_device_train_batch_size=1,per_device_eval_batch_size=1,learning_rate=3e-4,gradient_accumulation_steps=4,evaluation_strategy="steps" if VAL_SET_SIZE > 0 else "no",save_strategy="steps",eval_steps=2000 if VAL_SET_SIZE > 0 else None,save_steps=2000,output_dir=OUTPUT_DIR,report_to = "tensorboard",save_total_limit=3,load_best_model_at_end=True if VAL_SET_SIZE > 0 else False,optim="adamw_torch"),data_collator=transformers.DataCollatorForSeq2Seq(tokenizer,pad_to_multiple_of=8,return_tensors="pt",padding=True), )# 进行训练 trainer.train(resume_from_checkpoint=False) # 保存预训练模型 model.save_pretrained(OUTPUT_DIR)
2.2 baichuan-13B
// 待更
第三部分 ChatGLM2-6B的部署与微调
3.1 相比第一代的改进点:FlashAttention与Multi-Query Attention
ChatGLM2-6B(项目地址)是开源中英双语对话模型 ChatGLM-6B 的第二代版本,相比第一代,第二点引入了如下新特性:
- 数据集上
经过了 1.4T 中英标识符的预训练与人类偏好对齐训练 - 更长的上下文
基于 FlashAttention 技术,我们将基座模型的上下文长度(Context Length)由 ChatGLM-6B 的 2K 扩展到了 32K,并在对话阶段使用 8K 的上下文长度训练,允许更多轮次的对话
(当前版本的 ChatGLM2-6B 对单轮超长文档的理解能力有限,会在后续迭代升级中着重进行优化) - 更高效的推理
基于 Multi-Query Attention 技术,ChatGLM2-6B 有更高效的推理速度和更低的显存占用:在官方的模型实现下,推理速度相比初代提升了 42%,INT4 量化下,6G 显存支持的对话长度由 1K 提升到了 8K - 允许商业使用
- 准确性不足
尽管模型在训练的各个阶段都尽力确保数据的合规性和准确性,但由于 ChatGLM2-6B 模型规模较小,且模型受概率随机性因素影响,无法保证输出内容的准确性,且模型易被误导
3.1.1 FlashAttention:更长上下文的关键
FlashAttention是斯坦福联合纽约州立大学在22年6月份提出的一种具有 IO 感知,且兼具快速、内存高效的新型注意力算法「对应论文为:FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness,这是其GitHub地址,这是其解读之一」
// 待更
3.1.2 Muti Query Attention:各自Query矩阵、共享Key 和 Value 矩阵
Muti Query Attention是 19 年Google一研究者提出的一种新的 Attention 机制(对应论文为:Fast Transformer Decoding: One Write-Head is All You Need、这是其解读之一),其能够在保证模型效果的同时加快 decoder 生成 token 的速度
那其与17年 Google提出的transformer中多头注意力机制有啥本质区别呢?有意思的是,区别在于:
- 我们知道MHA的每个头都各自有一份不同的Key、Query、Value矩阵
- 而MQA 让所有的头之间 共享 同一份 Key 和 Value 矩阵,每个头只单独保留了一份 Query 参数,从而大大减少 Key 和 Value 矩阵的参数量
总之,MQA 实际上是将 head 中的 key 和 value 矩阵抽出来单独存为一份共享参数,而 query 则是依旧保留在原来的 head 中,每个 head 有一份自己独有的 query 参数
换言之,MHA 和 MQA 之间的区别只在于建立 Wqkv Layer 上
# Multi Head Attention
self.Wqkv = nn.Linear( # 【关键】Multi-Head Attention 的创建方法self.d_model, 3 * self.d_model, # 有 query, key, value 3 个矩阵, 所以是 3 * d_modeldevice=device
)query, key, value = qkv.chunk( # 【关键】每个 tensor 都是 (1, 512, 768)3, dim=2
)# Multi Query Attention
self.Wqkv = nn.Linear( # 【关键】Multi-Query Attention 的创建方法d_model,d_model + 2 * self.head_dim, # 只创建 query 的 head 向量,所以只有 1 个 d_modeldevice=device, # 而 key 和 value 不再具备单独的头向量
)query, key, value = qkv.split( # query -> (1, 512, 768)[self.d_model, self.head_dim, self.head_dim], # key -> (1, 512, 96)dim=2 # value -> (1, 512, 96)
)
对比上面的代码,你可以发现
- 在 MHA 中,query, key, value 每个向量均有 768 维度
- 而在 MQA 中,只有 query 是 768 维,而 key 和 value 均只剩下 96 维了,恰好是 1 个 head_dim 的维度
因此,可以确认:在 MQA 中,除了 query 向量还保存着 8 个头,key 和 value 向量都只剩 1 个「公共头」了,这也正好印证了论文中所说的「所有 head 之间共享一份 key 和 value 的参数」
剩下的问题就是如何将这 1 份参数同时让 8 个头都使用,代码里使用矩阵乘法 matmul 来广播,使得每个头都乘以这同一个 tensor,以此来实现参数共享:
def scaled_multihead_dot_product_attention(query,key,value,n_heads,multiquery=False,):q = rearrange(query, 'b s (h d) -> b h s d', h=n_heads) # (1, 512, 768) -> (1, 8, 512, 96)kv_n_heads = 1 if multiquery else n_headsk = rearrange(key, 'b s (h d) -> b h d s', h=kv_n_heads) # (1, 512, 768) -> (1, 8, 96, 512) if not multiquery # (1, 512, 96) -> (1, 1, 96, 512) if multiqueryv = rearrange(value, 'b s (h d) -> b h s d', h=kv_n_heads) # (1, 512, 768) -> (1, 8, 512, 96) if not multiquery # (1, 512, 96) -> (1, 1, 512, 96) if multiqueryattn_weight = q.matmul(k) * softmax_scale # (1, 8, 512, 512)attn_weight = torch.softmax(attn_weight, dim=-1) # (1, 8, 512, 512)out = attn_weight.matmul(v) # (1, 8, 512, 512) * (1, 1, 512, 96) = (1, 8, 512, 96)out = rearrange(out, 'b h s d -> b s (h d)') # (1, 512, 768)return out, attn_weight, past_key_value
// 待更