微调和部署对话模型ChatGLM2-6B#

ChatGLM2-6B是中英文对话模型ChatGLM-6B 的第二代版本,在保留了初代模型对话流畅、部署门槛较低等众多优秀特性的基础之上,ChatGLM2-6B 引入了多项升级,包括更强大的性能、更长的上下文、更高效的推理。

在本示例中,我们将展示:

  • 将ChatGLM2-6B部署到PAI创建推理服务,基于推理服务API和Gradio实现一个简易对话机器人。

  • 在PAI对ChatGLM2-6B进行微调训练,并将微调的模型部署创建推理服务。

准备工作#

前提条件#

安装和配置PAI Python SDK#

我们将使用PAI提供的Python SDK,提交训练作业,部署模型。可以通过以下命令安装PAI Python SDK。

!python -m pip install --upgrade alipai
!python -m pip install gradio

SDK需要配置访问阿里云服务需要的 AccessKey,以及当前使用的工作空间和OSS Bucket。在PAI Python SDK安装之后,通过在 命令行终端 中执行以下命令,按照引导配置密钥,工作空间等信息。

# 以下命令,请在 命令行终端 中执行.

python -m pai.toolkit.config

我们可以通过以下代码验证当前的配置。

import pai
from pai.session import get_default_session

print(pai.__version__)
sess = get_default_session()

assert sess.workspace_name is not None
print(sess.workspace_name)

直接部署ChatGLM2#

ChatGLM2-6B是一个对话语言模型,能够基于历史对话信息,和用户的Prompt输入,进行反馈。通过HuggingFace的transformers库用户可以直接使用ChatGLM2-6B提供的对话能力,示例如下:


>>> 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).half().cuda()
>>> model = model.eval()
>>> response, history = model.chat(tokenizer, "你好", history=[])
>>> print(response)
你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。
>>> response, history = model.chat(tokenizer, "晚上睡不着应该怎么办", history=history)
>>> print(response)
晚上睡不着可能会让你感到焦虑或不舒服,但以下是一些可以帮助你入睡的方法:

1. 制定规律的睡眠时间表:保持规律的睡眠时间表可以帮助你建立健康的睡眠习惯,使你更容易入睡。尽量在每天的相同时间上床,并在同一时间起床。
2. 创造一个舒适的睡眠环境:确保睡眠环境舒适,安静,黑暗且温度适宜。可以使用舒适的床上用品,并保持房间通风。
3. 放松身心:在睡前做些放松的活动,例如泡个热水澡,听些轻柔的音乐,阅读一些有趣的书籍等,有助于缓解紧张和焦虑,使你更容易入睡。
4. 避免饮用含有咖啡因的饮料:咖啡因是一种刺激性物质,会影响你的睡眠质量。尽量避免在睡前饮用含有咖啡因的饮料,例如咖啡,茶和可乐。
5. 避免在床上做与睡眠无关的事情:在床上做些与睡眠无关的事情,例如看电影,玩游戏或工作等,可能会干扰你的睡眠。
6. 尝试呼吸技巧:深呼吸是一种放松技巧,可以帮助你缓解紧张和焦虑,使你更容易入睡。试着慢慢吸气,保持几秒钟,然后缓慢呼气。

如果这些方法无法帮助你入睡,你可以考虑咨询医生或睡眠专家,寻求进一步的建议。



以下的流程中,我们将ChatGLM2-6B部署到PAI创建一个推理服务,然后基于推理服务的API,使用Gradio创建一个对话机器人。

获取ChatGLM2模型#

推理服务和训练作业中都需要加载使用模型,PAI在部分region上提供模型缓存,支持用户能够更快地获取到相应的模型。用户可以通过以下代码获取相应的模型,然后在训练作业和推理服务中加载使用。

from pai.model import RegisteredModel

m = RegisteredModel(
    "THUDM/chatglm2-6b",
    model_provider="huggingface",
)

model_uri = m.model_data
print(model_uri)

创建推理服务#

PAI-EAS是阿里云PAI提供模型在线服务平台,支持用户一键部署推理服务或是AIWeb应用,支持异构资源,弹性扩缩容。PAI-EAS支持使用镜像的方式部署模型,以下的流程,我们将使用PAI提供的PyTorch推理镜像,将以上的模型部署为推理服务。

在部署推理服务之前,我们需要准备相应的推理服务程序,他负责加载模型,提供对应的HTTP API服务。

!mkdir -p server_src

完整的推理服务代码如下:

%%writefile server_src/run.py
# source: https://github.com/THUDM/ChatGLM-6B/blob/main/api.py

import os

from fastapi import FastAPI, Request
from transformers import AutoTokenizer, AutoModel, AutoConfig
import uvicorn, json, datetime
import torch


model = None
tokenizer = None

# 默认的模型保存路径
chatglm_model_path = "/eas/workspace/model/"
# ptuning checkpoints保存路径
ptuning_checkpoint = "/ml/ptuning_checkpoints/"
pre_seq_len = 128
app = FastAPI()


def load_model():
    global model, tokenizer
    tokenizer = AutoTokenizer.from_pretrained(chatglm_model_path, trust_remote_code=True)

    if os.path.exists(ptuning_checkpoint):
        # P-tuning v2
        print(f"Loading model/ptuning_checkpoint weight...")
        config = AutoConfig.from_pretrained(chatglm_model_path, trust_remote_code=True)
        config.pre_seq_len = pre_seq_len
        config.prefix_projection = False

        model = AutoModel.from_pretrained(chatglm_model_path, config=config, trust_remote_code=True)
        tokenizer = AutoTokenizer.from_pretrained(chatglm_model_path, trust_remote_code=True)
        prefix_state_dict = torch.load(os.path.join(ptuning_checkpoint, "pytorch_model.bin"))
        new_prefix_state_dict = {}
        for k, v in prefix_state_dict.items():
            if k.startswith("transformer.prefix_encoder."):
                new_prefix_state_dict[k[len("transformer.prefix_encoder."):]] = v
        model.transformer.prefix_encoder.load_state_dict(new_prefix_state_dict)

        model = model.half().cuda()
        model.transformer.prefix_encoder.float().cuda()
        model.eval()
    else:
        print(f"Loading model weight...")
        model = AutoModel.from_pretrained(chatglm_model_path, trust_remote_code=True)
        model.half().cuda()
        model.eval()



@app.post("/")
async def create_item(request: Request):
    global model, tokenizer
    json_post_raw = await request.json()
    json_post = json.dumps(json_post_raw)
    json_post_list = json.loads(json_post)
    prompt = json_post_list.get('prompt')
    history = json_post_list.get('history')
    max_length = json_post_list.get('max_length')
    top_p = json_post_list.get('top_p')
    temperature = json_post_list.get('temperature')
    response, history = model.chat(tokenizer,
                                   prompt,
                                   history=history,
                                   max_length=max_length if max_length else 2048,
                                   top_p=top_p if top_p else 0.7,
                                   temperature=temperature if temperature else 0.95)
    now = datetime.datetime.now()
    time = now.strftime("%Y-%m-%d %H:%M:%S")
    answer = {
        "response": response,
        "history": history,
        "status": 200,
        "time": time
    }
    log = "[" + time + "] " + '", prompt:"' + prompt + '", response:"' + repr(response) + '"'
    print(log)
    return answer


if __name__ == '__main__':
    load_model()
    uvicorn.run(app, host='0.0.0.0', port=8000, workers=1)

我们将使用PyTorch镜像运行相应的推理服务,在启动服务之前需要安装模型依赖的相关的依赖。我们可以在server_src下准备依赖的requirements.txt,对应的requirements.txt会在推理服务启动之前被安装到环境中。

%%writefile server_src/requirements.txt

# 模型需要的依赖
transformers==4.30.2
accelerate
icetk
cpm_kernels

torch>=2.0,<2.1
gradio
mdtex2html
sentencepiece
accelerate

# 推理服务Server的依赖
fastapi
uvicorn

基于以上的推理服务程序,我们将使用PyTorch镜像和OSS上的模型在PAI创建一个推理服务,代码如下。

对于如何使用SDK创建推理服务的详细介绍,请见文档:创建推理服务

from pai.model import container_serving_spec, Model
from pai.image import retrieve, ImageScope
from pai.common.utils import random_str


# InferenceSpec用于描述如何创建推理服务
infer_spec = container_serving_spec(
    # 使用PAI提供的最新PyTorch的推理镜像
    image_uri=retrieve(
        "PyTorch",
        "latest",
        accelerator_type="GPU",
        image_scope=ImageScope.INFERENCE,
    ),
    source_dir="./server_src",
    command="python run.py",
)

m = Model(
    # 模型的OSS路径,默认模型会通过挂载的方式挂载到`/eas/workspace/model/`路径下。
    model_data=model_uri,
    inference_spec=infer_spec,
)


# 部署模型,创建推理服务.
p = m.deploy(
    service_name="chatglm_demo_{}".format(random_str(6)),
    instance_type="ecs.gn6i-c8g1.2xlarge",  # 8vCPU 31GB NVIDIA T4×1(GPU Mem 16GB)
    options={
        # 配置EAS RPC框架的超时时间, 单位为毫秒
        "metadata.rpc.keepalive": 20000,
    },
)

print(p.service_name)
print(p.service_status)

m.deploy返回一个Predictor对象,可以用于向创建的推理服务程序发送预测请求。

from pai.predictor import RawResponse

resp: RawResponse = p.raw_predict(
    {
        "prompt": "你好",
    }
)
print(resp.json()["response"])


resp = p.raw_predict(
    {
        "prompt": "晚上睡不着应该怎么办",
        "history": resp.json()["history"],
    },
    timeout=20,
)
print(resp.json())

基于以上的推理服务,我们可以使用Gradio创建一个简单的对话机器人demo。

import gradio as gr
import random
import time

with gr.Blocks() as demo:
    chatbot = gr.Chatbot()
    msg = gr.Textbox()
    clear = gr.Button("Clear")
    submit = gr.Button("Submit")

    def respond(message, chat_history):

        print(f"Message: {message}")
        print(f"ChatHistory: {chat_history}")
        resp = p.raw_predict(
            {
                "prompt": message,
                "history": chat_history,
            }
        ).json()
        print(f"Response: {resp['response']}")

        chat_history.append((message, resp["response"]))
        return "", chat_history

    submit.click(respond, [msg, chatbot], [msg, chatbot])

demo.launch(share=True)

通过以上创建的Gradio应用,我们可以在页面上与部署的ChatGLM模型进行对话。

在测试完成之后,我们可以通过以下的代码删除推理服务,释放资源。

请注意,删除在线推理服务之后,对应的Gradio的应用将无法使用。

p.delete_service()

微调ChatGLM2-6B#

我们可以使用领域数据对ChatGLM进行微调,从而使得模型在特定领域和任务下有更好的表现。ChatGLM团队提供了使用P-Tuning v2方式对模型进行微调的方案,我们将基于此方案展示如何将微调训练作业提交到PAI的训练服务执行。

准备训练数据集#

我们将使用了广告生成数据集,对ChatGLM进行微调。我们首先需要准备数据到OSS,供后续微调训练作业使用。

from pai.common.oss_utils import download, OssUriObj, upload
import zipfile

# 下载数据
data = download(
    # 当前的数据集在上海region,跨region下载,我们需要传递对应OSS Bucket所在Endpoint.
    OssUriObj(
        "oss://atp-modelzoo-sh.oss-cn-shanghai.aliyuncs.com/release/tutorials/chatGLM/AdvertiseGen_Simple.zip"
    ),
    local_path="./",
)

# 解压缩数据
with zipfile.ZipFile(data, "r") as zip_ref:
    zip_ref.extractall("./train_data/")

# 上传数据到OSS
train_data = "./train_data/AdvertiseGen_Simple/"
train_data_uri = upload(
    "./train_data/AdvertiseGen_Simple/", oss_path="chatglm_demo/data/advertisegen/"
)
print(train_data_uri)

相应的数据集数据格式如下:

!head -n 5 ./train_data/AdvertiseGen_Simple/train.json

准备微调训练作业脚本#

ChatGLM的官方提供微调训练脚本,支持使用P-Tuning v2的方式对ChatGLM模型进行微调。我们将基于相应的微调训练脚本,修改训练作业的拉起Shell脚本(train.sh),然后使用PAI Python SDK将微调训练作业提交到PAI执行。

# 下载ChatGLM代码
!git clone https://github.com/THUDM/ChatGLM2-6B.git

当训练作业提交到PAI执行时,需要按一定规范读取输入数据,以及将需要保存的模型写出到指定路径下,更加具体介绍请见文档:提交训练作业

修改后的训练作业拉起脚本如下:

%%writefile ChatGLM2-6B/ptuning/train.sh

PRE_SEQ_LEN=128
LR=2e-2
NUM_GPUS=`nvidia-smi --list-gpus | wc -l`

torchrun --standalone --nnodes=1 --nproc-per-node=$NUM_GPUS main.py \
    --do_train \
    --train_file /ml/input/data/train/train.json \
    --validation_file /ml/input/data/train/dev.json \
    --preprocessing_num_workers 10 \
    --prompt_column content \
    --response_column summary \
    --overwrite_cache \
    --model_name_or_path /ml/input/data/model \
    --output_dir /ml/output/model/ \
    --overwrite_output_dir \
    --max_source_length 64 \
    --max_target_length 128 \
    --per_device_train_batch_size 4 \
    --per_device_eval_batch_size 4 \
    --gradient_accumulation_steps 32 \
    --predict_with_generate \
    --num_train_epochs 10 \
    --save_strategy epoch \
    --learning_rate $LR \
    --pre_seq_len $PRE_SEQ_LEN \
    --quantization_bit 4

这里我们将使用PAI提供的PyTorch GPU训练镜像运行训练作业,需要安装部分第三方依赖包。用户可以通过提供requirements.txt的方式提供,相应的依赖会在训练作业执行前被安装到环境中

%%writefile ChatGLM2-6B/ptuning/requirements.txt
# 模型需要的依赖
transformers==4.30.2
accelerate
icetk
cpm_kernels

torch>=2.0,<2.1
sentencepiece
accelerate

rouge_chinese
nltk
jieba
datasets

提交训练作业#

我们将通过PAI Python SDK,将以上的训练作业提交到PAI执行。SDK在提交训练作业之后,会打印训练作业的链接,用户可以通过对应的链接查看作业的执行详情,输出日志。

Note:按当前示例教程使用的训练配置、数据集和机器规格,训练作业运行约10分钟左右。

from pai.estimator import Estimator
from pai.image import retrieve

# 使用PAI提供的最新的PyTorch推理镜像
image_uri = retrieve(
    "PyTorch",
    "latest",
    accelerator_type="GPU",
).image_uri


est = Estimator(
    command="bash train.sh",  # 启动命令
    source_dir="./ChatGLM2-6B/ptuning",  # 训练代码目录.
    image_uri=image_uri,  # 训练镜像
    instance_type="ecs.gn6e-c12g1.3xlarge",  # 使用的机器规格示例,V100(32G)
    base_job_name="chatglm2_finetune_",
)


# 提交训练作业
est.fit(
    inputs={
        "model": model_uri,
        "train": train_data_uri,
    }
)

默认estimator.fit会等待到作业执行完成。作业执行成功之后,用户可以通过est.model_data()获取输出模型在OSS上的路径地址。

print(est.model_data())

用户可以通过ossutil或是SDK提供的便利方法将模型下载到本地:

from pai.common.oss_util import download


# 使用SDK的便利方法下载模型到本地.
download(
	oss_path=est.model_data(),
	local_path="./output_model",
)

部署微调之后的模型#

微调训练之后获得的checkpoints,需要和原始的模型配合一起使用。我们需要通过以下代码获得对应的checkpoint路径.

用户通过修改微调训练的代码,使用Trainer.save_model()显式的保存相应的checkpoints,则可以直接通过estimator.model_data()下获得相应的checkpoints.

import os

# 以上的训练作业超参设置中,我们设置`epochs=2`, checkpoints保存的策略是`每一个epochs保存`。
# 默认最后一个checkpoint会被保存到`{output_dir}/checkpoint-2`路径下.
# 通过以下路径,我们可以获得模型训练获得的最后一个checkpoint的OSS路径.

checkpoint_uri = os.path.join(est.model_data(), "checkpoint-10/")
print(checkpoint_uri)

我们将复用ChatGLM2部署的推理服务程序创建推理服务。与直接部署ChatGLM2的不同点在于我们还需要提供微调之后获得的checkpoints。

通过InferenceSpec.mount API,我们可以将相应的OSS模型路径挂载到服务容器中,供推理服务程序使用。

import os
from pai.model import container_serving_spec, Model
from pai.image import retrieve, ImageScope


# InferenceSpec用于描述如何创建推理服务
infer_spec = container_serving_spec(
    image_uri=retrieve(  # 使用PAI提供的最新PyTorch的推理镜像
        "PyTorch",
        "latest",
        accelerator_type="GPU",
        image_scope=ImageScope.INFERENCE,
    ),
    source_dir="./server_src",  # 代码目录
    command="python run.py",  # 启动命令
)


# 将相应的checkpoints挂载到服务中,推理服务的程序通过检查目录(/ml/ptuning_checkpoints/)是否存在加载checkpoints
infer_spec.mount(checkpoint_uri, "/ml/ptuning_checkpoints")
print(infer_spec.to_dict())
from pai.common.utils import random_str

m = Model(
    model_data=model_uri,
    inference_spec=infer_spec,
)

# 部署模型
p = m.deploy(
    service_name="chatglm_ft_{}".format(random_str(6)),
    instance_type="ecs.gn6i-c16g1.4xlarge",  # 1 * T4
    options={
        # 配置EAS RPC框架的超时时间, 单位为毫秒
        "metadata.rpc.keepalive": 20000,
    },
)

向推理服务发送请求,测试推理服务是否正常启动。

resp = p.raw_predict(
    {
        "prompt": "你好",
    },
)
print(resp.json())

基于以上微调后模型的推理服务,我们可以使用Gradio创建一个新的机器人。

import gradio as gr
import random
import time

with gr.Blocks() as demo:
    chatbot = gr.Chatbot()
    msg = gr.Textbox()
    clear = gr.Button("Clear")
    submit = gr.Button("Submit")

    def respond(message, chat_history):

        print(f"Message: {message}")
        print(f"ChatHistory: {chat_history}")
        resp = p.raw_predict(
            {
                "prompt": message,
                "history": chat_history,
            }
        ).json()
        print(f"Response: {resp['response']}")

        chat_history.append((message, resp["response"]))
        return "", chat_history

    submit.click(respond, [msg, chatbot], [msg, chatbot])

demo.launch(share=True)

在测试完成之后,可以通过p.delete_service()删除服务,释放资源。

请注意,删除在线推理服务之后,对应的Gradio的应用将无法使用。

p.delete_service()