Triton Ensemble

박국현·2023년 5월 24일
0

공부

목록 보기
8/9

Triton Inference Server

공식 웹사이트
NVIDIA에서 만든 딥러닝 모델 서빙 소프트웨어이다. 요즘에 tf-serving이나 torch-serve보다 많이 쓰이고 있다고 한다...? 아마..?

공식 문서를 보면 python backend를 지원한다는 말이 나온다. 이 말은 django, flask, fastapi 등과 같은 파이썬 백엔드 프레임워크처럼 서버에 요청이 들어오면 특정 로직을 수행하는 역할도 할 수 있다는 의미이다.

다시 말해 요청이 들어오면 인풋 데이터 전처리 → 모델 inference → 아웃풋 데이터 후처리를 하나의 파이프라인으로 관리할 수 있다는 것이다. 이 기능을 NVIDIA는 앙상블(ensemble)이라고 부르는데, 앙상블 서버(?)를 구축하는 방법을 간단히 살펴보자.

nvidia gpu가 설치되어있지 않으면 에러가 발생한다!

세팅

우선 triton 서버의 기본적인 기능이 잘 동작하는지부터 확인해보자. Triton Inference Server는 여러 언어와 프레임워크를 지원하지만 나는 파이썬이 가장 익숙하므로 python_backend를 따라서 진행했다.

서버

  1. 서버 역할을 할 도커 컨테이너를 실행시킨다.
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

명령어가 잘 실행됐다면 컨테이너 내부에 들어와있을 것이다.

  1. Triton의 파이썬 백엔드 깃허브를 clone한다.
git clone https://github.com/triton-inference-server/python_backend -b r23.04
  1. 모델 역할을 할 모듈을 만든다. 아래는 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 폴더 안에 있는 파일 하나를 복붙한 것이다.

  1. Triton Server를 실행한다.
tritonserver --model-repository `pwd`/models

클라이언트

클라이언트 역할을 할, 다시 말해 요청을 보내는 역할을 할 서버를 실행해야 한다. 현재 서버 역할을 하는 컨테이너와 다른 컨테이너를 하나 만들어서 진행하면 된다.

  1. 클라이언트 컨테이너를 만든다. 이때 사용하는 tritonserver 이미지는 서버와 다르게 sdk 버전을 사용해야 한다.
docker run -ti --net host nvcr.io/nvidia/tritonserver:23.04-py3-sdk /bin/bash
  1. 서버와 같은 주소를 git clone 한다.
git clone https://github.com/triton-inference-server/python_backend -b r23.04
  1. 서버에 요청을 보내는 파이썬 파일을 실행한다.
python3 python_backend/examples/add_sub/client.py

두 입력 배열의 합과 차를 구하는 결과가 출력되면 된다!

앙상블

두 개 이상의 모델을 사용할 수 있기 때문에 '앙상블'이라는 용어를 사용하는 것 같지만, 무엇보다 중요한 기능은 전처리와 후처리를 수행하는 기능일 것이다.
현재 add_sub 기능을 모델처럼 사용하고 있으니 행렬의 곱셈(행렬곱이 아니라 원소별 곱셈)을 하는 mul 모듈을 추가하자.

두번째 모델

  1. 서버 역할 컨테이너에서 models 폴더에 mul 폴더를 생성하고 add 모듈을 복사한다.
mkdir mul
cp -R add_sub/* mul/
  1. 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 }]                  
  1. 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)                                
  1. add 경로의 파일들도 마찬가지로 config.pbtxt와 모델 코드를 수정한다. config.pbtxtname 부분들만 수정한다.
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 모델에 요청을 보낸다고 작성하면 해당 모듈이 작동하는 원리이다.

  1. ensemble 경로를 만들고 그 밑에 1 경로와 config.pbtxt 파일을 생성한다.
mkdir ensemble
mkdir ensemble/1
cp add_sub/config.pbtxt ensemble/
  1. 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"         
      }                         
    }                           
  ]                             
}                               
  1. 클라이언트 컨테이너에서 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')})")
                                                                                                     
  1. 서버를 다시 실행한 다음 client.py 파일을 실행한다. 결과가 잘 나오면 된다!
# 서버에서
$ tritonserver --model-repository `pwd`/models

# 클라이언트에서
$ python python_backend/examples/add_sub/client.py
INPUT0 ([...]),  INPUT1([...]) = OUTPUT ([...])
profile
공부하자!!

0개의 댓글