AI模型部署:Triton Inference Server部署ChatGLM3-6B实践

关键词:TritonChatGLM

前言

在之前的篇章《AI模型部署:一文搞定Triton Inference Server的常用基础配置和功能特性》,《AI模型部署:Triton Inference Server模型推理核心特性和配置汇总实践》,我们已经介绍了Triton的基础功能以及推理阶段的核心配置方法,有了前文的基础,本篇使用Triton Inference Server来实践部署大语言模型ChatGLM3-6B,同样以Python作为Triton的后端。


内容摘要

本篇先将搭建基础Triton设置模块,将ChatGLM3-6B部署为服务跑通,再加入动态批处理和模型预热来提升服务的性能和效率,包括以下几个模块

  • Docker镜像环境准备
  • 模型基础配置config.pbtxt
  • 自定义Python后端model.py
  • 模型服务加载卸载管理
  • 服务请求测试
  • 加入服务端动态批处理
  • 加入模型预热

Docker镜像环境准备

拉取Docker仓库下的nvcr.io/nvidia/tritonserver:21.02-py3,以此作为基础镜像,安装torch,transformers,sentencepiece等Python依赖构建一个新的镜像,下文中统一命名为triton_chatglm3_6b:v1,基础环境构建有疑问的读者可以翻阅笔者往期的文章,在本篇中此内容略过。


模型基础配置config.pbtxt

我们先交代模型仓库下的目录结构,在Triton要求的model_repository的目录下创建chatglm3-6b文件夹,结构如下

.
├── 1
│   ├── chatglm3-6b
│   │   ├── config.json
│   │   ├── configuration_chatglm.py
│   │   ├── gitattributes
│   │   ├── modeling_chatglm.py
│   │   ├── MODEL_LICENSE
│   │   ├── pytorch_model-00001-of-00007.bin
│   │   ├── pytorch_model-00002-of-00007.bin
│   │   ├── pytorch_model-00003-of-00007.bin
│   │   ├── pytorch_model-00004-of-00007.bin
│   │   ├── pytorch_model-00005-of-00007.bin
│   │   ├── pytorch_model-00006-of-00007.bin
│   │   ├── pytorch_model-00007-of-00007.bin
│   │   ├── pytorch_model.bin.index.json
│   │   ├── quantization.py
│   │   ├── README.md
│   │   ├── tokenization_chatglm.py
│   │   ├── tokenizer_config.json
│   │   └── tokenizer.model
│   └── model.py
├── config.pbtxt
└── warmup
    └── raw_data

其中1文件夹代表模型版本号,其下面又包含模型文件和自定义后端脚本model.py,config.pbtxt为Triton的配置信息,warmup文件夹存放模型预热需要的数据文件。
首先完成config.pbtxt的设置,主要包括输入输出要素约定,数据类型约定,设置如下

name: "chatglm3-6b"
backend: "python"

max_batch_size: 0
input [
  {
    name: "prompt"
    data_type: TYPE_STRING
    dims: [ -1 ]
  },
  {
    name: "history"
    data_type: TYPE_STRING
    dims: [ -1 ]
  },
  {
    name: "temperature"
    data_type: TYPE_STRING
    dims: [ -1 ]
  },
  {
    name: "max_token"
    data_type: TYPE_INT16
    dims: [ 1 ]
  },
  {
    name: "history_len"
    data_type: TYPE_INT16
    dims: [ 1 ]
  }
]
output [
  {
    name: "response"
    data_type: TYPE_STRING
    dims: [ -1 ]
  },
  {
    name: "history"
    data_type: TYPE_STRING
    dims: [ -1 ]
  }
]
instance_group [
  { 
      count: 1
      kind: KIND_GPU
      gpus: [ 2 ]
  }
]

对该文件中的要素做简要说明

  • max_batch_size:一次请求的最大批次batch_size,本例设置为0代表实际批次由下文的input中的dims决定,而dims为一维向量,代表客户端只允许输入一条文本
  • input/output:输入和输出约定,输入端需要传入prompt,history,温度系数,history上下文长度,最大推理token数,输出端需要输出回答response,以及历史累计的history,这些都是由chatglm3的特性决定。对于prompt和history它们都是自然语言字符串,设置为TYPE_STRING,且不定长因此dims设置为-1。温度系数设置为字符串会在服务端解析为浮点数。
  • instance_group:执行实例,本例中设置了只有一个GPU:2来执行推理,且只给了该块GPU一个实例,读者可以根据自己机器的条件自行设置

自定义Python后端model.py

config.pbtxt搭建起了客户端和服务端的桥梁,下一步编辑自定义后端脚本model.py,它基于config.pbtxt中的约定抽取对应的数据进行推理逻辑的编写,model.py内容如下

import os

os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:32'
os.environ['TRANSFORMERS_CACHE'] = os.path.dirname(os.path.abspath(__file__)) + "/work/"
os.environ['HF_MODULES_CACHE'] = os.path.dirname(os.path.abspath(__file__)) + "/work/"

import json
import triton_python_backend_utils as pb_utils
import sys
import gc
import time
import logging
import torch
from transformers import AutoTokenizer, AutoModel
import numpy as np

gc.collect()
torch.cuda.empty_cache()

logging.basicConfig(format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s',
                    level=logging.INFO)


class TritonPythonModel:
    def initialize(self, args):
        device = "cuda" if args["model_instance_kind"] == "GPU" else "cpu"
        device_id = args["model_instance_device_id"]
        self.device = f"{device}:{device_id}"

        self.model_config = json.loads(args['model_config'])
        output_response_config = pb_utils.get_output_config_by_name(self.model_config, "response")
        output_history_config = pb_utils.get_output_config_by_name(self.model_config, "history")
        self.output_response_dtype = pb_utils.triton_string_to_numpy(output_response_config['data_type'])
        self.output_history_dtype = pb_utils.triton_string_to_numpy(output_history_config['data_type'])
        ChatGLM_path = os.path.dirname(os.path.abspath(__file__)) + "/chatglm3-6b"
        self.tokenizer = AutoTokenizer.from_pretrained(ChatGLM_path, trust_remote_code=True)
        model = AutoModel.from_pretrained(ChatGLM_path,
                                          torch_dtype=torch.bfloat16,
                                          trust_remote_code=True).half().to(self.device)
        self.model = model.eval()
        logging.info("model init success")

    def execute(self, requests):
        responses = []
        for request in requests:
            prompt = pb_utils.get_input_tensor_by_name(request, "prompt").as_numpy()[0].decode('utf-8')
            history_origin = pb_utils.get_input_tensor_by_name(request, "history").as_numpy()[0].decode('utf-8')
            if history_origin:
                history = eval(history_origin)
            else:
                history = []
            temperature = float(pb_utils.get_input_tensor_by_name(request, "temperature").as_numpy()[0].decode("utf-8"))
            max_token = int(pb_utils.get_input_tensor_by_name(request, "max_token").as_numpy()[0])
            history_len = int(pb_utils.get_input_tensor_by_name(request, "history_len").as_numpy()[0])

            # 日志输出传入信息
            in_log_info = {
                "in_prompt": prompt,
                "in_history": history,
                "in_temperature": temperature,
                "in_max_token": max_token,
                "in_history_len": history_len
            }
            logging.info(in_log_info)
            response, history = self.model.chat(self.tokenizer,
                                                prompt,
                                                # 由于history的结构,问和答是分开的因此*2
                                                history=history[-history_len * 2:] if history_len > 0 else [],
                                                max_length=max_token,
                                                temperature=temperature)
            # 日志输出处理后的信息
            out_log_info = {
                "out_response": response,
                "out_history": history
            }
            logging.info(out_log_info)
            response = np.char.encode(np.array([response]))
            history = np.char.encode(np.array([str(history)]))

            response_output_tensor = pb_utils.Tensor("response", response.astype(self.output_response_dtype))
            history_output_tensor = pb_utils.Tensor("history", history.astype(self.output_response_dtype))

            final_inference_response = pb_utils.InferenceResponse(
                output_tensors=[response_output_tensor, history_output_tensor])
            responses.append(final_inference_response)

        return responses

    def finalize(self):
        print('Cleaning up...')

首先在初始化initialize中通过model_instance_device_id和model_instance_device_id拿到对应的设备device,通过HuggingFace加载模型并装载到GPU上,在execute中实现推理逻辑,从请求requests中解析出对应的prompt,history,温度系数等参数,执行chatglm3模型自带的chat方法即可完成推理,最终输出为Triton指定的类型格式包装输出。
这里对history做简要说明,在chatglm3中history的格式为由字典组成的列表,每字典包含角色和内容,例如

>>> history
>>> [{'role': 'user', 'content': '你好'}, {'role': 'assistant', 'metadata': '', 'content': '你好👋!我是人工智能助手 ChatGLM3-6B,很高兴见到你,欢迎问我任何问题。'}]

而在客户端笔者的处理方式直接作为STRING类型传给Triton后端,在服务端通过Python语法eval还原出字典列表格式,再传递给chatglm3做推理。


模型服务加载卸载管理

在config.pbtxt和model.py准备完毕之后,服务端代码层面已经ok了,我们采用explicit模式来启动Triton Inference Server,这种模式下Triton不会自动启动model_repository任何模型服务,而必须是客户端通过指令明确告知需要加载或者卸载哪个服务。
启动命令如下

docker run --rm --gpus=all \
-p18999:8000 -p18998:8001 -p18997:8002 \
-v /home/model_repository/:/models \
triton_chatglm3_6b:v1 \
tritonserver \
--model-repository=/models \
--model-control-mode explicit \
--load-model chatglm3-6b

启动日志如下,READY代表chatglm3-6b推理服务已经准备就绪

Loading checkpoint shards: 100%|██████████| 7/7 [00:09<00:00,  1.43s/it]
2024-04-04 00:28:21,716 - model.py[line:41] - INFO: model init success
I0404 00:28:21.716920 1 model_repository_manager.cc:960] successfully loaded 'chatglm3-6b' version 1

I0404 00:28:21.717332 1 server.cc:538] 
+-------------+---------+--------+
| Model       | Version | Status |
+-------------+---------+--------+
| chatglm3-6b | 1       | READY  |
+-------------+---------+--------+

也可以通过HTTP请求的方式来加载或者重新加载chatglm3-6b模型,语句如下

curl -X POST http://0.0.0.0:18999/v2/repository/models/chatglm3-6b/load

注意在模型已经加载的情况下,想通过该语句reload模型,只有chatglm3-6b目录下存在变动时才会生效。
同样的可以通过HTTP请求卸载模型

curl -X POST http://0.0.0.0:18999/v2/repository/models/chatglm3-6b/unload

服务请求测试

这里我们以HTTP请求的方式来和服务进行交互,客户端使用Python的requests来发送数据请求,代码如下

import requests
import json

url = "http://0.0.0.0:18999/v2/models/chatglm3-6b/infer"


def handle(prompt: str, history: str, temperature: float = 0.3, max_token: int = 1024, history_len: int = 10):
    raw_data = {
        "inputs": [
            {
                "name": "prompt",
                "datatype": "BYTES",
                "shape": [1],
                "data": [prompt],
            },
            {
                "name": "history",
                "datatype": "BYTES",
                "shape": [1],
                "data": [history],
            },
            {
                "name": "temperature",
                "datatype": "BYTES",
                "shape": [1],
                "data": [str(temperature)],
            },
            {
                "name": "max_token",
                "datatype": "INT16",
                "shape": [1],
                "data": [max_token],
            },
            {
                "name": "history_len",  # 上下文截取的历史对话论次长度,如果为0,则历史会话不使用
                "datatype": "INT16",
                "shape": [1],
                "data": [history_len],
            }
        ],
        "outputs": [
            {
                "name": "response",
            },
            {
                "name": "history",
            }
        ]
    }
    response = requests.post(url=url,
                             data=json.dumps(raw_data, ensure_ascii=True),
                             headers={"Content_Type": "application/json"},
                             timeout=2000)
    response = json.loads(response.text)["outputs"]
    answer = response[0]["data"][0]
    history = response[1]["data"][0]

    return answer, history

在raw_data中定义了输入和输出的维度和格式,其内容必须和config.pbtxt相对应,该函数封装了HTTP请求服务端的Triton模型服务,我们通过该函数进行多轮对话

>>> answer, history = handle("请介绍一下自己", "")
>>> answer
'你好,我是一个名为 ChatGLM3-6B 的人工智能助手,是基于清华大学 KEG 实验室和智谱 AI 公司于 2023 年共同训练的语言模型开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。由于我是一个计算机程序,所以我没有自我意识,也不能像人类一样感知世界。我只能通过分析我所学到的信息来回答问题。'
>>> history
"[{'role': 'user', 'content': '请介绍一下自己'}, {'role': 'assistant', 'metadata': '', 'content': '你好,我是一个名为 ChatGLM3-6B 的人工智能助手,是基于清华大学 KEG 实验室和智谱 AI 公司于 2023 年共同训练的语言模型开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。由于我是一个计算机程序,所以我没有自我意识,也不能像人类一样感知世界。我只能通过分析我所学到的信息来回答问题。'}]"

我们将输出的history传入下一轮对话

>>> answer, history = handle("你和其他大语言模型有什么不同", history)
>>> answer
'作为一个人工智能助手,我和其他大语言模型有许多相似之处,例如我都是基于大型语言模型开发的,都拥有强大的语言理解能力和生成能力,都可以用于回答用户的问题和要求。但是,我也有一些不同的地方。例如,我是由清华大学 KEG 实验室和智谱 AI 公司共同训练的,专门用于支持中文问答,而其他大语言模型可能更多地用于支持英文问答。此外,我还可能拥有某些特定的功能或特点,例如对某些领域或话题具有更多的知识或更强的能力。'
>>> history
"[{'role': 'user', 'content': '请介绍一下自己'}, {'role': 'assistant', 'metadata': '', 'content': '你好,我是一个名为 ChatGLM3-6B 的人工智能助手,是基于清华大学 KEG 实验室和智谱 AI 公司于 2023 年共同训练的语言模型开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。由于我是一个计算机程序,所以我没有自我意识,也不能像人类一样感知世界。我只能通过分析我所学到的信息来回答问题。'}, {'role': 'user', 'content': '你和其他大语言模型有什么不同'}, {'role': 'assistant', 'metadata': '', 'content': '作为一个人工智能助手,我和其他大语言模型有许多相似之处,例如我都是基于大型语言模型开发的,都拥有强大的语言理解能力和生成能力,都可以用于回答用户的问题和要求。但是,我也有一些不同的地方。例如,我是由清华大学 KEG 实验室和智谱 AI 公司共同训练的,专门用于支持中文问答,而其他大语言模型可能更多地用于支持英文问答。此外,我还可能拥有某些特定的功能或特点,例如对某些领域或话题具有更多的知识或更强的能力。'}]"

能够观察到history的拼接结构正常,多轮对话能够正常运行。


加入服务端动态批处理

目前我们在后端逻辑中针对每一个请求的问题进行回答,我们将他改造为对一批问题进行统一批量推理回答,此时ChatGLM3自带的chat方法已经不能满足需要,需要调用transformer原生的generate,代码改造如下

import os

os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:32'
os.environ['TRANSFORMERS_CACHE'] = os.path.dirname(os.path.abspath(__file__)) + "/work/"
os.environ['HF_MODULES_CACHE'] = os.path.dirname(os.path.abspath(__file__)) + "/work/"

import json
import triton_python_backend_utils as pb_utils
from copy import deepcopy
import sys
import gc
import time
import logging
import torch
from transformers import AutoTokenizer, AutoModel
from transformers.generation.logits_process import LogitsProcessor
from transformers.generation.utils import LogitsProcessorList
import numpy as np

gc.collect()
torch.cuda.empty_cache()

logging.basicConfig(format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s',
                    level=logging.INFO)


class TritonPythonModel:
    def initialize(self, args):
        device = "cuda" if args["model_instance_kind"] == "GPU" else "cpu"
        device_id = args["model_instance_device_id"]
        self.device = f"{device}:{device_id}"

        self.model_config = json.loads(args['model_config'])
        output_response_config = pb_utils.get_output_config_by_name(self.model_config, "response")
        output_history_config = pb_utils.get_output_config_by_name(self.model_config, "history")
        self.output_response_dtype = pb_utils.triton_string_to_numpy(output_response_config['data_type'])
        self.output_history_dtype = pb_utils.triton_string_to_numpy(output_history_config['data_type'])
        ChatGLM_path = os.path.dirname(os.path.abspath(__file__)) + "/chatglm3-6b"
        self.tokenizer = AutoTokenizer.from_pretrained(ChatGLM_path, trust_remote_code=True)
        model = AutoModel.from_pretrained(ChatGLM_path,
                                          torch_dtype=torch.bfloat16,
                                          trust_remote_code=True).half().to(self.device)
        self.model = model.eval()
        logging.info("model init success")

    def build_chat_input(self, query, history=None, role="user"):
        if history is None:
            history = []
        input_ids = []
        # TODO 直接将分词编码之后的id进行拼接
        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)
            # TODO 解析出role和content
            input_ids.extend(self.tokenizer.build_single_message(item["role"], item.get("metadata", ""), content))
        # TODO 最新这一轮的提问
        input_ids.extend(self.tokenizer.build_single_message(role, "", query))
        # TODO 助手回答起个头
        input_ids.extend([self.tokenizer.get_command("<|assistant|>")])
        return input_ids

    def process_response(self, output, history):
        content = ""
        history = deepcopy(history)
        for response in output.split("<|assistant|>"):
            metadata, content = response.split("\n", maxsplit=1)
            if not metadata.strip():
                content = content.strip()
                history.append({"role": "assistant", "metadata": metadata, "content": content})
                content = content.replace("[[训练时间]]", "2023年")
            else:
                history.append({"role": "assistant", "metadata": metadata, "content": content})
                if history[0]["role"] == "system" and "tools" in history[0]:
                    content = "\n".join(content.split("\n")[1:-1])

                    def tool_call(**kwargs):
                        return kwargs

                    parameters = eval(content)
                    content = {"name": metadata.strip(), "parameters": parameters}
                else:
                    content = {"name": metadata.strip(), "content": content}
        return content, history

    def execute(self, requests):
        responses = []
        input_ids_batch, prompt_batch, history_batch = [], [], []
        temperature, max_token = 0, 0
        for request in requests:
            prompt = pb_utils.get_input_tensor_by_name(request, "prompt").as_numpy()[0][0].decode('utf-8')
            history_origin = pb_utils.get_input_tensor_by_name(request, "history").as_numpy()[0][0].decode('utf-8')
            history = eval(history_origin) if history_origin else []
            history = history[-10:]
            t_value = float(pb_utils.get_input_tensor_by_name(request, "temperature").as_numpy()[0][0].decode('utf-8'))
            m_value = float(pb_utils.get_input_tensor_by_name(request, "max_token").as_numpy()[0][0].decode('utf-8'))
            temperature = max(temperature, t_value)
            max_token = max(max_token, m_value)
            prompt_batch.append(prompt)
            history_batch.append(history)
            # TODO 构造input_ids
            input_ids = self.build_chat_input(query=prompt, history=history)
            input_ids_batch.append(input_ids)
        input_ids_batch = self.tokenizer.batch_encode_plus(input_ids_batch, return_tensors="pt",
                                                           is_split_into_words=True, padding=True, truncation=True)

        input_ids_batch = input_ids_batch.to(self.device)
        eos_token_id = [self.tokenizer.eos_token_id, self.tokenizer.get_command("<|user|>"),
                        self.tokenizer.get_command("<|observation|>")]
        logits_processor = LogitsProcessorList()
        logits_processor.append(InvalidScoreLogitsProcessor())
        gen_kwargs = {"max_length": max_token or 1024, "num_beams": 1, "do_sample": True, "top_p": 0.8,
                      "temperature": temperature or 0.3, "logits_processor": logits_processor}
        outputs = self.model.generate(**input_ids_batch, **gen_kwargs, eos_token_id=eos_token_id)
        outputs = outputs[:, input_ids_batch["input_ids"].shape[1]:-1]
        outputs = self.tokenizer.batch_decode(outputs)

        # TODO 后处理
        for p, h, o in zip(prompt_batch, history_batch, outputs):
            h.append({"role": "user", "content": p})
            one_response, one_history = self.process_response(o, h)
            one_response = np.char.encode(np.array([one_response]))
            one_history = np.char.encode(np.array([str(one_history)]))
            response_output_tensor = pb_utils.Tensor("response", one_response.astype(self.output_response_dtype))
            history_output_tensor = pb_utils.Tensor("history", one_history.astype(self.output_response_dtype))
            final_inference_response = pb_utils.InferenceResponse(
                output_tensors=[response_output_tensor, history_output_tensor])
            responses.append(final_inference_response)

        return responses

    def finalize(self):
        print('Cleaning up...')


class InvalidScoreLogitsProcessor(LogitsProcessor):
    def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:
        if torch.isnan(scores).any() or torch.isinf(scores).any():
            scores.zero_()
            scores[..., 5] = 5e4
        return scores

其中generate推理部分参考了chatglm3-6b的源码modeling_chatglm.py,稍作改造拼接即可。在config.pbtxt中只需要修改一个max_batch_size参数,比如设置为4,服务端最大聚合4条样本为一个批次

max_batch_size: 4

我们修改客户端代码,用十条句子作为prompt,让服务端推理出回答和history

import requests
import json

url = "http://0.0.0.0:18999/v2/models/chatglm3-6b/infer"


def handle(info):
    prompt = info["prompt"]
    history = info["history"]
    temperature = info.get("temperature", 0.3)
    max_token = info.get("max_token", 1024)
    raw_data = {
        "inputs": [
            {
                "name": "prompt",
                "datatype": "BYTES",
                "shape": [1, 1],
                "data": [prompt],
            },
            {
                "name": "history",
                "datatype": "BYTES",
                "shape": [1, 1],
                "data": [history],
            },
            {
                "name": "temperature",
                "datatype": "BYTES",
                "shape": [1, 1],
                "data": [str(temperature)],
            },
            {
                "name": "max_token",
                "datatype": "BYTES",
                "shape": [1, 1],
                "data": [str(max_token)],
            }
        ],
        "outputs": [
            {
                "name": "response",
            },
            {
                "name": "history",
            }
        ]
    }
    response = requests.post(url=url,
                             data=json.dumps(raw_data, ensure_ascii=True),
                             headers={"Content_Type": "application/json"},
                             timeout=2000)
    response = json.loads(response.text)["outputs"]
    answer = response[0]["data"][0]
    history = response[1]["data"][0]

    return answer, history


if __name__ == "__main__":
    from concurrent.futures import ThreadPoolExecutor
    res = []
    a = ["请介绍一下自己", "你是谁", "你几岁", "你是哪个国家的", "你姓什么", "你是男的女的", "你的学历是什么", "你能做什么", "你的英语怎么样", "你会不会数学"]
    with ThreadPoolExecutor(4) as executor:
        futures = [executor.submit(handle, {"prompt": [a[i]], "history": [""]}) for i in range(10)]
        executor.shutdown(wait=True)
        for fut in futures:
            res.extend(fut.result())
    for r in res:
        print(r)

回答内容如下

你好,我是一个名为 ChatGLM3-6B 的人工智能助手,是基于清华大学 KEG 实验室和智谱 AI 公司于 2023 年共同训练的语言模型开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。由于我是一个计算机程序,所以我没有自我意识,也不能像人类一样感知世界。我只能通过分析我所学到的信息来回答问题。
[{'role': 'user', 'content': '请介绍一下自己'}, {'role': 'assistant', 'metadata': '', 'content': '你好,我是一个名为 ChatGLM3-6B 的人工智能助手,是基于清华大学 KEG 实验室和智谱 AI 公司于 2023 年共同训练的语言模型开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。由于我是一个计算机程序,所以我没有自我意识,也不能像人类一样感知世界。我只能通过分析我所学到的信息来回答问题。'}]
我是一个名为 ChatGLM3-6B 的人工智能助手,是基于清华大学 KEG 实验室和智谱 AI 公司于 2023 年共同训练的语言模型开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。
[{'role': 'user', 'content': '你是谁'}, {'role': 'assistant', 'metadata': '', 'content': '我是一个名为 ChatGLM3-6B 的人工智能助手,是基于清华大学 KEG 实验室和智谱 AI 公司于 2023 年共同训练的语言模型开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。'}]
...

服务端打印出批量大小日志,显示10个单条请求被聚合为4,4,2的三个批次

-----------requests 4
-----------requests 4
-----------requests 2

我们以单机单卡单实例的环境为例,分别对比一下没有动态批处理,和动态批处理最大批次为4,在并发调用场景下的单条响应耗时

没有动态批处理 动态批处理batch=4
4并发100条prompt 2.58(s) 1.42(s)

加入模型预热

最后我们给模型加上预热,预热就是在模型服务启动前给一些预设的数据,来促使模型能够完全初始化,我们在warmup目录下用以下代码依次生成数据,以prompt数据为例代码如下,通过serialize_byte_tensor方法将字符转化为服务端需要的序列化格式

import numpy as np
from tritonclient.utils import serialize_byte_tensor

serialized = serialize_byte_tensor(
    np.array(["介绍一下中国".encode("utf-8")], dtype=object)
)

with open("/home/model_repository/chatglm3-6b/warmup/prompt", "wb") as fh:
    fh.write(serialized.item())

然后在config.pbtxt加入model_warmup配置,在inputs中需要加入4个预设数据,它们的key和input中的name要一一对应

model_warmup  [
  {
    name: "random_prompt"
    batch_size: 1
    inputs: [{
      key: "prompt"
      value: {
        data_type: TYPE_STRING
        dims: [ 1 ]
        input_data_file: "prompt"
          }
       },
   {
      key: "history"
      value: {
        data_type: TYPE_STRING
        dims: [ 1 ]
        input_data_file: "history"
          }
       },
   {
      key: "temperature"
      value: {
        data_type: TYPE_STRING
        dims: [ 1 ]
        input_data_file: "temperature"
          }
       },
   {
      key: "max_token"
      value: {
        data_type: TYPE_STRING
        dims: [ 1 ]
        input_data_file: "max_token"
          }
       }]
   }
]

我们启动服务,查看启动日志,在服务READY之前已经推理了一条,该条数据为warmup目录下的测试数据

...
2024-04-08 05:40:01,278 - model.py[line:121] - INFO: {'out_response': ['\n 中国,全名中华人民共和国,是位于东亚的国家,世界人口最多的国家,国土面积仅次于俄罗斯、加拿大、美利坚合众国位列世界第四。我国有着五千年的悠久历史,是四大文明古国之一,中华文明对全球历史和文化产生了深远影响。\n\n中国现实行社会主义制度,共产党是国家的执政党。经过改革开放和现代化建设,我国已经成为世界第二大经济体,综合国力不断增强。同时,我国在国际事务中发挥着越来越重要的作用,积极参与全球治理,推动构建人类命运共同体。\n\n中国有着丰富的自然和人文景观,有着独特的民族文化和传统,如中医、中庸、儒家思想、道家学说等。此外,我国的传统节日如春节、中秋节、端午节等,也具有丰富的文化内涵和独特的魅力。\n\n中国是一个多民族国家,拥有56个民族,汉族是最大的民族,占总人口的近90%。此外,还有回、藏、维吾尔、蒙古族、藏族、维吾尔族、彝族、土家族、蒙古族、汉族等55个少数民族。各个民族之间相互尊重、平等、团结,共同繁荣发展。\n\n总之,中国是一个历史悠久、文化底蕴丰厚、国土辽阔、民族 diverse的国家,在世界舞台上日益崛起,展现着强大的国家实力和民族魅力。']}
I0408 05:40:01.279463 1 model_repository_manager.cc:960] successfully loaded 'chatglm3-6b' version 1
I0408 05:40:01.279802 1 server.cc:495] 
+-------------+-----------------------------------------------------------------+------+
| Backend     | Config                                                          | Path |
+-------------+-----------------------------------------------------------------+------+
| pytorch     | /opt/tritonserver/backends/pytorch/libtriton_pytorch.so         | {}   |
| onnxruntime | /opt/tritonserver/backends/onnxruntime/libtriton_onnxruntime.so | {}   |
| tensorflow  | /opt/tritonserver/backends/tensorflow1/libtriton_tensorflow1.so | {}   |
| python      | /opt/tritonserver/backends/python/libtriton_python.so           | {}   |
+-------------+-----------------------------------------------------------------+------+

I0408 05:40:01.279886 1 server.cc:538] 
+-------------+---------+--------+
| Model       | Version | Status |
+-------------+---------+--------+
| chatglm3-6b | 1       | READY  |
+-------------+---------+--------+

全文完毕。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容