關(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的利用率。

根據(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í)如下圖所示

其中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_size和max_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ù)。

測試結(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)
全文完畢。