22.10.8 작성
TL; DR
- 그러니까 KServe라는 건 그냥 아주 쉽게 모델을 마운트해서 쓸 수 있게 다 코드를 준비해놓은 Tornado로 만든 웹서버인 것이다.
배경
KServe를 KFServing일 시절부터 테스트용으로 사용은 해왔지만 몇 개월 전부터 나름 production level로 사용을 하다보니, 한번 전체 구동 방식을 기록해두자 라는 생각이 들었다.
원래 KServe는 Kubeflow의 하부 프로젝트로 KFServing이라는 이름이었다.
그리고 0.7 버전부터 KServe라는 이름으로 변경되었고, KFServing에서의 migrating 을 지원한다.
현재 (22.10.8) 시점에서는 0.9버전이 release 되어 있으며, 점점 복잡해지는 Model Serving 들을 위한 기능이 추가되고 있다.
그 중 하나로, 0.9 버전에서 생긴 Inference Graph라는 기능은 많은 모델들을 통과해서야 원하는 inference 결과물을 얻을 수 있는 최근 모델들의 요구사항을 반영하는 것 처럼 보인다.
예를 들면 최근에 가장 유명한 Stable-diffusion을 떠올릴 수 있다. Stable-diffusion은 여러 모델들을 통과하고 그 중 Unet 은 많은 loop를 돌려야 하는 등 모델 자체를 뜯어보면 inference 과정이 복잡하다.
이런 요구사항들을 반영하고 있는 것 같다.
KServe란 무엇인가
https://kserve.github.io/website/master/
KServe는 만들어진 ML/DL 모델을 실제로 서비스하기 위해 API를 쉽게 만들 수 있도록 도와주는 툴이다.
아주 단순하게 생각하면 어딘가에 모델을 저장하고 그걸 그냥 InferenceService라는 Custom resource 를 배포하면
알아서 모델을 Pod에 마운트해주고 알아서 찔러볼 수 있는 API를 준다.
그냥 모델을 올리고 Inference를 해봤어요 라고 말하는 것까지가 목표라면 그렇게만 사용해봐도 상관은 없지만,
실제 비즈니스 요구사항을 반영해서 서비스를 만들겠다고 생각하면 생각보다 얘기는 훨씬 복잡해진다.
KServe는 Kubernetes 그 자체와 network가 사용되는 방식에 대한 이해를 필요로 하기 때문에,
그런 사람이 없거나, 사용하려는 범위가 무겁고 크지 않다면 그렇게 추천하고 싶지는 않다.
이건 KServe 의 아키텍쳐다.
Kubernetes로 묶여 있는 서버들이 있고
이제는 Optional하게 바뀌었지만 Knative + Istio 레이어가 있다.
내 생각에는 이 레이어를 Optional하게 쓸거면 굳이 KServe를 쓸 필요가 있나 싶긴하다.
그건 이후에 설명할 Istio + Knative 가 갖는 장점이 KServe의 장점이기 때문이다.
그리고 KServe가 있다.
Tensorflow, Pytorch, Scikit-learn, XGBoost, ONNX, TensorRT 등 잘 알려진 프레임워크들을 지원한다.
그 프레임워크들로 만들어진 모델들을 AWS, Google, Azure의 오브젝트 스토리지들, NFS 를 지원하는 어떤 스토리지든 올리면
지원하는 범위 내에서 그대로 쓸 수 있음을 의미한다.
KServe의 장점은 무엇인가
KServe를 더 자세히 뜯어보기 전에 장점을 먼저 봐야겠다.
공식 홈페이지를 들어가면 바로 보인다.
"Highly scalable and standards based Model Inference Platform on Kubernetes for Trusted AI"
- KServe is a standard Model Inference Platform on Kubernetes, built for highly scalable use cases.
- Provides performant, standardized inference protocol across ML frameworks.
- Support modern serverless inference workload with Autoscaling including Scale to Zero on GPU.
- Provides high scalability, density packing and intelligent routing using ModelMesh
- Simple and Pluggable production serving for production ML serving including prediction, pre/post processing, monitoring and explainability.
- Advanced deployments with canary rollout, experiments, ensembles and transformers.
일단 Kubernetes 위에서 구동하니까 scalable하다.
그리고 쉽고 간편하고 강력한 API를 만들 수 있다.
서버리스를 지원한다. 이건 KNative를 사용하기 때문이다. Zero scale이 가능하니 Pod가 없다가(GPU를 사용하지 않고 있다가) 호출을 받으면 Pod가 떠서 inference가 수행되는 행위가 가능하다는 의미이다.
그리고 predict, pre/post processing, monitoring, explain 같은 여러 inference 과정에서 필요한 동작들을 필요에 따라 manifest에 추가하거나 빼는 것만으로도 조절해서 배포가 가능하다.
또 버전을 올리면서 카나리 배포를 하는 행위도 가능하다. 모델 1에 90, 모델 2에 10의 라우팅 분산을 한다던가 하는 일도 Kubernetes이기 때문에 쉽게 할 수 있다.
공식 홈페이지에서 명시적으로 설명하고 있는 것외에 개인적으로 느끼는 장점도 더 있다.
Istio + KNative 로 쓰기 때문에 생기는 장점을 생각해보면,
일일이 API 를 만들어줄 필요없이 여러 모델을 배포해야 할 때 엔드포인트 관리가 정말 쉬워진다.
왜냐하면 Istio Proxy에 도달하는 주소 이후에 모델에 도달하기 위한 주소는
Knative-serving에 의해 생성된 도메인으로의 라우팅을 요청시 Header로 관리하기 때문에
아무리 많은 모델들을 배포하더라도 엔드포인트를 계속 쉽게 늘려나갈 수 있다.
그리고 inference가 많아지는 production level의 서비스인 경우,
일단 Knative로부터 관리되는 queue-proxy 가 sidecar container로 붙는다.
따로 queue를 위한 컴포넌트를 올리지 않아도 버틸 수 있다.
KServe는 어떻게 동작하는가
https://kserve.github.io/website/0.9/modelserving/control_plane/
먼저 Control plane을 보자.
지금 보는 그림은 Single Model이고 Istio+KNative를 베이스로 Transformer(모델 아니고 KServe에서 사용하는 Pod이름임), Predictor를 사용하는 경우에 대한 구조이다.
어떤 HTTP나 GRPC 프로토콜로 Transformer에 inference 요청이 도달한다.
Transformer에는 Preprocess, Postprocess에 관한 사용자의 로직을 집어넣을 수 있다.
Transformer Pod에는 2개의 Container가 들어있고 Queue Proxy를 거쳐 Transformer에서 전처리가 수행된다.
그리고 전처리 결과를 HTTP나 GRPC 프로토콜로 Predictor Pod로 보낸다.
Predictor Pod에는 3개의 Container가 뜨면서 동작하는데 먼저 Storage Initializer container가 떠서 지정한 Storage에서
모델을 갖고와서 Model Server container의 /mnt/models 위치에 마운트한다.
그리고 storage initializer의 역할은 다했기 때문에 종료된다.
이후 Transformer에서의 요청을 받아 처리한후 다시 Transformer pod로 돌려보낸다.
그러면 그 결과를 다시 Transformer가 후처리를 해서 다시 돌려준다. 우리는 그 최종결과를 받아본다.
이 모든 동작은 InferenceService라는 Custom Resource 를 배포함으로서 이루어지게 할 수 있다.
InferenceService를 배포하면 Operator들이 이를 확인하고
우리가 살펴 본 Pod들을 동작하게 만드는 Deployment와 거기까지 요청이 도달 할 수 있게 만드는 Istio Virtual Service, Service 등을 모두 같이 관리한다.
우리가 배포하는건 단지 InferenceService라는 manifest 파일 하나면 된다.
KServe Controller는 우리가 다른 것들을 신경쓰지 않아도 되게 많은 컴포넌트들을 배포해주고 단지 최종적으로 엔드포인트만을 알려준다.
https://kserve.github.io/website/0.9/modelserving/data_plane/
데이터 플레인 페이지를 보면 API 설명을 하고 있다.
물론 KServe가 모델을 어떻게 관리하고 호출하는지까지 뜯어보면 더 복잡하고 글이 길어질테니
일단은 앞부분에 해당하는 API 받는 부분까지를 봐야겠다.
Transformer와 Predictor를 구성하는 Pod와 코드들을 뜯어보면 내부적으로 Tornado를 사용해서 API를 띄우고 있다.
그러니까 KServe라는 건 그냥 아주 쉽게 모델을 마운트해서 쓸 수 있게 다 코드를 준비해놓은 Tornado로 만든 웹서버인 것이다.
가볍고 Asynchronous한 처리를 묶어서 쉽게 사용할 수 있기 때문에 Tornado를 사용하는 것같다는 생각을 했다.
아래 코드를 보면 쉽게 이해할 수 있다.
https://github.com/kserve/kserve/blob/release-0.9/python/kserve/kserve/model_server.py#L69
class ModelServer:
def __init__(self, http_port: int = args.http_port,
grpc_port: int = args.grpc_port,
max_buffer_size: int = args.max_buffer_size,
workers: int = args.workers,
max_asyncio_workers: int = args.max_asyncio_workers,
registered_models: ModelRepository = ModelRepository(),
enable_latency_logging: bool = args.enable_latency_logging):
self.registered_models = registered_models
self.http_port = http_port
self.grpc_port = grpc_port
self.max_buffer_size = max_buffer_size
self.workers = workers
self.max_asyncio_workers = max_asyncio_workers
self._http_server: Optional[tornado.httpserver.HTTPServer] = None
self.enable_latency_logging = validate_enable_latency_logging(enable_latency_logging)
def create_application(self):
return tornado.web.Application([
(r"/metrics", MetricsHandler),
# Server Liveness API returns 200 if server is alive.
(r"/", handlers.LivenessHandler),
(r"/v2/health/live", handlers.LivenessHandler),
(r"/v1/models",
handlers.ListHandler, dict(models=self.registered_models)),
(r"/v2/models",
handlers.ListHandler, dict(models=self.registered_models)),
# Model Health API returns 200 if model is ready to serve.
(r"/v1/models/([a-zA-Z0-9_-]+)",
handlers.HealthHandler, dict(models=self.registered_models)),
(r"/v2/models/([a-zA-Z0-9_-]+)/status",
handlers.HealthHandler, dict(models=self.registered_models)),
(r"/v1/models/([a-zA-Z0-9_-]+):predict",
handlers.PredictHandler, dict(models=self.registered_models)),
(r"/v2/models/([a-zA-Z0-9_-]+)/infer",
...
KServe의 request flow
https://kserve.github.io/website/0.9/developer/debug/#debug-kserve-request-flow
KServe 홈페이지에서는 눈에 안띄게 (불친절하게도) 디버깅 가이드에 KServe request flow를 올려놨다.
이걸 보면 어떤 순서를 거쳐서 사용자의 request가 모델까지 도달하는 지 이해를 할 수 있고
어느 구간에서 문제가 생겨서 바로 잡아야 하는지도 알 수 있다.
또한 Istio와 Knative-serving이 이 과정에서 어떤 역할을 하여 쉽게 엔드포인트가 만들어지는 지에 대한 이해도 할 수 있다.
Istio와 Knatvie-serving의 조합이 없다면 우리는 엔드포인트를 이렇게 쉽게 만들 수 없는 것이다.
InferenceService 를 배포하면 이렇게 많은 것들이 생성된다.
+----------------------+ +-----------------------+ +--------------------------+
|Istio Virtual Service | |Istio Virtual Service | | K8S Service |
| | | | | |
|sklearn-iris | |sklearn-iris-predictor | | sklearn-iris-predictor |
| +------->|-default +----->| -default-$revision |
| | | | | |
|KServe Route | |Knative Route | | Knative Revision Service |
+----------------------+ +-----------------------+ +------------+-------------+
Knative Ingress Gateway Knative Local Gateway Kube Proxy
(Istio gateway) (Istio gateway) |
|
|
+-------------------------------------------------------+ |
| Knative Revision Pod | |
| | |
| +-------------------+ +-----------------+ | |
| | | | | | |
| |kserve-container |<-----+ Queue Proxy | |<------------------+
| | | | | |
| +-------------------+ +--------------^--+ |
| | |
+-----------------------^-------------------------------+
| scale deployment |
+--------+--------+ | pull metrics
| Knative | |
| Autoscaler |-----------
| KPA/HPA |
+-----------------+
KServe의 배포
https://kserve.github.io/website/0.9/get_started/first_isvc/
얘기는 길었지만 결국 KServe의 배포는 아래와 같이 생긴 manifest면 끝난다.
predictor에 어떤 모델인지를 알려주고, 그 모델의 위치를 작성한다.
apiVersion: "serving.kserve.io/v1beta1"
kind: "InferenceService"
metadata:
name: "sklearn-iris"
spec:
predictor:
model:
modelFormat:
name: sklearn
storageUri: "gs://kfserving-examples/models/sklearn/1.0/model"
배포 후에는 아래와 같이 확인된다.
아래의 URL 은 호출시 Header에 들어가게 된다.
{우리가 배포한 InferenceService의 이름}.{네임스페이스}.example.com인데
기본값이 example.com이긴 하지만 이건 좀 이상하니
이 도메인은 KNative의 configmap을 수정해서 원하는 대로 변경이 가능하다.
kubectl get inferenceservices sklearn-iris -n kserve-test
NAME URL READY PREV LATEST PREVROLLEDOUTREVISION LATESTREADYREVISION AGE
sklearn-iris http://sklearn-iris.kserve-test.example.com True 100 sklearn-iris-predictor-default-47q2g 7d23h
그러면 이렇게 생긴 input을 만들어서
(물론 이 input 형태도 변경하면 된다)
cat <<EOF > "./iris-input.json"
{
"instances": [
[6.8, 2.8, 4.8, 1.4],
[6.0, 3.4, 4.5, 1.6]
]
}
EOF
호출을 해볼 수 있다.
curl -v http://sklearn-iris.kserve-test.${CUSTOM_DOMAIN}/v1/models/sklearn-iris:predict -d @./iris-input.json
그러면 이렇게 생긴 결과값을 받을 수 있을 것이다.
{"predictions": [1, 1]}
정리
KServe로 하는 Model Serving 자체에 대한 이해를 목적으로 글을 썼다.
실제 비즈니스 환경에서 사용하기 위해서는 더 많은 수정들이 필요할 것이다.
이에 대한 건 다른 글에서 써야겠다.
Reference
'MLOps' 카테고리의 다른 글
KServe ML Model monitoring 해보기 : Knative, Loki (0) | 2023.04.01 |
---|---|
이미지 예측 모델을 쿠버네티스에 배포하기 A to Z (Kubernetes Kubeflow KFServing InferenceService Custom Image) (0) | 2022.01.05 |