AI模型部署:Triton Inference Server模型推理核心特性和配置匯總實(shí)踐

關(guān)鍵詞:Triton

前言

在前文《AI模型部署:一文搞定Triton Inference Server的常用基礎(chǔ)配置和功能特性》中介紹了Triton Inference Server的基礎(chǔ)配置,包括輸入輸出、模型和版本管理、前后預(yù)處理等,本篇介紹在推理階段常用的配置,包括多實(shí)例并發(fā)、動(dòng)態(tài)批處理、模型預(yù)熱,這些是Triton的核心特性。本篇以Python作為Triton的后端,和其他后端的設(shè)置有特殊和不同,公開資料較少,建議收藏。


內(nèi)容摘要

  • 執(zhí)行實(shí)例設(shè)置
  • 并發(fā)請(qǐng)求測試
  • 模型預(yù)熱
  • 請(qǐng)求合并動(dòng)態(tài)批處理

執(zhí)行實(shí)例設(shè)置和并發(fā)

Triton通過config.pbtxt中的instance_group來設(shè)置模型執(zhí)行的實(shí)例,包括實(shí)例數(shù)量,CPU/GPU設(shè)備資源。如果在config.pbtxt中不指定instance_group,默認(rèn)情況下Triton會(huì)給當(dāng)前環(huán)境下所有可得的每個(gè)GPU設(shè)置一個(gè)執(zhí)行實(shí)例。
在docker run啟動(dòng)命名中指定--gpus參數(shù),將gpu設(shè)備添加到容器中,all代表將所有g(shù)pu設(shè)備都添加進(jìn)去

docker run --gpus=all \
--rm -p18999:8000 -p18998:8001 -p18997:8002 \
-v /home/model_repository/:/models \
nvcr.io/nvidia/tritonserver:21.02-py3 \
tritonserver \
--model-repository=/models

觀察Triton的啟動(dòng)日志,一共2個(gè)模型string和string_batch,在3個(gè)gpu(0,1,2)上分別分配了一個(gè)執(zhí)行實(shí)例,相當(dāng)于每個(gè)模型有3個(gè)gpu執(zhí)行實(shí)例,對(duì)應(yīng)后臺(tái)Triton會(huì)啟動(dòng)3個(gè)子進(jìn)程

...
I0328 06:42:26.406186 1 python.cc:615] TRITONBACKEND_ModelInstanceInitialize: string_batch (GPU device 0)
I0328 06:42:26.504449 1 python.cc:615] TRITONBACKEND_ModelInstanceInitialize: string (GPU device 0)
I0328 06:42:34.868080 1 python.cc:615] TRITONBACKEND_ModelInstanceInitialize: string (GPU device 1)
I0328 06:42:34.874191 1 python.cc:615] TRITONBACKEND_ModelInstanceInitialize: string_batch (GPU device 1)
I0328 06:42:40.886786 1 python.cc:615] TRITONBACKEND_ModelInstanceInitialize: string (GPU device 2)
I0328 06:42:40.887770 1 python.cc:615] TRITONBACKEND_ModelInstanceInitialize: string_batch (GPU device 2)
...

如果不添加gpu設(shè)備到容器,在docker run中刪除--gpus參數(shù),此時(shí)Triton只能使用cpu作為計(jì)算設(shè)備

docker run  \
--rm -p18999:8000 -p18998:8001 -p18997:8002 \
-v /home/model_repository/:/models \
nvcr.io/nvidia/tritonserver:21.02-py3 \
tritonserver \
--model-repository=/models

Triton啟動(dòng)日志如下,每個(gè)模型在cpu下啟動(dòng)了一個(gè)實(shí)例

I0328 06:51:17.795794 1 python.cc:615] TRITONBACKEND_ModelInstanceInitialize: string_batch (CPU device 0)
I0328 06:51:17.897220 1 python.cc:615] TRITONBACKEND_ModelInstanceInitialize: string (CPU device 0)

默認(rèn)的每個(gè)gpu分配一個(gè)執(zhí)行實(shí)例的效果等同于在config.txtpb中設(shè)置如下

  instance_group [
    {
      count: 1
      kind: KIND_GPU
      gpus: [ 0 ]
    },
    {
      count: 1
      kind: KIND_GPU
      gpus: [ 1 ]
    },
    {
      count: 1
      kind: KIND_GPU
      gpus: [ 2 ]
    }
  ]

或者

  instance_group [
    {
      count: 1
      kind: KIND_GPU
      gpus: [ 0, 1, 2 ]
    }
  ]

其中count表示每個(gè)設(shè)備下執(zhí)行實(shí)例數(shù)量,kind代表計(jì)算設(shè)備,KIND_GPU代表gpu設(shè)備,gpus指定gpu編號(hào),同樣如果設(shè)置計(jì)算設(shè)備為cpu,即使容器中有g(shù)pu設(shè)備也會(huì)按照cpu類運(yùn)行,設(shè)置cpu兩個(gè)執(zhí)行實(shí)例如下

 instance_group [
    {
      count: 2
      kind: KIND_CPU
    }
  ]

對(duì)于以Python為后端的情況,盡管在Triton服務(wù)端已經(jīng)申明了GPU設(shè)備,還是需要在model.py腳本層再顯式申明一次,將模型和數(shù)據(jù)加載到指定GPU設(shè)備上,否則Python后端會(huì)自動(dòng)將所有實(shí)例加載在GPU:0上。具體操作方法是在model.py的初始化階段通過model_instance_kind,model_instance_device_id參數(shù)拿到config.pbtxt中指定的設(shè)備,在model.py中獲取設(shè)備信息代碼樣例如下

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 = BertForSequenceClassification.from_pretrained(model_path).to(self.device).eval()


def execute(self, requests):
  encoding = self.tokenizer.batch_encode_plus(
                text,
                max_length=512,
                add_special_tokens=True,
                return_token_type_ids=False,
                padding=True,
                return_attention_mask=True,
                return_tensors='pt',
                truncation=True
            ).to(self.device)

并發(fā)請(qǐng)求測試

Triton啟動(dòng)的多個(gè)模型實(shí)例可以并行的處理請(qǐng)求,Trion會(huì)自動(dòng)分配請(qǐng)求到空閑的執(zhí)行實(shí)例,從而加快推理服務(wù)的處理速度,提高GPU的利用率。

多個(gè)實(shí)例并行執(zhí)行示意圖

根據(jù)上一節(jié)所交代的設(shè)置,分別在config.pbtxt中設(shè)置對(duì)應(yīng)的kind,gpus,count參數(shù),通過不同的設(shè)備和執(zhí)行實(shí)力數(shù)來測試Triton服務(wù)對(duì)一個(gè)Bert-Base微調(diào)的情感分類模型的推理性能,設(shè)置如下,最多三塊GPU設(shè)備,每個(gè)設(shè)備最多3個(gè)實(shí)例

實(shí)例 kind gpus count
CPU 1 instance KIND_CPU - 1
CPU 2 instance KIND_CPU - 2
CPU 3 instance KIND_CPU - 3
1 GPU * 1 instance KIND_GPU [ 2 ] 1
1 GPU * 2 instance KIND_GPU [ 2 ] 2
1 GPU * 3 instance KIND_GPU [ 2 ] 3
2 GPU * 1 instance KIND_GPU [ 1, 2 ] 1
2 GPU * 2 instance KIND_GPU [ 1, 2 ] 2
2 GPU * 3 instance KIND_GPU [ 1, 2 ] 3
3 GPU * 1 instance KIND_GPU [ 0, 1, 2 ] 1
3 GPU * 2 instance KIND_GPU [ 0, 1, 2 ] 2
3 GPU * 3 instance KIND_GPU [ 0, 1, 2 ] 3

在客戶端使用Python線程池設(shè)置20個(gè)并發(fā)500個(gè)請(qǐng)求任務(wù),每個(gè)任務(wù)的batch_size為64,即64個(gè)句子的情感推理,請(qǐng)求代碼如下

import re
import time
import json
import requests
from concurrent.futures import ThreadPoolExecutor

import torch.nn


def handle(sid):
    print("---------start:{}".format(sid))
    text = ["句子1...", "句子2...", "句子3...", "句子4..."] * 16
    url = "http://0.0.0.0:18999/v2/models/sentiment/infer"
    raw_data = {
        "inputs": [
            {
                "name": "text",
                "datatype": "BYTES",
                "shape": [64, 1],
                "data": text
            }
        ],
        "outputs": [
            {
                "name": "prob",
                "shape": [64, -1],
            }
        ]
    }
    res = requests.post(url, json.dumps(raw_data, ensure_ascii=True), headers={"Content_Type": "application/json"},
                        timeout=2000)

    print(str(sid) + ",".join(json.loads(res.text)["outputs"][0]["data"]))


if __name__ == '__main__':
    n = 500
    sid_list = list(range(n))
    t1 = time.time()
    with ThreadPoolExecutor(20) as executor:
        for i in sid_list:
            executor.submit(handle, i)
    t2 = time.time()
    print("耗時(shí):", (t2 - t1) / n)

計(jì)算平均響應(yīng)耗時(shí)如下圖所示

情感分類Bert模型的推理平均響應(yīng)時(shí)間

其中GPU設(shè)備推理性能自然遠(yuǎn)強(qiáng)于CPU,在本例中至少是50倍的差距。GPU設(shè)備數(shù)量和推理性能呈現(xiàn)出倍數(shù)的關(guān)系,部署的GPU越多,性能越強(qiáng)。在實(shí)例數(shù)量相同的時(shí)候,多GPU單實(shí)例部署比單GPU多實(shí)例部署性能更高,考慮到是單GPU負(fù)載過高導(dǎo)致性能下降,同樣的在GPU數(shù)相同的情況下,每塊GPU 3個(gè)實(shí)例性能反而還略低于2個(gè)實(shí)例。


模型預(yù)熱

一般的,服務(wù)后端需要先對(duì)模型進(jìn)行加載和初始化,在某些情況下會(huì)存在延遲初始化,直到后端接受到第一條或者少量的推理請(qǐng)求,這導(dǎo)致服務(wù)端在推理第一批的請(qǐng)求時(shí)耗時(shí)異常高。Triton在config.txtpb配置中設(shè)置了模型預(yù)熱參數(shù)model_warmup,使得在正式提供推理服務(wù)之前模型能夠完全初始化。
以上一節(jié)的自然語言情感分類為例,我們給模型的每一個(gè)實(shí)例設(shè)置預(yù)熱如下

input [ 
    {
        name: "text"
        dims: [ -1 ]
        data_type: TYPE_STRING
    }
]
output [...]
instance_group [
  {
  count: 1
  kind: KIND_GPU
  gpus: [ 0, 1 ]
  }
]
model_warmup  [
  {
    name: "random_input"
    batch_size: 1
    inputs: {
      key: "text"
      value: {
        data_type: TYPE_STRING
        dims: [ 1 ]
        input_data_file: "raw_data"
          }
       }
   }
]

預(yù)熱的本質(zhì)是提前給到一組數(shù)據(jù),讓模型在加載初始化之后對(duì)這組數(shù)據(jù)進(jìn)行推理,從而完成完整的模型初始化步驟,在model_warmup的inputs指定了預(yù)設(shè)數(shù)據(jù)的信息,其中key要和input的name對(duì)應(yīng),data_type和input的data_type對(duì)應(yīng),dims必須是確定的維度,不能為-1,input_data_file約定了預(yù)設(shè)的數(shù)據(jù)在一個(gè)路徑文件中,Triton會(huì)去容器中的/models/model_name/warmup/input_data_file下拿到這個(gè)文件的數(shù)據(jù),映射到宿主機(jī)上該位置在和config.pbtxt同一級(jí)目錄下,input_data_file的位置如下

.
├── 1
│   ├── model.py
│   ├── sentiment
│   │   ├── config.json
│   │   ├── pytorch_model.bin
│   │   ├── special_tokens_map.json
│   │   ├── tokenizer_config.json
│   │   └── vocab.txt
├── config.pbtxt
└── warmup
    └── raw_data

input_data_file內(nèi)容為自定義構(gòu)造的預(yù)設(shè)數(shù)據(jù),對(duì)于字符串輸入使用tritonclient客戶端進(jìn)行構(gòu)造,將字符串轉(zhuǎn)化為輸入需要的字節(jié)形式,例如將“我愛你美麗的中國”改造為預(yù)設(shè)數(shù)據(jù)輸入

# pip install tritonclient
import numpy as np
from tritonclient.utils import serialize_byte_tensor

serialized = serialize_byte_tensor(
    np.array(["我愛你美麗的中國".encode("utf-8")], dtype=object)
)
with open("raw_data", "wb") as fh:
    fh.write(serialized.item())

config.pbtxt和預(yù)設(shè)的樣例數(shù)據(jù)文件./warmup/raw_data設(shè)置完畢后,啟動(dòng)Triton服務(wù),日志如下

2024-04-02 06:13:53,199 - model.py[line:107] - INFO: {'text': ['我愛你美麗的中國']}
2024-04-02 06:13:54,310 - model.py[line:129] - INFO: {'prob': array([b'\xe6\xad\xa3\xe5\x90\x91'], dtype='|S6')}
2024-04-02 06:13:54,312 - model.py[line:107] - INFO: {'text': ['我愛你美麗的中國']}
2024-04-02 06:13:55,319 - model.py[line:129] - INFO: {'prob': array([b'\xe6\xad\xa3\xe5\x90\x91'], dtype='|S6')}

日志顯示在每個(gè)執(zhí)行實(shí)例上都將樣例數(shù)據(jù)輸送給模型推理了一次,同樣的我們測試在加入模型預(yù)熱之后模型的推理性能,和上一節(jié)的測試結(jié)果做對(duì)比

沒有預(yù)熱 加上預(yù)熱 響應(yīng)時(shí)間降低
1 GPU×1 instance 0.0658 0.0651 -0.7ms
2 GPU×1 instance 0.0343 0.0321 -2.2ms
3 GPU×1 instance 0.0234 0.0218 -1.6ms

在本例中添加預(yù)熱的模式平均響應(yīng)耗時(shí)比不添加預(yù)熱下降了約1~2毫秒,降低了模型初次推理導(dǎo)致的性能損失。
本例以input_data_file引入外部數(shù)據(jù)文件的形式構(gòu)造預(yù)設(shè)數(shù)據(jù),除此之外更簡單的一種是在model_warmup直接指定random_data,按照指定的維度和數(shù)據(jù)類型生成一組隨機(jī)數(shù),model_warmup設(shè)置如下

model_warmup  [
  {
    name: "random_input"
    batch_size: 1
    inputs: {
      key: "x"
      value: {
        data_type: TYPE_FP32
        dims: [ 3 ]
        random_data: true
          }
       }
   }
]

該例子表示隨機(jī)生成一個(gè)[1, 3]的浮點(diǎn)數(shù)向量,輸入給模型進(jìn)行預(yù)熱。


請(qǐng)求合并動(dòng)態(tài)批處理

批量推理可以提高推理吞吐量和GPU的利用率,動(dòng)態(tài)批處理指的是Triton服務(wù)端會(huì)組合推理請(qǐng)求,從而動(dòng)態(tài)創(chuàng)建批處理來高吞吐量,這塊內(nèi)容由Triton的調(diào)度策略決定。
默認(rèn)的調(diào)度策略Default Scheduler不會(huì)主動(dòng)合并請(qǐng)求,而是僅將所有請(qǐng)求分發(fā)到各個(gè)執(zhí)行實(shí)例上,動(dòng)態(tài)批處理策略Dynamic Batcher它可以在服務(wù)端將多個(gè)batch_size較小的請(qǐng)求組合在一起形成一個(gè)batch_size較大的任務(wù),從而提高吞吐量和GPU利用率,Dynamic Batcher在config.pbtxt中進(jìn)行指定,一個(gè)例子如下

max_batch_size: 16
dynamic_batching {
    preferred_batch_size: [ 4, 8 ]
    max_queue_delay_microseconds: 100
}
  • preferred_batch_size:期望達(dá)到的batch_size,可以指定一個(gè)數(shù)值,也可以是一個(gè)包含多個(gè)值的數(shù)組,本例代表期望組合成大小為4或者8的batch_size,盡可能的將batch_size組為指定的值,batch_size不能超過max_batch_size
  • max_queue_delay_microseconds:組合batch的最大時(shí)間限制,單位為微秒,本例代表組合batch最長時(shí)間為100微秒,超過這個(gè)時(shí)間則停止組合batch,會(huì)把已經(jīng)打進(jìn)batch的請(qǐng)求進(jìn)行推理。這個(gè)時(shí)間限制越大,延遲越大,但是越容易組合到大的batch_size,這個(gè)時(shí)間限制越小。延遲越小,但是吞吐量降低,因此該參數(shù)是一個(gè)延遲和吞吐之間的trade off

配置文件中的dynamic_batching只是將請(qǐng)求進(jìn)行聚合,在model.py的自定義后端中實(shí)際的作用是requests變成了多個(gè)request組成的集合,而不開啟dynamic_batching則requests里面只有一個(gè)request,這一點(diǎn)在execute方法的注釋中有說明 **“Depending on the batching configuration (e.g. Dynamic Batching) used, requests may contain multiple requests” **

    def execute(self, requests):
        """Depending on the batching configuration (e.g. Dynamic
        Batching) used, `requests` may contain multiple requests. 
        """
        for request in requests:
            pass

在以Python作為后端的情況下,光是在配置中設(shè)置dynamic_batching是不夠的,它僅能夠聚合請(qǐng)求,而要實(shí)現(xiàn)真正的模型批量推理,需要對(duì)model.py進(jìn)行改造,簡單而言就是將requests下多個(gè)request的請(qǐng)求數(shù)據(jù)沿著第一個(gè)維度拼接起來形成一個(gè)更大的batch,對(duì)完成的batch做一次推理,推理完成后再根據(jù)沒有request自身的batch大小,按照順序拆分成每個(gè)response,注意返回的response數(shù)量和request數(shù)量必須相等,我們對(duì)情感分類的后端model.py進(jìn)行改造如下

    def execute(self, requests):
        responses = []
        # TODO 記錄下每個(gè)請(qǐng)求的數(shù)據(jù)和數(shù)據(jù)batch大小
        batch_text, batch_len = [], []
        for request in requests:
            text = pb_utils.get_input_tensor_by_name(request, "text").as_numpy()
            text = np.char.decode(text, "utf-8").squeeze(1).tolist()
            batch_text.extend(text)
            batch_len.append(len(text))
        # 日志輸出傳入信息
        in_log_info = {
            "text": batch_text,
        }
        logging.info(in_log_info)

        encoding = self.tokenizer.batch_encode_plus(
            batch_text,
            max_length=512,
            add_special_tokens=True,
            return_token_type_ids=False,
            padding=True,
            return_attention_mask=True,
            return_tensors='pt',
            truncation=True
        ).to(self.device)
        with torch.no_grad():
            outputs = self.model(**encoding)
            prob = torch.nn.functional.softmax(outputs.logits, dim=1).argmax(dim=1).detach().cpu().numpy().tolist()
            prob = np.array([self.label_map[x].encode("utf8") for x in prob])

            # 日志輸出處理后的信息
            out_log_info = {
                "prob": prob
            }
            logging.info(out_log_info)

        # TODO 響應(yīng)數(shù)要和請(qǐng)求數(shù)一致
        start = 0
        for i in range(len(requests)):
            end = start + batch_len[i]
            out_tensor = pb_utils.Tensor("prob", np.array(prob[start:end]).astype(self.output_response_dtype))
            start += batch_len[i]
            final_inference_response = pb_utils.InferenceResponse(output_tensors=[out_tensor])
            responses.append(final_inference_response)
            
        return responses

簡單而言該段代碼將for循環(huán)中所有request數(shù)據(jù)進(jìn)行拼接,最終只推理一次,返回時(shí)又根據(jù)各request大小做拆分。
接下來我們重新設(shè)置config.pbtxt,設(shè)置max_queue_delay_microseconds為200000,即0.2秒,preferred_batch_size暫不設(shè)置,另外還設(shè)置了最大批次大小max_batch_size為10,根據(jù)前文所述,批量聚合應(yīng)該最大不超過max_batch_size

max_batch_size: 10
dynamic_batching {
    max_queue_delay_microseconds: 200000
}

額外的我們?cè)趍odel.py中打印出requests的數(shù)量,觀察動(dòng)態(tài)批處理是否生效,以及是否在requests上生效

def execute(self, requests):
        print("---------本次獲取的請(qǐng)求數(shù)", len(requests))
        responses = []
        # TODO 記錄下每個(gè)請(qǐng)求的數(shù)據(jù)和數(shù)據(jù)batch大小
        batch_text, batch_len = [], []
        for request in requests:

我們只使用一個(gè)實(shí)例啟動(dòng)服務(wù),然后在客戶端我們對(duì)每個(gè)請(qǐng)求只發(fā)送一條句子也就是batch_size=1,采用20個(gè)并發(fā)請(qǐng)求該服務(wù),一共請(qǐng)求100個(gè)句子

samples = []
with open("./ChnSentiCorp.txt", encoding="utf8") as f:
    for line in f.readlines():
        samples.append(",".join(line.strip().split(",")[1:]))

samples = samples[:100]

def handle(sid):
    print("---------start:{}".format(sid))
    text = [samples[sid]]
    url = "http://0.0.0.0:18999/v2/models/sentiment/infer"
    raw_data = {
        "inputs": [
            {
                "name": "text",
                "datatype": "BYTES",
                "shape": [1, 1],
                "data": text
            }
        ],
        "outputs": [
            {
                "name": "prob",
                "shape": [1, -1],
            }
        ]
    }
    res = requests.post(url, json.dumps(raw_data, ensure_ascii=True), headers={"Content_Type": "application/json"},
                        timeout=2000)

    print("{},{},{}".format(str(sid), text, str(json.loads(res.text)["outputs"])))


if __name__ == '__main__':
    n = 100
    sid_list = list(range(n))
    t1 = time.time()
    with ThreadPoolExecutor(20) as executor:
        for i in sid_list:
            executor.submit(handle, i)
    t2 = time.time()
    print("耗時(shí):", (t2 - t1) / n)

運(yùn)行客戶端,我們觀察服務(wù)端日志,除第一次請(qǐng)求外,每次請(qǐng)求長度都是10,和max_batch_size一致,第一次請(qǐng)求長度1原因是模型預(yù)熱導(dǎo)致。

---------本次獲取的請(qǐng)求數(shù) 1
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10

我們看原生的返回日志,發(fā)現(xiàn)推理的結(jié)果也是10個(gè)一批,說明不僅請(qǐng)求層面,模型推理層面也是實(shí)現(xiàn)了10個(gè)一次批處理

2024-04-03 02:59:42,001 - model.py[line:133] - INFO: {'prob': array([b'\xe6\xad\xa3\xe5\x90\x91', b'\xe6\xad\xa3\xe5\x90\x91',
       b'\xe6\xad\xa3\xe5\x90\x91', b'\xe6\xad\xa3\xe5\x90\x91',
       b'\xe8\xb4\x9f\xe9\x9d\xa2', b'\xe6\xad\xa3\xe5\x90\x91',
       b'\xe6\xad\xa3\xe5\x90\x91', b'\xe6\xad\xa3\xe5\x90\x91',
       b'\xe6\xad\xa3\xe5\x90\x91', b'\xe6\xad\xa3\xe5\x90\x91'],
      dtype='|S6')}

再觀察推理的結(jié)果,雖然在后端將部分請(qǐng)求合并和推理,但是返回層由于加入了拆分邏輯,最后還是一個(gè)請(qǐng)求對(duì)應(yīng)一條返回結(jié)果,推理的結(jié)果和不使用dynamic_batching的結(jié)果是完全一致的,筆者已經(jīng)做過比對(duì)。

...
93,['"入住的是度假區(qū)的豪華海景房,前臺(tái)給了5樓(最高6樓),然后差不多100%的海景,雖然是掛牌5星的,但是本人覺得是4星的標(biāo)準(zhǔn),和我后來入住的5星喜來登差了蠻多的,不過整體來說還是符合他家的價(jià)錢的."'],[{'name': 'prob', 'datatype': 'BYTES', 'shape': [1], 'data': ['正向']}]
97,['酒店的基本設(shè)施一般,但服務(wù)態(tài)度確實(shí)很不錯(cuò),房間8樓以下就是新裝修的,8樓的房間就比較成舊,洗澡有單獨(dú)的整體浴室,水比較大空調(diào)的風(fēng)也很足,這個(gè)賓館好像是屬于海軍的南海艦隊(duì),地理位置也很好,靠近省委火車站,離黃興步行街也就三站地,值得入住'],[{'name': 'prob', 'datatype': 'BYTES', 'shape': [1], 'data': ['正向']}]
90,['"其他都可以,盡管不夠5星標(biāo)準(zhǔn),但還是很干凈寬敞,最不能忍受的在度假區(qū)吃的自助海鮮晚餐,整個(gè)被蒼蠅包圍了,眼看著食物上落滿了蒼蠅,花了不少錢,落了一肚火....過道的海灘很美,很靜,"'],[{'name': 'prob', 'datatype': 'BYTES', 'shape': [1], 'data': ['負(fù)面']}]
...

在初步跑通了Python后端的dynamic_batching之后,我們調(diào)整部分參數(shù)的設(shè)置,看看會(huì)有什么變化,我們先將請(qǐng)求數(shù)從100調(diào)整為102,這樣注定有2個(gè)請(qǐng)求無法合并為最大批次10,我們看看Triton會(huì)如何處理,服務(wù)端日志如下

---------本次獲取的請(qǐng)求數(shù) 1
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 10
---------本次獲取的請(qǐng)求數(shù) 2

可見最后一批次只合并了2個(gè)請(qǐng)求,而此時(shí)max_queue_delay_microseconds生效,超過了0.2秒也是會(huì)打包這個(gè)批次送給模型推理。我們調(diào)整max_queue_delay_microseconds,使其變?yōu)?0秒,重啟服務(wù)

dynamic_batching {
    max_queue_delay_microseconds: 20000000
}

以同樣的并發(fā)請(qǐng)求102條數(shù)據(jù)到服務(wù)端,日志如下

2024-04-03 03:14:38,322 - model.py[line:133] - INFO: {'prob': array([b'\xe6\xad\xa3\xe5\x90\x91', b'\xe8\xb4\x9f\xe9\x9d\xa2',
       b'\xe6\xad\xa3\xe5\x90\x91', b'\xe6\xad\xa3\xe5\x90\x91',
       b'\xe6\xad\xa3\xe5\x90\x91', b'\xe6\xad\xa3\xe5\x90\x91',
       b'\xe6\xad\xa3\xe5\x90\x91', b'\xe6\xad\xa3\xe5\x90\x91',
       b'\xe6\xad\xa3\xe5\x90\x91', b'\xe6\xad\xa3\xe5\x90\x91'],
      dtype='|S6')}
2024-04-03 03:14:58,255 - model.py[line:133] - INFO: {'prob': array([b'\xe8\xb4\x9f\xe9\x9d\xa2', b'\xe8\xb4\x9f\xe9\x9d\xa2'],
      dtype='|S6')}

服務(wù)端推理在最后一個(gè)批次卡住,卡住20秒,最后批次在2024-04-03 03:14:58推理了兩條樣本,而上一個(gè)批次在2024-04-03 03:14:38推理了10條樣本,中間正好間隔20秒,可見此時(shí)max_queue_delay_microseconds的超時(shí)生效,那為什么之前沒有感覺到明顯的延遲?因?yàn)橹癿ax_queue_delay_microseconds很小只有0.2秒。max_queue_delay_microseconds越大越有可能拼接到大的batch_size,而帶來的后果是高延遲。
我們?cè)賹?duì)請(qǐng)求批次大小進(jìn)行測試,剛才都是以batch_size=1進(jìn)行請(qǐng)求,如果每次請(qǐng)求的批次量不定,比如為2,3,4..,dynamic_batching能否正常工作?修改客戶端代碼如下

def handle(sid):
    print("---------start:{}".format(sid))
    import random
    rand = random.randint(1, 6)
    text = [samples[sid:sid + rand]]
    url = "http://0.0.0.0:18999/v2/models/sentiment/infer"
    raw_data = {
        "inputs": [
            {
                "name": "text",
                "datatype": "BYTES",
                "shape": [rand, 1],
                "data": text
            }
        ],
        "outputs": [
            {
                "name": "prob",
                "shape": [rand, -1],
            }
        ]
    }
    res = requests.post(url, json.dumps(raw_data, ensure_ascii=True), headers={"Content_Type": "application/json"},
                        timeout=2000)

每次請(qǐng)求都隨機(jī)從數(shù)據(jù)集中挑選1~6條數(shù)據(jù)作為一個(gè)批次請(qǐng)求,服務(wù)端日志如下

---------本次獲取的請(qǐng)求數(shù) 1
---------本次獲取的請(qǐng)求數(shù) 3
---------本次獲取的請(qǐng)求數(shù) 2
---------本次獲取的請(qǐng)求數(shù) 4
...
---------本次獲取的請(qǐng)求數(shù) 2
---------本次獲取的請(qǐng)求數(shù) 2

由于請(qǐng)求的批次不確定,因此每次合并的request也不定,有些requests合并4個(gè)才夠10,而有些只合并2個(gè)就達(dá)到10了。
在官方文檔中不推薦設(shè)置preferred_batch_size,大部分模型設(shè)置preferred_batch_size沒有意義,除非模型對(duì)某個(gè)指定批次下有異于其他批次大小的突出性能能力,一般而言只需要設(shè)置max_batch_sizemax_queue_delay_microseconds即可。
接下來我們測試使用動(dòng)態(tài)批處理和不是用動(dòng)態(tài)批處理的平均響應(yīng)時(shí)間,我們控制推理設(shè)備,實(shí)例等其他外部條件不變,具體是使用1塊GPU,1個(gè)實(shí)例,并發(fā)64,服務(wù)端最大推理批次64,服務(wù)端組合批次最大等待時(shí)間0.02秒,并發(fā)請(qǐng)求500次,在此條件下分別測試加入動(dòng)態(tài)批處理和不加入動(dòng)態(tài)批處理的性能,分別的我們調(diào)整客戶端每個(gè)請(qǐng)求自身所帶的batch_size大小,分別測試攜帶1,2,4,8,16條數(shù)據(jù)。

動(dòng)態(tài)批處理對(duì)推理效率的影響

測試結(jié)論如上圖所示,得到以下2點(diǎn)結(jié)論

  • 1.開啟動(dòng)態(tài)批處理單條數(shù)據(jù)的響應(yīng)時(shí)間低于不開啟,推理效率明顯更高,但是隨著單個(gè)請(qǐng)求自身的batch_size增大,這個(gè)差距越來越小,就是說如果客戶端逐漸進(jìn)行批量發(fā)送,服務(wù)端動(dòng)態(tài)批處理的效果越來越不明顯
  • 2.如果服務(wù)端開啟了動(dòng)態(tài)批處理,客戶端已經(jīng)沒有必要刻意的批量發(fā)送數(shù)據(jù)了,從圖上看黃線在客戶端批次是1,2,4,8,16的時(shí)候,響應(yīng)時(shí)間并沒有明顯的波動(dòng)

全文完畢。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容