첫번째 글에서는 ML 모델 서빙프레임워크인 Trtiton inference server에 대한 개념을 알아보았고,
2024.07.19-Triton Inference Server 모델서빙1 - NVIDA Triton(트리톤)이란?
두번째 글에서는 직접 Trtiton inference server를 실행해보고 inference response 받는 것까지 테스트해보았다.
2024.08.03-Triton Inference Server 모델서빙2 - 직접 우리 모델을 서빙해보자!
이번글에서는 실제로 우리 서비스에 Trtiton inference server를 적용하기 위해 겪은 시행착오의 내용을 기록해보려고 한다.
앞서 잠시 다시 처음으로 되돌아가서, 우리가 ML 모델 서빙 프레임워크를 적용하려고 했던 목적은 FastAPI를 전부 걷어 내는 것이였다. 단순한 파이썬 웹 프레임워크인 FastAPI로 모델을 서빙하면 여러가지 플랫폼(onnx,torch 등)에 따라서 추론코드와 서빙코드를 각각 다르게 구성해야했고, 특별히 inference 관련해서 지원되는 기능도 없었기 때문이다.
그러나 결론부터 말하자면 실제로 FastAPI를 완전히 걷어내진 못했다.
오로지 Trtiton inference server만 사용해서 우리 모델을 서빙하기에는 비즈니스 로직상 약간의 문제가 있어서 결국엔 기존 FastAPI 서버에 트리톤 서버를 하나 더 추가하는 격이 되었다. 아쉽게도 테스트해 본 성능은 좋지 않았고, 실제 서비스 적용은 머나먼 미래로 미뤄졌다.
그래서 이번글에서는 우리 서비스에 Ttiton inference server를 적용하기까지 전반적인 과정을 남겨두려고 한다. 그리고 만약 트리톤 서버를 적용해서 기존 FastAPI 서버와 함께 서비스할 경우 성능이 어떨지까지 테스트해본 내용까지 공유하려고 한다. 아마 다음글은 성능을 올리기 위해 여러가지 튜닝을 하면서 테스트해본 내용이 되지 않을까 싶다..(끝이 없는 테스트..)
DockerFile 수정
앞서 trition 테스트를 진행 했을때는 단순히 트리톤을 실행해보는게 목적이였기에 일단 컨테이너를 띄웠다. 하지만 이번에는 기존의 우리 서비스에다가 triton을 적용해보는게 목적이기에 아래와 같이 현재 서비스를 실행시키는 dockerfile을 수정하였다.
먼저 우리 서비스를 배포하기 위해서 amazon/aws-cli 라는 도커이미지를 사용하고 있었다. 기본적으로 멀티 스테이지 도커빌드를 사용하는데, 요 컨테이너는 builder 스테이지로 지정했다. 어플리케이션이 아니라 실파일이나 빌드에 필요한 도구들만 세팅한 것이다. (빈깡통?)
사실상 어플리케이션은 트리톤 서버이다. runner 스테이지로 지정하고, 엔비디아에서 제공해주는 trtionserver 이미지를 추가했다.
FROM amazon/aws-cli:2.15.30 as builder
FROM nvcr.io/nvidia/tritonserver:24.06-py3 as runner
또한 후술하겠지만 내부적으로 트리톤 서버에 접속하기 위한 목적으로 트리톤 클라이언트 패키지도 추가하였다.
pip3 install --no-cache-dir tritonclient[all]==2.47.0
그리고 triton으로 모델을 Load 하기 위한 config 파일과 모델파일은 로컬경로를 마운트하도록 수정했다. 또한 트리톤서버와 포트포워딩을 위해 8000, 8001, 8002번 포트도 열어둔다. 참고로 8000번은 http 클라이언트, 8001번 gprc 클라이언트가 접속하는 endpoint이며 8002번은 metric 세부정보를 확인할수 있는 endpoint가 된다.
ARG MODEL_PATH="$RESOURCES_PATH/models"
ARG RESOURCES_PATH="$APP_PATH/resources"
COPY resources $RESOURCES_PATH
COPY --from=builder $RESOURCES_CACHE/models $RESOURCES_PATH/models
ENV MODEL_PATH="$RESOURCES_PATH/models"
EXPOSE 8000
EXPOSE 8001
EXPOSE 8002
마지막으로 트리톤 컨테이너에 접속해서 trtion 서버를 실행시키는 명령어는 별도의 쉘 스크립트 (run_trition.sh)를 만들었다.
#!/usr/bin/env bash
tritonserver --model-repository=$MODEL_PATH --cache-config=local,size=104857600 --log-verbose=1 --http-thread-count=2
그래서 우리 서비스가 실행될때 해당 스크립트도 실행되도록 RUN 명령어를 통해 dockerfile을 수정하였다.
COPY docker/run_triton.sh /opt/run_triton.sh
RUN chmod +x /opt/run_triton.sh
triton으로 load 할 model 과 config 파일
기존에 테스트해봣던 ensemble 모델과 다르게 전처리 및 후처리의 작업은 서비스단에서 진행하도록 하고, 실질적인 recongizer와 detector 부분만 트리톤서버를 통해 inference를 수행하려고 했다.
그래서 예를 들어 detector모델의 경우 아래와 같은 config파일을 구성했다. config파일을 구성할 때 backend로 onnx 뿐만 아니라 pytorch나 tensorflow 처럼 여러가지 모델포맷에 맞춰서 수정할수 있다는것 또한 트리톤의 장점이다.
name: "detector"
backend: "onnxruntime"
max_batch_size: 3
input [
{
name: "input"
data_type: TYPE_FP32
dims: [ 3,640,640 ]
}
]
output [
{
name: "output"
data_type: TYPE_FP32
dims: [ 640,640 ]
}
]
다음글에서 관련해서 후술하겠지만, config 파일을 작성할때 max_batch_size를 지정해야한다.
그렇지 않으면 아래와 같이 모델을 load할 경우에 에러가 발생하며, 오류 메세지에 따라 max_batch_size 대신 전체 Inpu,output 의 shape에 -1 (어떤값이던 가능한 dynamic shape)을 추가하면 해결할수 있긴 하다.
triton의 강력한 기능 중 하나인 dynamic batch를 활성화 하는 옵션으로, 동시에 처리할수 있는 input의 최대 크기를 어떻게 조절하느냐에 따라서 성능이 결정될 수 있다.
그리고 모델파일 또한 직접 지정한 경로에 모델을 만들어서 저장해두도록 했다. dockerfile에서 해당 경로를 마운트해서 triton으로 load할 모델과 config파일에 접근 하는 것이다.
Inference 코드수정
위의 dockerFile과 트리톤을 위한 configvk 바탕으로 컨테이너를 실행하게 되면, 이제 내부적으로 트리톤 서버도 띄워져있다.
그리고 8001번 포트를 통해 gprc 클라이언트로 접근할수 있게 된다.
그래서 서비스를 실행할때 트리톤 client를 통해 접속할수 있도록 초기화를 해두었다.
# main.py
triton_url: str = os.environ.get("APP_TRITON_URL", "0.0.0.0:8001")
ocr_service.init_triton_client(config.triton_url)
def init_triton_client(triton_url: str):
global triton_client
triton_client = InferenceServerClient(url=triton_url)
logger.info("Completed loading the triton client")
그리고 본격적으로 실제 inference 하는 부분을 아래처럼 수정했다.
import tritonclient.grpc.aio as grpcclient
from tritonclient.grpc import InferenceServerClient
inputs = [grpcclient.InferInput("input", prep_image.shape, "FP32")]
prep_image_np = prep_image.numpy().astype(np.float32)
inputs[0].set_data_from_numpy(prep_image_np)
results = triton_client.infer(
model_name="detector",
inputs=inputs,
outputs=[
grpcclient.InferRequestedOutput("output")
],
)
# predict_array = await ocr_service.detect_texts(prep_image)
predict_array = results.as_numpy("output")
logger.debug(f"predict_array.shape = {predict_array.shape}")
이때 triton client는 gRPC 를 사용했다. 이 글에 따르면 gRPC는 HTTP2 기반으로 데이터형이 JSON이 아니라 이진기반의 protonuf 형식이라 일반적인 HTTP, Rest API 보다 빠르고 성능이 좋다고 한다. 그래서 단순히 http 클라이언트보다 대용량 데이터나 실시간 데이터를 전송할때 유용하다.
트리톤 공식문서에 나와있는 example을 참고해서, infer함수를 사용했으며 모델이름과 Input, output의 타입과 이름까지 맞췄다.
성능테스트 : 그냥 fastAPI보다 안 좋은데..?
결론적으로 triton을 적용하여 테스트를 해본 결과 오히려 성능이 좋지 않았다.
테스트 환경은 로컬에서 Locust를 실행해서 성능테스트를 했고, GPU를 사용하기 위해서 g4dn.xlarge 타입의 인스턴스에서 서비스를 실행했다. 그리고 운영환경과 동일한 리소스을 맞추기 위해 docker에서 CPU 코어 수와 메모리를 제한했다.
단순히 fastAPI로만 서빙할 경우 적어도 RPS가 10은 나왔던거 같은데 triton을 적용하여 테스트를 해보니 RPS가 10도 채 나오지 않았다.
vUser | Median | p99 (ms) | Average (ms) | Max (ms) | RPS |
8 | 890 | 4300 | 1139.43 | 5718 | 6.4 |
16 | 2400 | 5800 | 2521.78 | 7285 | 6.3 |
아무래도 기존에 비해 리소스가 추가되서 그런가 싶기도 하다. 이런 상황이라면 굳이 트리톤을 적용하는게 큰 이점이 없을 것 같다.
심지어 dynamic batch도 제대로 이루어지고 있는지 확신이 없는 상황이라, 아직 실질적인 서비스에 적용하는건 조심스럽다고 판단했다.
뭔가 테스트가 잘못된걸까 싶은 마음에 다음글에서는 dynamic batch 를 비롯해서 조금 더 트리톤의 성능에 대해서 알아 보려고한다...!
'🌿 Data Engineering' 카테고리의 다른 글
pgVector 기반 VectorDB 구축 및 효율적인 리소스(메모리,스토리지) 사용 (6) | 2024.10.13 |
---|