쿠버네티스에 딥러닝 모델 서빙하기.
0. 요약
- 쿠버네티스에 딥러닝 모델 서빙하면서 겪은 문제.
- 서빙을 위한 서버 구축 및 쿠버네티스에 올리기.
- 모델 라이브러리 호환 문제 해결
- 6배 최적화
1. 들어가며 - 프로젝트 설명
이번 학기는 학교 수업에서 앱개발을 하는 프로젝트가 있습니다.
저희 팀이 기획한 것은 부모와 아이가 함께 캐릭터를 그리면 그 캐릭터를 gif 애니메이션으로 바꿔주는 앱입니다. 다른 핸드폰에서도 실시간으로 캔버스가 동기화되어 그림을 함께 그릴 수 있고, 그 캐릭터가 살아 숨쉬며 움직이는 모습을 통해 아이와 부모 사이에 추억을 쌓자는 목표의 프로젝트 였습니다.
제가 맡은 역할은 Django를 통한 백엔드 개발, (ec2에 배포되어 있으며 s3 bucket에 이미지 저장을 하고 rds에 mysql을 올려 운영하고 있습니다.) 실시간 그림 그리기 구현(android - django에 걸쳐), 딥러닝 모델 서빙입니다.
그 중 가장 오랜 시간이 걸렸고 좌충우돌 문제가 정말 많았던 딥러닝 모델 서빙에 대해 써보고자 합니다. 하면서 오기가 생겨 새벽 7시까지 달려 완성하였으나, 처음부터 익숙했다면 시간 절약을 훨씬 할 수 있었을 것 같습니다. 하시는 분들마다 사용하시는 딥러닝 모델도 다르고 서빙하는 방식도 다를 것이라 생각합니다. 또한 이 프로젝트는 엄밀히 말하면 facebook에서 model에 대해선 torchserve를 통해 서빙까지 하는 모델을 제공하였고 제가 한 일은 그 레포지토리의 라이브러리 의존성 해결과 애니메이팅 최적화, api 구축이었습니다. 그럼에도 공유되는 부분은 있을 것이라 생각하고 다른 분들은 같은 시행착오를 겪지 않았으면 하는 마음에 이 글을 작성합니다.
저희가 올리고자 했던 모델은 mmcv입니다. 정확히는
https://github.com/facebookresearch/AnimatedDrawings
페이스북에서 만든 animated Drawing 레포지토리를 바탕으로 딥러닝 모델을 서빙하기로 하였고 해당 리포지토리에서 내부적으로 mmcv를 사용하고 있습니다.
animated Drawing 레포지토리는의 동작은 다음과 같습니다.
1. 주어진 docker image를 통해 mmcv 모델을 torchserve로 서빙한다. 포트 번호는 8080이다.
2. 이미지를 전처리하여 torchserve로 보내고 rigging 된 이미지를 결과로 받아온다.
3. rigging 데이터를 통해 애니메이팅 하여 gif 혹은 mp4로 만든다.
다른 aws instance (ec2, s3, rds) 같은 경우엔 학과 예산으로 지원되어 배포가 어렵지 않았지만 ai-model을 올릴 instance의 경우 비용 문제로 지원되지 않았습니다. 특히 저희의 경우 이미지 처리를 하는 model (mmcv)였기 때문에 속도가 최대한 빠른 것을 원하였고 사비로 하기엔 비용이 만만치 않았습니다. 사용하는 ec2에 올리기엔 저희 모델은 CUDA core를 사용하지만 gpu가 없었고 무엇보다 한 서버에 모든 부담을 지우는 것이 마음에 들지 않았습니다.
그래서 학교 GPU 서버를 대여하여 허가를 받고 모델을 올리기로 하였습니다. 학교 GPU 서버는 쿠버네티스를 통해 docker container를 오케스트레이션 하고 있었고 저희 모델을 올리기 위해서는 두 가지 일이 필요했습니다.
1. 서버 구축.
위 프로젝트는 웹프로젝트 기반이 아니었기 때문에 api를 주고받을 수 있는 서버가 필요하였습니다. 이미지를 애니메이팅하여 돌려받기만 하면 되는 간단한 요구사항으로 굳이 복잡한 프레임워크를 사용할 필요가 없다고 판단하였습니다. 그리고 기존 프로젝트의 폴더 구조를 건드리지 않아도 되는 방향성이 좋아보였습니다. 따라서 프레임워크는 flask로 결정하였고 내부 함수를 호출하여 서빙하는 서버를 간단하게 만들 수 있었습니다. 애니메이팅한 image는 flask에서 바로 s3 bucket에 올리고 그 url을 motion을 key로 json 파싱하여 돌려주도록 설계하였습니다.
그렇게 만든 서버를 쿠버네티스 환경에서 구동하기 위해 ubuntu 18.04를 base image로 하여 conda를 설치하고 서버를 위한 환경 설정을 해준 뒤 docker image로 commit 하였습니다. base image를 ubuntu 구버전으로 고른 것은 원본 레포지토리가 검증한 환경이 ubuntu 18.04였기 때문입니다. 모델이 올라가 있는 부분은 위 레포지토리에서 이미 docker image로 제공하고 있기 때문에 추가 작업이 할 일이 없었습니다. (없을 것이라 생각했습니다...)
우선 배포 과정에서 첫번째 문제가 발생하였는데 학교 gpu 쿠버네티스는 연구용이라 웹서버 배포용으로 최적화되어 있지 않았고 접근 권한도 없었다는 점입니다. 대표적으로 node 정보에 대해 접근 권한이 없어 외부에서 우리 node로 접근할 수 있도록 하는데 많은 노력과 추론이 필요했습니다. 다만 이 부분은 보안 염려가 있어 자세히 서술하지 않도록 하겠습니다.
학교 gpu 서버와 연결되어 있는 harbor registry에 두 docker image를 올린 뒤 쿠버네티스 구동을 위해 deployment, service yaml을 작성하였습니다. model에서는 주로 gpu를 사용하고 애니메이팅에서는 주로 cpu를 사용하는 것을 확인하여 그에 맞게 자원 분배를 해주었습니다.
쿠버네티스를 처음 다루는 것이었고 학습 하는 과정에서 다양한 기능에 정말 놀랐습니다. 특히 job controller, secret, ingress 등 현재 프로젝트에서는 필요성이 없어 딱히 사용하지 않았지만 서버를 운영하는데 있어 요즘 왜 쿠버네티스가 각광받는지 알게되었습니다. cronJob을 통한 주기적인 작업 수행, deployment와 service를 응용한 카나리 배포 등 모두 매력적이고도 활용하기 쉽게 구성된 기능이었습니다.
저희는
1. 외부에서 이미지를 request로 받아 전처리하고 model로 보낸 뒤 그 결과로 애니메이팅을 하는 서버가 담긴 컨테이너 이미지.
2. 모델이 올라가 있는 컨테이너 이미지
이렇게 두 이미지를 준비하였습니다. 두 이미지가 1번의 경우 CPU를 주로 쓰며 2번의 경우 GPU를 주로 사용하여 할 필요가 없도록 같은 pod에 두 컨테이너를 올려 각각에 맞게 자원 배치를 해주었습니다. 또한 같은 pod으로 서로 localhost로 통신할 수 있도록 하였습니다.
우여곡절 끝 대망의 구동 결과는... model에서 500 에러를 반환하였습니다. model 서버가 정상 작동하는지 확인하기 위해 curl을 통해 ping을 보내보면 분명 healthy로 응답이 왔지만 이미지를 처리하려고 하면 prediction failed가 반복되었습니다.
2. 모델 라이브러리 dependency 해결.
모델 서버에 들어가 log를 확인해보니 mmcv cuda compiler를 찾지 못하고 있었습니다. 그래서 계속 500 에러를 보내고 있었던 것입니다.
facebook에서 준 dockerfile을 그대로 사용하여 라이브러리 의존성 문제는 정말 예상치 못했었습니다. docker에서 환경 설정 문제가 발생하다니..! 엎친데 덮친 격으로 mmcv의 경우 칭화대에서 개발한 것이었기 때문에 관련 많은 자료들이 중국어로 되어 있었습니다. 위 facebook animated drawing에서도 issue에서 model의 문제는 본인들이 관여하지 않는다고 답변하고 있었습니다.
여러 검색을 거쳐 직접 mmcv 모델의 github issue를 찾아본 결과
https://github.com/open-mmlab/mmdetection/issues/4471
위 링크처럼 docker image build를 할 때 option을 줘야 한다는 것을 알게 되었습니다. 또한 쿠버네티스에서 할당받은 GPU (A100)와 드라이버에 따라 cuda, pytorch, python, mmcv-full 버전을 모두 맞춰야 된다는 것을 알게 되었습니다.
(위 글은 정말 정말 도움이 많이 되었습니다.)
mmcv-full의 경우 특히 다른 라이브러리들과 겨우 minor 버전 차이만 나도 작동이 안 될 정도로 호환에 아주 민감하였고 결국 우린 그나마 호환성이 높은(?) 다른 라이브러리들을 mmcv-full을 중심으로 driver까지 모든 연결점이 호환성 문제가 없도록 처음부터 다시 세팅했습니다.
그리고 다시 테스트해 본 결과.
애니메이팅이 아주 잘 되었습니다!
감격과 뿌듯함도 잠시...
서버가 너무 불안정하고 속도가 느리다는 사실을 깨달았습니다. 특히 여러명이 동시에 요청하는 경우 시간이 기하급수적으로 늘어나 timeout이 빈번했습니다. 그리고 한 명만 요청하더라도 3분에서 5분까지도 소요되었습니다.
3. 최적화
3.1 동시 접속
사실 여러명이 요청하는 경우 그만큼 시간이 늘어나는 것은 예상하기 어렵지 않은 문제였습니다. flask 서버로 구축하면서 추후에 미뤄놓았지만 gunicorn과 같은 웹 서버 게이트웨이 인터페이스 (wsgi)를 사용하지 않았고 이럴 경우
위 링크에서 설명하는 것처럼 flask 내장 웹서버는 단일 스레드로 서버가 작동하기 때문입니다. python GIL 특성상 단일 스레드보다도 싱글 프로세스, 여기에 더해 flask는 동기식 웹 프레임워크이기 때문에 정말로 먼저 온 분 먼저 하셔야죠.. 이러고 다들 기다리고 있는 상황이 벌어집니다. 안 그래도 오랜 시간이 걸리는 딥러닝 모델 서빙에 이렇게 구현되어 있으면 더욱 치명적입니다. 따라서 gunicorn과 gevent를 얹어 worker와 thread를 늘려주었습니다.
gunicorn 공식 문서와 내부 구조, python GIL, numpy 사용을 고려하여 적절하게 조정하였고 테스트를 통해 실제 속도를 비교해가면서 워커와 스레드 개수를 조절하였습니다. 서버가 가용 가능한 만큼의 동시 접속을 최대한 해결하였습니다.
더 알아볼 점 :
gevent를 사용한 이유는 이미지와 리깅 파일 등 파일 I/O가 많았기 때문이었습니다. 하지만 저희 서버의 경우 애니메이팅이 cpu bound에서 이루어지고 있고 대부분의 시간을 소모한다는 점에서 cpu-heavy한 workload라고 할 수 있습니다.
따라서 gevent를 선택하는 이점이 비교적 적다고 할 수 있습니다. 다만 테스트 결과 gevent worker가 다소 빨랐습니다. cpu-heavy한 것은 사실이지만 I/O 작업도 적은 것은 절대 아니기 때문으로 보입니다.
https://stackoverflow.com/questions/38425620/gunicorn-workers-and-threads
https://luis-sena.medium.com/gunicorn-worker-types-youre-probably-using-them-wrong-381239e13594
테스트 결과 w스레드를 늘리는 것이 단일 접속의 처리 시간도 개선시키는 것을 발견하였습니다. 위 stackoverflow 글을 보면 비동기 특성상 스레드를 늘리는 것이 의미가 없다고 하고 있습니다만 속도가 늘어난다는 점은 추가 공부가 필요할 것 같습니다. sync worker를 사용할 때는 GIL이 I/O bound 작업까지는 영향을 못 미치기 때문에 성능이 늘어나는 것은 당연하지만 async에서 성능이 늘어나는 이유는 다소 공부가 필요할 것 같습니다.
또 궁금했던 점은 numpy가 gunicorn + flask 조합에서 동작하는 방식입니다. 내부에서 애니메이팅을 위해 numpy를 사용하고 있는데 numpy의 내부 코드는 c로 되어있기 때문에 python의 GIL에 구애받지 않고 멀티스레딩을 처리해줍니다. 그렇다면 이 multithreading은 gunicorn에서 늘려준 thread에도 영향을 받는지에 대해서입니다. 추가 공부가 필요한 부분이고, python gunicorn worker에 대해 글을 따로 쓰면서 작성하고자 합니다.
3.2 단일 접속
그럼에도 단일한 접속 자체도 시간이 오래 걸렸습니다. 코드에 어디서 병목이 걸리는 지 log를 추가하여 tail을 통해 확인해보니 의외로 모델 서버에서는 1초 이내로 응답이 왔고 애니메이팅에 대부분의 시간이 걸렸었습니다.
애니메이팅 과정을 살펴보니 bvh(애니메이팅을 정의하는 파일)에서 2초가 안되는 애니메이션에 무려 850프레임 가량을 소모하고 있었다는 걸 깨달았습니다. blender를 사용하여 프레임을 150프레임까지 줄였고 시간을 훨씬 단축할 수 있었습니다. 또한 휴대폰에서만 돌아가는 app 특성상 애니메이팅에 있어서는 받은 image size를 줄여도 괜찮다는 판단을 하였고 이를 통해서도 시간을 단축할 수 있었습니다.
3.1과 3.2를 통해 원본 facebook repository에서 3분 ~ 5분이 소요 되던 것을 30초 ~ 80초까지 6배 단축할 수 있었습니다.
4. 아쉬운 점
사용을 허락 받은 gpu는 하나였고 이는 모델에서 사용되었기 때문에 cupy 등을 사용하거나 하여 최적화를 하지는 못한 점이 아쉽습니다. 애니메이팅을 원본 코드보다 병렬 처리할 수 있는 방법은 분명 있는 것으로 보였고 더욱더 최적화가 가능할 것으로 보입니다.
모델 서빙에서 오랜 시간이 걸렸다면
https://tech.kakaopay.com/post/model-serving-framework/
이런 좋은 자료를 참고할 수 있겠지만 이미 facebook repository가 torchserve를 잘 활용해서 모델을 서빙하고 있었고, 오래 걸리는 것은 animating 부분이었기 때문에 관련 작업을 하지 못한 것도 아쉬웠습니다.
다음엔 프로젝트에서 실시간 그림 동기화를 위해 사용했던 방법과 구현 방식에 대해 서술해보겠습니다! (획 단위의 동기화)
5. 프로젝트 url
https://github.com/snuhcs-course/swpp-2023-project-team-9/wiki/Design-Documentation (팀프로젝트)
https://github.com/SHEOMM/animated_drawing (inference server)