공식 웹사이트
NVIDIA에서 만든 딥러닝 모델 서빙 소프트웨어이다. 요즘에 tf-serving이나 torch-serve보다 많이 쓰이고 있다고 한다...? 아마..?
공식 문서를 보면 python backend를 지원한다는 말이 나온다. 이 말은 django, flask, fastapi 등과 같은 파이썬 백엔드 프레임워크처럼 서버에 요청이 들어오면 특정 로직을 수행하는 역할도 할 수 있다는 의미이다.
다시 말해 요청이 들어오면 인풋 데이터 전처리 → 모델 inference → 아웃풋 데이터 후처리를 하나의 파이프라인으로 관리할 수 있다는 것이다. 이 기능을 NVIDIA는 앙상블(ensemble)이라고 부르는데, 앙상블 서버(?)를 구축하는 방법을 간단히 살펴보자.
nvidia gpu가 설치되어있지 않으면 에러가 발생한다!
우선 triton 서버의 기본적인 기능이 잘 동작하는지부터 확인해보자. Triton Inference Server는 여러 언어와 프레임워크를 지원하지만 나는 파이썬이 가장 익숙하므로 python_backend를 따라서 진행했다.
docker run --shm-size=1g --ulimit memlock=-1 -p 8000:8000 -p 8001:8001 -p 8002:8002 --ulimit stack=67108864 -ti nvcr.io/nvidia/tritonserver:23.04-py3
명령어가 잘 실행됐다면 컨테이너 내부에 들어와있을 것이다.
git clone https://github.com/triton-inference-server/python_backend -b r23.04
add_sub
이라는 이름의 모듈이 모델의 역할을 하도록 만든 것이다. 이 모델은 두 개의 numpy array를 더하는 간단한 작업을 수행한다. cd python_backend
mkdir -p models/add_sub/1/
cp examples/add_sub/model.py models/add_sub/1/model.py
cp examples/add_sub/config.pbtxt models/add_sub/config.pbtxt
정확히는 cp
명령어를 통해 깃헙 리포에서 examples
폴더 안에 있는 파일 하나를 복붙한 것이다.
tritonserver --model-repository `pwd`/models
클라이언트 역할을 할, 다시 말해 요청을 보내는 역할을 할 서버를 실행해야 한다. 현재 서버 역할을 하는 컨테이너와 다른 컨테이너를 하나 만들어서 진행하면 된다.
docker run -ti --net host nvcr.io/nvidia/tritonserver:23.04-py3-sdk /bin/bash
git clone https://github.com/triton-inference-server/python_backend -b r23.04
python3 python_backend/examples/add_sub/client.py
두 입력 배열의 합과 차를 구하는 결과가 출력되면 된다!
두 개 이상의 모델을 사용할 수 있기 때문에 '앙상블'이라는 용어를 사용하는 것 같지만, 무엇보다 중요한 기능은 전처리와 후처리를 수행하는 기능일 것이다.
현재 add_sub
기능을 모델처럼 사용하고 있으니 행렬의 곱셈(행렬곱이 아니라 원소별 곱셈)을 하는 mul
모듈을 추가하자.
models
폴더에 mul
폴더를 생성하고 add
모듈을 복사한다.mkdir mul
cp -R add_sub/* mul/
mul/config.pbtxt
를 수정한다.name: "mul"
backend: "python"
input [
{
name: "mul_input0"
data_type: TYPE_FP32
dims: [ 4 ]
}
]
input [
{
name: "mul_input1"
data_type: TYPE_FP32
dims: [ 4 ]
}
]
output [
{
name: "mul_output"
data_type: TYPE_FP32
dims: [ 4 ]
}
]
instance_group [{ kind: KIND_CPU }]
mul/1/model.py
코드도 수정한다.(수정한 부분만 작성)def initialize(self, args):
...
# Get OUTPUT configuration
mul_output_config = pb_utils.get_output_config_by_name(
model_config, "mul_output")
# Convert Triton types to numpy types
self.mul_output_dtype = pb_utils.triton_string_to_numpy(
mul_output_config['data_type'])
def execute(self, requests):
...
mul_output_dtype = self.mul_output_dtype
responses = []
...
for request in requests:
# Get INPUT0
in_0 = pb_utils.get_input_tensor_by_name(request, "mul_input0")
# Get INPUT1
in_1 = pb_utils.get_input_tensor_by_name(request, "mul_input1")
...
out = in_0.as_numpy() * in_1.as_numpy()
out_tensor = pb_utils.Tensor("mul_output",
out.astype(mul_output_dtype))
inference_response = pb_utils.InferenceResponse(
output_tensors=[out_tensor])
responses.append(inference_response)
add
경로의 파일들도 마찬가지로 config.pbtxt
와 모델 코드를 수정한다. config.pbtxt
는 name
부분들만 수정한다.name: "add_sub"
backend: "python"
input [
{
name: "add_sub_input0"
data_type: TYPE_FP32
dims: [ 4 ]
}
]
input [
{
name: "add_sub_input1"
data_type: TYPE_FP32
dims: [ 4 ]
}
]
output [
{
name: "added_output"
data_type: TYPE_FP32
dims: [ 4 ]
}
]
output [
{
name: "subbed_output"
data_type: TYPE_FP32
dims: [ 4 ]
}
]
instance_group [{ kind: KIND_CPU }]
def initialize(self, args):
...
# Get OUTPUT0 configuration
added_output_config = pb_utils.get_output_config_by_name(
model_config, "added_output")
# Get OUTPUT1 configuration
subbed_output_config = pb_utils.get_output_config_by_name(
model_config, "subbed_output")
# Convert Triton types to numpy types
self.added_output_dtype = pb_utils.triton_string_to_numpy(
added_output_config['data_type'])
self.subbed_output_dtype= pb_utils.triton_string_to_numpy(
subbed_output_config['data_type'])
def execute(self, requests): added_output_dtype = self.added_output_dtype
subbed_output_dtype = self.subbed_output_dtype
...
responses = []
for request in requests:
# Get INPUT0
in_0 = pb_utils.get_input_tensor_by_name(request, "add_sub_input0")
# Get INPUT1
in_1 = pb_utils.get_input_tensor_by_name(request, "add_sub_input1")
out_0, out_1 = (in_0.as_numpy() + in_1.as_numpy(),
in_0.as_numpy() - in_1.as_numpy())
out_tensor_0 = pb_utils.Tensor("added_output",
out_0.astype(added_output_dtype))
out_tensor_1 = pb_utils.Tensor("subbed_output",
out_1.astype(subbed_output_dtype))
이제 모델들을 묶어주는 모델 역할을 할 앙상블 모듈을 만들면 된다.
클라이언트에서 서버에 요청을 보내는 client.py
에서 볼 수 있듯이 Triton Inference Server에 요청을 보낼 때 어떤 모델에 inference를 요청하는지 명시해야 하는데, 이때 ensemble 모델에 요청을 보낸다고 작성하면 해당 모듈이 작동하는 원리이다.
ensemble
경로를 만들고 그 밑에 1
경로와 config.pbtxt
파일을 생성한다.mkdir ensemble
mkdir ensemble/1
cp add_sub/config.pbtxt ensemble/
config.pbtxt
를 수정한다. 이때 앙상블로 묶어주는 두 모델의 input이름과 output이름에 주의한다!name: "ensemble"
platform: "ensemble"
# max_batch_size: 100
input [
{
name: "INPUT0"
data_type: TYPE_FP32
dims: [ 4 ]
},
{
name: "INPUT1"
data_type: TYPE_FP32
dims: [ 4 ]
}
]
output [
{
name: "OUTPUT"
data_type: TYPE_FP32
dims: [ 4 ]
}
]
ensemble_scheduling {
step [
{
model_name: "add_sub"
model_version: -1
input_map {
key: "add_sub_input0"
value: "INPUT0"
}
input_map {
key: "add_sub_input1"
value: "INPUT1"
}
output_map {
key: "added_output"
value: "added_tensor"
}
output_map {
key: "subbed_output"
value: "subbed_tensor"
}
},
{
model_name: "mul"
model_version: -1
input_map {
key: "mul_input0"
value: "added_tensor"
}
input_map {
key: "mul_input1"
value: "subbed_tensor"
}
output_map {
key: "mul_output"
value: "OUTPUT"
}
}
]
}
client.py
파일을 수정해 ensemble
모듈에 요청이 가도록 한다. 모델의 output이 바뀌었으므로 해당 코드도 수정해야 한다.model_name = "ensemble"
...
with httpclient.InferenceServerClient("localhost:8000") as client:
...
outputs = [
httpclient.InferRequestedOutput("OUTPUT"),
]
...
print(f"INPUT0 ({input0_data}), INPUT1({input1_data}) = OUTPUT ({response.as_numpy('OUTPUT')})")
client.py
파일을 실행한다. 결과가 잘 나오면 된다!# 서버에서
$ tritonserver --model-repository `pwd`/models
# 클라이언트에서
$ python python_backend/examples/add_sub/client.py
INPUT0 ([...]), INPUT1([...]) = OUTPUT ([...])