딥러닝(Deep Learning)/Etc.

[TRTIS] Triton Inference Server 사용해보기 - 2 (서버 실행, 모델 배포 맛보기)

내 이름인데 윤기를 왜 못써 2024. 4. 21. 20:41

목차

    2024.03.30 - [딥러닝(Deep Learning)/Etc.] - [TRTIS] Triton Inference Server 사용해보기 - 1 (개요 및 설치)

     

    저번 글에 이어서, 이번 글은 서버 실행과 모델 배포에 관한 내용을 간단하게 작성해보겠다.

    위의 글에서 이어지니까, 처음 이 시리즈를 방문했다면, 위의 글부터 읽고 차근차근 따라오자.

     

    서버 구동

    먼저 가동된 컨테이너 내에서 `tritonserver` 커맨드를 입력해 보자. 그럼 Server를 시작할 수 있다.

     

    그럼 위와 같이 오류가 날 것이다.

    바로 `model-repositroy`를 옵션으로 명시해 달라는 것이다.

     

    간단하게 최상위 디렉터리에 `model_repository`라는 폴더를 만들고, 인자에 해당 경로를 주고 다시 시작해 보겠다.

     

    그럼 이제 아래와 같이 문제없이 서버가 실행되는 모습을 볼 수 있다.(물론 속 빈 강정이지만..)

     

    그럼 이제 `model-repositroy`란 무엇인가에 대해서 알아보자.

     

    Model Repository

    Triton Inference Server는 다양한 프레임워크의 딥러닝 모델을 동시에 배포할 수 있는 주요한 기능이 있었다. 그런 배포할 모델들을 저장해 두는 위치가 바로 model repository가 되는 것이다.

    그럼 그냥 model repository에 아무렇게나 쌓아두면 알아서 배포가 되나 생각할 수 있지만, 그건 아니다. 아래의 레이아웃을 맞춰서 모델을 적재해 줘야지 TRTIS에서 인식해서 배포를 진행한다.

    <model-repository 경로> /
        <model_1 이름> /
        	[config.pbtxt]
            <version>/
            	<model File>
            <other version> /
            	<model File>
        <model_2 이름> /
        	[config.pbtxt]
            <version> /
            	<model File>
            <other version> /
            	<model File>

     

     

    Server를 실행할 때 인자로 넘겨준 model repository 경로 아래에는, 모델이 사용할 이름을 갖는 디렉터리가 있어야 한다.

    또 그 모델의 디렉터리 하위에는 사용할 버전의 이름을 갖는 디렉터리와 `config.pbtxt` 파일이 있어야 한다. 이제 버전의 디렉터리 하위엔 진짜 모델 파일이 들어가게 된다. 모델 파일들은 사용하는 프레임워크에 따라 확장자는 다르지만 파일의 이름은 무조건 `model`이 되어야만 한다. 아래는 프레임워크에 따른 파일이름과 확장자를 포함한 파일의 전체이름이다.

    • Python Models : `model.py`
    • TorchScript Models : `model.pt`
    • ONNX Models : `model.onnx`
    • TensorRT Models : `model.plan`
    • TensorFlow Models : `model.graphdef` or `model.savedmodel/~~`
    • OpenVINO Models : `model.xml`, `model.bin`
    • DALI Models : `model.dali`

    Model Repository에 관한 자세한 내용은 아래 문서를 참조하자.

    https://github.com/triton-inference-server/server/blob/main/docs/user_guide/model_repository.md

     

    server/docs/user_guide/model_repository.md at main · triton-inference-server/server

    The Triton Inference Server provides an optimized cloud and edge inferencing solution. - triton-inference-server/server

    github.com

     

    config.pbtxt

    `config.pbtxt`는 프레임워크에 상관없이 모델에 대한 설정을 하기 위한 파일이다. 파일 이름은 고정이고, 다양한 모델을 사용한다면 모델 디렉터리마다 해당 파일이 있어야 한다.

    이 파일 또한 작성하기 위한 방법과 옵션이 정해져 있다.

     

    먼저 파일에 들어갈 굵직한 항목들을 몇 개 나열해 보겠다.

    • `name` : 모델이 사용할 이름. 작성하지 않으면, 위에 작성한 디렉터리 이름이 모델의 이름이 된다.(명시해 주는 걸 좋아해서 작성해 두는 편)
    • `platform` or `backend` : 사용할 프레임워크 또는 백엔드 환경. backend는 자신의 모델의 프레임워크에 맞춰서 `pytorch`, `onnxruntime`, `tensorflow`, `python`, `fil`, `openvino` 등이 될 수 있다. 또한, 모델 포맷에 따라서 platform을 체크하기도 하는데 TensorFlow 처럼 `graphdef`, `savedmodel`로 나뉜다면 `<backend>_<format>` 형식으로 platform에 맞춰서 작성해줘야 한다.
    • `max_batch_size` : 모델이 수용할 최대 배치 사이즈. 배치를 지원하지 않는다면 0으로 설정하면 된다.
    • `input` : 입력 이름과 차원 및 자료형 타입
    • `output` : 출력 이름과 차원 및 자료형 타입

    이렇게 이 정도 설정만 있어도 기본적인 모델 배포는 가능하다.

    주의해야 할 점은 `max_batch_size`인데 배치를 지원하지 않는다면 꼭 0으로 설정해 주자. 기본값이 4로 되어있다..

    만약 `max_batch_size`가 0보다 커진다면, 주어진 input의 0차원을 배치차원으로 추가해서 인식한다.

    예를 들어 input의 차원을 `[1, 3]`으로 줬다면, 실제 주어지는 입력은 `[batch_dim, 1, 3]`이 되어야 한다는 것이다.

     

    이제 `input`과 `output`을 작성하는 법에 대해서 알아보자.

    주어진 모델의 입력값이나 출력값이 여러 개가 될 수 있다. 그렇기 때문에 `input`과 `output`은 리스트 형태로 주어진다.

    또한 각각의 형태는 `name`, `data_type`, `dims`를 포함하고 있어야 한다. 아래의 예시를 참고하자.

    input [
    	{
        	name: "input0"
            data_type: TYPE_FP32
            dims: [ 16 ]
        }
    ]
    output [
    	{
        	name: "output0"
            data_type: TYPE_FP32
            dims: [ 16 ]
        }
     ]

     

    작성 시에 주의해야 할 점이 2가지 있다.

    `data_type`

    아마 처음 보는 데이터 형태일 것이다. 이건 Model Config에 명시하기 위한 형태인데, 실제 데이터들에서 호환되는 형태는 아래와 같다. 자신의 프레임워크에서 입력으로 주어지는 형태를 참고해서 각각 지정해 주자.

     

    `dims`

    기본적으로 차원을 입력하는 법은 익숙할 것이라 생각한다. 그 생각 그대로 여기서도 작성하면 편하다.

    일반적으로 차원에 `-1`값을 주면 dynamic 한 값으로 생각하듯이 여기서도 그렇다.

    만약 `[-1, 10]` 으로 값을 준다면, 입력으로 `[2, 10]`, `[5, 10]`, `[9, 10]` 이 오든 상관없다는 것이다.

    응용하면, `max_batch_size`의 값을 0으로 지정해 주고, `dims`를 `[-1, n, m]` 이런 식으로 넘겨주면 똑같은 배칭설정이 가능한 것이다.

     

    이렇게 기본적인 `config.pbtxt`의 설정을 마쳤다. 사실 더 많은 설정이 있고, 가속화를 위한 내용도 있지만 차근차근 설명을 진행하면서 추가적으로 얘기할 내용이다. 궁금하다면, 아래의 문서를 참조해서 여러 개 건드려보는 것도 좋다.

    https://github.com/triton-inference-server/server/blob/main/docs/user_guide/model_configuration.md

     

    server/docs/user_guide/model_configuration.md at main · triton-inference-server/server

    The Triton Inference Server provides an optimized cloud and edge inferencing solution. - triton-inference-server/server

    github.com

     

     

    Model File

    일반적으로 대다수의 프레임워크는 모델의 출력 결과물(`.onnx`, `.pt`, `.grpahdef`..)이 있다. 이런 프레임워크들은 버전 디렉터리 하위로 해당 파일을 넣어주면 끝이다.

    하지만 전처리나 후처리 과정, 아니면 모델을 실제로 불러와서 결과를 내고 싶어 하는 사람도 있을 것이다. 그럴 땐 Python 백엔드만을 이용해서 모델을 작성할 텐데, Python 백엔드의 모델은 작성하는 템플릿이 정해져 있다. 그래서 모델의 작성법에 대해서 얘기해보려고 한다.

     

    먼저 파이썬 스크립트를 이용해서 작성한 모델 파일의 이름은 무조건 `model.py`여야 한다. 이 스크립트가 버전 디렉터리 하위에 들어가게 되는 것이다.

    이제 스크립트 작성 템플릿에 대해 알아보자.

    아래는 NVIDIA에서 제공하는 템플릿에 대한 설명이다.

    import triton_python_backend_utils as pb_utils
    
    
    class TritonPythonModel:
        """Your Python model must use the same class name. Every Python model
        that is created must have "TritonPythonModel" as the class name.
        """
    
        @staticmethod
        def auto_complete_config(auto_complete_model_config):
            """`auto_complete_config` is called only once when loading the model
            assuming the server was not started with
            `--disable-auto-complete-config`. Implementing this function is
            optional. No implementation of `auto_complete_config` will do nothing.
            This function can be used to set `max_batch_size`, `input` and `output`
            properties of the model using `set_max_batch_size`, `add_input`, and
            `add_output`. These properties will allow Triton to load the model with
            minimal model configuration in absence of a configuration file. This
            function returns the `pb_utils.ModelConfig` object with these
            properties. You can use the `as_dict` function to gain read-only access
            to the `pb_utils.ModelConfig` object. The `pb_utils.ModelConfig` object
            being returned from here will be used as the final configuration for
            the model.
    
            Note: The Python interpreter used to invoke this function will be
            destroyed upon returning from this function and as a result none of the
            objects created here will be available in the `initialize`, `execute`,
            or `finalize` functions.
    
            Parameters
            ----------
            auto_complete_model_config : pb_utils.ModelConfig
              An object containing the existing model configuration. You can build
              upon the configuration given by this object when setting the
              properties for this model.
    
            Returns
            -------
            pb_utils.ModelConfig
              An object containing the auto-completed model configuration
            """
            inputs = [{
                'name': 'INPUT0',
                'data_type': 'TYPE_FP32',
                'dims': [4],
                # this parameter will set `INPUT0 as an optional input`
                'optional': True
            }, {
                'name': 'INPUT1',
                'data_type': 'TYPE_FP32',
                'dims': [4]
            }]
            outputs = [{
                'name': 'OUTPUT0',
                'data_type': 'TYPE_FP32',
                'dims': [4]
            }, {
                'name': 'OUTPUT1',
                'data_type': 'TYPE_FP32',
                'dims': [4]
            }]
    
            # Demonstrate the usage of `as_dict`, `add_input`, `add_output`,
            # `set_max_batch_size`, and `set_dynamic_batching` functions.
            # Store the model configuration as a dictionary.
            config = auto_complete_model_config.as_dict()
            input_names = []
            output_names = []
            for input in config['input']:
                input_names.append(input['name'])
            for output in config['output']:
                output_names.append(output['name'])
    
            for input in inputs:
                # The name checking here is only for demonstrating the usage of
                # `as_dict` function. `add_input` will check for conflicts and
                # raise errors if an input with the same name already exists in
                # the configuration but has different data_type or dims property.
                if input['name'] not in input_names:
                    auto_complete_model_config.add_input(input)
            for output in outputs:
                # The name checking here is only for demonstrating the usage of
                # `as_dict` function. `add_output` will check for conflicts and
                # raise errors if an output with the same name already exists in
                # the configuration but has different data_type or dims property.
                if output['name'] not in output_names:
                    auto_complete_model_config.add_output(output)
    
            auto_complete_model_config.set_max_batch_size(0)
    
            # To enable a dynamic batcher with default settings, you can use
            # auto_complete_model_config set_dynamic_batching() function. It is
            # commented in this example because the max_batch_size is zero.
            #
            # auto_complete_model_config.set_dynamic_batching()
    
            return auto_complete_model_config
    
        def initialize(self, args):
            """`initialize` is called only once when the model is being loaded.
            Implementing `initialize` function is optional. This function allows
            the model to initialize any state associated with this model.
    
            Parameters
            ----------
            args : dict
              Both keys and values are strings. The dictionary keys and values are:
              * model_config: A JSON string containing the model configuration
              * model_instance_kind: A string containing model instance kind
              * model_instance_device_id: A string containing model instance device
                ID
              * model_repository: Model repository path
              * model_version: Model version
              * model_name: Model name
            """
            print('Initialized...')
    
        def execute(self, requests):
            """`execute` must be implemented in every Python model. `execute`
            function receives a list of pb_utils.InferenceRequest as the only
            argument. This function is called when an inference is requested
            for this model.
    
            Parameters
            ----------
            requests : list
              A list of pb_utils.InferenceRequest
    
            Returns
            -------
            list
              A list of pb_utils.InferenceResponse. The length of this list must
              be the same as `requests`
            """
    
            responses = []
    
            # Every Python backend must iterate through list of requests and create
            # an instance of pb_utils.InferenceResponse class for each of them.
            # Reusing the same pb_utils.InferenceResponse object for multiple
            # requests may result in segmentation faults. You should avoid storing
            # any of the input Tensors in the class attributes as they will be
            # overridden in subsequent inference requests. You can make a copy of
            # the underlying NumPy array and store it if it is required.
            for request in requests:
                # Perform inference on the request and append it to responses
                # list...
    
            # You must return a list of pb_utils.InferenceResponse. Length
            # of this list must match the length of `requests` list.
            return responses
    
        def finalize(self):
            """`finalize` is called only once when the model is being unloaded.
            Implementing `finalize` function is optional. This function allows
            the model to perform any necessary clean ups before exit.
            """
            print('Cleaning up...')

     

    딱 정해져 있는 게 4가지 있다.

    • Class Name = `TritonPythonModel` : 클래스 이름이 고정되어 있다.
    • `auto_complete_config` : 모델이 로딩될 때 auto_complete_config를 사용한다면, 사용되는 부분이다. 하지만 `config.pbtxt`에 명시해서 조절하는 것을 추천한다.
    • `initialize` : 모델이 처음 로드될 때 호출되는 함수. Pytorch 모델을 파이썬 스크립트에서 사용한다면, 이 부분에서 불러와서 저장해 둘 필요가 있다. 
    • `excute` : 요청이 들어오면, 실제 모델이 수행되는 함수. 모델의 실제 Inference가 이루어지는 부분이다. 
    • `finalize` : 모델이 언로드 될 때 수행되는 함수. 끝내기 전에 필요한 클린업 등 정리를 할 수 있는 부분이다.

    `initialize` 함수

    일반적으로, 모델 선언과 `input`, `output`의 데이터타입을 각각 로드하는 작업을 해둔다.

    args로 넘어오는 정보들은 아래와 같다.

    • `model_config` : model config를 담고 있는 JSON string
    • `model_instance_kind` : model instance kind(실행되고 있는 instance 종류)
    • `model_instance_device_id` : model instance의 device id
    • `model_repositroy` : Model Repository 경로
    • `model_version` : 모델 버전
    • `model_name` : 모델 이름

     `execute` 함수

    `initialze`함수에서 선언된 모델을 이용하거나, 이 모델이 불러와졌을 때 수행할 특정 로직을 입력해 두는 부분이다.

    `InferenceRequest`라고 하는 요청객체들이 인자로 넘어와져서, 해당 요청을 처리하고 같은 길이의 결과를 반환해야 한다.

     

    `finalize`함수

    클린업을 위한 부분. 실제로는 아무것도 명시하지 않아도 괜찮다.

     

    위 함수들을 보다 보면, 불러온 라이브러리인 `triton_python_backend_utils as pb_utils`들 내부에 있는 것들을 이용하는 것을 볼 수 있다. Python Backend에서 유용한 함수들이 선언되어 있는 라이브러리다.
    라이브러리 내부의 자세한 함수와 요소들은 링크를 참고하자.

     


    Python Backend 모델 작성(더하기 예제)

    이제 실제로 Python Backend로 모델을 작성해 보도록 하겠다.

    모델이 하는 기능은 간단하다.

    • `INPUT__0` : 인자 1
    • `INPUT__1` : 인자 2

    를 받아서 더한 결과물 `OUPTUT__0`을 내는 것이다.

     

    시작이니 간단하게 모델 사용법을 익힌다는 생각으로 따라오면 좋을 것 같다.

    먼저 프로젝트 폴더에 `model_repository`와 하위 모델 디렉터리 및 `config.pbtxt`를 아래와 같은 구조로 만든다.

    .
    └── model_repository
        └── add_net
            ├── 1
            └── config.pbtxt

    이제 `config.pbtxt`부터 설정을 시작해 보겠다.

    `config.pbtxt`

    우리의 환경을 정리해 보자.

    • Model Name : add_net
    • Backend : Python
    • Input : `INPUT__0(FP32)`, `INPUT__1(FP32)`
    • Output : `OUTPUT__0(FP32)`
    • max_batch_size : 0(배치 안 하도록)

    예외사항이 생기지 않도록, 데이터형은 FP32로 모두 설정해 두겠다.

     

    그럼 위의 환경에 맞춰서 `config.pbtxt`를 작성해 보겠다.

    name: "add_net"
    backend: "python"
    max_batch_size: 0
    
    input [
        {
            name: "INPUT__0"
            data_type: TYPE_FP32
            dims: [-1]
        },
        {
            name: "INPUT__1"
            data_type: TYPE_FP32
            dims: [-1]
        }
    ]
    output [
        {
            name: "OUTPUT__0"
            data_type: TYPE_FP32
            dims: [-1]
        }
    ]
    
    instance_group [
        {
            count: 1
            kind: KIND_CPU
            
            # For using gpu instance
            # kind: KIND_GPU
            # gpus: [0]
        }
    ]

     

    이렇게 `config.pbtxt`를 작성했으면, 이제 `model.py`을 작성해 보자.

     

    `model.py`

    import json
    import numpy as np
    import triton_python_backend_utils as pb_utils
    
    
    class TritonPythonModel:
        def initialize(self, args):
            self.model_config = model_config = json.loads(args["model_config"])
            output_configs = model_config["output"]
    
            self.output_names = [output_config["name"] for output_config in output_configs]
            self.output_dtypes = [
                pb_utils.triton_string_to_numpy(output_config["data_type"]) for output_config in output_configs
            ]
    
        def execute(self, requests):
            responses = []
            for request in requests:
                input_1_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT__0")
                input_2_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT__1")
                input_1_data = input_1_tensor.as_numpy()
                input_2_data = input_2_tensor.as_numpy()
    
                output_data = np.add(input_1_data, input_2_data)
                output_tensor = pb_utils.Tensor("OUTPUT__0", output_data.astype(self.output_dtypes[0]))
                response = pb_utils.InferenceResponse(output_tensors=[output_tensor])
                responses.append(response)
            return responses
    
        def finalize(self):
            pass

     

    `initialize()`

    위에 TritonModel 템플릿의 예제에 따르면, `initialize()`함수에 주어지는 args를 참고해서 보자.

    먼저 JSON String으로 작성된 model_config를 로드하자. 이때 python에서 JSON String을 읽어야 하기에, json 라이브러리가 사용된다.

    self.model_config = model_config = json.loads(args["model_config"])

     

    그다음에는 `output`의 이름과 데이터형을 얻어온다.

    output_configs = model_config["output"]
    
    self.output_names = [output_config["name"] for output_config in output_configs]
    self.output_dtypes = [
        pb_utils.triton_string_to_numpy(output_config["data_type"]) for output_config in output_configs
    ]

     

    사실 이 과정은 생략하고, 직접 작성해 줘도 괜찮다. 그걸 보여주기 위해서 입력에 대한 config는 얻어오지 않았다. 하지만 입력이나 출력이 많아진다면, 직접 입력하는 게 귀찮을 것이기에 `model_config` 에서 얻어오는 것이 현명하다.

     

    `execute()`

    모델에 요청이 들어왔을 때, 수행해야 하는 작업이 명시되어 있는 함수이다.

    요청이 들어왔을 때, 입력을 얻고 우리가 작업을 하기 위해서 데이터 변환과 실제 작업을 수행하는 과정이 포함되어 있다.

    def execute(self, requests):
        responses = []
        for request in requests:
            input_1_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT__0")
            input_2_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT__1")
            input_1_data = input_1_tensor.as_numpy()
            input_2_data = input_2_tensor.as_numpy()
    
            output_data = np.add(input_1_data, input_2_data)
            output_tensor = pb_utils.Tensor("OUTPUT__0", output_data.astype(self.output_dtypes[0]))
            response = pb_utils.InferenceResponse(output_tensors=[output_tensor])
            responses.append(response)
        return responses

    보면 requests를 받아서, 요청을 하나씩 처리해 준다.

    위의 NVIDIA 템플릿의 내용을 보면, requests는 `list [InferenceRequest]`형으로 요청이 여러 개가 한 번에 들어올 수 있기에 각각 처리해서 반환해줘야 한다.

     

    triton에서 제공하는 python backend를 위한 유용한 util들을 모아둔 `triton_python_backend_utils`라이브러리를 이용해서, 입력이나 출력의, 여러 데이터 조작을 수행한다. 관련된 내용은 위에 Model File 섹션 마지막에 언급해 둔 부분의 링크에서 확인하자.

     

    내용이 그리 어렵진 않기에, 줄마다 수행하는 작업에 대해서 짧게 설명하고 넘어가겠다.

     

    아래는 requests에서 입력의 이름을 이용해서 데이터를 받아오는 과정이다.

    input_1_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT__0")
    input_2_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT__1")

     

    입력이 Tensor형태로 주어지기에, `as_numpy()`함수를 통해 우리가 사용하는 numpy 배열로 바꿔준다.

    input_1_data = input_1_tensor.as_numpy()
    input_2_data = input_2_tensor.as_numpy()

     

    입력 두 개를 더하는 작업을 실제로 수행하는 부분이다.

    output_data = np.add(input_1_data, input_2_data)

     

    작업이 완료된 data는 다시 Tensor로 변환해서 Response로 돌려준다.

    output_tensor = pb_utils.Tensor("OUTPUT__0", output_data.astype(self.output_dtypes[0]))
    response = pb_utils.InferenceResponse(output_tensors=[output_tensor])

     

    이렇게 서버 측에서 추론을 위해서 모델을 구성하는 작업을 할 수 있다.

     

    다음 시리즈는 추론 서버를 구성했으면, 요청하는 Client에 대해서 짤막하게 소개하려 한다.

    더 나아가  다른 백엔드(ONNX, Pytorch 등..)를 이용한 모델을 다루는 법, Ensemble모델을 다루는 법 등을 추가적으로 설명 예정이다.

     

    긴 글이었지만, 이 글이 내가 겪은 어려움을 똑같이 겪고 있는 사람들에게 조금이나마 도움이 되었으면 좋겠다.

    틀린 내용이 있을 수 있습니다. 틀린 내용을 발견하셨다면, 댓글이나 레포에 남겨주세요!
    또한 궁금한 점이 있으시다면, 댓글에 남겨주시면 제 지식선에서 최대한 설명해드리겠습니다. 

    튜토리얼 코드

    https://github.com/dbsrlskfdk/triton-tutorial/tree/main/triton_server_sample

     

    triton-tutorial/triton_server_sample at main · dbsrlskfdk/triton-tutorial

    Contribute to dbsrlskfdk/triton-tutorial development by creating an account on GitHub.

    github.com

     

    반응형