MLOps

[Lecture] Deploying Machine Learning Models in Production Week2 - 3. Online Inference

HJChung 2021. 11. 21. 21:30
머신러닝 모델을 만들면 Product에 모델을 배포하고, inference 요청에 대한 응답(모델의 예측값을 사용자에게 응답)을 할 수 있도록 모델 API를 제공해야 한다. 즉, model serving을 해야 한다. 
어떤 식으로 모델을 배포할 수 있는지 소개하는 <Deploying Machine Learning Models in Production> 과정의 Week 2: Model Serving: Patterns and Infrastructure을 학습했고,
그중 3번째 주제인 Online Inference와 실습에 대해서 배운 것을 정리해 보았다. 

 

Online Inference

inference방식으로는 batch inference와 online inference가 있다. 
batch inference는  말 그대로 데이터를 배치 단위로 인퍼런스 하는 것이다. 이러한 배치 단위의 작업들은 일정 주기마다 반복적으로 돌아가는 작업에서 진행되며 결과는 DB 등에 저장된다. 
반면 online inference는 기계학습 모델에 요청이 올 때마다 수행하는 것을 말한다.

즉, model과 API caller 사이의 Interaction이 바로바로 되는 것으로 real time으로 요청에 대한 ML prediction결과를 얻을 수 있고, 그래서 inference optimizing이 중요한 이슈가 된다. 

 

online inference optimization

online inference optimization은 사용성을 위해서든, 여러 방면에 중요하고, 이 때 고려할 수 있는 사항들에는 Latency, Throughput, Cost 등이 있다. 


이런 것들은 앞서 살펴본 [Lecture] Deploying Machine Learning Models in Production Week 2 - 2. Scaling Infrastructure과도 연관이 있고, 또 model architecture, accuracy 등과도 다 trade off같은 상관이 있으니 balance를 맞춰서 전략을 잘 설정하는 것이 중요해 보인다. 

 

실습 -  Latency testing with Docker Compose and Locust

실습 repo: https://github.com/Gracechung-sw/inference-latency-test/tree/latency-test

 

 

Locust를 사용하여 여러 inference 유형의 server에 request 상황에 따른 load가 얼마 난지 test 해보는 실습이다. 

Locust란?  Open Source load testing tool 

 

 

stress test를 해 볼 inference server 유형은 아래와 같다. 

우선 예측 모델은 Wine의 유형을 분류해주는 모델이며  inference server는 FastAPI로 직접 구현한 것이다. 

  1.  batch를 support해주지 않는 서버인 경우: 해당 ML model에 대한 설명과 inference server에 대한 설명은 이 README를 참고할 수 있다. 
  2.  batch를 support하는 서버인 경우: 해당 ML model에 대한 설명과 inference server에 대한 설명은README 를 참고할 수 있다.
    1) 1개의 batch를 사용하는 server (batches of 1)
    2) 32개의 batch를 사용하는 server (batches of 32)
    3) 64개의 batch를 사용하는 server (batches of 64)

 

Locust를 실행할 1개의 container, 위에서 언급한 4개 유형의 server container, 총 5개의 container를 docker-compose로 실행시켜 볼 것이다. 

Docker-compose란? allows you to run multiple-container applications link them together via a network.

 

 

먼저 locust image를 pull 받는다. 

docker pull locustio/locust

 

그리고 batch를 support 해주는 서버의 이미지는 

https://github.com/Gracechung-sw/inference-latency-test/tree/latency-test 여기에서

cd no-batch
docker build -t mlepc4w2-ugl:no-batch .

batch를 support 해주지 않는 서버의 이미지는

cd with-batch
docker build -t mlepc4w2-ugl:with-batch .

 

docker-compose로 Locust를 실행할 1개의 container, 위에서 언급한 4개의 경우로 실행되는 server container, 총 5개의 container를 실행시켜 보기 위해서 docker-compose 파일을 작성한다. 

docker-compose.yml

version: "3.9"
services:
  no-batch:
    image: mlepc4w2-ugl:no-batch
    links:
      - locust
  batch-1:
    image: mlepc4w2-ugl:with-batch
    links:
      - locust
  batch-32:
    image: mlepc4w2-ugl:with-batch
    links:
      - locust
  batch-64:
    image: mlepc4w2-ugl:with-batch
    links:
      - locust
  locust:
    image: locustio/locust
    ports:
      - "8089:8089"
    volumes:
      - ./:/mnt/locust
    command: -f /mnt/locust/locustfile.py

docker-compose의 links: 옵션은, locust service가 생성한 network를 통해서 no-batch, batch-1, batch-32, batch-64가 서로 통신해야 함을 의미한다. 즉, 4개의 model inference server를 locust service에 연결한다. 

 

locust service는 가상의 users를 생성해서 테스트 하고자하는 서버로 계속 request를 반복해서 날려줌으로써 서버 load를 test 해 준다.

이를 통해서 RPS (requests per second) 또는 각 request마다 걸리는 평균 시간 을 측정해 볼 수 있다. 

RPS를 측정해보는 것이 중요한 이유는, server가 production환경 배포됐을 때 얼마큼의 request를 핸들링할 수 있는지 확인하기 위해서이다(like a stress test). 

locustfile.py

from locust import HttpUser, task, constant

class LoadTest(HttpUser):
    wait_time = constant(0) # 각 요청 사이에 대기할 시간. 극한 조건에서 서버가 어떻게 작동하는지 확인하기 위해 가능한 빨리 요청을 보낸다. 
    host = "http://localhost" # 테스트 중인 서비스가 호스팅되는 host를 지정. 

    @task
    def predict_batch_1(self):
        request_body = {"batches": [[1.0 for i in range(13)]]}
        self.client.post(
            "http://batch-1:80/predict", json=request_body, name="batch-1"
        )

    @task
    def predict_batch_32(self):
        request_body = {"batches": [[1.0 for i in range(13)] for i in range(32)]}
        self.client.post(
            "http://batch-32:80/predict", json=request_body, name="batch-32"
        )

    @task
    def predict_batch_64(self):
        request_body = {"batches": [[1.0 for i in range(13)] for i in range(64)]}
        self.client.post(
            "http://batch-64:80/predict", json=request_body, name="batch-64"
        )

    @task
    def predict_no_batch(self):
        request_body = {
            "alcohol": 1.0,
            "malic_acid": 1.0,
            "ash": 1.0,
            "alcalinity_of_ash": 1.0,
            "magnesium": 1.0,
            "total_phenols": 1.0,
            "flavanoids": 1.0,
            "nonflavanoid_phenols": 1.0,
            "proanthocyanins": 1.0,
            "color_intensity": 1.0,
            "hue": 1.0,
            "od280_od315_of_diluted_wines": 1.0,
            "proline": 1.0,
        }
        self.client.post(
            "http://no-batch:80/predict", json=request_body, name="0:batch"
        )

이제 이 코드로 서버 부하 test을 해보자. 

docker-compose up

이제 http://localhost:8089/ 로 이동하면 locust의 인터페이스가 표시된다. 여기에서 원하는 사용자 수와 생성 속도를 선택할 수 있다.

이를 설정하면 초 당 얼만큼의 사용자가 생성되고, 최대로 생성되는 사용자는 몇인지를 지정해주는 것이다. 

 

원하는 최대 생성 사용자 수는 처음부터 알 수가 없고, 점차 해 보면서 증가시켜나가는 것이 좋다. 한 번에 높은 값을 줘버리면 application이 crashing 되는 문제가 있을 수 있기 때문이다. 처음 시작은 users 10, spawn rate 10으로 시작해보는 것을 추천한다. 

 

http://localhost:8089에서 보이는 locust의 인터페이스에서 start swarming을 클릭하면 load test가 시작된다. 

  • Name: service이름
  • # Requests, #Fails: 각 서버에 보낸 요청 수와 실패한 요청 수
  • highlighted in orange:  statistics about the latency of the servers
  • highlighted in blue: RPS of each server.

 

이 test를 중지하고 싶다면 우측 상단의 stop 버튼을 누르고 또 users와 swarming을 높여가면서 test를 반복 해본다. 

 

 

위 실습을 다 진행하였다면 

ctrl+C 로 docker-compose up으로 실행한 multi-container application을 중지, 

docker-compose down으로 네트워크와 함께 multi-container application을 제거.

 

여기서 배운 것은, production환경으로 가기 전에 이와 같은 locust와 같은 도구들을 이용해서 확인 과정을 거치는 것이 중요하다는 것!이다.