From ce5c4bc4c14454c9146215bd0a6f7f4481e8322b Mon Sep 17 00:00:00 2001 From: Joaquin Date: Mon, 15 Apr 2024 08:45:27 +0300 Subject: [PATCH 01/20] update contributors file --- CONTRIBUTORS.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1b7e320..48b91fb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -2,7 +2,7 @@ This file lists all the individuals who have contributed to this project. Thanks to each and every one of you for your valuable contributions! -### Silo AI: +### [Silo AI](https://www.silo.ai/): Original project leads and main developers: @@ -40,12 +40,14 @@ Other developers and testers of the platform: - Kristian Sikiric - Kaustav Tamuly - Jonathan Burdge +- Ammar Aldhahyani -### IML4E project and other contributors: +### [IML4E](https://itea4.org/project/iml4e.html) project and other contributors: Univerwity of Helsinki: - Niila Siilasjoki +- Dennis Muiruri Fraunhofer Institute: From 483579da1221200006074c62d0f77504d6eb1a79 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Mon, 15 Apr 2024 11:42:25 +0300 Subject: [PATCH 02/20] refactor to allow a more modular installation with different component combination options --- .gitignore | 3 +- config.env | 4 +- deployment/README.md | 14 +- .../kserve-custom/base}/kserve-sa.yaml | 2 - .../kserve-custom/base}/kustomization.yaml | 1 - .../env/kubeflow/kustomization.yaml | 7 + .../kserve-inference-namespace.yaml | 4 + .../env/standalone-kfp/kustomization.yaml | 8 + .../kubeflow-custom/base}/aws-secret.yaml | 1 - .../kubeflow-custom/base/kustomization.yaml | 5 + .../env/kubeflow/kustomization.yaml | 7 + .../kserve-deployer.yaml | 110 +++++++++++++ .../standalone-kfp-kserve/kustomization.yaml | 8 + .../env/standalone-kfp/kustomization.yaml | 7 + .../kubeflow-monitoring/kustomization.yaml | 9 ++ deployment/envs/kubeflow/kustomization.yaml | 8 + .../kustomization.yaml | 9 ++ .../standalone-kfp-kserve/kustomization.yaml | 8 + .../kustomization.yaml | 8 + .../envs/standalone-kfp/kustomization.yaml | 7 + .../in-cluster-setup/kubeflow/README.md | 1 + .../kubeflow/kustomization.yaml | 87 +++++++++++ .../in-cluster-setup/kustomization.yaml | 87 ----------- .../standalone-kfp-kserve/README.md | 1 + .../standalone-kfp-kserve/kustomization.yaml | 57 +++++++ .../in-cluster-setup/standalone-kfp/README.md | 1 + .../standalone-kfp/kustomization.yaml | 54 +++++++ deployment/kustomization.yaml | 8 - .../monitoring/alert-manager/deployment.yaml | 2 +- .../grafana/grafana-deployment.yaml | 2 +- scripts/create_cluster.sh | 6 +- scripts/install_helm.sh | 2 +- scripts/install_local_registry.sh | 2 +- scripts/install_ray.sh | 2 +- scripts/install_tools.sh | 2 +- scripts/install_tools_mac.sh | 2 +- scripts/run_tests.sh | 2 +- setup.md | 45 +++--- setup.sh | 146 +++++++++++++++--- tests/conftest.py | 7 +- tests/resources/kfp/build_image.sh | 2 +- tests/resources/registry/build_push_image.sh | 3 - 42 files changed, 590 insertions(+), 161 deletions(-) rename deployment/{kubeflow-custom => custom/kserve-custom/base}/kserve-sa.yaml (83%) rename deployment/{kubeflow-custom => custom/kserve-custom/base}/kustomization.yaml (83%) create mode 100644 deployment/custom/kserve-custom/env/kubeflow/kustomization.yaml create mode 100644 deployment/custom/kserve-custom/env/standalone-kfp/kserve-inference-namespace.yaml create mode 100644 deployment/custom/kserve-custom/env/standalone-kfp/kustomization.yaml rename deployment/{kubeflow-custom => custom/kubeflow-custom/base}/aws-secret.yaml (86%) create mode 100644 deployment/custom/kubeflow-custom/base/kustomization.yaml create mode 100644 deployment/custom/kubeflow-custom/env/kubeflow/kustomization.yaml create mode 100644 deployment/custom/kubeflow-custom/env/standalone-kfp-kserve/kserve-deployer.yaml create mode 100644 deployment/custom/kubeflow-custom/env/standalone-kfp-kserve/kustomization.yaml create mode 100644 deployment/custom/kubeflow-custom/env/standalone-kfp/kustomization.yaml create mode 100644 deployment/envs/kubeflow-monitoring/kustomization.yaml create mode 100644 deployment/envs/kubeflow/kustomization.yaml create mode 100644 deployment/envs/standalone-kfp-kserve-monitoring/kustomization.yaml create mode 100644 deployment/envs/standalone-kfp-kserve/kustomization.yaml create mode 100644 deployment/envs/standalone-kfp-monitoring/kustomization.yaml create mode 100644 deployment/envs/standalone-kfp/kustomization.yaml create mode 100644 deployment/kubeflow/manifests/in-cluster-setup/kubeflow/README.md create mode 100644 deployment/kubeflow/manifests/in-cluster-setup/kubeflow/kustomization.yaml delete mode 100644 deployment/kubeflow/manifests/in-cluster-setup/kustomization.yaml create mode 100644 deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/README.md create mode 100644 deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/kustomization.yaml create mode 100644 deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/README.md create mode 100644 deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/kustomization.yaml delete mode 100644 deployment/kustomization.yaml diff --git a/.gitignore b/.gitignore index 2bc18e4..9094443 100644 --- a/.gitignore +++ b/.gitignore @@ -62,4 +62,5 @@ tutorials/openstack/secure.yaml # Others old/ istio* -temp/ \ No newline at end of file +temp/ +.platform/ \ No newline at end of file diff --git a/config.env b/config.env index c0a7bdc..90f77b7 100644 --- a/config.env +++ b/config.env @@ -1,4 +1,2 @@ HOST_IP="127.0.0.1" -CLUSTER_NAME="kind-ep" -INSTALL_LOCAL_REGISTRY="true" -INSTALL_RAY="false" \ No newline at end of file +CLUSTER_NAME="mlops-platform" \ No newline at end of file diff --git a/deployment/README.md b/deployment/README.md index fb32127..49a2fff 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -1,7 +1,19 @@ ## Deploy the stack +Choose the deployment option that best fits your needs: +1. `kubeflow-monitoring`: Full Kubeflow deployment with all components. +2. `kubeflow`: Full Kubeflow deployment without monitoring components (prometheus, grafana). +3. `standalone-kfp-monitoring`: Standalone KFP deployment. +4. `standalone-kfp`: Standalone KFP deployment without monitoring components (prometheus, grafana). +5. `standalone-kfp-kserve-monitoring`: Standalone KFP and Kserve deployment. +6. `standalone-kfp-kserve`: Standalone KFP and Kserve deployment without monitoring components (prometheus, grafana). + +```bash +export DEPLOYMENT_OPTION=kubeflow-monitoring +``` + Deploy to your kubernetes cluster with the following command: ```bash -while ! kustomize build deployment | kubectl apply -f -; do echo "Retrying to apply resources"; sleep 10; done +while ! kustomize build "deployment/envs/$DEPLOYMENT_OPTION" | kubectl apply -f -; do echo "Retrying to apply resources"; sleep 10; done ``` \ No newline at end of file diff --git a/deployment/kubeflow-custom/kserve-sa.yaml b/deployment/custom/kserve-custom/base/kserve-sa.yaml similarity index 83% rename from deployment/kubeflow-custom/kserve-sa.yaml rename to deployment/custom/kserve-custom/base/kserve-sa.yaml index dd2d9cb..a300f81 100644 --- a/deployment/kubeflow-custom/kserve-sa.yaml +++ b/deployment/custom/kserve-custom/base/kserve-sa.yaml @@ -2,7 +2,6 @@ apiVersion: v1 kind: Secret metadata: name: mysecret - namespace: kubeflow-user-example-com annotations: serving.kserve.io/s3-endpoint: mlflow-minio-service.mlflow.svc.cluster.local:9000 serving.kserve.io/s3-usehttps: "0" @@ -15,6 +14,5 @@ apiVersion: v1 kind: ServiceAccount metadata: name: kserve-sa - namespace: kubeflow-user-example-com secrets: - name: mysecret diff --git a/deployment/kubeflow-custom/kustomization.yaml b/deployment/custom/kserve-custom/base/kustomization.yaml similarity index 83% rename from deployment/kubeflow-custom/kustomization.yaml rename to deployment/custom/kserve-custom/base/kustomization.yaml index 2bf776f..12b6241 100644 --- a/deployment/kubeflow-custom/kustomization.yaml +++ b/deployment/custom/kserve-custom/base/kustomization.yaml @@ -2,5 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- aws-secret.yaml - kserve-sa.yaml \ No newline at end of file diff --git a/deployment/custom/kserve-custom/env/kubeflow/kustomization.yaml b/deployment/custom/kserve-custom/env/kubeflow/kustomization.yaml new file mode 100644 index 0000000..0ad2c65 --- /dev/null +++ b/deployment/custom/kserve-custom/env/kubeflow/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: kubeflow-user-example-com + +resources: +- ../../base diff --git a/deployment/custom/kserve-custom/env/standalone-kfp/kserve-inference-namespace.yaml b/deployment/custom/kserve-custom/env/standalone-kfp/kserve-inference-namespace.yaml new file mode 100644 index 0000000..950c92b --- /dev/null +++ b/deployment/custom/kserve-custom/env/standalone-kfp/kserve-inference-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kserve-inference \ No newline at end of file diff --git a/deployment/custom/kserve-custom/env/standalone-kfp/kustomization.yaml b/deployment/custom/kserve-custom/env/standalone-kfp/kustomization.yaml new file mode 100644 index 0000000..762483e --- /dev/null +++ b/deployment/custom/kserve-custom/env/standalone-kfp/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: kserve-inference + +resources: + - ../../base + - kserve-inference-namespace.yaml diff --git a/deployment/kubeflow-custom/aws-secret.yaml b/deployment/custom/kubeflow-custom/base/aws-secret.yaml similarity index 86% rename from deployment/kubeflow-custom/aws-secret.yaml rename to deployment/custom/kubeflow-custom/base/aws-secret.yaml index b63fb1e..7203f52 100644 --- a/deployment/kubeflow-custom/aws-secret.yaml +++ b/deployment/custom/kubeflow-custom/base/aws-secret.yaml @@ -2,7 +2,6 @@ apiVersion: v1 kind: Secret metadata: name: aws-secret - namespace: kubeflow-user-example-com type: Opaque data: # your BASE64 encoded AWS_ACCESS_KEY_ID diff --git a/deployment/custom/kubeflow-custom/base/kustomization.yaml b/deployment/custom/kubeflow-custom/base/kustomization.yaml new file mode 100644 index 0000000..e6ae779 --- /dev/null +++ b/deployment/custom/kubeflow-custom/base/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- aws-secret.yaml \ No newline at end of file diff --git a/deployment/custom/kubeflow-custom/env/kubeflow/kustomization.yaml b/deployment/custom/kubeflow-custom/env/kubeflow/kustomization.yaml new file mode 100644 index 0000000..0ad2c65 --- /dev/null +++ b/deployment/custom/kubeflow-custom/env/kubeflow/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: kubeflow-user-example-com + +resources: +- ../../base diff --git a/deployment/custom/kubeflow-custom/env/standalone-kfp-kserve/kserve-deployer.yaml b/deployment/custom/kubeflow-custom/env/standalone-kfp-kserve/kserve-deployer.yaml new file mode 100644 index 0000000..0ac0071 --- /dev/null +++ b/deployment/custom/kubeflow-custom/env/standalone-kfp-kserve/kserve-deployer.yaml @@ -0,0 +1,110 @@ +# Required for deploy model to have the necessery permissions to create inference services + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kserve-deployer +rules: + - verbs: + - '*' + apiGroups: + - '' + resources: + - secrets + - serviceaccounts + - verbs: + - get + - watch + - list + apiGroups: + - '' + resources: + - configmaps + - verbs: + - '*' + apiGroups: + - '' + resources: + - persistentvolumes + - persistentvolumeclaims + - verbs: + - create + - delete + - get + apiGroups: + - snapshot.storage.k8s.io + resources: + - volumesnapshots + - verbs: + - get + - list + - watch + - update + - patch + apiGroups: + - argoproj.io + resources: + - workflows + - verbs: + - '*' + apiGroups: + - '' + resources: + - pods + - pods/exec + - pods/log + - services + - verbs: + - '*' + apiGroups: + - '' + - apps + - extensions + resources: + - deployments + - replicasets + - verbs: + - '*' + apiGroups: + - kubeflow.org + resources: + - '*' + - verbs: + - '*' + apiGroups: + - batch + resources: + - jobs + - verbs: + - '*' + apiGroups: + - machinelearning.seldon.io + resources: + - seldondeployments + - verbs: + - '*' + apiGroups: + - serving.kserve.io + resources: + - '*' + - verbs: + - '*' + apiGroups: + - networking.istio.io + resources: + - '*' +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: pipeline-runner-binding-cluster + labels: + application-crd-id: kubeflow-pipelines +subjects: + - kind: ServiceAccount + name: pipeline-runner + namespace: kubeflow +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kserve-deployer \ No newline at end of file diff --git a/deployment/custom/kubeflow-custom/env/standalone-kfp-kserve/kustomization.yaml b/deployment/custom/kubeflow-custom/env/standalone-kfp-kserve/kustomization.yaml new file mode 100644 index 0000000..1eec75e --- /dev/null +++ b/deployment/custom/kubeflow-custom/env/standalone-kfp-kserve/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: kubeflow + +resources: + - ../../base + - kserve-deployer.yaml diff --git a/deployment/custom/kubeflow-custom/env/standalone-kfp/kustomization.yaml b/deployment/custom/kubeflow-custom/env/standalone-kfp/kustomization.yaml new file mode 100644 index 0000000..1241abe --- /dev/null +++ b/deployment/custom/kubeflow-custom/env/standalone-kfp/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: kubeflow + +resources: + - ../../base diff --git a/deployment/envs/kubeflow-monitoring/kustomization.yaml b/deployment/envs/kubeflow-monitoring/kustomization.yaml new file mode 100644 index 0000000..3e7c6ce --- /dev/null +++ b/deployment/envs/kubeflow-monitoring/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../kubeflow/manifests/in-cluster-setup/kubeflow +- ../../custom/kubeflow-custom/env/kubeflow +- ../../custom/kserve-custom/env/kubeflow +- ../../mlflow/env/local +- ../../monitoring \ No newline at end of file diff --git a/deployment/envs/kubeflow/kustomization.yaml b/deployment/envs/kubeflow/kustomization.yaml new file mode 100644 index 0000000..c4c1ca5 --- /dev/null +++ b/deployment/envs/kubeflow/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../kubeflow/manifests/in-cluster-setup/kubeflow +- ../../custom/kubeflow-custom/env/kubeflow +- ../../custom/kserve-custom/env/kubeflow +- ../../mlflow/env/local \ No newline at end of file diff --git a/deployment/envs/standalone-kfp-kserve-monitoring/kustomization.yaml b/deployment/envs/standalone-kfp-kserve-monitoring/kustomization.yaml new file mode 100644 index 0000000..9a23144 --- /dev/null +++ b/deployment/envs/standalone-kfp-kserve-monitoring/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve +- ../../custom/kubeflow-custom/env/standalone-kfp +- ../../custom/kserve-custom/env/standalone-kfp +- ../../mlflow/env/local +- ../../monitoring \ No newline at end of file diff --git a/deployment/envs/standalone-kfp-kserve/kustomization.yaml b/deployment/envs/standalone-kfp-kserve/kustomization.yaml new file mode 100644 index 0000000..c090b17 --- /dev/null +++ b/deployment/envs/standalone-kfp-kserve/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve +- ../../custom/kubeflow-custom/env/standalone-kfp +- ../../custom/kserve-custom/env/standalone-kfp +- ../../mlflow/env/local \ No newline at end of file diff --git a/deployment/envs/standalone-kfp-monitoring/kustomization.yaml b/deployment/envs/standalone-kfp-monitoring/kustomization.yaml new file mode 100644 index 0000000..696c68b --- /dev/null +++ b/deployment/envs/standalone-kfp-monitoring/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../kubeflow/manifests/in-cluster-setup/standalone-kfp +- ../../custom/kubeflow-custom/env/standalone-kfp +- ../../mlflow/env/local +- ../../monitoring \ No newline at end of file diff --git a/deployment/envs/standalone-kfp/kustomization.yaml b/deployment/envs/standalone-kfp/kustomization.yaml new file mode 100644 index 0000000..16cc703 --- /dev/null +++ b/deployment/envs/standalone-kfp/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../kubeflow/manifests/in-cluster-setup/standalone-kfp +- ../../custom/kubeflow-custom/env/standalone-kfp +- ../../mlflow/env/local \ No newline at end of file diff --git a/deployment/kubeflow/manifests/in-cluster-setup/kubeflow/README.md b/deployment/kubeflow/manifests/in-cluster-setup/kubeflow/README.md new file mode 100644 index 0000000..f87f5c1 --- /dev/null +++ b/deployment/kubeflow/manifests/in-cluster-setup/kubeflow/README.md @@ -0,0 +1 @@ +# TODO \ No newline at end of file diff --git a/deployment/kubeflow/manifests/in-cluster-setup/kubeflow/kustomization.yaml b/deployment/kubeflow/manifests/in-cluster-setup/kubeflow/kustomization.yaml new file mode 100644 index 0000000..d3c11cf --- /dev/null +++ b/deployment/kubeflow/manifests/in-cluster-setup/kubeflow/kustomization.yaml @@ -0,0 +1,87 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +sortOptions: + order: legacy + legacySortOptions: + orderFirst: + - Namespace + - ResourceQuota + - StorageClass + - CustomResourceDefinition + - MutatingWebhookConfiguration + - ServiceAccount + - PodSecurityPolicy + - Role + - ClusterRole + - RoleBinding + - ClusterRoleBinding + - ConfigMap + - Secret + - Endpoints + - Service + - LimitRange + - PriorityClass + - PersistentVolume + - PersistentVolumeClaim + - Deployment + - StatefulSet + - CronJob + - PodDisruptionBudget + orderLast: + - ValidatingWebhookConfiguration + +resources: +# Cert-Manager +- ../../common/cert-manager/cert-manager/base +- ../../common/cert-manager/kubeflow-issuer/base +# Istio +- ../../common/istio-1-17/istio-crds/base +- ../../common/istio-1-17/istio-namespace/base +- ../../common/istio-1-17/istio-install/base +# OIDC Authservice +- ../../common/oidc-client/oidc-authservice/base +# Dex +- ../../common/dex/overlays/istio +# KNative +- ../../common/knative/knative-serving/overlays/gateways +- ../../common/knative/knative-eventing/base +- ../../common/istio-1-17/cluster-local-gateway/base +# Kubeflow namespace +- ../../common/kubeflow-namespace/base +# Kubeflow Roles +- ../../common/kubeflow-roles/base +# Kubeflow Istio Resources +- ../../common/istio-1-17/kubeflow-istio-resources/base + + +# Kubeflow Pipelines +- ../../apps/pipeline/upstream/env/cert-manager/platform-agnostic-multi-user +# Katib +- ../../apps/katib/upstream/installs/katib-with-kubeflow +# Central Dashboard +- ../../apps/centraldashboard/upstream/overlays/kserve +# Admission Webhook +- ../../apps/admission-webhook/upstream/overlays/cert-manager +# Jupyter Web App +- ../../apps/jupyter/jupyter-web-app/upstream/overlays/istio +# Notebook Controller +- ../../apps/jupyter/notebook-controller/upstream/overlays/kubeflow +# Profiles + KFAM +- ../../apps/profiles/upstream/overlays/kubeflow +# PVC Viewer +- ../../apps/pvcviewer-controller/upstream/base/ +# Volumes Web App +- ../../apps/volumes-web-app/upstream/overlays/istio +# Tensorboards Controller +- ../../apps/tensorboard/tensorboard-controller/upstream/overlays/kubeflow +# Tensorboard Web App +- ../../apps/tensorboard/tensorboards-web-app/upstream/overlays/istio +# Training Operator +- ../../apps/training-operator/upstream/overlays/kubeflow +# User namespace +- ../../common/user-namespace/base + +# KServe +- ../../contrib/kserve/kserve +- ../../contrib/kserve/models-web-app/overlays/kubeflow diff --git a/deployment/kubeflow/manifests/in-cluster-setup/kustomization.yaml b/deployment/kubeflow/manifests/in-cluster-setup/kustomization.yaml deleted file mode 100644 index c1a8578..0000000 --- a/deployment/kubeflow/manifests/in-cluster-setup/kustomization.yaml +++ /dev/null @@ -1,87 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -sortOptions: - order: legacy - legacySortOptions: - orderFirst: - - Namespace - - ResourceQuota - - StorageClass - - CustomResourceDefinition - - MutatingWebhookConfiguration - - ServiceAccount - - PodSecurityPolicy - - Role - - ClusterRole - - RoleBinding - - ClusterRoleBinding - - ConfigMap - - Secret - - Endpoints - - Service - - LimitRange - - PriorityClass - - PersistentVolume - - PersistentVolumeClaim - - Deployment - - StatefulSet - - CronJob - - PodDisruptionBudget - orderLast: - - ValidatingWebhookConfiguration - -resources: -# Cert-Manager -- ../common/cert-manager/cert-manager/base -- ../common/cert-manager/kubeflow-issuer/base -# Istio -- ../common/istio-1-17/istio-crds/base -- ../common/istio-1-17/istio-namespace/base -- ../common/istio-1-17/istio-install/base -# OIDC Authservice -- ../common/oidc-client/oidc-authservice/base -# Dex -- ../common/dex/overlays/istio -# KNative -- ../common/knative/knative-serving/overlays/gateways -- ../common/knative/knative-eventing/base -- ../common/istio-1-17/cluster-local-gateway/base -# Kubeflow namespace -- ../common/kubeflow-namespace/base -# Kubeflow Roles -- ../common/kubeflow-roles/base -# Kubeflow Istio Resources -- ../common/istio-1-17/kubeflow-istio-resources/base - - -# Kubeflow Pipelines -- ../apps/pipeline/upstream/env/cert-manager/platform-agnostic-multi-user -# Katib -- ../apps/katib/upstream/installs/katib-with-kubeflow -# Central Dashboard -- ../apps/centraldashboard/upstream/overlays/kserve -# Admission Webhook -- ../apps/admission-webhook/upstream/overlays/cert-manager -# Jupyter Web App -- ../apps/jupyter/jupyter-web-app/upstream/overlays/istio -# Notebook Controller -- ../apps/jupyter/notebook-controller/upstream/overlays/kubeflow -# Profiles + KFAM -- ../apps/profiles/upstream/overlays/kubeflow -# PVC Viewer -- ../apps/pvcviewer-controller/upstream/base/ -# Volumes Web App -- ../apps/volumes-web-app/upstream/overlays/istio -# Tensorboards Controller -- ../apps/tensorboard/tensorboard-controller/upstream/overlays/kubeflow -# Tensorboard Web App -- ../apps/tensorboard/tensorboards-web-app/upstream/overlays/istio -# Training Operator -- ../apps/training-operator/upstream/overlays/kubeflow -# User namespace -- ../common/user-namespace/base - -# KServe -- ../contrib/kserve/kserve -- ../contrib/kserve/models-web-app/overlays/kubeflow diff --git a/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/README.md b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/README.md new file mode 100644 index 0000000..f87f5c1 --- /dev/null +++ b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/README.md @@ -0,0 +1 @@ +# TODO \ No newline at end of file diff --git a/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/kustomization.yaml b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/kustomization.yaml new file mode 100644 index 0000000..d4e9892 --- /dev/null +++ b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/kustomization.yaml @@ -0,0 +1,57 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +sortOptions: + order: legacy + legacySortOptions: + orderFirst: + - Namespace + - ResourceQuota + - StorageClass + - CustomResourceDefinition + - MutatingWebhookConfiguration + - ServiceAccount + - PodSecurityPolicy + - Role + - ClusterRole + - RoleBinding + - ClusterRoleBinding + - ConfigMap + - Secret + - Endpoints + - Service + - LimitRange + - PriorityClass + - PersistentVolume + - PersistentVolumeClaim + - Deployment + - StatefulSet + - CronJob + - PodDisruptionBudget + orderLast: + - ValidatingWebhookConfiguration + +resources: +# Cert-Manager +- ../../common/cert-manager/cert-manager/base +- ../../common/cert-manager/kubeflow-issuer/base + +# Istio +- ../../common/istio-1-17/istio-crds/base +- ../../common/istio-1-17/istio-namespace/base +- ../../common/istio-1-17/istio-install/base + +# KNative +- ../../common/knative/knative-serving/overlays/gateways +- ../../common/knative/knative-eventing/base +- ../../common/istio-1-17/cluster-local-gateway/base + +# Kubeflow Istio Resources +- ../../common/istio-1-17/kubeflow-istio-resources/base + +# Kubeflow Pipelines +- ../../apps/pipeline/upstream/cluster-scoped-resources +- ../../apps/pipeline/upstream/env/platform-agnostic-emissary + +# KServe +- ../../contrib/kserve/kserve \ No newline at end of file diff --git a/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/README.md b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/README.md new file mode 100644 index 0000000..f87f5c1 --- /dev/null +++ b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/README.md @@ -0,0 +1 @@ +# TODO \ No newline at end of file diff --git a/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/kustomization.yaml b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/kustomization.yaml new file mode 100644 index 0000000..a92789a --- /dev/null +++ b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/kustomization.yaml @@ -0,0 +1,54 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +sortOptions: + order: legacy + legacySortOptions: + orderFirst: + - Namespace + - ResourceQuota + - StorageClass + - CustomResourceDefinition + - MutatingWebhookConfiguration + - ServiceAccount + - PodSecurityPolicy + - Role + - ClusterRole + - RoleBinding + - ClusterRoleBinding + - ConfigMap + - Secret + - Endpoints + - Service + - LimitRange + - PriorityClass + - PersistentVolume + - PersistentVolumeClaim + - Deployment + - StatefulSet + - CronJob + - PodDisruptionBudget + orderLast: + - ValidatingWebhookConfiguration + +resources: +# Cert-Manager +- ../../common/cert-manager/cert-manager/base +- ../../common/cert-manager/kubeflow-issuer/base + +# Istio +- ../../common/istio-1-17/istio-crds/base +- ../../common/istio-1-17/istio-namespace/base +- ../../common/istio-1-17/istio-install/base + +# KNative +- ../../common/knative/knative-serving/overlays/gateways +- ../../common/knative/knative-eventing/base +- ../../common/istio-1-17/cluster-local-gateway/base + +# Kubeflow Istio Resources +- ../../common/istio-1-17/kubeflow-istio-resources/base + +# Kubeflow Pipelines +- ../../apps/pipeline/upstream/cluster-scoped-resources +- ../../apps/pipeline/upstream/env/platform-agnostic-emissary \ No newline at end of file diff --git a/deployment/kustomization.yaml b/deployment/kustomization.yaml deleted file mode 100644 index c5d654e..0000000 --- a/deployment/kustomization.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- ./kubeflow/manifests/in-cluster-setup -- ./kubeflow-custom -- ./mlflow/env/local -- ./monitoring \ No newline at end of file diff --git a/deployment/monitoring/alert-manager/deployment.yaml b/deployment/monitoring/alert-manager/deployment.yaml index 2e44389..96028d2 100644 --- a/deployment/monitoring/alert-manager/deployment.yaml +++ b/deployment/monitoring/alert-manager/deployment.yaml @@ -25,7 +25,7 @@ spec: containerPort: 9093 resources: requests: - cpu: 500m + cpu: 250m memory: 500M limits: cpu: 1 diff --git a/deployment/monitoring/grafana/grafana-deployment.yaml b/deployment/monitoring/grafana/grafana-deployment.yaml index 71032e1..8e3144e 100644 --- a/deployment/monitoring/grafana/grafana-deployment.yaml +++ b/deployment/monitoring/grafana/grafana-deployment.yaml @@ -28,7 +28,7 @@ spec: cpu: "1000m" requests: memory: 500M - cpu: "500m" + cpu: "250m" volumeMounts: - mountPath: /var/lib/grafana name: grafana-storage diff --git a/scripts/create_cluster.sh b/scripts/create_cluster.sh index bbe9697..ae01799 100755 --- a/scripts/create_cluster.sh +++ b/scripts/create_cluster.sh @@ -1,11 +1,11 @@ #!/bin/bash -set -xeoa pipefail +set -eoa pipefail ####################################################################################### # Create and configure a cluster with Kind # -# Usage: $ export HOST_IP=127.0.0.1; export CLUSTER_NAME="kind-ep"; ./create_cluster.sh +# Usage: $ export HOST_IP=127.0.0.1; export CLUSTER_NAME="mlops-platform"; ./create_cluster.sh ####################################################################################### @@ -67,7 +67,7 @@ fi # see https://github.com/kubernetes-sigs/kind/issues/2586 -CONTAINER_ID=$(docker ps -aqf "name=kind-ep-control-plane") +CONTAINER_ID=$(docker ps -aqf "name=$CLUSTER_NAME-control-plane") docker exec -t ${CONTAINER_ID} bash -c "echo 'fs.inotify.max_user_watches=1048576' >> /etc/sysctl.conf" docker exec -t ${CONTAINER_ID} bash -c "echo 'fs.inotify.max_user_instances=512' >> /etc/sysctl.conf" docker exec -i ${CONTAINER_ID} bash -c "sysctl -p /etc/sysctl.conf" diff --git a/scripts/install_helm.sh b/scripts/install_helm.sh index 4a15055..ef44c43 100644 --- a/scripts/install_helm.sh +++ b/scripts/install_helm.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -xeo pipefail +set -eo pipefail function add_local_bin_to_path { # make sure ~/.local/bin is in $PATH diff --git a/scripts/install_local_registry.sh b/scripts/install_local_registry.sh index 4665d4b..3f1844b 100755 --- a/scripts/install_local_registry.sh +++ b/scripts/install_local_registry.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -xeoa pipefail +set -eoa pipefail ####################################################################################### # The following shell script will create a local docker registry and connect the diff --git a/scripts/install_ray.sh b/scripts/install_ray.sh index b53d539..1e1a864 100644 --- a/scripts/install_ray.sh +++ b/scripts/install_ray.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -xeo pipefail +set -eo pipefail helm repo add kuberay https://ray-project.github.io/kuberay-helm/ helm repo update diff --git a/scripts/install_tools.sh b/scripts/install_tools.sh index d4dc587..105415f 100755 --- a/scripts/install_tools.sh +++ b/scripts/install_tools.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -xeoa pipefail +set -eoa pipefail ####################################################################################### # CHECK PRE-REQUISITES diff --git a/scripts/install_tools_mac.sh b/scripts/install_tools_mac.sh index ad87a1b..a7c30c9 100644 --- a/scripts/install_tools_mac.sh +++ b/scripts/install_tools_mac.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -xeoa pipefail +set -eoa pipefail ####################################################################################### # CHECK PRE-REQUISITES diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index ea1cf7d..0df3505 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -xeoa pipefail +set -eoa pipefail ####################################################################################### # RUN TESTS diff --git a/setup.md b/setup.md index bf89b2e..9bed5b0 100644 --- a/setup.md +++ b/setup.md @@ -15,6 +15,16 @@ Install the experimentation platform with: > **WARNING:** Using the `--test` flag will install the `requirements-tests.txt` in your current python environment. +## Deployment options + +1. **Kubeflow:** Full Kubeflow deployment with all components. +2. **Kubeflow (without monitoring):** Full Kubeflow deployment without monitoring components (prometheus, grafana). +3. **Standalone KFP:** Standalone KFP deployment. +4. **Standalone KFP (without monitoring):** Standalone KFP deployment without monitoring components (prometheus, grafana). +5. **Standalone KFP and Kserve:** Standalone KFP and Kserve deployment. +6. **Standalone KFP and Kserve (without monitoring):** Standalone KFP and Kserve deployment without monitoring components (prometheus, grafana). + + ## Test the deployment (manually) If you just deployed the platform, it will take a while to become ready. You can use @@ -40,15 +50,27 @@ pytest tests/ [-vrP] [--log-cli-level=INFO] *These are the same tests that are run automatically if you use the `--test` flag on installation.* -## Deleting the deployment +## Uninstall + +Uninstall the MLOps Platform with: -Delete the cluster: ```bash -# e.g. $ kind delete cluster --name kind-ep +./uninstall.sh +``` + +### Manual deletion + +The `uninstall.sh` script should delete everything, but if you need to manually remove the platform, you can do it with: + +```bash +# list kind clusters +kind get clusters + +# delete the kind cluster kind delete cluster --name [CLUSTER_NAME] ``` -If you also installed the local docker registry (`config.env` > `INSTALL_LOCAL_REGISTRY="true"`): +If you also installed the local docker registry: ```bash # check if it is running (kind-registry) @@ -68,20 +90,7 @@ docker rm -f $(docker ps -aqf "name=kind-registry") ### Error: namespace "kubeflow-user-example-com" not found This is not an error, and it is expected. Some of the things being deployed depend on other components, which need to be deployed and become ready first. -For example, the namespace `kubeflow-user-example-com` is created by a `kubeflow` component. That's why we deploy in a loop until everything is applied successfully: - -```bash -while true; do - if kubectl apply -f "$tmpfile"; then - echo "Resources successfully applied." - rm "$tmpfile" - break - else - echo "Retrying to apply resources. Be patient, this might take a while..." - sleep 10 - fi -done -``` +For example, the namespace `kubeflow-user-example-com` is created by a `kubeflow` component. That's why we deploy in a loop until everything is applied successfully. Once the main `kubeflow` deployment is ready, the `kubeflow-user-example-com` namespace will be created, and the command should finish successfully. diff --git a/setup.sh b/setup.sh index 2304afb..6c5dafc 100755 --- a/setup.sh +++ b/setup.sh @@ -1,8 +1,15 @@ #!/bin/bash -set -xeoa pipefail +set -eoa pipefail -source config.env +# Internal directory where to store platform settings +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +PLATFORM_DIR="$SCRIPT_DIR/.platform" +mkdir -p "$PLATFORM_DIR" +PLATFORM_CONFIG="$PLATFORM_DIR/.config" +cp "$SCRIPT_DIR/config.env" $PLATFORM_CONFIG + +source $PLATFORM_CONFIG RUN_TESTS=false LOG_LEVEL_TESTS="WARNING" @@ -23,13 +30,66 @@ echo Cluster name set to: "$CLUSTER_NAME" echo Host IP set to: "$HOST_IP" echo Run tests after installation set to: "$RUN_TESTS" +DEFAULT_DEPLOYMENT_OPTION="kubeflow-monitoring" +echo +echo "Please choose the deployment option:" +echo "[1] Kubeflow (all components)" +echo "[2] Kubeflow (without monitoring)" +echo "[3] Standalone KFP" +echo "[4] Standalone KFP (without monitoring)" +echo "[5] Standalone KFP and Kserve" +echo "[6] Standalone KFP and Kserve (without monitoring)" +read -p "Enter the number of your choice [1-6] (default is [1]): " choice +case "$choice" in + 1 ) DEPLOYMENT_OPTION="kubeflow-monitoring" ;; + 2 ) DEPLOYMENT_OPTION="kubeflow" ;; + 3 ) DEPLOYMENT_OPTION="standalone-kfp-monitoring" ;; + 4 ) DEPLOYMENT_OPTION="standalone-kfp" ;; + 5 ) DEPLOYMENT_OPTION="standalone-kfp-kserve-monitoring" ;; + 6 ) DEPLOYMENT_OPTION="standalone-kfp-kserve" ;; + * ) DEPLOYMENT_OPTION="$DEFAULT_DEPLOYMENT_OPTION" ;; +esac + +INSTALL_LOCAL_REGISTRY=true +echo +read -p "Install local Docker registry? (y/n) (default is [y]): " choice +case "$choice" in + n|N ) INSTALL_LOCAL_REGISTRY=false ;; + * ) INSTALL_LOCAL_REGISTRY=true ;; +esac + +INSTALL_RAY=false +echo +read -p "Install Ray? (y/n) (default is [n]): " choice +case "$choice" in + y|Y ) INSTALL_RAY=true ;; + * ) INSTALL_RAY=false ;; +esac + +# Save selections to settings file +echo -e "\nDEPLOYMENT_OPTION=$DEPLOYMENT_OPTION" >> $PLATFORM_CONFIG +echo -e "\nINSTALL_LOCAL_REGISTRY=$INSTALL_LOCAL_REGISTRY" >> $PLATFORM_CONFIG +echo -e "\nINSTALL_RAY=$INSTALL_RAY" >> $PLATFORM_CONFIG + # CHECK DISK SPACE -RECOMMENDED_DISK_SPACE=26214400 -RECOMMENDED_DISK_SPACE_GB=$(($RECOMMENDED_DISK_SPACE / 1024 / 1024)) +RECOMMENDED_DISK_SPACE_KUBEFLOW=26214400 +RECOMMENDED_DISK_SPACE_KUBEFLOW_GB=$(($RECOMMENDED_DISK_SPACE / 1024 / 1024)) +RECOMMENDED_DISK_SPACE_KFP=20971520 +RECOMMENDED_DISK_SPACE_KFP_GB=$(($RECOMMENDED_DISK_SPACE / 1024 / 1024)) + +if [[ $DEPLOYMENT_OPTION == *"kfp"* ]]; then + RECOMMENDED_DISK_SPACE=$RECOMMENDED_DISK_SPACE_KFP + RECOMMENDED_DISK_SPACE_GB=$RECOMMENDED_DISK_SPACE_KFP_GB +else + RECOMMENDED_DISK_SPACE=$RECOMMENDED_DISK_SPACE_KUBEFLOW + RECOMMENDED_DISK_SPACE_GB=$RECOMMENDED_DISK_SPACE_KUBEFLOW_GB +fi DISK_SPACE=$(df -k . | awk -F ' ' '{print $4}' | sed -n '2 p') DISK_SPACE_GB=$(($DISK_SPACE / 1024 / 1024)) +# TODO: Set required depending on the deployment, ray, etc. + if [[ DISK_SPACE < $RECOMMENDED_DISK_SPACE ]]; then echo "WARNING: Not enough disk space detected!" echo "The recommended is > ${RECOMMENDED_DISK_SPACE_GB} GB of disk space. You have ${DISK_SPACE_GB} GB." @@ -44,7 +104,19 @@ if [[ DISK_SPACE < $RECOMMENDED_DISK_SPACE ]]; then fi # CHECK CPU COUNT -RECOMMENDED_CPUS=16 +RECOMMENDED_CPUS_KUBEFLOW=12 +RECOMMENDED_CPUS_KFP=8 +EXTRA_RAY_CPUS=4 + +if [[ $DEPLOYMENT_OPTION == *"kfp"* ]]; then + RECOMMENDED_CPUS=$RECOMMENDED_CPUS_KFP +else + RECOMMENDED_CPUS=$RECOMMENDED_CPUS_KUBEFLOW +fi + +if [ "$INSTALL_RAY" = true ]; then + RECOMMENDED_CPUS=$(($RECOMMENDED_CPUS + $EXTRA_RAY_CPUS)) +fi # Detect the OS OS=$(uname) @@ -59,12 +131,13 @@ fi if [[ $CPU_COUNT -lt $RECOMMENDED_CPUS ]]; then echo "WARNING: Not enough CPU cores detected!" - echo "The recommended is >= ${RECOMMENDED_CPUS} CPU cores. You have ${CPU_COUNT} cores." + echo "The recommended is >= ${RECOMMENDED_CPUS} CPU cores for this deployment configuration. You have ${CPU_COUNT} cores." while true; do read -p "Do you want to continue with the installation? (y/n): " yn case $yn in [Yy]* ) break;; [Nn]* ) exit 1;; + "" ) echo "Please enter a response.";; * ) echo "Please answer yes or no.";; esac done @@ -72,9 +145,9 @@ fi # INSTALL TOOLS if [[ "$(uname)" == "Darwin" ]]; then - bash scripts/install_tools_mac.sh # Using default bash because /bin/bash is an old version (3) + bash "$SCRIPT_DIR/scripts/install_tools_mac.sh" # Using default bash because /bin/bash is an old version (3) else - /bin/bash scripts/install_tools.sh + /bin/bash "$SCRIPT_DIR/scripts/install_tools.sh" fi # CREATE CLUSTER @@ -83,30 +156,61 @@ function fail { exit "${2-1}" ## Return a code specified by $2, or 1 by default. } -/bin/bash scripts/create_cluster.sh || fail +# Check if the kind cluster already exists +if kind get clusters | grep -q "^$CLUSTER_NAME$"; then + echo + echo "Kind cluster with name \"$CLUSTER_NAME\" already exists. It can be deleted with the following command: kind delete cluster --name $CLUSTER_NAME" + while true; do + read -p "Do you want to continue the installation on the existing cluster? (y/n): " choice + case "$choice" in + y|Y ) echo "Using existing kind cluster..."; break;; + n|N ) exit 0 ;; + * ) echo "Invalid response. Please enter y or n." ;; + "" ) echo "Please enter a response." ;; + esac + done +else + echo "Creating kind cluster..." + /bin/bash "$SCRIPT_DIR/scripts/create_cluster.sh" +fi kubectl cluster-info --context kind-$CLUSTER_NAME # DEPLOY LOCAL DOCKER REGISTRY if [ "$INSTALL_LOCAL_REGISTRY" = true ]; then - /bin/bash scripts/install_local_registry.sh + /bin/bash "$SCRIPT_DIR/scripts/install_local_registry.sh" fi # DEPLOY STACK kubectl config use-context kind-$CLUSTER_NAME -# Create a temporary file -tmpfile=$(mktemp) # Build the kustomization and store the output in the temporary file -kustomize build deployment > "$tmpfile" - +tmp_file=$(mktemp) +DEPLOYMENT_ROOT="$SCRIPT_DIR/deployment/envs/$DEPLOYMENT_OPTION" +echo "Deployment root set to: $DEPLOYMENT_ROOT" +echo +echo "Building manifests..." +kustomize build $DEPLOYMENT_ROOT > "$tmp_file" +echo "Manifests built successfully." +echo +echo "Applying resources..." while true; do - if kubectl apply -f "$tmpfile"; then + if kubectl apply -f "$tmp_file"; then echo "Resources successfully applied." - rm "$tmpfile" + rm "$tmp_file" break else - echo "Retrying to apply resources. Be patient, this might take a while..." + echo + echo "Retrying to apply resources." + echo "Be patient, this might take a while... (Errors are expected until all resources are available!)" + echo + echo "Help:" + echo " If the errors persists, please check the pods status with: kubectl get pods --all-namespaces" + echo " All pods should be either in Running state, or ContainerCreating if they are still starting up." + echo " Check specific pod errors with: kubectl describe pod -n [NAMESPACE] [POD_NAME]" + echo " For further help, see the Troubleshooting section in setup.md" + echo + sleep 10 fi done @@ -114,17 +218,17 @@ done # DEPLOY RAY if [ "$INSTALL_RAY" = true ]; then echo "Installing Ray" - /bin/bash scripts/install_helm.sh - /bin/bash scripts/install_ray.sh + /bin/bash "$SCRIPT_DIR/scripts/install_helm.sh" + /bin/bash "$SCRIPT_DIR/scripts/install_ray.sh" fi echo -echo Installation completed! +echo "Installation completed!" echo # TESTS if [ "$RUN_TESTS" = "true" ]; then - /bin/bash scripts/run_tests.sh + /bin/bash "$SCRIPT_DIR/scripts/run_tests.sh" fi exit 0 diff --git a/tests/conftest.py b/tests/conftest.py index 1df8804..003ce94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,8 @@ from dotenv import load_dotenv import os -ENV_FILE = pathlib.Path(__file__).parent.parent / "config.env" +ENV_FILE = pathlib.Path(__file__).parent.parent / ".platform/.config" +assert ENV_FILE.exists(), f"File not found: {ENV_FILE} (autogenerated by the platform on installation)" # noqa load_dotenv(dotenv_path=ENV_FILE) CLUSTER_NAME = os.getenv("CLUSTER_NAME") @@ -14,8 +15,8 @@ assert HOST_IP is not None # MLFLOW -MLFLOW_ENV_FILE = pathlib.Path(__file__).parent.parent / "deployment/mlflow/env/local" / "config.env" -MLFLOW_SECRETS_FILE = pathlib.Path(__file__).parent.parent / "deployment/mlflow/env/local" / "secret.env" +MLFLOW_ENV_FILE = pathlib.Path(__file__).parent.parent / "deployment/mlflow/env/local" / "config.env" # noqa +MLFLOW_SECRETS_FILE = pathlib.Path(__file__).parent.parent / "deployment/mlflow/env/local" / "secret.env" # noqa load_dotenv(dotenv_path=MLFLOW_ENV_FILE, override=True) AWS_ACCESS_KEY_ID = os.getenv("MINIO_ACCESS_KEY") diff --git a/tests/resources/kfp/build_image.sh b/tests/resources/kfp/build_image.sh index 6ced22e..779bec1 100755 --- a/tests/resources/kfp/build_image.sh +++ b/tests/resources/kfp/build_image.sh @@ -12,7 +12,7 @@ cd "$(dirname "$0")" docker build -t "$FULL_IMAGE_NAME" . -# load the image into the local "kind" cluster with name "kind-ep" +# load the image into the local "kind" cluster kind load docker-image "$FULL_IMAGE_NAME" --name $CLUSTER_NAME # to push the image to a remote repository instead diff --git a/tests/resources/registry/build_push_image.sh b/tests/resources/registry/build_push_image.sh index 513de3c..14bf947 100755 --- a/tests/resources/registry/build_push_image.sh +++ b/tests/resources/registry/build_push_image.sh @@ -14,8 +14,5 @@ cd "$(dirname "$0")" docker build -t "$FULL_IMAGE_NAME" . -# load the image into the local "kind" cluster with name "kind-ep" -#kind load docker-image "$FULL_IMAGE_NAME" --name kind-ep - # to push the image to a remote repository instead docker push "$FULL_IMAGE_NAME" \ No newline at end of file From 1bf9fe3a1ca08ee5e89f48a85293ae98373c2a74 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Mon, 15 Apr 2024 11:43:22 +0300 Subject: [PATCH 03/20] update tutorials and add a demo notebook for the KFP option --- ...-with-fairness-and-energy-monitoring.ipynb | 77 +- .../demo_pipeline/demo-pipeline.ipynb | 214 +++-- .../demo_pipeline_stanlone_kfp/.gitignore | 2 + .../demo_pipeline_stanlone_kfp/README.md | 9 + .../demo-pipeline.ipynb | 868 ++++++++++++++++++ .../demo_pipeline_stanlone_kfp/graph.png | Bin 0 -> 74336 bytes .../01_Setup_local_cluster.md | 2 +- .../local_deployment/02_Deploy_Kubeflow.md | 4 +- tutorials/ray/ray_train/README.md | 2 +- 9 files changed, 1093 insertions(+), 85 deletions(-) create mode 100644 tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/.gitignore create mode 100644 tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/README.md create mode 100644 tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb create mode 100644 tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/graph.png diff --git a/tutorials/demo_notebooks/demo_fairness_and_energy_monitoring/demo-pipeline-with-fairness-and-energy-monitoring.ipynb b/tutorials/demo_notebooks/demo_fairness_and_energy_monitoring/demo-pipeline-with-fairness-and-energy-monitoring.ipynb index 7c1a121..39ac79f 100644 --- a/tutorials/demo_notebooks/demo_fairness_and_energy_monitoring/demo-pipeline-with-fairness-and-energy-monitoring.ipynb +++ b/tutorials/demo_notebooks/demo_fairness_and_energy_monitoring/demo-pipeline-with-fairness-and-energy-monitoring.ipynb @@ -778,10 +778,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "ef37a9bc", - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2024-04-13T09:16:14.457318Z", + "start_time": "2024-04-13T09:16:14.094491Z" + } + }, "source": [ "@component(\n", " base_image=\"python:3.9\",\n", @@ -800,6 +803,7 @@ " from kserve import V1beta1InferenceServiceSpec\n", " from kserve import V1beta1PredictorSpec\n", " from kserve import V1beta1SKLearnSpec\n", + " from kubernetes.client import V1ResourceRequirements\n", " import logging\n", " \n", " deploy_model_component_landmark = 'KFP_component'\n", @@ -815,25 +819,45 @@ " api_version = constants.KSERVE_GROUP + '/' + kserve_version\n", "\n", " isvc = V1beta1InferenceService(\n", - " api_version=api_version,\n", - " kind=constants.KSERVE_KIND,\n", - " metadata=client.V1ObjectMeta(\n", - " name=model_name,\n", - " namespace=namespace,\n", - " annotations={'sidecar.istio.io/inject':'false'}\n", + " api_version = api_version,\n", + " kind = constants.KSERVE_KIND,\n", + " metadata = client.V1ObjectMeta(\n", + " name = model_name,\n", + " namespace = namespace,\n", + " annotations = {'sidecar.istio.io/inject':'false'}\n", " ),\n", - " spec=V1beta1InferenceServiceSpec(\n", + " spec = V1beta1InferenceServiceSpec(\n", " predictor=V1beta1PredictorSpec(\n", " service_account_name=\"kserve-sa\",\n", + " min_replicas=1,\n", + " max_replicas = 1,\n", " sklearn=V1beta1SKLearnSpec(\n", - " storage_uri=model_uri\n", - " )\n", + " storage_uri=model_uri,\n", + " resources=V1ResourceRequirements(\n", + " requests={\"cpu\": \"100m\", \"memory\": \"512Mi\"},\n", + " limits={\"cpu\": \"300m\", \"memory\": \"512Mi\"}\n", + " )\n", + " ),\n", " )\n", " )\n", " )\n", " KServe = KServeClient()\n", " KServe.create(isvc)" - ] + ], + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'component' is not defined", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mNameError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[1], line 1\u001B[0m\n\u001B[0;32m----> 1\u001B[0m \u001B[38;5;129m@component\u001B[39m(\n\u001B[1;32m 2\u001B[0m base_image\u001B[38;5;241m=\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mpython:3.9\u001B[39m\u001B[38;5;124m\"\u001B[39m,\n\u001B[1;32m 3\u001B[0m packages_to_install\u001B[38;5;241m=\u001B[39m[\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mkserve=0.12.0\u001B[39m\u001B[38;5;124m\"\u001B[39m],\n\u001B[1;32m 4\u001B[0m output_component_file\u001B[38;5;241m=\u001B[39m\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mcomponents/deploy_model_component.yaml\u001B[39m\u001B[38;5;124m'\u001B[39m,\n\u001B[1;32m 5\u001B[0m )\n\u001B[1;32m 6\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mdeploy_model\u001B[39m(model_name: \u001B[38;5;28mstr\u001B[39m, storage_uri: \u001B[38;5;28mstr\u001B[39m):\n\u001B[1;32m 7\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"\u001B[39;00m\n\u001B[1;32m 8\u001B[0m \u001B[38;5;124;03m Deploy the model as a inference service with Kserve.\u001B[39;00m\n\u001B[1;32m 9\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[1;32m 10\u001B[0m \u001B[38;5;28;01mfrom\u001B[39;00m \u001B[38;5;21;01mkubernetes\u001B[39;00m \u001B[38;5;28;01mimport\u001B[39;00m client\n", + "\u001B[0;31mNameError\u001B[0m: name 'component' is not defined" + ] + } + ], + "execution_count": 1 }, { "cell_type": "markdown", @@ -853,10 +877,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "b90d1839", - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2024-04-13T09:16:14.719526Z", + "start_time": "2024-04-13T09:16:14.679920Z" + } + }, "source": [ "@component(\n", " base_image=\"python:3.9\", # kserve on python 3.10 comes with a dependency that fails to get installed\n", @@ -1025,7 +1052,21 @@ " if response.status_code != 200:\n", " raise RuntimeError(f\"HTTP status code '{response.status_code}': {response.json()}\")\n", " logger.info(f\"\\nPrediction response:\\n{response.text}\\n\")" - ] + ], + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'component' is not defined", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mNameError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[2], line 1\u001B[0m\n\u001B[0;32m----> 1\u001B[0m \u001B[38;5;129m@component\u001B[39m(\n\u001B[1;32m 2\u001B[0m base_image\u001B[38;5;241m=\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mpython:3.9\u001B[39m\u001B[38;5;124m\"\u001B[39m, \u001B[38;5;66;03m# kserve on python 3.10 comes with a dependency that fails to get installed\u001B[39;00m\n\u001B[1;32m 3\u001B[0m packages_to_install\u001B[38;5;241m=\u001B[39m[\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mkserve\u001B[39m\u001B[38;5;124m\"\u001B[39m, \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mscikit-learn~=1.0.2\u001B[39m\u001B[38;5;124m\"\u001B[39m],\n\u001B[1;32m 4\u001B[0m output_component_file\u001B[38;5;241m=\u001B[39m\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mcomponents/inference_component.yaml\u001B[39m\u001B[38;5;124m'\u001B[39m,\n\u001B[1;32m 5\u001B[0m )\n\u001B[1;32m 6\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21minference\u001B[39m(\n\u001B[1;32m 7\u001B[0m model_name: \u001B[38;5;28mstr\u001B[39m\n\u001B[1;32m 8\u001B[0m ):\n\u001B[1;32m 9\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"\u001B[39;00m\n\u001B[1;32m 10\u001B[0m \u001B[38;5;124;03m Test inference.\u001B[39;00m\n\u001B[1;32m 11\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[1;32m 12\u001B[0m \u001B[38;5;28;01mfrom\u001B[39;00m \u001B[38;5;21;01mkserve\u001B[39;00m \u001B[38;5;28;01mimport\u001B[39;00m KServeClient\n", + "\u001B[0;31mNameError\u001B[0m: name 'component' is not defined" + ] + } + ], + "execution_count": 2 }, { "cell_type": "markdown", @@ -1311,7 +1352,7 @@ "If not, removing the cluster and reinstalling everything is usually easier. A more surgical approach is to use kubectl to check the logs of pods in the given namespace and configure used YAMLs to fix the issue. When the modified YAMLs have been saved, just apply them and then rollout restart the pods. It is recommended to stop any dashboards before doing this. The required commands cluster removal, and kubectl fixing are:\n", "\n", "Cluster removal:\n", - "- Cluster deletion = kind delete cluster --name kind-ep\n", + "- Cluster deletion = kind delete cluster --name mlops-platform\n", "- Registry deletion = docker rm -f $(docker ps -aqf \"name=kind-registry\")\n", "\n", "Optional docker clean up:\n", diff --git a/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb b/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb index 19a3c6c..eb8bd36 100644 --- a/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb +++ b/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb @@ -43,11 +43,13 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:58:49.962380Z", + "start_time": "2024-04-13T10:58:49.654567Z" + } }, - "outputs": [], "source": [ "import warnings\n", "warnings.filterwarnings(\"ignore\")\n", @@ -64,7 +66,9 @@ " Artifact,\n", " Model\n", ")" - ] + ], + "outputs": [], + "execution_count": 1 }, { "cell_type": "markdown", @@ -83,8 +87,6 @@ }, { "cell_type": "code", - "execution_count": null, - "outputs": [], "source": [ "import re\n", "import requests\n", @@ -188,13 +190,17 @@ " return auth_session" ], "metadata": { - "collapsed": false - } + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:04.879732Z", + "start_time": "2024-04-13T10:59:04.873095Z" + } + }, + "outputs": [], + "execution_count": 2 }, { "cell_type": "code", - "execution_count": null, - "outputs": [], "source": [ "import kfp\n", "\n", @@ -212,8 +218,14 @@ "# print(client.list_experiments())" ], "metadata": { - "collapsed": false - } + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:07.184929Z", + "start_time": "2024-04-13T10:59:06.799007Z" + } + }, + "outputs": [], + "execution_count": 3 }, { "cell_type": "markdown", @@ -239,11 +251,13 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:09.359693Z", + "start_time": "2024-04-13T10:59:09.344825Z" + } }, - "outputs": [], "source": [ "@component(\n", " base_image=\"python:3.10\",\n", @@ -258,7 +272,9 @@ "\n", " df = pd.read_csv(url, sep=\";\")\n", " df.to_csv(data.path, index=None)" - ] + ], + "outputs": [], + "execution_count": 4 }, { "cell_type": "markdown", @@ -271,11 +287,13 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:10.283769Z", + "start_time": "2024-04-13T10:59:10.275472Z" + } }, - "outputs": [], "source": [ "@component(\n", " base_image=\"python:3.10\",\n", @@ -312,7 +330,9 @@ "\n", " train.to_csv(train_set.path, index=None)\n", " test.to_csv(test_set.path, index=None)" - ] + ], + "outputs": [], + "execution_count": 5 }, { "cell_type": "markdown", @@ -325,11 +345,13 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:11.874150Z", + "start_time": "2024-04-13T10:59:11.857230Z" + } }, - "outputs": [], "source": [ "from typing import NamedTuple\n", "\n", @@ -443,7 +465,9 @@ "\n", " # return str(mlflow.get_artifact_uri())\n", " return output(mlflow.get_artifact_uri(), run_id)" - ] + ], + "outputs": [], + "execution_count": 6 }, { "cell_type": "markdown", @@ -456,11 +480,13 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:14.116630Z", + "start_time": "2024-04-13T10:59:14.108461Z" + } }, - "outputs": [], "source": [ "@component(\n", " base_image=\"python:3.10\",\n", @@ -500,7 +526,9 @@ " logger.error(f\"Metric {key} failed. Evaluation not passed!\")\n", " return False\n", " return True" - ] + ], + "outputs": [], + "execution_count": 7 }, { "cell_type": "markdown", @@ -513,20 +541,22 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:39.240509Z", + "start_time": "2024-04-13T10:59:39.232049Z" + } }, - "outputs": [], "source": [ "@component(\n", " base_image=\"python:3.9\",\n", - " packages_to_install=[\"kserve\"],\n", + " packages_to_install=[\"kserve==0.11.0\"],\n", " output_component_file='components/deploy_model_component.yaml',\n", ")\n", "def deploy_model(model_name: str, storage_uri: str):\n", " \"\"\"\n", - " Deploy the model as a inference service with Kserve.\n", + " Deploy the model as an inference service with Kserve.\n", " \"\"\"\n", " import logging\n", " from kubernetes import client\n", @@ -537,6 +567,7 @@ " from kserve import V1beta1InferenceServiceSpec\n", " from kserve import V1beta1PredictorSpec\n", " from kserve import V1beta1SKLearnSpec\n", + " from kubernetes.client import V1ResourceRequirements\n", "\n", " logging.basicConfig(level=logging.INFO)\n", " logger = logging.getLogger(__name__)\n", @@ -544,32 +575,38 @@ " model_uri = f\"{storage_uri}/{model_name}\"\n", " logger.info(f\"MODEL URI: {model_uri}\")\n", "\n", - " # namespace = 'kserve-inference'\n", " namespace = utils.get_default_target_namespace()\n", " kserve_version='v1beta1'\n", " api_version = constants.KSERVE_GROUP + '/' + kserve_version\n", "\n", - "\n", " isvc = V1beta1InferenceService(\n", - " api_version=api_version,\n", - " kind=constants.KSERVE_KIND,\n", - " metadata=client.V1ObjectMeta(\n", - " name=model_name,\n", - " namespace=namespace,\n", - " annotations={'sidecar.istio.io/inject':'false'}\n", + " api_version = api_version,\n", + " kind = constants.KSERVE_KIND,\n", + " metadata = client.V1ObjectMeta(\n", + " name = model_name,\n", + " namespace = namespace,\n", + " annotations = {'sidecar.istio.io/inject':'false'}\n", " ),\n", - " spec=V1beta1InferenceServiceSpec(\n", + " spec = V1beta1InferenceServiceSpec(\n", " predictor=V1beta1PredictorSpec(\n", " service_account_name=\"kserve-sa\",\n", + " min_replicas=1,\n", + " max_replicas = 1,\n", " sklearn=V1beta1SKLearnSpec(\n", - " storage_uri=model_uri\n", - " )\n", + " storage_uri=model_uri,\n", + " resources=V1ResourceRequirements(\n", + " requests={\"cpu\": \"100m\", \"memory\": \"512Mi\"},\n", + " limits={\"cpu\": \"300m\", \"memory\": \"512Mi\"}\n", + " )\n", + " ),\n", " )\n", " )\n", " )\n", " KServe = KServeClient()\n", " KServe.create(isvc)" - ] + ], + "outputs": [], + "execution_count": 8 }, { "cell_type": "markdown", @@ -582,15 +619,17 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:49.515412Z", + "start_time": "2024-04-13T10:59:49.437666Z" + } }, - "outputs": [], "source": [ "@component(\n", " base_image=\"python:3.9\", # kserve on python 3.10 comes with a dependency that fails to get installed\n", - " packages_to_install=[\"kserve\", \"scikit-learn~=1.0.2\"],\n", + " packages_to_install=[\"kserve==0.11.0\", \"scikit-learn~=1.0.2\"],\n", " output_component_file='components/inference_component.yaml',\n", ")\n", "def inference(\n", @@ -761,7 +800,9 @@ " raise RuntimeError(f\"HTTP status code '{response.status_code}': {response.json()}\")\n", " \n", " logger.info(f\"\\nPrediction response:\\n{response.json()}\\n\")" - ] + ], + "outputs": [], + "execution_count": 9 }, { "cell_type": "markdown", @@ -776,11 +817,13 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:54.012447Z", + "start_time": "2024-04-13T10:59:54.005413Z" + } }, - "outputs": [], "source": [ "@dsl.pipeline(\n", " name='demo-pipeline',\n", @@ -833,7 +876,9 @@ " scaler_in=preprocess_task.outputs[\"scaler_out\"]\n", " )\n", " inference_task.after(deploy_model_task)" - ] + ], + "outputs": [], + "execution_count": 10 }, { "cell_type": "markdown", @@ -846,11 +891,13 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:57.039157Z", + "start_time": "2024-04-13T10:59:57.035254Z" + } }, - "outputs": [], "source": [ "# Specify pipeline argument values\n", "\n", @@ -867,7 +914,9 @@ " \"l1_ratio\": 0.5,\n", " \"threshold_metrics\": eval_threshold_metrics\n", "}" - ] + ], + "outputs": [], + "execution_count": 11 }, { "cell_type": "markdown", @@ -880,11 +929,13 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T10:59:58.264108Z", + "start_time": "2024-04-13T10:59:57.967784Z" + } }, - "outputs": [], "source": [ "run_name = \"demo-run\"\n", "experiment_name = \"demo-experiment\"\n", @@ -898,7 +949,44 @@ " enable_caching=False,\n", " namespace=\"kubeflow-user-example-com\"\n", ")" - ] + ], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "Experiment details." + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "Run details." + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "RunPipelineResult(run_id=85a3f207-98ed-4716-8964-74ea655611c0)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 12 }, { "cell_type": "markdown", @@ -942,7 +1030,7 @@ "
\n", "\n", "```bash\n", - "$ kubectl -n mlflow port-forward svc/mlflow 5000:5000\n", + "kubectl -n mlflow port-forward svc/mlflow 5000:5000\n", "```\n", "\n", "
\n", diff --git a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/.gitignore b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/.gitignore new file mode 100644 index 0000000..b4a2938 --- /dev/null +++ b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/.gitignore @@ -0,0 +1,2 @@ +components/* +components/.gitkeep \ No newline at end of file diff --git a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/README.md b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/README.md new file mode 100644 index 0000000..ef669ea --- /dev/null +++ b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/README.md @@ -0,0 +1,9 @@ +# Demo pipeline (standalone KFP) + +Jupyter notebook with a demo pipeline that uses the installed standalone Kubeflow Pipelines (KFP), MLflow and Kserve components. + +> **NOTE:** This demo is intended standalone-KFP. + +
+ +![Pipeline Graph](graph.png) \ No newline at end of file diff --git a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb new file mode 100644 index 0000000..2791de0 --- /dev/null +++ b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb @@ -0,0 +1,868 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "# Demo KFP pipeline" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Install requirements:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "pip install kfp~=1.8.14" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Imports:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T08:15:21.341469Z", + "start_time": "2024-04-13T08:15:20.727955Z" + } + }, + "source": [ + "import warnings\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "import kfp\n", + "import kfp.dsl as dsl\n", + "from kfp.aws import use_aws_secret\n", + "from kfp.v2.dsl import (\n", + " component,\n", + " Input,\n", + " Output,\n", + " Dataset,\n", + " Metrics,\n", + " Artifact,\n", + " Model\n", + ")" + ], + "outputs": [], + "execution_count": 1 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 1. Connect to client\n", + "\n", + "Run the following to port-forward to the KFP UI:\n", + "\n", + "```sh\n", + "kubectl port-forward svc/ml-pipeline-ui -n kubeflow 8080:80\n", + "```\n", + "\n", + "Now the KFP UI should be reachable at [`http://localhost:8080`](http://localhost:8080)." + ] + }, + { + "cell_type": "code", + "source": [ + "import kfp\n", + "\n", + "KFP_ENDPOINT = \"http://localhost:8080\"\n", + "\n", + "client = kfp.Client(host=KFP_ENDPOINT)\n", + "# print(client.list_experiments())" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T08:15:24.507074Z", + "start_time": "2024-04-13T08:15:24.492525Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'experiments': [{'created_at': datetime.datetime(2024, 4, 13, 8, 2, 14, tzinfo=tzutc()),\n", + " 'description': 'All runs created without specifying an '\n", + " 'experiment will be grouped here.',\n", + " 'id': 'b00807f2-d7c6-4961-be17-6d94130e1d56',\n", + " 'name': 'Default',\n", + " 'resource_references': None,\n", + " 'storage_state': 'STORAGESTATE_AVAILABLE'}],\n", + " 'next_page_token': None,\n", + " 'total_size': 1}\n" + ] + } + ], + "execution_count": 2 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 2. Components\n", + "\n", + "There are different ways to define components in KFP. Here, we use the **@component** decorator to define the components as Python function-based components.\n", + "\n", + "The **@component** annotation converts the function into a factory function that creates pipeline steps that execute this function. This example also specifies the base container image to run you component in." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Pull data component:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T08:25:39.305901Z", + "start_time": "2024-04-13T08:25:39.296130Z" + } + }, + "source": [ + "@component(\n", + " base_image=\"python:3.10\",\n", + " packages_to_install=[\"pandas~=1.4.2\"],\n", + " output_component_file='components/pull_data_component.yaml',\n", + ")\n", + "def pull_data(url: str, data: Output[Dataset]):\n", + " \"\"\"\n", + " Pull data component.\n", + " \"\"\"\n", + " import pandas as pd\n", + "\n", + " df = pd.read_csv(url, sep=\";\")\n", + " df.to_csv(data.path, index=None)" + ], + "outputs": [], + "execution_count": 3 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Preprocess component:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T08:25:40.451932Z", + "start_time": "2024-04-13T08:25:40.443549Z" + } + }, + "source": [ + "@component(\n", + " base_image=\"python:3.10\",\n", + " packages_to_install=[\"pandas~=1.4.2\", \"scikit-learn~=1.0.2\"],\n", + " output_component_file='components/preprocess_component.yaml',\n", + ")\n", + "def preprocess(\n", + " data: Input[Dataset],\n", + " scaler_out: Output[Artifact],\n", + " train_set: Output[Dataset],\n", + " test_set: Output[Dataset],\n", + " target: str = \"quality\",\n", + "):\n", + " \"\"\"\n", + " Preprocess component.\n", + " \"\"\"\n", + " import pandas as pd\n", + " import pickle\n", + " from sklearn.model_selection import train_test_split\n", + " from sklearn.preprocessing import StandardScaler\n", + "\n", + " data = pd.read_csv(data.path)\n", + "\n", + " # Split the data into training and test sets. (0.75, 0.25) split.\n", + " train, test = train_test_split(data)\n", + "\n", + " scaler = StandardScaler()\n", + "\n", + " train[train.drop(target, axis=1).columns] = scaler.fit_transform(train.drop(target, axis=1))\n", + " test[test.drop(target, axis=1).columns] = scaler.transform(test.drop(target, axis=1))\n", + "\n", + " with open(scaler_out.path, 'wb') as fp:\n", + " pickle.dump(scaler, fp, pickle.HIGHEST_PROTOCOL)\n", + "\n", + " train.to_csv(train_set.path, index=None)\n", + " test.to_csv(test_set.path, index=None)" + ], + "outputs": [], + "execution_count": 4 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Train component:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T08:25:45.188150Z", + "start_time": "2024-04-13T08:25:45.167796Z" + } + }, + "source": [ + "from typing import NamedTuple\n", + "\n", + "@component(\n", + " base_image=\"python:3.10\",\n", + " packages_to_install=[\"numpy\", \"pandas~=1.4.2\", \"scikit-learn~=1.0.2\", \"mlflow~=2.4.1\", \"boto3~=1.21.0\"],\n", + " output_component_file='components/train_component.yaml',\n", + ")\n", + "def train(\n", + " train_set: Input[Dataset],\n", + " test_set: Input[Dataset],\n", + " saved_model: Output[Model],\n", + " mlflow_experiment_name: str,\n", + " mlflow_tracking_uri: str,\n", + " mlflow_s3_endpoint_url: str,\n", + " model_name: str,\n", + " alpha: float,\n", + " l1_ratio: float,\n", + " target: str = \"quality\",\n", + ") -> NamedTuple(\"Output\", [('storage_uri', str), ('run_id', str),]):\n", + " \"\"\"\n", + " Train component.\n", + " \"\"\"\n", + " import numpy as np\n", + " import pandas as pd\n", + " from sklearn.linear_model import ElasticNet\n", + " from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score\n", + " import mlflow\n", + " import mlflow.sklearn\n", + " import os\n", + " import logging\n", + " import pickle\n", + " from collections import namedtuple\n", + "\n", + " logging.basicConfig(level=logging.INFO)\n", + " logger = logging.getLogger(__name__)\n", + "\n", + " def eval_metrics(actual, pred):\n", + " rmse = np.sqrt(mean_squared_error(actual, pred))\n", + " mae = mean_absolute_error(actual, pred)\n", + " r2 = r2_score(actual, pred)\n", + " return rmse, mae, r2\n", + "\n", + " os.environ['MLFLOW_S3_ENDPOINT_URL'] = mlflow_s3_endpoint_url\n", + "\n", + " # load data\n", + " train = pd.read_csv(train_set.path)\n", + " test = pd.read_csv(test_set.path)\n", + "\n", + " # The predicted column is \"quality\" which is a scalar from [3, 9]\n", + " train_x = train.drop([target], axis=1)\n", + " test_x = test.drop([target], axis=1)\n", + " train_y = train[[target]]\n", + " test_y = test[[target]]\n", + "\n", + " logger.info(f\"Using MLflow tracking URI: {mlflow_tracking_uri}\")\n", + " mlflow.set_tracking_uri(mlflow_tracking_uri)\n", + "\n", + " logger.info(f\"Using MLflow experiment: {mlflow_experiment_name}\")\n", + " mlflow.set_experiment(mlflow_experiment_name)\n", + "\n", + " with mlflow.start_run() as run:\n", + "\n", + " run_id = run.info.run_id\n", + " logger.info(f\"Run ID: {run_id}\")\n", + "\n", + " model = ElasticNet(alpha=alpha, l1_ratio=l1_ratio, random_state=42)\n", + "\n", + " logger.info(\"Fitting model...\")\n", + " model.fit(train_x, train_y)\n", + "\n", + " logger.info(\"Predicting...\")\n", + " predicted_qualities = model.predict(test_x)\n", + "\n", + " (rmse, mae, r2) = eval_metrics(test_y, predicted_qualities)\n", + "\n", + " logger.info(\"Elasticnet model (alpha=%f, l1_ratio=%f):\" % (alpha, l1_ratio))\n", + " logger.info(\" RMSE: %s\" % rmse)\n", + " logger.info(\" MAE: %s\" % mae)\n", + " logger.info(\" R2: %s\" % r2)\n", + "\n", + " logger.info(\"Logging parameters to MLflow\")\n", + " mlflow.log_param(\"alpha\", alpha)\n", + " mlflow.log_param(\"l1_ratio\", l1_ratio)\n", + " mlflow.log_metric(\"rmse\", rmse)\n", + " mlflow.log_metric(\"r2\", r2)\n", + " mlflow.log_metric(\"mae\", mae)\n", + "\n", + " # save model to mlflow\n", + " logger.info(\"Logging trained model\")\n", + " mlflow.sklearn.log_model(\n", + " model,\n", + " model_name,\n", + " registered_model_name=\"ElasticnetWineModel\",\n", + " serialization_format=\"pickle\"\n", + " )\n", + "\n", + " logger.info(\"Logging predictions artifact to MLflow\")\n", + " np.save(\"predictions.npy\", predicted_qualities)\n", + " mlflow.log_artifact(\n", + " local_path=\"predictions.npy\", artifact_path=\"predicted_qualities/\"\n", + " )\n", + "\n", + " # save model as KFP artifact\n", + " logging.info(f\"Saving model to: {saved_model.path}\")\n", + " with open(saved_model.path, 'wb') as fp:\n", + " pickle.dump(model, fp, pickle.HIGHEST_PROTOCOL)\n", + "\n", + " # prepare output\n", + " output = namedtuple('Output', ['storage_uri', 'run_id'])\n", + "\n", + " # return str(mlflow.get_artifact_uri())\n", + " return output(mlflow.get_artifact_uri(), run_id)" + ], + "outputs": [], + "execution_count": 5 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Evaluate component:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T08:25:50.816475Z", + "start_time": "2024-04-13T08:25:50.802017Z" + } + }, + "source": [ + "@component(\n", + " base_image=\"python:3.10\",\n", + " packages_to_install=[\"numpy\", \"mlflow~=2.4.1\"],\n", + " output_component_file='components/evaluate_component.yaml',\n", + ")\n", + "def evaluate(\n", + " run_id: str,\n", + " mlflow_tracking_uri: str,\n", + " threshold_metrics: dict\n", + ") -> bool:\n", + " \"\"\"\n", + " Evaluate component: Compares metrics from training with given thresholds.\n", + "\n", + " Args:\n", + " run_id (string): MLflow run ID\n", + " mlflow_tracking_uri (string): MLflow tracking URI\n", + " threshold_metrics (dict): Minimum threshold values for each metric\n", + " Returns:\n", + " Bool indicating whether evaluation passed or failed.\n", + " \"\"\"\n", + " from mlflow.tracking import MlflowClient\n", + " import logging\n", + "\n", + " logging.basicConfig(level=logging.INFO)\n", + " logger = logging.getLogger(__name__)\n", + "\n", + " client = MlflowClient(tracking_uri=mlflow_tracking_uri)\n", + " info = client.get_run(run_id)\n", + " training_metrics = info.data.metrics\n", + "\n", + " logger.info(f\"Training metrics: {training_metrics}\")\n", + "\n", + " # compare the evaluation metrics with the defined thresholds\n", + " for key, value in threshold_metrics.items():\n", + " if key not in training_metrics or training_metrics[key] > value:\n", + " logger.error(f\"Metric {key} failed. Evaluation not passed!\")\n", + " return False\n", + " return True" + ], + "outputs": [], + "execution_count": 6 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Deploy model component:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T09:37:55.751098Z", + "start_time": "2024-04-13T09:37:55.738332Z" + } + }, + "source": [ + "@component(\n", + " base_image=\"python:3.9\",\n", + " packages_to_install=[\"kserve==0.11.0\"],\n", + " output_component_file='components/deploy_model_component.yaml',\n", + ")\n", + "def deploy_model(model_name: str, storage_uri: str):\n", + " \"\"\"\n", + " Deploy the model as an inference service with Kserve.\n", + " \"\"\"\n", + " import logging\n", + " from kubernetes import client\n", + " from kserve import KServeClient\n", + " from kserve import constants\n", + " from kserve import V1beta1InferenceService\n", + " from kserve import V1beta1InferenceServiceSpec\n", + " from kserve import V1beta1PredictorSpec\n", + " from kserve import V1beta1SKLearnSpec\n", + " from kubernetes.client import V1ResourceRequirements\n", + "\n", + " logging.basicConfig(level=logging.INFO)\n", + " logger = logging.getLogger(__name__)\n", + "\n", + " model_uri = f\"{storage_uri}/{model_name}\"\n", + " logger.info(f\"MODEL URI: {model_uri}\")\n", + "\n", + " namespace = 'kserve-inference'\n", + " kserve_version='v1beta1'\n", + " api_version = constants.KSERVE_GROUP + '/' + kserve_version\n", + "\n", + " isvc = V1beta1InferenceService(\n", + " api_version = api_version,\n", + " kind = constants.KSERVE_KIND,\n", + " metadata = client.V1ObjectMeta(\n", + " name = model_name,\n", + " namespace = namespace,\n", + " annotations = {'sidecar.istio.io/inject':'false'}\n", + " ),\n", + " spec = V1beta1InferenceServiceSpec(\n", + " predictor=V1beta1PredictorSpec(\n", + " service_account_name=\"kserve-sa\",\n", + " min_replicas=1,\n", + " max_replicas = 1,\n", + " sklearn=V1beta1SKLearnSpec(\n", + " storage_uri=model_uri,\n", + " resources=V1ResourceRequirements(\n", + " requests={\"cpu\": \"100m\", \"memory\": \"512Mi\"},\n", + " limits={\"cpu\": \"300m\", \"memory\": \"512Mi\"}\n", + " )\n", + " ),\n", + " )\n", + " )\n", + " )\n", + " KServe = KServeClient()\n", + " KServe.create(isvc)" + ], + "outputs": [], + "execution_count": 32 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Inference component:" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-04-13T09:37:56.721843Z", + "start_time": "2024-04-13T09:37:56.712749Z" + } + }, + "cell_type": "code", + "source": [ + " @component(\n", + " base_image=\"python:3.9\", # kserve on python 3.10 comes with a dependency that fails to get installed\n", + " packages_to_install=[\"kserve==0.11.0\", \"scikit-learn~=1.0.2\"],\n", + " output_component_file='components/inference_component.yaml',\n", + ")\n", + "def inference(\n", + " model_name: str,\n", + " scaler_in: Input[Artifact]\n", + "):\n", + " \"\"\"\n", + " Test inference.\n", + " \"\"\"\n", + " from kserve import KServeClient\n", + " import requests\n", + " import pickle\n", + " import logging\n", + "\n", + " logging.basicConfig(level=logging.INFO)\n", + " logger = logging.getLogger(__name__)\n", + "\n", + " namespace = 'kserve-inference'\n", + "\n", + " input_sample = [[5.6, 0.54, 0.04, 1.7, 0.049, 5, 13, 0.9942, 3.72, 0.58, 11.4],\n", + " [11.3, 0.34, 0.45, 2, 0.082, 6, 15, 0.9988, 2.94, 0.66, 9.2]]\n", + "\n", + " logger.info(f\"Loading standard scaler from: {scaler_in.path}\")\n", + " with open(scaler_in.path, 'rb') as fp:\n", + " scaler = pickle.load(fp)\n", + "\n", + " logger.info(f\"Standardizing sample: {scaler_in.path}\")\n", + " input_sample = scaler.transform(input_sample)\n", + "\n", + " # get inference service\n", + " KServe = KServeClient()\n", + "\n", + " # wait for deployment to be ready\n", + " KServe.get(model_name, namespace=namespace, watch=True, timeout_seconds=120)\n", + "\n", + " inference_service = KServe.get(model_name, namespace=namespace)\n", + " is_url = inference_service['status']['address']['url']\n", + "\n", + " logger.info(f\"\\nInference service status:\\n{inference_service['status']}\")\n", + " logger.info(f\"\\nInference service URL:\\n{is_url}\\n\")\n", + "\n", + " inference_input = {\n", + " 'instances': input_sample.tolist()\n", + " }\n", + "\n", + " response = requests.post(is_url, json=inference_input)\n", + " logger.info(f\"\\nPrediction response:\\n{response.text}\\n\")" + ], + "outputs": [], + "execution_count": 33 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 3. Pipeline\n", + "\n", + "Pipeline definition:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T09:37:57.453669Z", + "start_time": "2024-04-13T09:37:57.448782Z" + } + }, + "source": [ + "@dsl.pipeline(\n", + " name='demo-pipeline',\n", + " description='An example pipeline that performs addition calculations.',\n", + ")\n", + "def pipeline(\n", + " url: str,\n", + " target: str,\n", + " mlflow_experiment_name: str,\n", + " mlflow_tracking_uri: str,\n", + " mlflow_s3_endpoint_url: str,\n", + " model_name: str,\n", + " alpha: float,\n", + " l1_ratio: float,\n", + " threshold_metrics: dict,\n", + "):\n", + " pull_task = pull_data(url=url)\n", + "\n", + " preprocess_task = preprocess(data=pull_task.outputs[\"data\"])\n", + "\n", + " train_task = train(\n", + " train_set=preprocess_task.outputs[\"train_set\"],\n", + " test_set=preprocess_task.outputs[\"test_set\"],\n", + " target=target,\n", + " mlflow_experiment_name=mlflow_experiment_name,\n", + " mlflow_tracking_uri=mlflow_tracking_uri,\n", + " mlflow_s3_endpoint_url=mlflow_s3_endpoint_url,\n", + " model_name=model_name,\n", + " alpha=alpha,\n", + " l1_ratio=l1_ratio\n", + " )\n", + " train_task.apply(use_aws_secret(secret_name=\"aws-secret\"))\n", + "\n", + " evaluate_trask = evaluate(\n", + " run_id=train_task.outputs[\"run_id\"],\n", + " mlflow_tracking_uri=mlflow_tracking_uri,\n", + " threshold_metrics=threshold_metrics\n", + " )\n", + "\n", + " eval_passed = evaluate_trask.output\n", + "\n", + " with dsl.Condition(eval_passed == \"true\"):\n", + " deploy_model_task = deploy_model(\n", + " model_name=model_name,\n", + " storage_uri=train_task.outputs[\"storage_uri\"],\n", + " )\n", + "\n", + " inference_task = inference(\n", + " model_name=model_name,\n", + " scaler_in=preprocess_task.outputs[\"scaler_out\"]\n", + " )\n", + " inference_task.after(deploy_model_task)" + ], + "outputs": [], + "execution_count": 34 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Pipeline arguments:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T09:37:58.275887Z", + "start_time": "2024-04-13T09:37:58.273306Z" + } + }, + "source": [ + "# Specify pipeline argument values\n", + "\n", + "eval_threshold_metrics = {'rmse': 0.9, 'r2': 0.3, 'mae': 0.8}\n", + "\n", + "arguments = {\n", + " \"url\": \"http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv\",\n", + " \"target\": \"quality\",\n", + " \"mlflow_tracking_uri\": \"http://mlflow.mlflow.svc.cluster.local:5000\",\n", + " \"mlflow_s3_endpoint_url\": \"http://mlflow-minio-service.mlflow.svc.cluster.local:9000\",\n", + " \"mlflow_experiment_name\": \"demo-notebook\",\n", + " \"model_name\": \"wine-quality\",\n", + " \"alpha\": 0.5,\n", + " \"l1_ratio\": 0.5,\n", + " \"threshold_metrics\": eval_threshold_metrics\n", + "}" + ], + "outputs": [], + "execution_count": 35 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 4. Submit run" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-13T09:37:59.349350Z", + "start_time": "2024-04-13T09:37:59.056885Z" + } + }, + "source": [ + "run_name = \"demo-run\"\n", + "experiment_name = \"demo-experiment\"\n", + "\n", + "client.create_run_from_pipeline_func(\n", + " pipeline_func=pipeline,\n", + " run_name=run_name,\n", + " experiment_name=experiment_name,\n", + " arguments=arguments,\n", + " mode=kfp.dsl.PipelineExecutionMode.V2_COMPATIBLE,\n", + " enable_caching=False,\n", + ")" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "Experiment details." + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "Run details." + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "RunPipelineResult(run_id=a988bfe4-12c5-4b27-9645-409c93cb3fcc)" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 36 + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 5. Check run" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Kubeflow Pipelines UI\n", + "\n", + "The default way of accessing KFP UI is via port-forward. This enables you to get started quickly without imposing any requirements on your environment. Run the following to port-forward KFP UI to local port `8080`:\n", + "\n", + "```sh\n", + "kubectl port-forward svc/ml-pipeline-ui -n kubeflow 8080:80\n", + "```\n", + "\n", + "Now the KFP UI should be reachable at [`http://localhost:8080`](http://localhost:8080)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### MLFlow UI\n", + "\n", + "To access MLFlow UI, open a terminal and forward a local port to MLFlow server:\n", + "\n", + "
\n", + "\n", + "```bash\n", + "$ kubectl -n mlflow port-forward svc/mlflow 5000:5000\n", + "```\n", + "\n", + "
\n", + "\n", + "Now MLFlow's UI should be reachable at [`http://localhost:5000`](http://localhost:5000)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 6. Check deployed model\n", + "\n", + "```bash\n", + "# get inference services\n", + "kubectl -n kserve-inference get inferenceservice\n", + "\n", + "# get deployed model pods\n", + "kubectl -n kserve-inference get pods\n", + "\n", + "# delete inference service\n", + "kubectl -n kserve-inference delete inferenceservice wine-quality\n", + "```\n", + "
\n", + "\n", + "If something goes wrong, check the logs with:\n", + "\n", + "
\n", + "\n", + "```bash\n", + "kubectl logs -n kserve-inference kserve-container\n", + "\n", + "kubectl logs -n kserve-inference queue-proxy\n", + "\n", + "kubectl logs -n kserve-inference storage-initializer\n", + "```\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "iml4e", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.15 (default, Nov 24 2022, 08:57:44) \n[Clang 14.0.6 ]" + }, + "vscode": { + "interpreter": { + "hash": "2976e1db094957a35b33d12f80288a268286b510a60c0d029aa085f0b10be691" + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/graph.png b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/graph.png new file mode 100644 index 0000000000000000000000000000000000000000..14bf10cb7e8f888ba05c543ded751848645a8d5f GIT binary patch literal 74336 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 f8wMLi(?U(&8Vk`FsBN!g-cb%g4{e~Rc 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 U&ebCz;DTfkDq{C`et2T;-`Ni=cK`qY literal 0 HcmV?d00001 diff --git a/tutorials/local_deployment/01_Setup_local_cluster.md b/tutorials/local_deployment/01_Setup_local_cluster.md index 48a3c8f..da37881 100644 --- a/tutorials/local_deployment/01_Setup_local_cluster.md +++ b/tutorials/local_deployment/01_Setup_local_cluster.md @@ -21,7 +21,7 @@ sudo mv ./kind /usr/local/bin/kind ### 3. Create a cluster ```bash -export CLUSTER_NAME="kind-ep" +export CLUSTER_NAME="mlops-platform" export HOST_IP="127.0.0.1" # cluster IP address cat < Date: Mon, 15 Apr 2024 11:43:45 +0300 Subject: [PATCH 04/20] add uninstall script --- uninstall.sh | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 uninstall.sh diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..b034d3f --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -eoa pipefail + +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +PLATFORM_DIR="$SCRIPT_DIR/.platform" +PLATFORM_CONFIG="$PLATFORM_DIR/.config" + +# check if the platform directory exists +if [ -d "$PLATFORM_DIR" ]; then + # source the configuration file + source $PLATFORM_CONFIG +else + echo "The platform directory not found: $PLATFORM_DIR" + echo "Please run the setup.sh script first, or check the 'Manual deletion' section in the setup.md for manual deletion of the platform." + exit 1 +fi + +# Delete the cluster +kind delete cluster --name $CLUSTER_NAME + +if [ "$INSTALL_LOCAL_REGISTRY" = "true" ]; then + # Delete the local Docker registry + echo "Deleting the local Docker registry..." + docker stop kind-registry + docker rm kind-registry + echo "Local Docker registry deleted." +fi + +rm -rf "$PLATFORM_DIR" + +echo "The platform has been successfully uninstalled." +exit 0 From 4e1961ffe91fc0bab52c61ab9bd2d1ba7c092929 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Mon, 15 Apr 2024 11:47:35 +0300 Subject: [PATCH 05/20] update gcp quickstart tutorial --- .../gcp_quickstart/02_Deploy_the_stack.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tutorials/gcp_quickstart/02_Deploy_the_stack.md b/tutorials/gcp_quickstart/02_Deploy_the_stack.md index 121b443..ae1531e 100644 --- a/tutorials/gcp_quickstart/02_Deploy_the_stack.md +++ b/tutorials/gcp_quickstart/02_Deploy_the_stack.md @@ -5,12 +5,27 @@ - [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/) - [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/) +## 2. Choose the deployment option + +Choose the deployment option that best fits your needs: + +1. `kubeflow-monitoring`: Full Kubeflow deployment with all components. +2. `kubeflow`: Full Kubeflow deployment without monitoring components (prometheus, grafana). +3. `standalone-kfp-monitoring`: Standalone KFP deployment. +4. `standalone-kfp`: Standalone KFP deployment without monitoring components (prometheus, grafana). +5. `standalone-kfp-kserve-monitoring`: Standalone KFP and Kserve deployment. +6. `standalone-kfp-kserve`: Standalone KFP and Kserve deployment without monitoring components (prometheus, grafana). + +```bash +export DEPLOYMENT_OPTION=kubeflow-monitoring +``` + ## 2. Deploy the stack Deploy all the components of the platform with: ```bash -while ! kustomize build deployment | kubectl apply -f -; do echo "Retrying to apply resources"; sleep 10; done +while ! kustomize build "deployment/envs/$DEPLOYMENT_OPTION" | kubectl apply -f -; do echo "Retrying to apply resources"; sleep 10; done ``` ## Troubleshooting @@ -28,7 +43,7 @@ Race condition errors can occur when deploying Kubeflow. If this happens, delete ```bash kubectl delete ns kubeflow -while ! kustomize build deployment | kubectl apply -f -; do echo "Retrying to apply resources"; sleep 10; done +while ! kustomize build "deployment/envs/$DEPLOYMENT_OPTION" | kubectl apply -f -; do echo "Retrying to apply resources"; sleep 10; done ``` Sometimes, just deleting the failing pod, so that it get recreated, will fix the issue. From 18afabf3a874c123541807961b6c2a02a1b5c61d Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 11:52:40 +0300 Subject: [PATCH 06/20] change default mlflow bucket name --- deployment/mlflow/base/config.env | 2 +- deployment/mlflow/env/gcp-cloudsql/params.env | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/mlflow/base/config.env b/deployment/mlflow/base/config.env index 5d6dfc3..e24035e 100644 --- a/deployment/mlflow/base/config.env +++ b/deployment/mlflow/base/config.env @@ -4,4 +4,4 @@ DB_HOST=postgres DB_PORT=5432 DB_NAME=mlflow -DEFAULT_ARTIFACT_ROOT=gs://mlflow-platformv2 +DEFAULT_ARTIFACT_ROOT=gs://mlflow-mlops-platform diff --git a/deployment/mlflow/env/gcp-cloudsql/params.env b/deployment/mlflow/env/gcp-cloudsql/params.env index 69446ff..f83af45 100644 --- a/deployment/mlflow/env/gcp-cloudsql/params.env +++ b/deployment/mlflow/env/gcp-cloudsql/params.env @@ -1 +1 @@ -GCP_CLOUDSQL_INSTANCE_NAME=mlops-platform-v2:europe-west1:mlops-platformv2 +GCP_CLOUDSQL_INSTANCE_NAME=mlops-platform-v2:europe-west1:mlops-platform From ce710628c27cbb0ddf6df415cd859a6e6a2d8130 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 12:09:43 +0300 Subject: [PATCH 07/20] complete kubeflow envs readmes --- .../manifests/in-cluster-setup/kubeflow/README.md | 11 ++++++++++- .../in-cluster-setup/standalone-kfp-kserve/README.md | 6 +++++- .../in-cluster-setup/standalone-kfp/README.md | 5 ++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/deployment/kubeflow/manifests/in-cluster-setup/kubeflow/README.md b/deployment/kubeflow/manifests/in-cluster-setup/kubeflow/README.md index f87f5c1..8dc1847 100644 --- a/deployment/kubeflow/manifests/in-cluster-setup/kubeflow/README.md +++ b/deployment/kubeflow/manifests/in-cluster-setup/kubeflow/README.md @@ -1 +1,10 @@ -# TODO \ No newline at end of file +# Kubeflow + +Components: +- Multiuser isolation +- Central Dashboard +- Jupyter Notebooks +- Kubeflow Pipelines (KFP) +- Kserve +- Katib +- TensorBoard \ No newline at end of file diff --git a/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/README.md b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/README.md index f87f5c1..f954728 100644 --- a/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/README.md +++ b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve/README.md @@ -1 +1,5 @@ -# TODO \ No newline at end of file +# Standalone KFP + Kserve + +Components: +- Kubeflow Pipelines (KFP) +- Kserve \ No newline at end of file diff --git a/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/README.md b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/README.md index f87f5c1..a2245f6 100644 --- a/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/README.md +++ b/deployment/kubeflow/manifests/in-cluster-setup/standalone-kfp/README.md @@ -1 +1,4 @@ -# TODO \ No newline at end of file +# Standalone KFP + +Components: +- Kubeflow Pipelines (KFP) \ No newline at end of file From a28fbd1e0d7b7330cdec93b4883240edfd754f88 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 12:21:21 +0300 Subject: [PATCH 08/20] clear notebooks outputs --- .../demo_pipeline/demo-pipeline.ipynb | 139 ++++------------ .../demo-pipeline.ipynb | 150 ++++-------------- 2 files changed, 55 insertions(+), 234 deletions(-) diff --git a/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb b/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb index eb8bd36..844b878 100644 --- a/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb +++ b/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb @@ -21,16 +21,16 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { "collapsed": false }, - "outputs": [], "source": [ "%%bash\n", "\n", "pip install kfp~=1.8.14" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -44,11 +44,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:58:49.962380Z", - "start_time": "2024-04-13T10:58:49.654567Z" - } + "collapsed": false }, "source": [ "import warnings\n", @@ -68,7 +64,7 @@ ")" ], "outputs": [], - "execution_count": 1 + "execution_count": null }, { "cell_type": "markdown", @@ -190,14 +186,10 @@ " return auth_session" ], "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:04.879732Z", - "start_time": "2024-04-13T10:59:04.873095Z" - } + "collapsed": false }, "outputs": [], - "execution_count": 2 + "execution_count": null }, { "cell_type": "code", @@ -218,14 +210,10 @@ "# print(client.list_experiments())" ], "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:07.184929Z", - "start_time": "2024-04-13T10:59:06.799007Z" - } + "collapsed": false }, "outputs": [], - "execution_count": 3 + "execution_count": null }, { "cell_type": "markdown", @@ -252,11 +240,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:09.359693Z", - "start_time": "2024-04-13T10:59:09.344825Z" - } + "collapsed": false }, "source": [ "@component(\n", @@ -274,7 +258,7 @@ " df.to_csv(data.path, index=None)" ], "outputs": [], - "execution_count": 4 + "execution_count": null }, { "cell_type": "markdown", @@ -288,11 +272,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:10.283769Z", - "start_time": "2024-04-13T10:59:10.275472Z" - } + "collapsed": false }, "source": [ "@component(\n", @@ -332,7 +312,7 @@ " test.to_csv(test_set.path, index=None)" ], "outputs": [], - "execution_count": 5 + "execution_count": null }, { "cell_type": "markdown", @@ -346,11 +326,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:11.874150Z", - "start_time": "2024-04-13T10:59:11.857230Z" - } + "collapsed": false }, "source": [ "from typing import NamedTuple\n", @@ -467,7 +443,7 @@ " return output(mlflow.get_artifact_uri(), run_id)" ], "outputs": [], - "execution_count": 6 + "execution_count": null }, { "cell_type": "markdown", @@ -481,11 +457,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:14.116630Z", - "start_time": "2024-04-13T10:59:14.108461Z" - } + "collapsed": false }, "source": [ "@component(\n", @@ -528,7 +500,7 @@ " return True" ], "outputs": [], - "execution_count": 7 + "execution_count": null }, { "cell_type": "markdown", @@ -542,11 +514,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:39.240509Z", - "start_time": "2024-04-13T10:59:39.232049Z" - } + "collapsed": false }, "source": [ "@component(\n", @@ -606,7 +574,7 @@ " KServe.create(isvc)" ], "outputs": [], - "execution_count": 8 + "execution_count": null }, { "cell_type": "markdown", @@ -620,11 +588,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:49.515412Z", - "start_time": "2024-04-13T10:59:49.437666Z" - } + "collapsed": false }, "source": [ "@component(\n", @@ -802,7 +766,7 @@ " logger.info(f\"\\nPrediction response:\\n{response.json()}\\n\")" ], "outputs": [], - "execution_count": 9 + "execution_count": null }, { "cell_type": "markdown", @@ -818,11 +782,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:54.012447Z", - "start_time": "2024-04-13T10:59:54.005413Z" - } + "collapsed": false }, "source": [ "@dsl.pipeline(\n", @@ -878,7 +838,7 @@ " inference_task.after(deploy_model_task)" ], "outputs": [], - "execution_count": 10 + "execution_count": null }, { "cell_type": "markdown", @@ -892,11 +852,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:57.039157Z", - "start_time": "2024-04-13T10:59:57.035254Z" - } + "collapsed": false }, "source": [ "# Specify pipeline argument values\n", @@ -916,7 +872,7 @@ "}" ], "outputs": [], - "execution_count": 11 + "execution_count": null }, { "cell_type": "markdown", @@ -930,11 +886,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T10:59:58.264108Z", - "start_time": "2024-04-13T10:59:57.967784Z" - } + "collapsed": false }, "source": [ "run_name = \"demo-run\"\n", @@ -950,43 +902,8 @@ " namespace=\"kubeflow-user-example-com\"\n", ")" ], - "outputs": [ - { - "data": { - "text/plain": [ - "" - ], - "text/html": [ - "Experiment details." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "" - ], - "text/html": [ - "Run details." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "RunPipelineResult(run_id=85a3f207-98ed-4716-8964-74ea655611c0)" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "execution_count": 12 + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", diff --git a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb index 2791de0..5a6f32a 100644 --- a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb +++ b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb @@ -21,16 +21,16 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { "collapsed": false }, - "outputs": [], "source": [ "%%bash\n", "\n", "pip install kfp~=1.8.14" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -44,11 +44,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T08:15:21.341469Z", - "start_time": "2024-04-13T08:15:20.727955Z" - } + "collapsed": false }, "source": [ "import warnings\n", @@ -68,7 +64,7 @@ ")" ], "outputs": [], - "execution_count": 1 + "execution_count": null }, { "cell_type": "markdown", @@ -98,30 +94,10 @@ "# print(client.list_experiments())" ], "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T08:15:24.507074Z", - "start_time": "2024-04-13T08:15:24.492525Z" - } + "collapsed": false }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'experiments': [{'created_at': datetime.datetime(2024, 4, 13, 8, 2, 14, tzinfo=tzutc()),\n", - " 'description': 'All runs created without specifying an '\n", - " 'experiment will be grouped here.',\n", - " 'id': 'b00807f2-d7c6-4961-be17-6d94130e1d56',\n", - " 'name': 'Default',\n", - " 'resource_references': None,\n", - " 'storage_state': 'STORAGESTATE_AVAILABLE'}],\n", - " 'next_page_token': None,\n", - " 'total_size': 1}\n" - ] - } - ], - "execution_count": 2 + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -148,11 +124,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T08:25:39.305901Z", - "start_time": "2024-04-13T08:25:39.296130Z" - } + "collapsed": false }, "source": [ "@component(\n", @@ -170,7 +142,7 @@ " df.to_csv(data.path, index=None)" ], "outputs": [], - "execution_count": 3 + "execution_count": null }, { "cell_type": "markdown", @@ -184,11 +156,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T08:25:40.451932Z", - "start_time": "2024-04-13T08:25:40.443549Z" - } + "collapsed": false }, "source": [ "@component(\n", @@ -228,7 +196,7 @@ " test.to_csv(test_set.path, index=None)" ], "outputs": [], - "execution_count": 4 + "execution_count": null }, { "cell_type": "markdown", @@ -242,11 +210,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T08:25:45.188150Z", - "start_time": "2024-04-13T08:25:45.167796Z" - } + "collapsed": false }, "source": [ "from typing import NamedTuple\n", @@ -363,7 +327,7 @@ " return output(mlflow.get_artifact_uri(), run_id)" ], "outputs": [], - "execution_count": 5 + "execution_count": null }, { "cell_type": "markdown", @@ -377,11 +341,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T08:25:50.816475Z", - "start_time": "2024-04-13T08:25:50.802017Z" - } + "collapsed": false }, "source": [ "@component(\n", @@ -424,7 +384,7 @@ " return True" ], "outputs": [], - "execution_count": 6 + "execution_count": null }, { "cell_type": "markdown", @@ -438,11 +398,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T09:37:55.751098Z", - "start_time": "2024-04-13T09:37:55.738332Z" - } + "collapsed": false }, "source": [ "@component(\n", @@ -501,7 +457,7 @@ " KServe.create(isvc)" ], "outputs": [], - "execution_count": 32 + "execution_count": null }, { "cell_type": "markdown", @@ -513,12 +469,7 @@ ] }, { - "metadata": { - "ExecuteTime": { - "end_time": "2024-04-13T09:37:56.721843Z", - "start_time": "2024-04-13T09:37:56.712749Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ " @component(\n", @@ -573,7 +524,7 @@ " logger.info(f\"\\nPrediction response:\\n{response.text}\\n\")" ], "outputs": [], - "execution_count": 33 + "execution_count": null }, { "cell_type": "markdown", @@ -589,11 +540,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T09:37:57.453669Z", - "start_time": "2024-04-13T09:37:57.448782Z" - } + "collapsed": false }, "source": [ "@dsl.pipeline(\n", @@ -649,7 +596,7 @@ " inference_task.after(deploy_model_task)" ], "outputs": [], - "execution_count": 34 + "execution_count": null }, { "cell_type": "markdown", @@ -663,11 +610,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T09:37:58.275887Z", - "start_time": "2024-04-13T09:37:58.273306Z" - } + "collapsed": false }, "source": [ "# Specify pipeline argument values\n", @@ -687,7 +630,7 @@ "}" ], "outputs": [], - "execution_count": 35 + "execution_count": null }, { "cell_type": "markdown", @@ -701,11 +644,7 @@ { "cell_type": "code", "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-13T09:37:59.349350Z", - "start_time": "2024-04-13T09:37:59.056885Z" - } + "collapsed": false }, "source": [ "run_name = \"demo-run\"\n", @@ -720,43 +659,8 @@ " enable_caching=False,\n", ")" ], - "outputs": [ - { - "data": { - "text/plain": [ - "" - ], - "text/html": [ - "Experiment details." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "" - ], - "text/html": [ - "Run details." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "RunPipelineResult(run_id=a988bfe4-12c5-4b27-9645-409c93cb3fcc)" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], - "execution_count": 36 + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", From 88b516de4692fc3469f115bdbc3ac637f4b07f06 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 12:44:01 +0300 Subject: [PATCH 09/20] typo --- .../demo_pipeline_standalone_kfp/.gitignore | 2 + .../demo_pipeline_standalone_kfp/README.md | 9 + .../demo-pipeline.ipynb | 772 ++++++++++++++++++ .../demo_pipeline_standalone_kfp/graph.png | Bin 0 -> 74336 bytes 4 files changed, 783 insertions(+) create mode 100644 tutorials/demo_notebooks/demo_pipeline_standalone_kfp/.gitignore create mode 100644 tutorials/demo_notebooks/demo_pipeline_standalone_kfp/README.md create mode 100644 tutorials/demo_notebooks/demo_pipeline_standalone_kfp/demo-pipeline.ipynb create mode 100644 tutorials/demo_notebooks/demo_pipeline_standalone_kfp/graph.png diff --git a/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/.gitignore b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/.gitignore new file mode 100644 index 0000000..b4a2938 --- /dev/null +++ b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/.gitignore @@ -0,0 +1,2 @@ +components/* +components/.gitkeep \ No newline at end of file diff --git a/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/README.md b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/README.md new file mode 100644 index 0000000..ef669ea --- /dev/null +++ b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/README.md @@ -0,0 +1,9 @@ +# Demo pipeline (standalone KFP) + +Jupyter notebook with a demo pipeline that uses the installed standalone Kubeflow Pipelines (KFP), MLflow and Kserve components. + +> **NOTE:** This demo is intended standalone-KFP. + +
+ +![Pipeline Graph](graph.png) \ No newline at end of file diff --git a/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/demo-pipeline.ipynb b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/demo-pipeline.ipynb new file mode 100644 index 0000000..5a6f32a --- /dev/null +++ b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/demo-pipeline.ipynb @@ -0,0 +1,772 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "# Demo KFP pipeline" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Install requirements:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false + }, + "source": [ + "%%bash\n", + "\n", + "pip install kfp~=1.8.14" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Imports:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false + }, + "source": [ + "import warnings\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "import kfp\n", + "import kfp.dsl as dsl\n", + "from kfp.aws import use_aws_secret\n", + "from kfp.v2.dsl import (\n", + " component,\n", + " Input,\n", + " Output,\n", + " Dataset,\n", + " Metrics,\n", + " Artifact,\n", + " Model\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 1. Connect to client\n", + "\n", + "Run the following to port-forward to the KFP UI:\n", + "\n", + "```sh\n", + "kubectl port-forward svc/ml-pipeline-ui -n kubeflow 8080:80\n", + "```\n", + "\n", + "Now the KFP UI should be reachable at [`http://localhost:8080`](http://localhost:8080)." + ] + }, + { + "cell_type": "code", + "source": [ + "import kfp\n", + "\n", + "KFP_ENDPOINT = \"http://localhost:8080\"\n", + "\n", + "client = kfp.Client(host=KFP_ENDPOINT)\n", + "# print(client.list_experiments())" + ], + "metadata": { + "collapsed": false + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 2. Components\n", + "\n", + "There are different ways to define components in KFP. Here, we use the **@component** decorator to define the components as Python function-based components.\n", + "\n", + "The **@component** annotation converts the function into a factory function that creates pipeline steps that execute this function. This example also specifies the base container image to run you component in." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Pull data component:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false + }, + "source": [ + "@component(\n", + " base_image=\"python:3.10\",\n", + " packages_to_install=[\"pandas~=1.4.2\"],\n", + " output_component_file='components/pull_data_component.yaml',\n", + ")\n", + "def pull_data(url: str, data: Output[Dataset]):\n", + " \"\"\"\n", + " Pull data component.\n", + " \"\"\"\n", + " import pandas as pd\n", + "\n", + " df = pd.read_csv(url, sep=\";\")\n", + " df.to_csv(data.path, index=None)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Preprocess component:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false + }, + "source": [ + "@component(\n", + " base_image=\"python:3.10\",\n", + " packages_to_install=[\"pandas~=1.4.2\", \"scikit-learn~=1.0.2\"],\n", + " output_component_file='components/preprocess_component.yaml',\n", + ")\n", + "def preprocess(\n", + " data: Input[Dataset],\n", + " scaler_out: Output[Artifact],\n", + " train_set: Output[Dataset],\n", + " test_set: Output[Dataset],\n", + " target: str = \"quality\",\n", + "):\n", + " \"\"\"\n", + " Preprocess component.\n", + " \"\"\"\n", + " import pandas as pd\n", + " import pickle\n", + " from sklearn.model_selection import train_test_split\n", + " from sklearn.preprocessing import StandardScaler\n", + "\n", + " data = pd.read_csv(data.path)\n", + "\n", + " # Split the data into training and test sets. (0.75, 0.25) split.\n", + " train, test = train_test_split(data)\n", + "\n", + " scaler = StandardScaler()\n", + "\n", + " train[train.drop(target, axis=1).columns] = scaler.fit_transform(train.drop(target, axis=1))\n", + " test[test.drop(target, axis=1).columns] = scaler.transform(test.drop(target, axis=1))\n", + "\n", + " with open(scaler_out.path, 'wb') as fp:\n", + " pickle.dump(scaler, fp, pickle.HIGHEST_PROTOCOL)\n", + "\n", + " train.to_csv(train_set.path, index=None)\n", + " test.to_csv(test_set.path, index=None)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Train component:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false + }, + "source": [ + "from typing import NamedTuple\n", + "\n", + "@component(\n", + " base_image=\"python:3.10\",\n", + " packages_to_install=[\"numpy\", \"pandas~=1.4.2\", \"scikit-learn~=1.0.2\", \"mlflow~=2.4.1\", \"boto3~=1.21.0\"],\n", + " output_component_file='components/train_component.yaml',\n", + ")\n", + "def train(\n", + " train_set: Input[Dataset],\n", + " test_set: Input[Dataset],\n", + " saved_model: Output[Model],\n", + " mlflow_experiment_name: str,\n", + " mlflow_tracking_uri: str,\n", + " mlflow_s3_endpoint_url: str,\n", + " model_name: str,\n", + " alpha: float,\n", + " l1_ratio: float,\n", + " target: str = \"quality\",\n", + ") -> NamedTuple(\"Output\", [('storage_uri', str), ('run_id', str),]):\n", + " \"\"\"\n", + " Train component.\n", + " \"\"\"\n", + " import numpy as np\n", + " import pandas as pd\n", + " from sklearn.linear_model import ElasticNet\n", + " from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score\n", + " import mlflow\n", + " import mlflow.sklearn\n", + " import os\n", + " import logging\n", + " import pickle\n", + " from collections import namedtuple\n", + "\n", + " logging.basicConfig(level=logging.INFO)\n", + " logger = logging.getLogger(__name__)\n", + "\n", + " def eval_metrics(actual, pred):\n", + " rmse = np.sqrt(mean_squared_error(actual, pred))\n", + " mae = mean_absolute_error(actual, pred)\n", + " r2 = r2_score(actual, pred)\n", + " return rmse, mae, r2\n", + "\n", + " os.environ['MLFLOW_S3_ENDPOINT_URL'] = mlflow_s3_endpoint_url\n", + "\n", + " # load data\n", + " train = pd.read_csv(train_set.path)\n", + " test = pd.read_csv(test_set.path)\n", + "\n", + " # The predicted column is \"quality\" which is a scalar from [3, 9]\n", + " train_x = train.drop([target], axis=1)\n", + " test_x = test.drop([target], axis=1)\n", + " train_y = train[[target]]\n", + " test_y = test[[target]]\n", + "\n", + " logger.info(f\"Using MLflow tracking URI: {mlflow_tracking_uri}\")\n", + " mlflow.set_tracking_uri(mlflow_tracking_uri)\n", + "\n", + " logger.info(f\"Using MLflow experiment: {mlflow_experiment_name}\")\n", + " mlflow.set_experiment(mlflow_experiment_name)\n", + "\n", + " with mlflow.start_run() as run:\n", + "\n", + " run_id = run.info.run_id\n", + " logger.info(f\"Run ID: {run_id}\")\n", + "\n", + " model = ElasticNet(alpha=alpha, l1_ratio=l1_ratio, random_state=42)\n", + "\n", + " logger.info(\"Fitting model...\")\n", + " model.fit(train_x, train_y)\n", + "\n", + " logger.info(\"Predicting...\")\n", + " predicted_qualities = model.predict(test_x)\n", + "\n", + " (rmse, mae, r2) = eval_metrics(test_y, predicted_qualities)\n", + "\n", + " logger.info(\"Elasticnet model (alpha=%f, l1_ratio=%f):\" % (alpha, l1_ratio))\n", + " logger.info(\" RMSE: %s\" % rmse)\n", + " logger.info(\" MAE: %s\" % mae)\n", + " logger.info(\" R2: %s\" % r2)\n", + "\n", + " logger.info(\"Logging parameters to MLflow\")\n", + " mlflow.log_param(\"alpha\", alpha)\n", + " mlflow.log_param(\"l1_ratio\", l1_ratio)\n", + " mlflow.log_metric(\"rmse\", rmse)\n", + " mlflow.log_metric(\"r2\", r2)\n", + " mlflow.log_metric(\"mae\", mae)\n", + "\n", + " # save model to mlflow\n", + " logger.info(\"Logging trained model\")\n", + " mlflow.sklearn.log_model(\n", + " model,\n", + " model_name,\n", + " registered_model_name=\"ElasticnetWineModel\",\n", + " serialization_format=\"pickle\"\n", + " )\n", + "\n", + " logger.info(\"Logging predictions artifact to MLflow\")\n", + " np.save(\"predictions.npy\", predicted_qualities)\n", + " mlflow.log_artifact(\n", + " local_path=\"predictions.npy\", artifact_path=\"predicted_qualities/\"\n", + " )\n", + "\n", + " # save model as KFP artifact\n", + " logging.info(f\"Saving model to: {saved_model.path}\")\n", + " with open(saved_model.path, 'wb') as fp:\n", + " pickle.dump(model, fp, pickle.HIGHEST_PROTOCOL)\n", + "\n", + " # prepare output\n", + " output = namedtuple('Output', ['storage_uri', 'run_id'])\n", + "\n", + " # return str(mlflow.get_artifact_uri())\n", + " return output(mlflow.get_artifact_uri(), run_id)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Evaluate component:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false + }, + "source": [ + "@component(\n", + " base_image=\"python:3.10\",\n", + " packages_to_install=[\"numpy\", \"mlflow~=2.4.1\"],\n", + " output_component_file='components/evaluate_component.yaml',\n", + ")\n", + "def evaluate(\n", + " run_id: str,\n", + " mlflow_tracking_uri: str,\n", + " threshold_metrics: dict\n", + ") -> bool:\n", + " \"\"\"\n", + " Evaluate component: Compares metrics from training with given thresholds.\n", + "\n", + " Args:\n", + " run_id (string): MLflow run ID\n", + " mlflow_tracking_uri (string): MLflow tracking URI\n", + " threshold_metrics (dict): Minimum threshold values for each metric\n", + " Returns:\n", + " Bool indicating whether evaluation passed or failed.\n", + " \"\"\"\n", + " from mlflow.tracking import MlflowClient\n", + " import logging\n", + "\n", + " logging.basicConfig(level=logging.INFO)\n", + " logger = logging.getLogger(__name__)\n", + "\n", + " client = MlflowClient(tracking_uri=mlflow_tracking_uri)\n", + " info = client.get_run(run_id)\n", + " training_metrics = info.data.metrics\n", + "\n", + " logger.info(f\"Training metrics: {training_metrics}\")\n", + "\n", + " # compare the evaluation metrics with the defined thresholds\n", + " for key, value in threshold_metrics.items():\n", + " if key not in training_metrics or training_metrics[key] > value:\n", + " logger.error(f\"Metric {key} failed. Evaluation not passed!\")\n", + " return False\n", + " return True" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Deploy model component:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false + }, + "source": [ + "@component(\n", + " base_image=\"python:3.9\",\n", + " packages_to_install=[\"kserve==0.11.0\"],\n", + " output_component_file='components/deploy_model_component.yaml',\n", + ")\n", + "def deploy_model(model_name: str, storage_uri: str):\n", + " \"\"\"\n", + " Deploy the model as an inference service with Kserve.\n", + " \"\"\"\n", + " import logging\n", + " from kubernetes import client\n", + " from kserve import KServeClient\n", + " from kserve import constants\n", + " from kserve import V1beta1InferenceService\n", + " from kserve import V1beta1InferenceServiceSpec\n", + " from kserve import V1beta1PredictorSpec\n", + " from kserve import V1beta1SKLearnSpec\n", + " from kubernetes.client import V1ResourceRequirements\n", + "\n", + " logging.basicConfig(level=logging.INFO)\n", + " logger = logging.getLogger(__name__)\n", + "\n", + " model_uri = f\"{storage_uri}/{model_name}\"\n", + " logger.info(f\"MODEL URI: {model_uri}\")\n", + "\n", + " namespace = 'kserve-inference'\n", + " kserve_version='v1beta1'\n", + " api_version = constants.KSERVE_GROUP + '/' + kserve_version\n", + "\n", + " isvc = V1beta1InferenceService(\n", + " api_version = api_version,\n", + " kind = constants.KSERVE_KIND,\n", + " metadata = client.V1ObjectMeta(\n", + " name = model_name,\n", + " namespace = namespace,\n", + " annotations = {'sidecar.istio.io/inject':'false'}\n", + " ),\n", + " spec = V1beta1InferenceServiceSpec(\n", + " predictor=V1beta1PredictorSpec(\n", + " service_account_name=\"kserve-sa\",\n", + " min_replicas=1,\n", + " max_replicas = 1,\n", + " sklearn=V1beta1SKLearnSpec(\n", + " storage_uri=model_uri,\n", + " resources=V1ResourceRequirements(\n", + " requests={\"cpu\": \"100m\", \"memory\": \"512Mi\"},\n", + " limits={\"cpu\": \"300m\", \"memory\": \"512Mi\"}\n", + " )\n", + " ),\n", + " )\n", + " )\n", + " )\n", + " KServe = KServeClient()\n", + " KServe.create(isvc)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Inference component:" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + " @component(\n", + " base_image=\"python:3.9\", # kserve on python 3.10 comes with a dependency that fails to get installed\n", + " packages_to_install=[\"kserve==0.11.0\", \"scikit-learn~=1.0.2\"],\n", + " output_component_file='components/inference_component.yaml',\n", + ")\n", + "def inference(\n", + " model_name: str,\n", + " scaler_in: Input[Artifact]\n", + "):\n", + " \"\"\"\n", + " Test inference.\n", + " \"\"\"\n", + " from kserve import KServeClient\n", + " import requests\n", + " import pickle\n", + " import logging\n", + "\n", + " logging.basicConfig(level=logging.INFO)\n", + " logger = logging.getLogger(__name__)\n", + "\n", + " namespace = 'kserve-inference'\n", + "\n", + " input_sample = [[5.6, 0.54, 0.04, 1.7, 0.049, 5, 13, 0.9942, 3.72, 0.58, 11.4],\n", + " [11.3, 0.34, 0.45, 2, 0.082, 6, 15, 0.9988, 2.94, 0.66, 9.2]]\n", + "\n", + " logger.info(f\"Loading standard scaler from: {scaler_in.path}\")\n", + " with open(scaler_in.path, 'rb') as fp:\n", + " scaler = pickle.load(fp)\n", + "\n", + " logger.info(f\"Standardizing sample: {scaler_in.path}\")\n", + " input_sample = scaler.transform(input_sample)\n", + "\n", + " # get inference service\n", + " KServe = KServeClient()\n", + "\n", + " # wait for deployment to be ready\n", + " KServe.get(model_name, namespace=namespace, watch=True, timeout_seconds=120)\n", + "\n", + " inference_service = KServe.get(model_name, namespace=namespace)\n", + " is_url = inference_service['status']['address']['url']\n", + "\n", + " logger.info(f\"\\nInference service status:\\n{inference_service['status']}\")\n", + " logger.info(f\"\\nInference service URL:\\n{is_url}\\n\")\n", + "\n", + " inference_input = {\n", + " 'instances': input_sample.tolist()\n", + " }\n", + "\n", + " response = requests.post(is_url, json=inference_input)\n", + " logger.info(f\"\\nPrediction response:\\n{response.text}\\n\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 3. Pipeline\n", + "\n", + "Pipeline definition:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false + }, + "source": [ + "@dsl.pipeline(\n", + " name='demo-pipeline',\n", + " description='An example pipeline that performs addition calculations.',\n", + ")\n", + "def pipeline(\n", + " url: str,\n", + " target: str,\n", + " mlflow_experiment_name: str,\n", + " mlflow_tracking_uri: str,\n", + " mlflow_s3_endpoint_url: str,\n", + " model_name: str,\n", + " alpha: float,\n", + " l1_ratio: float,\n", + " threshold_metrics: dict,\n", + "):\n", + " pull_task = pull_data(url=url)\n", + "\n", + " preprocess_task = preprocess(data=pull_task.outputs[\"data\"])\n", + "\n", + " train_task = train(\n", + " train_set=preprocess_task.outputs[\"train_set\"],\n", + " test_set=preprocess_task.outputs[\"test_set\"],\n", + " target=target,\n", + " mlflow_experiment_name=mlflow_experiment_name,\n", + " mlflow_tracking_uri=mlflow_tracking_uri,\n", + " mlflow_s3_endpoint_url=mlflow_s3_endpoint_url,\n", + " model_name=model_name,\n", + " alpha=alpha,\n", + " l1_ratio=l1_ratio\n", + " )\n", + " train_task.apply(use_aws_secret(secret_name=\"aws-secret\"))\n", + "\n", + " evaluate_trask = evaluate(\n", + " run_id=train_task.outputs[\"run_id\"],\n", + " mlflow_tracking_uri=mlflow_tracking_uri,\n", + " threshold_metrics=threshold_metrics\n", + " )\n", + "\n", + " eval_passed = evaluate_trask.output\n", + "\n", + " with dsl.Condition(eval_passed == \"true\"):\n", + " deploy_model_task = deploy_model(\n", + " model_name=model_name,\n", + " storage_uri=train_task.outputs[\"storage_uri\"],\n", + " )\n", + "\n", + " inference_task = inference(\n", + " model_name=model_name,\n", + " scaler_in=preprocess_task.outputs[\"scaler_out\"]\n", + " )\n", + " inference_task.after(deploy_model_task)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Pipeline arguments:" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false + }, + "source": [ + "# Specify pipeline argument values\n", + "\n", + "eval_threshold_metrics = {'rmse': 0.9, 'r2': 0.3, 'mae': 0.8}\n", + "\n", + "arguments = {\n", + " \"url\": \"http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv\",\n", + " \"target\": \"quality\",\n", + " \"mlflow_tracking_uri\": \"http://mlflow.mlflow.svc.cluster.local:5000\",\n", + " \"mlflow_s3_endpoint_url\": \"http://mlflow-minio-service.mlflow.svc.cluster.local:9000\",\n", + " \"mlflow_experiment_name\": \"demo-notebook\",\n", + " \"model_name\": \"wine-quality\",\n", + " \"alpha\": 0.5,\n", + " \"l1_ratio\": 0.5,\n", + " \"threshold_metrics\": eval_threshold_metrics\n", + "}" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 4. Submit run" + ] + }, + { + "cell_type": "code", + "metadata": { + "collapsed": false + }, + "source": [ + "run_name = \"demo-run\"\n", + "experiment_name = \"demo-experiment\"\n", + "\n", + "client.create_run_from_pipeline_func(\n", + " pipeline_func=pipeline,\n", + " run_name=run_name,\n", + " experiment_name=experiment_name,\n", + " arguments=arguments,\n", + " mode=kfp.dsl.PipelineExecutionMode.V2_COMPATIBLE,\n", + " enable_caching=False,\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 5. Check run" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Kubeflow Pipelines UI\n", + "\n", + "The default way of accessing KFP UI is via port-forward. This enables you to get started quickly without imposing any requirements on your environment. Run the following to port-forward KFP UI to local port `8080`:\n", + "\n", + "```sh\n", + "kubectl port-forward svc/ml-pipeline-ui -n kubeflow 8080:80\n", + "```\n", + "\n", + "Now the KFP UI should be reachable at [`http://localhost:8080`](http://localhost:8080)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### MLFlow UI\n", + "\n", + "To access MLFlow UI, open a terminal and forward a local port to MLFlow server:\n", + "\n", + "
\n", + "\n", + "```bash\n", + "$ kubectl -n mlflow port-forward svc/mlflow 5000:5000\n", + "```\n", + "\n", + "
\n", + "\n", + "Now MLFlow's UI should be reachable at [`http://localhost:5000`](http://localhost:5000)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## 6. Check deployed model\n", + "\n", + "```bash\n", + "# get inference services\n", + "kubectl -n kserve-inference get inferenceservice\n", + "\n", + "# get deployed model pods\n", + "kubectl -n kserve-inference get pods\n", + "\n", + "# delete inference service\n", + "kubectl -n kserve-inference delete inferenceservice wine-quality\n", + "```\n", + "
\n", + "\n", + "If something goes wrong, check the logs with:\n", + "\n", + "
\n", + "\n", + "```bash\n", + "kubectl logs -n kserve-inference kserve-container\n", + "\n", + "kubectl logs -n kserve-inference queue-proxy\n", + "\n", + "kubectl logs -n kserve-inference storage-initializer\n", + "```\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "iml4e", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.15 (default, Nov 24 2022, 08:57:44) \n[Clang 14.0.6 ]" + }, + "vscode": { + "interpreter": { + "hash": "2976e1db094957a35b33d12f80288a268286b510a60c0d029aa085f0b10be691" + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/graph.png b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/graph.png new file mode 100644 index 0000000000000000000000000000000000000000..14bf10cb7e8f888ba05c543ded751848645a8d5f GIT binary patch literal 74336 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 f8wMLi(?U(&8Vk`FsBN!g-cb%g4{e~Rc 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 U&ebCz;DTfkDq{C`et2T;-`Ni=cK`qY literal 0 HcmV?d00001 From 19d98e680d547d1b1536d85072e30f079485bd4c Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 12:44:17 +0300 Subject: [PATCH 10/20] typo --- .../demo_pipeline_stanlone_kfp/.gitignore | 2 - .../demo_pipeline_stanlone_kfp/README.md | 9 - .../demo-pipeline.ipynb | 772 ------------------ .../demo_pipeline_stanlone_kfp/graph.png | Bin 74336 -> 0 bytes 4 files changed, 783 deletions(-) delete mode 100644 tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/.gitignore delete mode 100644 tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/README.md delete mode 100644 tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb delete mode 100644 tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/graph.png diff --git a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/.gitignore b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/.gitignore deleted file mode 100644 index b4a2938..0000000 --- a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -components/* -components/.gitkeep \ No newline at end of file diff --git a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/README.md b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/README.md deleted file mode 100644 index ef669ea..0000000 --- a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Demo pipeline (standalone KFP) - -Jupyter notebook with a demo pipeline that uses the installed standalone Kubeflow Pipelines (KFP), MLflow and Kserve components. - -> **NOTE:** This demo is intended standalone-KFP. - -
- -![Pipeline Graph](graph.png) \ No newline at end of file diff --git a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb deleted file mode 100644 index 5a6f32a..0000000 --- a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/demo-pipeline.ipynb +++ /dev/null @@ -1,772 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "# Demo KFP pipeline" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Install requirements:" - ] - }, - { - "cell_type": "code", - "metadata": { - "collapsed": false - }, - "source": [ - "%%bash\n", - "\n", - "pip install kfp~=1.8.14" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Imports:" - ] - }, - { - "cell_type": "code", - "metadata": { - "collapsed": false - }, - "source": [ - "import warnings\n", - "warnings.filterwarnings(\"ignore\")\n", - "\n", - "import kfp\n", - "import kfp.dsl as dsl\n", - "from kfp.aws import use_aws_secret\n", - "from kfp.v2.dsl import (\n", - " component,\n", - " Input,\n", - " Output,\n", - " Dataset,\n", - " Metrics,\n", - " Artifact,\n", - " Model\n", - ")" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "## 1. Connect to client\n", - "\n", - "Run the following to port-forward to the KFP UI:\n", - "\n", - "```sh\n", - "kubectl port-forward svc/ml-pipeline-ui -n kubeflow 8080:80\n", - "```\n", - "\n", - "Now the KFP UI should be reachable at [`http://localhost:8080`](http://localhost:8080)." - ] - }, - { - "cell_type": "code", - "source": [ - "import kfp\n", - "\n", - "KFP_ENDPOINT = \"http://localhost:8080\"\n", - "\n", - "client = kfp.Client(host=KFP_ENDPOINT)\n", - "# print(client.list_experiments())" - ], - "metadata": { - "collapsed": false - }, - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "## 2. Components\n", - "\n", - "There are different ways to define components in KFP. Here, we use the **@component** decorator to define the components as Python function-based components.\n", - "\n", - "The **@component** annotation converts the function into a factory function that creates pipeline steps that execute this function. This example also specifies the base container image to run you component in." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Pull data component:" - ] - }, - { - "cell_type": "code", - "metadata": { - "collapsed": false - }, - "source": [ - "@component(\n", - " base_image=\"python:3.10\",\n", - " packages_to_install=[\"pandas~=1.4.2\"],\n", - " output_component_file='components/pull_data_component.yaml',\n", - ")\n", - "def pull_data(url: str, data: Output[Dataset]):\n", - " \"\"\"\n", - " Pull data component.\n", - " \"\"\"\n", - " import pandas as pd\n", - "\n", - " df = pd.read_csv(url, sep=\";\")\n", - " df.to_csv(data.path, index=None)" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Preprocess component:" - ] - }, - { - "cell_type": "code", - "metadata": { - "collapsed": false - }, - "source": [ - "@component(\n", - " base_image=\"python:3.10\",\n", - " packages_to_install=[\"pandas~=1.4.2\", \"scikit-learn~=1.0.2\"],\n", - " output_component_file='components/preprocess_component.yaml',\n", - ")\n", - "def preprocess(\n", - " data: Input[Dataset],\n", - " scaler_out: Output[Artifact],\n", - " train_set: Output[Dataset],\n", - " test_set: Output[Dataset],\n", - " target: str = \"quality\",\n", - "):\n", - " \"\"\"\n", - " Preprocess component.\n", - " \"\"\"\n", - " import pandas as pd\n", - " import pickle\n", - " from sklearn.model_selection import train_test_split\n", - " from sklearn.preprocessing import StandardScaler\n", - "\n", - " data = pd.read_csv(data.path)\n", - "\n", - " # Split the data into training and test sets. (0.75, 0.25) split.\n", - " train, test = train_test_split(data)\n", - "\n", - " scaler = StandardScaler()\n", - "\n", - " train[train.drop(target, axis=1).columns] = scaler.fit_transform(train.drop(target, axis=1))\n", - " test[test.drop(target, axis=1).columns] = scaler.transform(test.drop(target, axis=1))\n", - "\n", - " with open(scaler_out.path, 'wb') as fp:\n", - " pickle.dump(scaler, fp, pickle.HIGHEST_PROTOCOL)\n", - "\n", - " train.to_csv(train_set.path, index=None)\n", - " test.to_csv(test_set.path, index=None)" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Train component:" - ] - }, - { - "cell_type": "code", - "metadata": { - "collapsed": false - }, - "source": [ - "from typing import NamedTuple\n", - "\n", - "@component(\n", - " base_image=\"python:3.10\",\n", - " packages_to_install=[\"numpy\", \"pandas~=1.4.2\", \"scikit-learn~=1.0.2\", \"mlflow~=2.4.1\", \"boto3~=1.21.0\"],\n", - " output_component_file='components/train_component.yaml',\n", - ")\n", - "def train(\n", - " train_set: Input[Dataset],\n", - " test_set: Input[Dataset],\n", - " saved_model: Output[Model],\n", - " mlflow_experiment_name: str,\n", - " mlflow_tracking_uri: str,\n", - " mlflow_s3_endpoint_url: str,\n", - " model_name: str,\n", - " alpha: float,\n", - " l1_ratio: float,\n", - " target: str = \"quality\",\n", - ") -> NamedTuple(\"Output\", [('storage_uri', str), ('run_id', str),]):\n", - " \"\"\"\n", - " Train component.\n", - " \"\"\"\n", - " import numpy as np\n", - " import pandas as pd\n", - " from sklearn.linear_model import ElasticNet\n", - " from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score\n", - " import mlflow\n", - " import mlflow.sklearn\n", - " import os\n", - " import logging\n", - " import pickle\n", - " from collections import namedtuple\n", - "\n", - " logging.basicConfig(level=logging.INFO)\n", - " logger = logging.getLogger(__name__)\n", - "\n", - " def eval_metrics(actual, pred):\n", - " rmse = np.sqrt(mean_squared_error(actual, pred))\n", - " mae = mean_absolute_error(actual, pred)\n", - " r2 = r2_score(actual, pred)\n", - " return rmse, mae, r2\n", - "\n", - " os.environ['MLFLOW_S3_ENDPOINT_URL'] = mlflow_s3_endpoint_url\n", - "\n", - " # load data\n", - " train = pd.read_csv(train_set.path)\n", - " test = pd.read_csv(test_set.path)\n", - "\n", - " # The predicted column is \"quality\" which is a scalar from [3, 9]\n", - " train_x = train.drop([target], axis=1)\n", - " test_x = test.drop([target], axis=1)\n", - " train_y = train[[target]]\n", - " test_y = test[[target]]\n", - "\n", - " logger.info(f\"Using MLflow tracking URI: {mlflow_tracking_uri}\")\n", - " mlflow.set_tracking_uri(mlflow_tracking_uri)\n", - "\n", - " logger.info(f\"Using MLflow experiment: {mlflow_experiment_name}\")\n", - " mlflow.set_experiment(mlflow_experiment_name)\n", - "\n", - " with mlflow.start_run() as run:\n", - "\n", - " run_id = run.info.run_id\n", - " logger.info(f\"Run ID: {run_id}\")\n", - "\n", - " model = ElasticNet(alpha=alpha, l1_ratio=l1_ratio, random_state=42)\n", - "\n", - " logger.info(\"Fitting model...\")\n", - " model.fit(train_x, train_y)\n", - "\n", - " logger.info(\"Predicting...\")\n", - " predicted_qualities = model.predict(test_x)\n", - "\n", - " (rmse, mae, r2) = eval_metrics(test_y, predicted_qualities)\n", - "\n", - " logger.info(\"Elasticnet model (alpha=%f, l1_ratio=%f):\" % (alpha, l1_ratio))\n", - " logger.info(\" RMSE: %s\" % rmse)\n", - " logger.info(\" MAE: %s\" % mae)\n", - " logger.info(\" R2: %s\" % r2)\n", - "\n", - " logger.info(\"Logging parameters to MLflow\")\n", - " mlflow.log_param(\"alpha\", alpha)\n", - " mlflow.log_param(\"l1_ratio\", l1_ratio)\n", - " mlflow.log_metric(\"rmse\", rmse)\n", - " mlflow.log_metric(\"r2\", r2)\n", - " mlflow.log_metric(\"mae\", mae)\n", - "\n", - " # save model to mlflow\n", - " logger.info(\"Logging trained model\")\n", - " mlflow.sklearn.log_model(\n", - " model,\n", - " model_name,\n", - " registered_model_name=\"ElasticnetWineModel\",\n", - " serialization_format=\"pickle\"\n", - " )\n", - "\n", - " logger.info(\"Logging predictions artifact to MLflow\")\n", - " np.save(\"predictions.npy\", predicted_qualities)\n", - " mlflow.log_artifact(\n", - " local_path=\"predictions.npy\", artifact_path=\"predicted_qualities/\"\n", - " )\n", - "\n", - " # save model as KFP artifact\n", - " logging.info(f\"Saving model to: {saved_model.path}\")\n", - " with open(saved_model.path, 'wb') as fp:\n", - " pickle.dump(model, fp, pickle.HIGHEST_PROTOCOL)\n", - "\n", - " # prepare output\n", - " output = namedtuple('Output', ['storage_uri', 'run_id'])\n", - "\n", - " # return str(mlflow.get_artifact_uri())\n", - " return output(mlflow.get_artifact_uri(), run_id)" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Evaluate component:" - ] - }, - { - "cell_type": "code", - "metadata": { - "collapsed": false - }, - "source": [ - "@component(\n", - " base_image=\"python:3.10\",\n", - " packages_to_install=[\"numpy\", \"mlflow~=2.4.1\"],\n", - " output_component_file='components/evaluate_component.yaml',\n", - ")\n", - "def evaluate(\n", - " run_id: str,\n", - " mlflow_tracking_uri: str,\n", - " threshold_metrics: dict\n", - ") -> bool:\n", - " \"\"\"\n", - " Evaluate component: Compares metrics from training with given thresholds.\n", - "\n", - " Args:\n", - " run_id (string): MLflow run ID\n", - " mlflow_tracking_uri (string): MLflow tracking URI\n", - " threshold_metrics (dict): Minimum threshold values for each metric\n", - " Returns:\n", - " Bool indicating whether evaluation passed or failed.\n", - " \"\"\"\n", - " from mlflow.tracking import MlflowClient\n", - " import logging\n", - "\n", - " logging.basicConfig(level=logging.INFO)\n", - " logger = logging.getLogger(__name__)\n", - "\n", - " client = MlflowClient(tracking_uri=mlflow_tracking_uri)\n", - " info = client.get_run(run_id)\n", - " training_metrics = info.data.metrics\n", - "\n", - " logger.info(f\"Training metrics: {training_metrics}\")\n", - "\n", - " # compare the evaluation metrics with the defined thresholds\n", - " for key, value in threshold_metrics.items():\n", - " if key not in training_metrics or training_metrics[key] > value:\n", - " logger.error(f\"Metric {key} failed. Evaluation not passed!\")\n", - " return False\n", - " return True" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Deploy model component:" - ] - }, - { - "cell_type": "code", - "metadata": { - "collapsed": false - }, - "source": [ - "@component(\n", - " base_image=\"python:3.9\",\n", - " packages_to_install=[\"kserve==0.11.0\"],\n", - " output_component_file='components/deploy_model_component.yaml',\n", - ")\n", - "def deploy_model(model_name: str, storage_uri: str):\n", - " \"\"\"\n", - " Deploy the model as an inference service with Kserve.\n", - " \"\"\"\n", - " import logging\n", - " from kubernetes import client\n", - " from kserve import KServeClient\n", - " from kserve import constants\n", - " from kserve import V1beta1InferenceService\n", - " from kserve import V1beta1InferenceServiceSpec\n", - " from kserve import V1beta1PredictorSpec\n", - " from kserve import V1beta1SKLearnSpec\n", - " from kubernetes.client import V1ResourceRequirements\n", - "\n", - " logging.basicConfig(level=logging.INFO)\n", - " logger = logging.getLogger(__name__)\n", - "\n", - " model_uri = f\"{storage_uri}/{model_name}\"\n", - " logger.info(f\"MODEL URI: {model_uri}\")\n", - "\n", - " namespace = 'kserve-inference'\n", - " kserve_version='v1beta1'\n", - " api_version = constants.KSERVE_GROUP + '/' + kserve_version\n", - "\n", - " isvc = V1beta1InferenceService(\n", - " api_version = api_version,\n", - " kind = constants.KSERVE_KIND,\n", - " metadata = client.V1ObjectMeta(\n", - " name = model_name,\n", - " namespace = namespace,\n", - " annotations = {'sidecar.istio.io/inject':'false'}\n", - " ),\n", - " spec = V1beta1InferenceServiceSpec(\n", - " predictor=V1beta1PredictorSpec(\n", - " service_account_name=\"kserve-sa\",\n", - " min_replicas=1,\n", - " max_replicas = 1,\n", - " sklearn=V1beta1SKLearnSpec(\n", - " storage_uri=model_uri,\n", - " resources=V1ResourceRequirements(\n", - " requests={\"cpu\": \"100m\", \"memory\": \"512Mi\"},\n", - " limits={\"cpu\": \"300m\", \"memory\": \"512Mi\"}\n", - " )\n", - " ),\n", - " )\n", - " )\n", - " )\n", - " KServe = KServeClient()\n", - " KServe.create(isvc)" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Inference component:" - ] - }, - { - "metadata": {}, - "cell_type": "code", - "source": [ - " @component(\n", - " base_image=\"python:3.9\", # kserve on python 3.10 comes with a dependency that fails to get installed\n", - " packages_to_install=[\"kserve==0.11.0\", \"scikit-learn~=1.0.2\"],\n", - " output_component_file='components/inference_component.yaml',\n", - ")\n", - "def inference(\n", - " model_name: str,\n", - " scaler_in: Input[Artifact]\n", - "):\n", - " \"\"\"\n", - " Test inference.\n", - " \"\"\"\n", - " from kserve import KServeClient\n", - " import requests\n", - " import pickle\n", - " import logging\n", - "\n", - " logging.basicConfig(level=logging.INFO)\n", - " logger = logging.getLogger(__name__)\n", - "\n", - " namespace = 'kserve-inference'\n", - "\n", - " input_sample = [[5.6, 0.54, 0.04, 1.7, 0.049, 5, 13, 0.9942, 3.72, 0.58, 11.4],\n", - " [11.3, 0.34, 0.45, 2, 0.082, 6, 15, 0.9988, 2.94, 0.66, 9.2]]\n", - "\n", - " logger.info(f\"Loading standard scaler from: {scaler_in.path}\")\n", - " with open(scaler_in.path, 'rb') as fp:\n", - " scaler = pickle.load(fp)\n", - "\n", - " logger.info(f\"Standardizing sample: {scaler_in.path}\")\n", - " input_sample = scaler.transform(input_sample)\n", - "\n", - " # get inference service\n", - " KServe = KServeClient()\n", - "\n", - " # wait for deployment to be ready\n", - " KServe.get(model_name, namespace=namespace, watch=True, timeout_seconds=120)\n", - "\n", - " inference_service = KServe.get(model_name, namespace=namespace)\n", - " is_url = inference_service['status']['address']['url']\n", - "\n", - " logger.info(f\"\\nInference service status:\\n{inference_service['status']}\")\n", - " logger.info(f\"\\nInference service URL:\\n{is_url}\\n\")\n", - "\n", - " inference_input = {\n", - " 'instances': input_sample.tolist()\n", - " }\n", - "\n", - " response = requests.post(is_url, json=inference_input)\n", - " logger.info(f\"\\nPrediction response:\\n{response.text}\\n\")" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "## 3. Pipeline\n", - "\n", - "Pipeline definition:" - ] - }, - { - "cell_type": "code", - "metadata": { - "collapsed": false - }, - "source": [ - "@dsl.pipeline(\n", - " name='demo-pipeline',\n", - " description='An example pipeline that performs addition calculations.',\n", - ")\n", - "def pipeline(\n", - " url: str,\n", - " target: str,\n", - " mlflow_experiment_name: str,\n", - " mlflow_tracking_uri: str,\n", - " mlflow_s3_endpoint_url: str,\n", - " model_name: str,\n", - " alpha: float,\n", - " l1_ratio: float,\n", - " threshold_metrics: dict,\n", - "):\n", - " pull_task = pull_data(url=url)\n", - "\n", - " preprocess_task = preprocess(data=pull_task.outputs[\"data\"])\n", - "\n", - " train_task = train(\n", - " train_set=preprocess_task.outputs[\"train_set\"],\n", - " test_set=preprocess_task.outputs[\"test_set\"],\n", - " target=target,\n", - " mlflow_experiment_name=mlflow_experiment_name,\n", - " mlflow_tracking_uri=mlflow_tracking_uri,\n", - " mlflow_s3_endpoint_url=mlflow_s3_endpoint_url,\n", - " model_name=model_name,\n", - " alpha=alpha,\n", - " l1_ratio=l1_ratio\n", - " )\n", - " train_task.apply(use_aws_secret(secret_name=\"aws-secret\"))\n", - "\n", - " evaluate_trask = evaluate(\n", - " run_id=train_task.outputs[\"run_id\"],\n", - " mlflow_tracking_uri=mlflow_tracking_uri,\n", - " threshold_metrics=threshold_metrics\n", - " )\n", - "\n", - " eval_passed = evaluate_trask.output\n", - "\n", - " with dsl.Condition(eval_passed == \"true\"):\n", - " deploy_model_task = deploy_model(\n", - " model_name=model_name,\n", - " storage_uri=train_task.outputs[\"storage_uri\"],\n", - " )\n", - "\n", - " inference_task = inference(\n", - " model_name=model_name,\n", - " scaler_in=preprocess_task.outputs[\"scaler_out\"]\n", - " )\n", - " inference_task.after(deploy_model_task)" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Pipeline arguments:" - ] - }, - { - "cell_type": "code", - "metadata": { - "collapsed": false - }, - "source": [ - "# Specify pipeline argument values\n", - "\n", - "eval_threshold_metrics = {'rmse': 0.9, 'r2': 0.3, 'mae': 0.8}\n", - "\n", - "arguments = {\n", - " \"url\": \"http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv\",\n", - " \"target\": \"quality\",\n", - " \"mlflow_tracking_uri\": \"http://mlflow.mlflow.svc.cluster.local:5000\",\n", - " \"mlflow_s3_endpoint_url\": \"http://mlflow-minio-service.mlflow.svc.cluster.local:9000\",\n", - " \"mlflow_experiment_name\": \"demo-notebook\",\n", - " \"model_name\": \"wine-quality\",\n", - " \"alpha\": 0.5,\n", - " \"l1_ratio\": 0.5,\n", - " \"threshold_metrics\": eval_threshold_metrics\n", - "}" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "## 4. Submit run" - ] - }, - { - "cell_type": "code", - "metadata": { - "collapsed": false - }, - "source": [ - "run_name = \"demo-run\"\n", - "experiment_name = \"demo-experiment\"\n", - "\n", - "client.create_run_from_pipeline_func(\n", - " pipeline_func=pipeline,\n", - " run_name=run_name,\n", - " experiment_name=experiment_name,\n", - " arguments=arguments,\n", - " mode=kfp.dsl.PipelineExecutionMode.V2_COMPATIBLE,\n", - " enable_caching=False,\n", - ")" - ], - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "## 5. Check run" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "### Kubeflow Pipelines UI\n", - "\n", - "The default way of accessing KFP UI is via port-forward. This enables you to get started quickly without imposing any requirements on your environment. Run the following to port-forward KFP UI to local port `8080`:\n", - "\n", - "```sh\n", - "kubectl port-forward svc/ml-pipeline-ui -n kubeflow 8080:80\n", - "```\n", - "\n", - "Now the KFP UI should be reachable at [`http://localhost:8080`](http://localhost:8080)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "### MLFlow UI\n", - "\n", - "To access MLFlow UI, open a terminal and forward a local port to MLFlow server:\n", - "\n", - "
\n", - "\n", - "```bash\n", - "$ kubectl -n mlflow port-forward svc/mlflow 5000:5000\n", - "```\n", - "\n", - "
\n", - "\n", - "Now MLFlow's UI should be reachable at [`http://localhost:5000`](http://localhost:5000)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "## 6. Check deployed model\n", - "\n", - "```bash\n", - "# get inference services\n", - "kubectl -n kserve-inference get inferenceservice\n", - "\n", - "# get deployed model pods\n", - "kubectl -n kserve-inference get pods\n", - "\n", - "# delete inference service\n", - "kubectl -n kserve-inference delete inferenceservice wine-quality\n", - "```\n", - "
\n", - "\n", - "If something goes wrong, check the logs with:\n", - "\n", - "
\n", - "\n", - "```bash\n", - "kubectl logs -n kserve-inference kserve-container\n", - "\n", - "kubectl logs -n kserve-inference queue-proxy\n", - "\n", - "kubectl logs -n kserve-inference storage-initializer\n", - "```\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "iml4e", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.15 (default, Nov 24 2022, 08:57:44) \n[Clang 14.0.6 ]" - }, - "vscode": { - "interpreter": { - "hash": "2976e1db094957a35b33d12f80288a268286b510a60c0d029aa085f0b10be691" - } - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/graph.png b/tutorials/demo_notebooks/demo_pipeline_stanlone_kfp/graph.png deleted file mode 100644 index 14bf10cb7e8f888ba05c543ded751848645a8d5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74336 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 f8wMLi(?U(&8Vk`FsBN!g-cb%g4{e~Rc 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 U&ebCz;DTfkDq{C`et2T;-`Ni=cK`qY From 0a7015bfcea6247bdbb29b5f66c8accd8b82fccc Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 12:47:27 +0300 Subject: [PATCH 11/20] improve note --- tutorials/demo_notebooks/demo_pipeline_standalone_kfp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/README.md b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/README.md index ef669ea..5b57ad6 100644 --- a/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/README.md +++ b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/README.md @@ -2,7 +2,7 @@ Jupyter notebook with a demo pipeline that uses the installed standalone Kubeflow Pipelines (KFP), MLflow and Kserve components. -> **NOTE:** This demo is intended standalone-KFP. +> **NOTE:** This demo is intended for the standalone-KFP + Kserve deployment option.
From 32543ef230676877cd7093d763907378e32cf46d Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 14:33:03 +0300 Subject: [PATCH 12/20] typo and add link to University of Helsinki --- CONTRIBUTORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 48b91fb..caa8276 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -44,7 +44,7 @@ Other developers and testers of the platform: ### [IML4E](https://itea4.org/project/iml4e.html) project and other contributors: -Univerwity of Helsinki: +[University of Helsinki](https://www.helsinki.fi/en/researchgroups/empirical-software-engineering): - Niila Siilasjoki - Dennis Muiruri From 0476eda6e8e66f71ee4161260e2c8423ba351068 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 16:22:47 +0300 Subject: [PATCH 13/20] fix typo DISK_SPACE --- setup.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.sh b/setup.sh index 6c5dafc..d34fe85 100755 --- a/setup.sh +++ b/setup.sh @@ -73,9 +73,9 @@ echo -e "\nINSTALL_RAY=$INSTALL_RAY" >> $PLATFORM_CONFIG # CHECK DISK SPACE RECOMMENDED_DISK_SPACE_KUBEFLOW=26214400 -RECOMMENDED_DISK_SPACE_KUBEFLOW_GB=$(($RECOMMENDED_DISK_SPACE / 1024 / 1024)) +RECOMMENDED_DISK_SPACE_KUBEFLOW_GB=$(($RECOMMENDED_DISK_SPACE_KUBEFLOW / 1024 / 1024)) RECOMMENDED_DISK_SPACE_KFP=20971520 -RECOMMENDED_DISK_SPACE_KFP_GB=$(($RECOMMENDED_DISK_SPACE / 1024 / 1024)) +RECOMMENDED_DISK_SPACE_KFP_GB=$(($RECOMMENDED_DISK_SPACE_KFP / 1024 / 1024)) if [[ $DEPLOYMENT_OPTION == *"kfp"* ]]; then RECOMMENDED_DISK_SPACE=$RECOMMENDED_DISK_SPACE_KFP From 789b0ac1a41bb5b8446f1fd3d708f12283e8447a Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 16:24:50 +0300 Subject: [PATCH 14/20] add components dir to avoid error when dir doesn't exist --- .../demo_pipeline_standalone_kfp/components/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tutorials/demo_notebooks/demo_pipeline_standalone_kfp/components/.gitkeep diff --git a/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/components/.gitkeep b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/components/.gitkeep new file mode 100644 index 0000000..e69de29 From afe605ab148db6d0261b783af53115117ca19526 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 16:47:32 +0300 Subject: [PATCH 15/20] fix standalone-kfp-kserve install option to correct kubeflow-custom kserve manifests --- .../envs/standalone-kfp-kserve-monitoring/kustomization.yaml | 2 +- deployment/envs/standalone-kfp-kserve/kustomization.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/envs/standalone-kfp-kserve-monitoring/kustomization.yaml b/deployment/envs/standalone-kfp-kserve-monitoring/kustomization.yaml index 9a23144..5d27568 100644 --- a/deployment/envs/standalone-kfp-kserve-monitoring/kustomization.yaml +++ b/deployment/envs/standalone-kfp-kserve-monitoring/kustomization.yaml @@ -3,7 +3,7 @@ kind: Kustomization resources: - ../../kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve -- ../../custom/kubeflow-custom/env/standalone-kfp +- ../../custom/kubeflow-custom/env/standalone-kfp-kserve - ../../custom/kserve-custom/env/standalone-kfp - ../../mlflow/env/local - ../../monitoring \ No newline at end of file diff --git a/deployment/envs/standalone-kfp-kserve/kustomization.yaml b/deployment/envs/standalone-kfp-kserve/kustomization.yaml index c090b17..e757443 100644 --- a/deployment/envs/standalone-kfp-kserve/kustomization.yaml +++ b/deployment/envs/standalone-kfp-kserve/kustomization.yaml @@ -3,6 +3,6 @@ kind: Kustomization resources: - ../../kubeflow/manifests/in-cluster-setup/standalone-kfp-kserve -- ../../custom/kubeflow-custom/env/standalone-kfp +- ../../custom/kubeflow-custom/env/standalone-kfp-kserve - ../../custom/kserve-custom/env/standalone-kfp - ../../mlflow/env/local \ No newline at end of file From 64ebfcbed7c8f70cad85681674221ee921ed02d4 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 17:04:10 +0300 Subject: [PATCH 16/20] lower RECOMMENDED_DISK_SPACE_KFP to 18Gb --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index d34fe85..cfc2b7f 100755 --- a/setup.sh +++ b/setup.sh @@ -74,7 +74,7 @@ echo -e "\nINSTALL_RAY=$INSTALL_RAY" >> $PLATFORM_CONFIG # CHECK DISK SPACE RECOMMENDED_DISK_SPACE_KUBEFLOW=26214400 RECOMMENDED_DISK_SPACE_KUBEFLOW_GB=$(($RECOMMENDED_DISK_SPACE_KUBEFLOW / 1024 / 1024)) -RECOMMENDED_DISK_SPACE_KFP=20971520 +RECOMMENDED_DISK_SPACE_KFP=18874368 RECOMMENDED_DISK_SPACE_KFP_GB=$(($RECOMMENDED_DISK_SPACE_KFP / 1024 / 1024)) if [[ $DEPLOYMENT_OPTION == *"kfp"* ]]; then From f20f8ae132f8ad0fc4a963fe4513287d2dd4349b Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 17:05:03 +0300 Subject: [PATCH 17/20] add minimum recomended machine requirements info in setup.md --- setup.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.md b/setup.md index 9bed5b0..c424959 100644 --- a/setup.md +++ b/setup.md @@ -24,6 +24,9 @@ Install the experimentation platform with: 5. **Standalone KFP and Kserve:** Standalone KFP and Kserve deployment. 6. **Standalone KFP and Kserve (without monitoring):** Standalone KFP and Kserve deployment without monitoring components (prometheus, grafana). +> The minimum recommended machine requirements are: +> - **Kubeflow** options: 12 CPU cores, 25GB free disk space. +> - **Standalone KFP** options: 8 CPU cores, 18GB free disk space. ## Test the deployment (manually) From 6a47f97c9537ce8e82c57db6dd05dea80424f310 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Thu, 18 Apr 2024 17:14:02 +0300 Subject: [PATCH 18/20] add info about the extra CPUs required when prompting the user about installing Ray --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index cfc2b7f..797b6db 100755 --- a/setup.sh +++ b/setup.sh @@ -60,7 +60,7 @@ esac INSTALL_RAY=false echo -read -p "Install Ray? (y/n) (default is [n]): " choice +read -p "Install Ray? (It requires ~4 additional CPUs) (y/n) (default is [n]): " choice case "$choice" in y|Y ) INSTALL_RAY=true ;; * ) INSTALL_RAY=false ;; From 5392e4728e387e68dd02ea191b84e197d8819b6c Mon Sep 17 00:00:00 2001 From: Joaquin Date: Fri, 19 Apr 2024 08:28:27 +0300 Subject: [PATCH 19/20] adapt tests to standalone KFP --- tests/conftest.py | 5 +++ tests/test_kfp.py | 80 +++++++++++++++++++++++++++++------------- tests/test_registry.py | 39 +++++++++++--------- tests/utils.py | 16 +++++++++ 4 files changed, 99 insertions(+), 41 deletions(-) create mode 100644 tests/utils.py diff --git a/tests/conftest.py b/tests/conftest.py index 003ce94..38728fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,8 @@ from dotenv import load_dotenv import os +from .utils import parse_bool + ENV_FILE = pathlib.Path(__file__).parent.parent / ".platform/.config" assert ENV_FILE.exists(), f"File not found: {ENV_FILE} (autogenerated by the platform on installation)" # noqa load_dotenv(dotenv_path=ENV_FILE) @@ -26,6 +28,9 @@ AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") assert AWS_SECRET_ACCESS_KEY is not None +IS_STANDALONE_KFP = "kfp" in os.environ.get('DEPLOYMENT_OPTION') +SKIP_LOCAL_REGISTRY = not parse_bool(os.environ.get('INSTALL_LOCAL_REGISTRY')) + def pytest_sessionstart(session): """ diff --git a/tests/test_kfp.py b/tests/test_kfp.py index a4e5dea..2c292e5 100644 --- a/tests/test_kfp.py +++ b/tests/test_kfp.py @@ -1,3 +1,4 @@ +import os import subprocess import logging import pathlib @@ -9,7 +10,7 @@ import requests from urllib.parse import urlsplit -from .conftest import CLUSTER_NAME +from .conftest import CLUSTER_NAME, IS_STANDALONE_KFP logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -22,7 +23,7 @@ KUBEFLOW_ENDPOINT = "http://localhost:8080" KUBEFLOW_USERNAME = "user@example.com" KUBEFLOW_PASSWORD = "12341234" -NAMESPACE = "kubeflow-user-example-com" +KUBEFLOW_USER_NAMESPACE = "kubeflow-user-example-com" def get_istio_auth_session(url: str, username: str, password: str) -> dict: @@ -44,10 +45,8 @@ def get_istio_auth_session(url: str, username: str, password: str) -> dict: "is_secured": None, # True if KF endpoint is secured "session_cookie": None # Resulting session cookies in the form "key1=value1; key2=value2" } - # use a persistent session (for cookies) with requests.Session() as s: - ################ # Determine if Endpoint is Secured ################ @@ -56,7 +55,6 @@ def get_istio_auth_session(url: str, username: str, password: str) -> dict: raise RuntimeError( f"HTTP status code '{resp.status_code}' for GET against: {url}" ) - auth_session["redirect_url"] = resp.url # if we were NOT redirected, then the endpoint is UNSECURED @@ -101,7 +99,6 @@ def get_istio_auth_session(url: str, username: str, password: str) -> dict: f"HTTP status code '{resp.status_code}' " f"for GET against: {redirect_url_obj.geturl()}" ) - # set the login url auth_session["dex_login_url"] = resp.url @@ -118,7 +115,6 @@ def get_istio_auth_session(url: str, username: str, password: str) -> dict: f"Login credentials were probably invalid - " f"No redirect after POST to: {auth_session['dex_login_url']}" ) - # store the session cookies in a "key1=value1; key2=value2" string auth_session["session_cookie"] = "; ".join( [f"{c.name}={c.value}" for c in s.cookies] @@ -128,43 +124,37 @@ def get_istio_auth_session(url: str, username: str, password: str) -> dict: def run_pipeline(pipeline_file: str, experiment_name: str): - - with subprocess.Popen(["kubectl", "-n", "istio-system", "port-forward", "svc/istio-ingressgateway", "8080:80"], stdout=True) as proc: + """Run a pipeline on a Kubeflow cluster.""" + with subprocess.Popen(["kubectl", "-n", "istio-system", "port-forward", "svc/istio-ingressgateway", "8080:80"], stdout=True) as proc: # noqa: E501 try: time.sleep(2) # give some time to the port-forward connection - auth_session = get_istio_auth_session( url=KUBEFLOW_ENDPOINT, username=KUBEFLOW_USERNAME, password=KUBEFLOW_PASSWORD ) - client = kfp.Client( host=f"{KUBEFLOW_ENDPOINT}/pipeline", cookies=auth_session["session_cookie"], - namespace=NAMESPACE, + namespace=KUBEFLOW_USER_NAMESPACE, ) - created_run = client.create_run_from_pipeline_package( pipeline_file=pipeline_file, enable_caching=False, arguments={}, run_name="kfp_test_run", experiment_name=experiment_name, - namespace=NAMESPACE + namespace=KUBEFLOW_USER_NAMESPACE ) - run_id = created_run.run_id - logger.info(f"Submitted run with ID: {run_id}") - logger.info(f"Waiting for run {run_id} to complete....") run_detail = created_run.wait_for_run_completion() _handle_job_end(run_detail) # clean up experiment = client.get_experiment( - experiment_name=experiment_name, namespace=NAMESPACE + experiment_name=experiment_name, namespace=KUBEFLOW_USER_NAMESPACE ) client.delete_experiment(experiment.id) logger.info("Done") @@ -176,16 +166,46 @@ def run_pipeline(pipeline_file: str, experiment_name: str): proc.terminate() +def run_pipeline_standalone_kfp(pipeline_file: str, experiment_name: str): + """Run a pipeline on a standalone Kubeflow Pipelines cluster.""" + with subprocess.Popen(["kubectl", "-n", "kubeflow", "port-forward", "svc/ml-pipeline-ui", "8080:80"], stdout=True) as proc: # noqa: E501 + try: + time.sleep(2) # give some time to the port-forward connection + + client = kfp.Client( + host=f"{KUBEFLOW_ENDPOINT}/pipeline", + ) + created_run = client.create_run_from_pipeline_package( + pipeline_file=pipeline_file, + enable_caching=False, + arguments={}, + run_name="kfp_test_run", + experiment_name=experiment_name, + ) + run_id = created_run.run_id + logger.info(f"Submitted run with ID: {run_id}") + logger.info(f"Waiting for run {run_id} to complete....") + run_detail = created_run.wait_for_run_completion() + _handle_job_end(run_detail) + + # clean up + experiment = client.get_experiment(experiment_name=experiment_name) + client.delete_experiment(experiment.id) + logger.info("Done") + + except Exception as e: + logger.error(f"ERROR: {e}") + raise e + finally: + proc.terminate() + + def _handle_job_end(run_detail): finished_run = run_detail.to_dict()["run"] - created_at = finished_run["created_at"] finished_at = finished_run["finished_at"] - duration_secs = (finished_at - created_at).total_seconds() - status = finished_run["status"] - logger.info(f"Run finished in {round(duration_secs)} seconds with status: {status}") if status != "Succeeded": @@ -196,7 +216,6 @@ def build_load_image(): output = subprocess.check_output( ["docker", "exec", f"{CLUSTER_NAME}-control-plane", "crictl", "images"] ) - if IMAGE_NAME in output.decode(): logging.info(f"Image already in cluster.") else: @@ -206,14 +225,25 @@ def build_load_image(): @pytest.mark.order(6) @pytest.mark.timeout(240) +@pytest.mark.skipif(IS_STANDALONE_KFP, reason="It is not Kubeflow") def test_run_pipeline(): - # build the base docker image and load it into the cluster build_load_image() - # submit and run pipeline run_pipeline(pipeline_file=str(PIPELINE_FILE), experiment_name=EXPERIMENT_NAME) +@pytest.mark.order(6) +@pytest.mark.timeout(240) +@pytest.mark.skipif(not IS_STANDALONE_KFP, reason="It is not standalone KFP") +def test_run_pipeline_standalone_kfp(): + # build the base docker image and load it into the cluster + build_load_image() + # submit and run pipeline + run_pipeline_standalone_kfp( + pipeline_file=str(PIPELINE_FILE), experiment_name=EXPERIMENT_NAME + ) + + if __name__ == "__main__": test_run_pipeline() diff --git a/tests/test_registry.py b/tests/test_registry.py index e13cf83..dce083c 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -2,17 +2,16 @@ import logging import pathlib import pytest -import os from envsubst import envsubst -from .conftest import HOST_IP -from .test_kfp import run_pipeline +from .conftest import HOST_IP, IS_STANDALONE_KFP, SKIP_LOCAL_REGISTRY +from .test_kfp import run_pipeline, run_pipeline_standalone_kfp logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -BUILD_FILE = pathlib.Path(__file__).parent / "resources" / "registry" / "build_push_image.sh" -PIPELINE_TEMPLATE = pathlib.Path(__file__).parent / "resources" / "registry" / "pipeline.yaml.template" +BUILD_FILE = pathlib.Path(__file__).parent / "resources" / "registry" / "build_push_image.sh" # noqa +PIPELINE_TEMPLATE = pathlib.Path(__file__).parent / "resources" / "registry" / "pipeline.yaml.template" # noqa IMAGE_NAME = "kfp-registry-test-image" EXPERIMENT_NAME = "Test Experiment (Registry)" @@ -32,10 +31,7 @@ def render_pipeline_yaml(output: str): @pytest.mark.order(7) -@pytest.mark.skipif( - os.environ.get('INSTALL_LOCAL_REGISTRY') == 'false', - reason="No local image registry was installed." -) +@pytest.mark.skipif(SKIP_LOCAL_REGISTRY, reason="No local image registry was installed") def test_push_image(): # build the base docker image and load it into the cluster build_push_image() @@ -43,18 +39,29 @@ def test_push_image(): @pytest.mark.order(8) @pytest.mark.timeout(120) -@pytest.mark.skipif( - os.environ.get('INSTALL_LOCAL_REGISTRY') == 'false', - reason="No local image registry was installed." -) +@pytest.mark.skipif(SKIP_LOCAL_REGISTRY, reason="No local image registry was installed") +@pytest.mark.skipif(IS_STANDALONE_KFP, reason="It is not Kubeflow") def test_run_pipeline_using_registry(tmp_path): - # build the base docker image and load it into the cluster build_push_image() - # create pipeline.yaml with the right registry IP address pipeline_file = tmp_path / "pipeline.yaml" render_pipeline_yaml(output=str(pipeline_file)) - # submit and run pipeline run_pipeline(pipeline_file=str(pipeline_file), experiment_name=EXPERIMENT_NAME) + + +@pytest.mark.order(8) +@pytest.mark.timeout(120) +@pytest.mark.skipif(SKIP_LOCAL_REGISTRY, reason="No local image registry was installed") +@pytest.mark.skipif(not IS_STANDALONE_KFP, reason="It is not standalone KFP") +def test_run_pipeline_standalone_kfp_using_registry(tmp_path): + # build the base docker image and load it into the cluster + build_push_image() + # create pipeline.yaml with the right registry IP address + pipeline_file = tmp_path / "pipeline.yaml" + render_pipeline_yaml(output=str(pipeline_file)) + # submit and run pipeline + run_pipeline_standalone_kfp( + pipeline_file=str(pipeline_file), experiment_name=EXPERIMENT_NAME + ) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..6bd5ea6 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,16 @@ +from typing import Union + + +def parse_bool(val: Union[str, bool]) -> bool: + """Convert a string representation of truth to True or False. + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + """ + val = val.lower() + if val in ('y', 'yes', 't', 'true', 'on', '1', True): + return True + elif val in ('n', 'no', 'f', 'false', 'off', '0', False): + return False + else: + raise ValueError(f"Invalid truth value {val}") From 7b7e29debd271765a74e07cda25f2676694c062b Mon Sep 17 00:00:00 2001 From: Joaquin Date: Tue, 23 Apr 2024 09:04:55 +0300 Subject: [PATCH 20/20] fix inference demo_pipeline_standalone_kfp --- .../demo_pipeline/demo-pipeline.ipynb | 4 ++-- .../demo-pipeline.ipynb | 23 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb b/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb index 844b878..f700667 100644 --- a/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb +++ b/tutorials/demo_notebooks/demo_pipeline/demo-pipeline.ipynb @@ -751,8 +751,8 @@ " logger.info(f\"\\nInference service URL:\\n{is_url}\\n\")\n", "\n", " inference_input = {\n", - " 'instances': input_sample.tolist()\n", - " }\n", + " 'instances': input_sample.tolist()\n", + " }\n", " response = requests.post(\n", " is_url,\n", " json=inference_input,\n", diff --git a/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/demo-pipeline.ipynb b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/demo-pipeline.ipynb index 5a6f32a..471da4e 100644 --- a/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/demo-pipeline.ipynb +++ b/tutorials/demo_notebooks/demo_pipeline_standalone_kfp/demo-pipeline.ipynb @@ -493,7 +493,7 @@ " logger = logging.getLogger(__name__)\n", "\n", " namespace = 'kserve-inference'\n", - "\n", + " \n", " input_sample = [[5.6, 0.54, 0.04, 1.7, 0.049, 5, 13, 0.9942, 3.72, 0.58, 11.4],\n", " [11.3, 0.34, 0.45, 2, 0.082, 6, 15, 0.9988, 2.94, 0.66, 9.2]]\n", "\n", @@ -511,17 +511,24 @@ " KServe.get(model_name, namespace=namespace, watch=True, timeout_seconds=120)\n", "\n", " inference_service = KServe.get(model_name, namespace=namespace)\n", - " is_url = inference_service['status']['address']['url']\n", - "\n", + " header = {\"Host\": f\"{model_name}.{namespace}.example.com\"}\n", + " is_url = f\"http://istio-ingressgateway.istio-system.svc.cluster.local:80/v1/models/{model_name}:predict\"\n", + " \n", " logger.info(f\"\\nInference service status:\\n{inference_service['status']}\")\n", " logger.info(f\"\\nInference service URL:\\n{is_url}\\n\")\n", "\n", " inference_input = {\n", - " 'instances': input_sample.tolist()\n", - " }\n", - "\n", - " response = requests.post(is_url, json=inference_input)\n", - " logger.info(f\"\\nPrediction response:\\n{response.text}\\n\")" + " 'instances': input_sample.tolist()\n", + " }\n", + " response = requests.post(\n", + " is_url,\n", + " json=inference_input,\n", + " headers=header,\n", + " )\n", + " if response.status_code != 200:\n", + " raise RuntimeError(f\"HTTP status code '{response.status_code}': {response.json()}\")\n", + " \n", + " logger.info(f\"\\nPrediction response:\\n{response.json()}\\n\")" ], "outputs": [], "execution_count": null