From 17eb714ce6771891ef2f1a4633d4dc213ed09002 Mon Sep 17 00:00:00 2001 From: Andrea Lamparelli Date: Fri, 1 Mar 2024 11:14:21 +0100 Subject: [PATCH] Add documentation and get started guide Signed-off-by: Andrea Lamparelli --- Makefile | 2 +- csi/GET_STARTED.md | 249 +++++++++++++++++++++++++++ csi/README.md | 90 +++++++++- csi/scripts/install_modelregistry.sh | 48 ++++++ 4 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 csi/GET_STARTED.md create mode 100755 csi/scripts/install_modelregistry.sh diff --git a/Makefile b/Makefile index fe6c9933..e248fcdc 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ IMG_VERSION ?= main # container image repository IMG_REPO ?= model-registry # container image -IMG := ${IMG_REGISTRY}/$(IMG_ORG)/$(IMG_REPO) +IMG ?= ${IMG_REGISTRY}/$(IMG_ORG)/$(IMG_REPO) model-registry: build diff --git a/csi/GET_STARTED.md b/csi/GET_STARTED.md new file mode 100644 index 00000000..54155d93 --- /dev/null +++ b/csi/GET_STARTED.md @@ -0,0 +1,249 @@ +# Get Started + +Embark on your journey with this custom storage initializer by exploring a simple hello-world example. Learn how to seamlessly integrate and leverage the power of this tool in just a few steps. + +## Prerequisites + +* Install [Kind](https://kind.sigs.k8s.io/docs/user/quick-start) (Kubernetes in Docker) to run local Kubernetes cluster with Docker container nodes. +* Install the [Kubernetes CLI (kubectl)](https://kubernetes.io/docs/tasks/tools/), which allows you to run commands against Kubernetes clusters. +* Install the [Kustomize](https://kustomize.io/), which allows you to customize app configuration. + +## Environment Preparation + +We assume all [prerequisites](#prerequisites) are satisfied at this point. + +### Create the environment + +1. After having kind installed, create a kind cluster with: +```bash +kind create cluster +``` + +2. Configure `kubectl` to use kind context +```bash +kubectl config use-context kind-kind +``` + +3. Setup local deployment of *Kserve* using the provided *Kserve quick installation* script +```bash +curl -s "https://raw.githubusercontent.com/kserve/kserve/release-0.11/hack/quick_install.sh" | bash +``` + +4. Install *model registry* in the local cluster + +[Optional ]Use model registry with local changes: + +```bash +TAG=$(git rev-parse HEAD) && \ +MR_IMG=quay.io/$USER/model-registry:$TAG && \ +make -C ../ IMG_ORG=$USER IMG_VERSION=$TAG image/build && \ +kind load docker-image $MR_IMG +``` + +then: + +```bash +bash ./scripts/install_modelregistry.sh -i $MR_IMG +``` + +> _NOTE_: If you want to use a remote image you can simply remove the `-i` option + +> _NOTE_: The `./scripts/install_modelregistry.sh` will make some change to [base/kustomization.yaml](../manifests/kustomize/base/kustomization.yaml) that you DON'T need to commit!! + +5. [Optional] Use local container image for CSI + +```bash +IMG=quay.io/$USER/model-registry-storage-initializer:$(git rev-parse HEAD) && make IMG=$IMG docker-build && kind load docker-image $IMG +``` + +## First InferenceService with ModelRegistry URI + +In this tutorial, you will deploy an InferenceService with a predictor that will load a model indexed into the model registry, the indexed model refers to a scikit-learn model trained with the [iris](https://archive.ics.uci.edu/ml/datasets/iris) dataset. This dataset has three output class: Iris Setosa, Iris Versicolour, and Iris Virginica. + +You will then send an inference request to your deployed model in order to get a prediction for the class of iris plant your request corresponds to. + +Since your model is being deployed as an InferenceService, not a raw Kubernetes Service, you just need to provide the storage location of the model using the `model-registry://` URI format and it gets some super powers out of the box. + + +### Register a Model into ModelRegistry + +Apply `Port Forward` to the model registry service in order to being able to interact with it from the outside of the cluster. +```bash +kubectl port-forward --namespace kubeflow svc/model-registry-service 8080:8080 +``` + +And then (in another terminal): +```bash +export MR_HOSTNAME=localhost:8080 +``` + +Then, in the same terminal where you exported `MR_HOSTNAME`, perform the following actions: +1. Register an empty `RegisteredModel` + +```bash +curl --silent -X 'POST' \ + "$MR_HOSTNAME/api/model_registry/v1alpha2/registered_models" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "description": "Iris scikit-learn model", + "name": "iris" +}' +``` + +Expected output: +```bash +{"createTimeSinceEpoch":"1709287882361","customProperties":{},"description":"Iris scikit-learn model","id":"1","lastUpdateTimeSinceEpoch":"1709287882361","name":"iris"} +``` + +2. Register the first `ModelVersion` + +```bash +curl --silent -X 'POST' \ + "$MR_HOSTNAME/api/model_registry/v1alpha2/model_versions" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "description": "Iris model version v1", + "name": "v1", + "registeredModelID": "1" +}' +``` + +Expected output: +```bash +{"createTimeSinceEpoch":"1709287890365","customProperties":{},"description":"Iris model version v1","id":"2","lastUpdateTimeSinceEpoch":"1709287890365","name":"v1"} +``` + +3. Register the raw `ModelArtifact` + +This artifact defines where the actual trained model is stored, i.e., `gs://kfserving-examples/models/sklearn/1.0/model` + +```bash +curl --silent -X 'POST' \ + "$MR_HOSTNAME/api/model_registry/v1alpha2/model_versions/2/artifacts" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "description": "Model artifact for Iris v1", + "uri": "gs://kfserving-examples/models/sklearn/1.0/model", + "state": "UNKNOWN", + "name": "iris-model-v1", + "modelFormatName": "sklearn", + "modelFormatVersion": "1", + "artifactType": "model-artifact" +}' +``` + +Expected output: +```bash +{"artifactType":"model-artifact","createTimeSinceEpoch":"1709287972637","customProperties":{},"description":"Model artifact for Iris v1","id":"1","lastUpdateTimeSinceEpoch":"1709287972637","modelFormatName":"sklearn","modelFormatVersion":"1","name":"iris-model-v1","state":"UNKNOWN","uri":"gs://kfserving-examples/models/sklearn/1.0/model"} +``` + +> _NOTE_: double check the provided IDs are the expected ones. + +### Apply the `ClusterStorageContainer` resource + +Retrieve the model registry service and MLMD port: +```bash +MODEL_REGISTRY_SERVICE=model-registry-service +MODEL_REGISTRY_REST_PORT=$(kubectl get svc/$MODEL_REGISTRY_SERVICE -n kubeflow --output jsonpath='{.spec.ports[0].targetPort}' ) +``` + +Apply the cluster-scoped `ClusterStorageContainer` CR to setup configure the `model registry storage initilizer` for `model-registry://` URI formats. + +```bash +kubectl apply -f - < _NOTE_: as `$IMG` you could use either the one created during [env preparation](#environment-preparation) or any other remote img in the container registry. + +### Create an `InferenceService` + +1. Create a namespace +```bash +kubectl create namespace kserve-test +``` + +2. Create the `InferenceService` +```bash +kubectl apply -n kserve-test -f - < "/tmp/iris-input.json" +{ + "instances": [ + [6.8, 2.8, 4.8, 1.4], + [6.0, 3.4, 4.5, 1.6] + ] +} +EOF +``` + +If you do not have DNS, you can still curl with the ingress gateway external IP using the HOST Header. +```bash +SERVICE_HOSTNAME=$(kubectl get inferenceservice iris-model -n kserve-test -o jsonpath='{.status.url}' | cut -d "/" -f 3) +curl -v -H "Host: ${SERVICE_HOSTNAME}" -H "Content-Type: application/json" "http://${INGRESS_HOST}:${INGRESS_PORT}/v1/models/iris-v1:predict" -d @/tmp/iris-input.json +``` \ No newline at end of file diff --git a/csi/README.md b/csi/README.md index 30404ce4..329a8189 100644 --- a/csi/README.md +++ b/csi/README.md @@ -1 +1,89 @@ -TODO \ No newline at end of file +# Model Registry Custom Storage Initializer + +This is a Model Registry specific implementation of a KServe Custom Storage Initializer (CSI). +More details on what `Custom Storage Initializer` is can be found in the [KServe doc](https://kserve.github.io/website/0.11/modelserving/storage/storagecontainers/). + +## Implementation + +The Model Registry CSI is a simple Go executable that basically takes two positional arguments: +1. __Source URI__: identifies the `storageUri` set in the `InferenceService`, this must be a model-registry custom URI, i.e., `model-registry://...` +2. __Deestination Path__: the location where the model should be stored, e.g., `/mnt/models` + +The core logic of this CSI is pretty simple and it consists of three main steps: +1. Parse the custom URI in order to extract `registered model name` and `model version` +2. Query the model registry in order to retrieve the original model location (e.g., `http`, `s3`, `gcs` and so on) +3. Use `github.com/kserve/kserve/pkg/agent/storage` pkg to actually download the model from well-known protocols. + +### Workflow + +The below sequence diagram should highlight the workflow when this CSI is injected into the KServe pod deployment. + +```mermaid +sequenceDiagram + actor U as User + participant MR as Model Registry + participant KC as KServe Controller + participant MD as Model Deployment (Pod) + participant MRSI as Model Registry Storage Initializer + U->>+MR: Register ML Model + MR-->>-U: Indexed Model + U->>U: Create InferenceService CR + Note right of U: The InferenceService should
point to the model registry
indexed model, e.g.,:
model-registry:/// + KC->>KC: React to InferenceService creation + KC->>+MD: Create Model Deployment + MD->>+MRSI: Initialization (Download Model) + MRSI->>MRSI: Parse URI + MRSI->>+MR: Fetch Model Metadata + MR-->>-MRSI: Model Metadata + Note over MR,MRSI: The main information that is fetched is the artifact URI which specifies the real model location, e.g.,: https://.. or s3://... + MRSI->>MRSI: Download Model + Note right of MRSI: The storage initializer will use
the KServe default providers
to download the model
based on the artifact URI + MRSI-->>-MD: Downloaded Model + MD->>-MD: Deploy Model +``` + + +## Get Started + +Please look at [Get Started](./GET_STARTED.md) guide for a very simple quickstart that showcases how this custom storage initializer can be used for ML models serving operations. + +## Development + +### Build the executable + +You can just run: +```bash +make build +``` + +Which wil create the executable under `bin/mr-storage-initializer`. + +### Run the executable + +You can run `main.go` (without building the executable) by running: +```bash +./bin/mr-storage-initializer "model-registry://model/version" "./" +``` + +or directly running the `main.go` skipping the previous step: +```bash +make SOURCE_URI=model-registry://model/version DEST_PATH=./ run +``` + +> _NOTE_: a Model Registry service should be up and running at `localhost:8080`. + +### Build container image + +Run: +```bash +make docker-build +``` + +By default the container image name is `quay.io/${USER}/model-registry-storage-initializer:latest` but it can be overridden providing the `IMG` env variable, e.g., `make IMG=abc/ORG/NAME:TAG docker-build`. + +### Push container image + +Issue the following command: +```bash +make [IMG=..] docker-push +``` \ No newline at end of file diff --git a/csi/scripts/install_modelregistry.sh b/csi/scripts/install_modelregistry.sh new file mode 100755 index 00000000..8f63c31f --- /dev/null +++ b/csi/scripts/install_modelregistry.sh @@ -0,0 +1,48 @@ +set -e +set -o xtrace + +############################################################ +# Help # +############################################################ +Help() { + # Display Help + echo "ModelRegistry install script." + echo + echo "Syntax: [-n NAMESPACE] [-i IMAGE]" + echo "options:" + echo "n Namespace." + echo "i Model registry image." + echo +} + +MR_ROOT=".." + +namespace=kubeflow +image=quay.io/opendatahub/model-registry:latest +while getopts ":hn:i:" option; do + case $option in + h) # display Help + Help + exit;; + n) # override namespace + namespace=$OPTARG;; + i) # override model registry image + image=$OPTARG;; + \?) # Invalid option + echo "Error: Invalid option" + exit;; + esac +done + +# Create namespace if not already existing +if ! kubectl get namespace "$namespace" &> /dev/null; then + kubectl create namespace $namespace +fi +# Apply model-registry kustomize manifests +echo Using model registry image: $image +cd $MR_ROOT/manifests/kustomize/base && kustomize edit set image quay.io/opendatahub/model-registry:latest=${image} && cd - +kubectl -n $namespace apply -k "$MR_ROOT/manifests/kustomize/overlays/postgres" + +# Wait for model registry deployment +modelregistry=$(kubectl get pod -n kubeflow --selector="component=model-registry-server" --output jsonpath='{.items[0].metadata.name}') +kubectl wait --for=condition=Ready pod/$modelregistry -n $namespace --timeout=5m \ No newline at end of file