ChatGLM2-6B

模型描述

ChatGLM2-6B 是开源中英双语对话模型 ChatGLM2-6B 的第二代版本,在保留了初代模型对话流畅、部署门槛较低等众多优秀特性的基础之上,ChatGLM2-6B引入了新特征:更强大的性能更长的上下文更高效的推理更开放的协议

仓库介绍

chatGLM2-6B 基于 mindformers 实现,主要涉及的文件有:

  1. 模型具体实现:mindformers/models/glm2

    glm2
        ├── __init__.py
        ├── glm2.py                  # 模型实现
        ├── glm2_config.py           # 模型配置项
        ├── glm2_modules.py          # 模组实现
        ├── glm2_tokenizer.py        # tokenizer
        └── glm2_transformer.py      # transformer层实现
    
  2. 模型配置:configs/glm2

    glm2
        ├── run_glm2_6b_fintune.yaml  # 全量微调启动配置
        └── run_glm2_6b_lora.yaml     # lora低参微调启动配置
    

环境要求

  • 硬件:Ascend 910A

  • MindSpore:2.0

推理可在单机单卡上完成部署

全量微调训练需要最少单机8卡,Lora微调训练最少需要1卡

基线

测试环境同上述环境要求

性能

data parallel

model parallel

pipeline parallel

batch size

sink size

sequence length

accumulate

per step time (ms)

tokens/s/p

优化器并行

重计算

Memory (GB)

全量微调

8

1

1

8

4

193

1

1894

815.2059134

True

True

25.2

LoRA微调

4

1

1

8

4

193

1

476

3243.697479

False

False

22.38

评估指标

rouge-1

rouge-2

rouge-l

bleu-4

全量微调

30.784298224299064

7.073415046728972

24.773958598130843

7.466147757009345

LoRA微调

31.05639289719626

7.1753861682243

24.229674859813084

7.229435140186916

ChatGLM2-6B推理

需开发者提前pip安装。具体接口说明请参API接口

AutoClass推理

可以使用AutoClass接口,通过模型名称获取相应的模型/tokenizer实例,并自动下载并加载权重

from_pretrained() 接口会自动从云上下载预训练的模型,存储路径:mindformers/checkpoint_download/glm2

首次运行pipeline推理时需要进行模型编译,需等待一段时间

from mindformers import AutoTokenizer, AutoModel


tokenizer = AutoTokenizer.from_pretrained("glm2_6b")
model = AutoModel.from_pretrained("glm2_6b")

query = "你好"

prompted_inputs = tokenizer.build_prompt(query)
input_tokens = tokenizer([prompted_inputs])

outputs = model.generate(input_tokens["input_ids"], max_length=100)
response = tokenizer.decode(outputs)[0]
print(response)

pipeline推理

也可以不实例化构造模型,直接通过指定任务模型与模型名的方式进行pipeline的构造

>>> from mindformers import pipeline, TextGenerationPipeline
>>> task_pipeline = pipeline(task='text_generation', model='glm2_6b', max_length=2048)
>>> task_pipeline('你好')
[{'text_generation_text': ['你好,我是 ChatGLM2-6B, 一个人工智能助手。我背后使用的模型是 GLM2-6B, 是一种大型语言模型, 具有超过 2000 亿参数,支持多种任务。']}]
>>> pipeline = TextGenerationPipeline(model='glm2_6b', max_length=2048)
>>> pipeline("你好")
[{'text_generation_text': ['你好,我是 ChatGLM2-6B, 一个人工智能助手。我背后使用的模型是 GLM2-6B, 是一种大型语言模型, 具有超过 2000 亿参数,支持多种任务。']}]

微调

下面以 ADGEN (广告生成) 数据集为例介绍代码的使用方法

数据处理

ADGEN 数据集任务为根据输入(content)生成一段广告词(summary)。

{
    "content": "类型#上衣*版型#宽松*版型#显瘦*图案#线条*衣样式#衬衫*衣袖型#泡泡袖*衣款式#抽绳",
    "summary": "这件衬衫的款式非常的宽松,利落的线条可以很好的隐藏身材上的小缺点,穿在身上有着很好的显瘦效果。领口装饰了一个可爱的抽绳,漂亮的绳结展现出了十足的个性,配合时尚的泡泡袖型,尽显女性甜美可爱的气息。"
}

Google Drive 或者 Tsinghua Cloud 下载处理好的 ADGEN 数据集,目录结构为

AdvertiseGen
  ├── train.json
  └── dev.json

将任务配置文件 configs/glm2/run_glm2_6b_*.yaml 中的 ==== dataset config ==== 部分替换成:

train_dataset: &train_dataset
  data_loader:
    type: ADGenDataLoader
    dataset_dir: "/path/to/AdvertiseGen/train.json"
    shuffle: True
    phase: "train"
    version: 2
    origin_columns: ["content", "summary"]
  tokenizer:
    type: ChatGLM2Tokenizer
    vocab_file: "/path/to/tokenizer.model"
  input_columns: ["input_ids", "labels"]
  max_source_length: 64
  max_target_length: 128
  ignore_pad_token_for_loss: True
  num_parallel_workers: 8
  python_multiprocessing: False
  drop_remainder: True
  batch_size: 1
  repeat: 1
  numa_enable: False
  prefetch_size: 1
  seed: 0

train_dataset_task:
  type: KeyWordGenDataset
  dataset_config: *train_dataset

eval_dataset: &eval_dataset
  data_loader:
    type: ADGenDataLoader
    dataset_dir: "/path/to/AdvertiseGen/dev.json"
    shuffle: False
    phase: "eval"
    version: 2
    origin_columns: ["content", "summary"]
  tokenizer:
    type: ChatGLM2Tokenizer
    vocab_file: "/path/to/tokenizer.model"
  max_source_length: 256
  max_target_length: 256
  ignore_pad_token_for_loss: True
  input_columns: ["input_ids", "labels"]
  num_parallel_workers: 8
  python_multiprocessing: False
  drop_remainder: True
  batch_size: 1
  repeat: 1
  numa_enable: False
  prefetch_size: 1
  seed: 0

eval_dataset_task:
  type: KeyWordGenDataset
  dataset_config: *eval_dataset

生成HCCL文件

运行mindformers/tools/hccl_tools.py生成RANK_TABLE_FILE的json文件;

# step1:机器上运行如下命令,生成各自的RANK_TABLE_FILE的json文件
python ./mindformers/tools/hccl_tools.py --device_num "[0,8)"

注:若使用ModelArts的notebook环境,可从 /user/config/jobstart_hccl.json 路径下直接获取rank table,无需手动生成

RANK_TABLE_FILE 单机8卡参考样例:

{
    "version": "1.0",
    "server_count": "1",
    "server_list": [
        {
            "server_id": "xx.xx.xx.xx",
            "device": [
                {"device_id": "0","device_ip": "192.1.27.6","rank_id": "0"},
                {"device_id": "1","device_ip": "192.2.27.6","rank_id": "1"},
                {"device_id": "2","device_ip": "192.3.27.6","rank_id": "2"},
                {"device_id": "3","device_ip": "192.4.27.6","rank_id": "3"},
                {"device_id": "4","device_ip": "192.1.27.7","rank_id": "4"},
                {"device_id": "5","device_ip": "192.2.27.7","rank_id": "5"},
                {"device_id": "6","device_ip": "192.3.27.7","rank_id": "6"},
                {"device_id": "7","device_ip": "192.4.27.7","rank_id": "7"}],
             "host_nic_ip": "reserve"
        }
    ],
    "status": "completed"
}

全参微调

run_mindformers脚本启动全参微调

全参微调使用 configs/glm2/run_glm2_6b.yaml 配置文件,配置文件中定义了微调所需的各配置项

修改数据集/模型权重配置路径:

  • 数据集:修改 mindformers/configs/glm2/run_glm2_6b.yaml 脚本中train_datasetdataset_dir 为前文生成的数据集路径。

  • 加载预训练模型权重:修改 mindformers/configs/glm2/run_glm2_6b.yaml 脚本中的 load_checkpoint 为预训练模型权重路径。

启动全参微调脚本:

cd scripts
# Usage Help: bash run_distribute.sh [RANK_TABLE_FILE] [CONFIG_PATH] [DEVICE_RANGE] [RUN_STATUS]
bash run_distribute.sh /path/to/hccl_8p_01234567_127.0.1.1.json ../configs/glm2/run_glm2_6b.yaml '[0,8]' finetune
# 将此处rank_table_file替换为实际路径

参数说明

RANK_TABLE_FILE: 由mindformers/tools/hccl_tools.py生成的分布式json文件
CONFIG_PATH: 为configs文件夹下面的glm2/run_glm2_6b.yaml配置文件
DEVICE_RANGE: 为单机分布式卡的范围,如 '[0,8]' 为8卡分布式,不包含8本身
RUN_STATUS: 为任务运行状态,支持关键字 train\finetune\eval\predict

注:由于GLM2_6B的模型较大,无法在单卡上运行,此处仅提供分布式启动脚本

训练的log日志路径:mindformers/output/log

checkpoint存储路径:mindformers/output/checkpoint

LoRA低参微调

全参微调能够在微调数据集上取得良好效果,但存在遗忘预训练知识的现象 因此推荐使用低参微调算法,冻结原模型权重,仅在小规模参数量上进行训练,在微调数据集上取得良好效果的同时,缓解模型遗忘现象

run_mindformers脚本启动LoRA低参微调

使用LoRA算法进行低参微调时,使用 configs/glm2/run_glm2_6b_lora.yaml 配置文件,该配置文件包含了lora低参微调算法所需的配置项

修改数据集/模型权重配置路径:

  • 数据集:修改 mindformers/configs/glm2/run_glm2_6b_lora.yaml 脚本中train_datasetdataset_dir 为前文生成的数据集路径。

  • 加载预训练模型权重:修改 mindformers/configs/glm2/run_glm2_6b_lora.yaml 脚本中的 load_checkpoint 为预训练模型权重路径。

启动LoRA低参微调脚本(1卡):

执行命令:

cd scripts
# Usage Help: bash run_stanalone.sh [CONFIG_PATH] [DEVICE_ID] [RUN_STATUS]
bash run_standalone.sh ../configs/glm2/run_glm2_6b_lora.yaml 0 finetune

训练的log日志路径:mindformers/scripts/mf_standalone/

checkpoint存储路径:mindformers/scripts/mf_standalone/output/checkpoint

Trainer高阶接口启动LoRA低参微调

示例脚本如下,需要指定训练数据集路径和微调权重。

from mindformers import Trainer
trainer = Trainer(task="text_generation", model="glm2_6b", pet_method="lora",
                  train_dataset="/path/to/AdvertiseGen/train.json")
trainer.finetune(finetune_checkpoint="glm2_6b")

微调后推理

推理样例脚本

下面提供一个模型推理样例脚本 infer.py

from mindformers import AutoConfig, AutoModel, AutoTokenizer
import mindspore as ms

ms.set_context(mode=ms.GRAPH_MODE, device_target="Ascend", device_id=0)

config = AutoConfig.from_pretrained("glm2_6b")
config.checkpoint_name_or_path = "/path/to/glm2_6b_finetune.ckpt"
model = AutoModel.from_config(config)
tokenizer = AutoTokenizer.from_pretrained("glm2_6b")

inputs = tokenizer(tokenizer.build_prompt("你好"))["input_ids"]
print(inputs)
print(tokenizer.decode(inputs))
outputs = model.generate(inputs, max_length=128)
print(tokenizer.decode(outputs))
inputs = tokenizer(tokenizer.build_prompt("请介绍一下华为"))["input_ids"]
print(inputs)
outputs = model.generate(inputs, max_length=128)
print(tokenizer.decode(outputs))
inputs = tokenizer(tokenizer.build_prompt("晚上睡不着应该怎么办"))["input_ids"]
print(inputs)
outputs = model.generate(inputs, max_length=128)
print(tokenizer.decode(outputs))
inputs = tokenizer(tokenizer.build_prompt("类型#上衣*材质#牛仔布*颜色#白色*风格#简约*图案#刺绣*衣样式#外套*衣款式#破洞"))["input_ids"]
print(inputs)
outputs = model.generate(inputs, max_length=128)
print(tokenizer.decode(outputs))

评估

模型权重文件合一

微调所得到的权重文件为根据模型切分策略切分后的权重,我们需要手动将切分权重合一,以用于评估和推理

  1. 获取模型切分策略文件: 在执行全参微调脚本时,模型完成编译后,将会在运行路径下,生成名为 ckpt_strategy.ckpt 的切分策略文件,该文件将用于第二步模型合成

  2. MindSpore提供了根据切分策略转换模型权重切分的接口,mindspore.transform_checkpoints,执行以下python脚本,将8份模型文件合成一份

    from mindspore import transform_checkpoints
    transform_checkpoints(
        src_checkpoints_dir="./output/checkpoint/", # 原切分权重文件夹
        dst_checkpoints_dir="./target_checkpoint/", # 目标路径
        ckpt_prefix="glm2-6b", # .ckpt文件前缀名
        src_strategy_file="ckpt_stragery.ckpt", # 步骤1中的切分策略文件路径
        dst_strategy_file=None # None表示不切分,权重合一
    )
    

注:transform_checkpoints 接口当前仅mindspore 2.0以上版本支持,如当前硬件环境只支持2.0以下版本,可以新建conda环境安装mindspore 2.0的cpu版本以执行该脚本

使用全参微调权重

run_mindformers启动eval

使用全参微调权重时,启动如下shell脚本,执行单卡评估

配置文件选择 configs/glm2/run_glm2_6b.yaml glm2模型推理配置,修改其中model字段下model_configuse_past: True开启增量推理使评估速度更快

python run_mindformer.py --config configs/glm2/run_glm2_6b.yaml --run_mode eval --load_checkpoint /path/to/glm2_6b_finetune.ckpt --eval_dataset_dir /path/to/data/AdvertiseGen/ --device_id 0

注:使用离线生成数据方式时,将 eval_dataset_dir 一项指向.mindrecord文件,如 /path/to/data/AdvertiseGen/adgen_dev.mindrecord

各项参数:

  • config: 指定用于评估的配置文件名称,此处为configs/glm2/run_glm2_6b.yaml

  • run_mode: 指定执行模式,此为eval,表示为评估模式

  • load_checkpoint: 指定要加载的checkpoint路径,此处为/path/to/glm2_6b_finetune.ckpt,替换为需加载的权重的真实路径

  • eval_dataset_dir: 评估数据集的路径

  • device_id: 指定要使用的设备编号(从0开始)

评估完成后会打印评估指标 bleu-4rouge-1rouge-2rouge-l

注:由于默认评估指标的获取方式为生成完整文本后与预期文本做比较,评估速度将受限于模型大小与文本生成速度,评估流程可能较为缓慢

Trainer高阶接口启动eval

与上文类似:

from mindformers import Trainer, ChatGLM2Config, ChatGLM2ForConditionalGeneration

# 开启增量推理使评估速度更快
config = ChatGLM2Config(use_past=True)
model = ChatGLM2ForConditionalGeneration(config)
trainer = Trainer(task="text_generation", model=model,
                  eval_dataset="/path/to/AdvertiseGen/dev.json")
trainer.evaluate(eval_checkpoint="/path/to/glm2_6b_finetune.ckpt")

使用LoRA低参微调权重

run_mindformers启动lora eval

使用LoRA低参微调权重时,启动如下shell脚本,执行单卡评估

配置文件选择 configs/glm2/run_glm2_6b_lora.yaml glm2_lora模型推理配置,此配置可用于lora模型,修改其中model字段下model_configuse_past: True开启增量推理使评估速度更快

python run_mindformer.py --config configs/glm2/run_glm2_6b_lora.yaml --run_mode eval --load_checkpoint /path/to/glm2_6b_lora.ckpt --eval_dataset_dir /path/to/data/AdvertiseGen/ --device_id 0

各项参数同上,路径需替换为实际路径

Trainer高阶接口启动lora eval

与上文类似:

from mindformers import Trainer, ChatGLM2Config, ChatGLM2WithLora
from mindformers.pet.pet_config import LoraConfig

# 开启增量推理使评估速度更快
config = ChatGLM2Config(use_past=True)
config.pet_config = LoraConfig()
model = ChatGLM2WithLora(config)
trainer = Trainer(task="text_generation", model=model,
                  eval_dataset="/path/to/AdvertiseGen/dev.json")
trainer.evaluate(eval_checkpoint="/path/to/glm2_6b_lora.ckpt")

模型权重转化

本仓库中的glm2来自于HuggingFace的 ChatGLM2-6B,基于下述的步骤获取:

  1. 克隆chatglm2-6b代码仓,下载分布式的模型文件。

    git lfs install
    git clone https://huggingface.co/THUDM/chatglm2-6b
    
  2. 执行 python 脚本,合并模型权重。

    from transformers import AutoTokenizer, AutoModel
    import torch
    
    tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm2-6b", trust_remote_code=True)
    model = AutoModel.from_pretrained("THUDM/chatglm2-6b", trust_remote_code=True)
    
    with open("pt_model_arch.txt", "w") as fp:
        print(model, file=fp, flush=True)
    with open("pt_ckpt.txt", "w") as fp:
        for name, param in model.named_parameters():
            fp.write(f"{name} {param.shape} {param.dtype}\n")
    torch.save(model.state_dict(), "glm2_6b.pth")
    
  3. 执行转换脚本,得到转换后的输出文件glm2_6b.ckpt

    import mindspore as ms
    import torch as pt
    from tqdm import tqdm
    
    pt_ckpt_path = "glm2_6b.pth"
    pt_param = pt.load(pt_ckpt_path)
    
    type_map = {"torch.float16": "ms.float16",
                "torch.float32": "ms.float32"}
    ms_param = []
    with open("check_pt_ckpt.txt", "w") as fp:
        for k, v in tqdm(pt_param.items()):
            if "word_embeddings.weight" in k:
                k = k.replace("word_embeddings.weight", "embedding_table")
            fp.write(f"{k} {v.shape} {v.dtype}\n")
            ms_param.append({"name": k, "data": ms.Tensor(v.numpy())})
    
    ms.save_checkpoint(ms_param, "glm2_6b.ckpt")