Files
model_garden/notebooks/official/pipelines/lightweight_functions_component_io_kfp.ipynb
T

30 KiB
Raw Blame History

In [ ]:
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

Vertex AI Pipelines: Lightweight Python function-based components, and component I/O

Google Colaboratory logo
Open in Colab
Google Cloud Colab Enterprise logo
Open in Colab Enterprise
GitHub logo
View on GitHub
Vertex AI logo
Open in Vertex AI Workbench



Overview

This notebooks shows how to use the Kubeflow Pipelines (KFP) SDK to build Vertex AI Pipelines that use lightweight Python function based components, as well as supporting component I/O using the KFP SDK.

Learn more about Vertex AI Pipelines.

Objective

In this tutorial, you learn to use the KFP SDK to build lightweight Python function-based components, and then you learn to use Vertex AI Pipelines to execute the pipeline.

This tutorial uses the following Google Cloud ML services:

  • Vertex AI Pipelines

The steps performed include:

  • Build Python function-based KFP components.
  • Construct a KFP pipeline.
  • Pass Artifacts and parameters between components, both by path reference and by value.
  • Use the kfp.dsl.importer method.
  • Compile the KFP pipeline.
  • Execute the KFP pipeline using Vertex AI Pipelines

KFP Python function-based components

A Kubeflow pipeline component is a self-contained set of code that performs one step in your ML workflow. A pipeline component is composed of:

  • The component code, which implements the logic need to perform a step in your ML workflow.
  • A component specification, which defines the following:
    • The component’s metadata, its name and description.
    • The component’s interface, the component’s inputs and outputs.
  • The component’s implementation, the Docker container image to run, how to pass inputs to your component code, and how to get the component’s outputs.

Lightweight Python function-based components make it easier to iterate quickly by letting you build your component code as a Python function and generating the component specification for you. This notebook shows how to create Python function-based components for use in Vertex AI Pipelines.

Python function-based components use the Kubeflow Pipelines SDK to handle the complexity of passing inputs into your component and passing your function’s outputs back to your pipeline.

There are two categories of inputs/outputs supported in Python function-based components: artifacts and parameters.

  • Parameters are passed to your component by value and typically contain int, float, bool, or small string values.
  • Artifacts are passed to your component as a reference to a path, to which you can write a file or a subdirectory structure. In addition to the artifact’s data, you can also read and write the artifact’s metadata. This lets you record arbitrary key-value pairs for an artifact such as the accuracy of a trained model, and use metadata in downstream components – for example, you could use metadata to decide if a model is accurate enough to deploy for predictions.

Costs

This tutorial uses billable components of Google Cloud:

  • Vertex AI
  • Cloud Storage

Learn about Vertex AI pricing and Cloud Storage pricing and use the Pricing Calculator to generate a cost estimate based on your projected usage.

Get Started

Install Vertex AI SDK for Python and other required packages

In [ ]:
! pip3 install --upgrade --quiet google-cloud-aiplatform \
                                 google-cloud-storage \
                                 kfp \
                                 "numpy<2" \
                                 google-cloud-pipeline-components

Restart runtime (Colab only)

Authenticate your environment on Google Colab.

In [ ]:
import sys

if "google.colab" in sys.modules:

    import IPython

    app = IPython.Application.instance()
    app.kernel.do_shutdown(True)

Authenticate your notebook environment (Colab only)

Authenticate your environment on Google Colab.

In [ ]:
import sys

if "google.colab" in sys.modules:

    from google.colab import auth

    auth.authenticate_user()

Set Google Cloud project information

Learn more about setting up a project and a development environment.

In [ ]:
PROJECT_ID = "[your-project-id]"  # @param {type:"string"}
LOCATION = "us-central1"  # @param {type:"string"}

Create a Cloud Storage bucket

Create a storage bucket to store intermediate artifacts such as datasets.

  • {Note to notebook author: For any user-provided strings that need to be unique (like bucket names or model ID's), append "-unique" to the end so proper testing can occur}
In [ ]:
BUCKET_URI = f"gs://your-bucket-name-{PROJECT_ID}-unique"  # @param {type:"string"}

If your bucket doesn't already exist: Run the following cell to create your Cloud Storage bucket.

In [ ]:
! gcloud storage buckets create --location={LOCATION} --project={PROJECT_ID} {BUCKET_URI}

Service Account

If you don't know your service account, try to get your service account using gcloud command by executing the second cell below.

In [ ]:
SERVICE_ACCOUNT = "[your-service-account]"  # @param {type:"string"}
In [ ]:
import sys

IS_COLAB = "google.colab" in sys.modules
if (
    SERVICE_ACCOUNT == ""
    or SERVICE_ACCOUNT is None
    or SERVICE_ACCOUNT == "[your-service-account]"
):
    # Get your service account from gcloud
    if not IS_COLAB:
        shell_output = !gcloud auth list 2>/dev/null
        SERVICE_ACCOUNT = shell_output[2].replace("*", "").strip()

    if IS_COLAB:
        shell_output = ! gcloud projects describe  $PROJECT_ID
        project_number = shell_output[-1].split(":")[1].strip().replace("'", "")
        SERVICE_ACCOUNT = f"{project_number}-compute@developer.gserviceaccount.com"

    print("Service Account:", SERVICE_ACCOUNT)

Set service account access for Vertex AI Pipelines

Run the following commands to grant your service account access to read and write pipeline artifacts in the bucket that you created in the previous step -- you only need to run these once per service account.

In [ ]:
! gcloud storage buckets add-iam-policy-binding $BUCKET_URI --member=serviceAccount:{SERVICE_ACCOUNT} --role=roles/storage.objectCreator

! gcloud storage buckets add-iam-policy-binding $BUCKET_URI --member=serviceAccount:{SERVICE_ACCOUNT} --role=roles/storage.objectViewer

Set up variables

Next, set up some variables used throughout the tutorial.

Import libraries and define constants

In [ ]:
from typing import NamedTuple

import kfp
from google.cloud import aiplatform
from kfp import compiler, dsl
from kfp.dsl import (Artifact, Dataset, Input, InputPath, Model, Output,
                     OutputPath, component)

Vertex AI Pipelines constants

Set up up the following constants for Vertex AI Pipelines:

In [ ]:
PIPELINE_ROOT = "{}/pipeline_root/shakespeare".format(BUCKET_URI)

Initialize Vertex AI SDK for Python

Initialize the Vertex AI SDK for Python for your project and corresponding bucket.

In [ ]:
aiplatform.init(project=PROJECT_ID, staging_bucket=BUCKET_URI)

Define Python function-based pipeline components

In this tutorial, you define function-based components that consume parameters and produce (typed) Artifacts and parameters. Functions can produce Artifacts in three ways:

  • Accept an output local path using OutputPath
  • Accept an OutputArtifact which gives the function a handle to the output artifact's metadata
  • Return an Artifact (or Dataset, Model, Metrics, etc) in a NamedTuple

These options for producing Artifacts are demonstrated.

Define preprocess component

The first component definition, preprocess, shows a component that outputs two Dataset Artifacts, as well as an output parameter. (For this example, the datasets don't reflect real data).

For the parameter output, you would typically use the approach shown here, using the OutputPath type, for "larger" data. For "small data", like a short string, it might be more convenient to use the NamedTuple function output as shown in the second component instead.

In [ ]:
@component(base_image="python:3.9")
def preprocess(
    # An input parameter of type string.
    message: str,
    # Use Output to get a metadata-rich handle to the output artifact
    # of type `Dataset`.
    output_dataset_one: Output[Dataset],
    # A locally accessible filepath for another output artifact of type
    # `Dataset`.
    output_dataset_two_path: OutputPath("Dataset"),
    # A locally accessible filepath for an output parameter of type string.
    output_parameter_path: OutputPath(str),
):
    """'Mock' preprocessing step.
    Writes out the passed in message to the output "Dataset"s and the output message.
    """
    output_dataset_one.metadata["hello"] = "there"
    # Use OutputArtifact.path to access a local file path for writing.
    # One can also use OutputArtifact.uri to access the actual URI file path.
    with open(output_dataset_one.path, "w") as f:
        f.write(message)

    # OutputPath is used to just pass the local file path of the output artifact
    # to the function.
    with open(output_dataset_two_path, "w") as f:
        f.write(message)

    with open(output_parameter_path, "w") as f:
        f.write(message)

Define train component

The second component definition, train, defines as input both an InputPath of type Dataset, and an InputArtifact of type Dataset (as well as other parameter inputs). It uses the NamedTuple format for function output. As shown, these outputs can be Artifacts as well as parameters.

Additionally, this component writes some metrics metadata to the model output Artifact. This information is displayed in the Cloud Console user interface when the pipeline runs.

In [ ]:
@component(
    base_image="python:3.9",  # Use a different base image.
)
def train(
    # An input parameter of type string.
    message: str,
    # Use InputPath to get a locally accessible path for the input artifact
    # of type `Dataset`.
    dataset_one_path: InputPath("Dataset"),
    # Use InputArtifact to get a metadata-rich handle to the input artifact
    # of type `Dataset`.
    dataset_two: Input[Dataset],
    # Output artifact of type Model.
    imported_dataset: Input[Dataset],
    model: Output[Model],
    # An input parameter of type int with a default value.
    num_steps: int = 3,
    # Use NamedTuple to return either artifacts or parameters.
    # When returning artifacts like this, return the contents of
    # the artifact. The assumption here is that this return value
    # fits in memory.
) -> NamedTuple(
    "Outputs",
    [
        ("output_message", str),  # Return parameter.
        ("generic_artifact", Artifact),  # Return generic Artifact.
    ],
):
    """'Mock' Training step.
    Combines the contents of dataset_one and dataset_two into the
    output Model.
    Constructs a new output_message consisting of message repeated num_steps times.
    """

    # Directly access the passed in GCS URI as a local file (uses GCSFuse).
    with open(dataset_one_path) as input_file:
        dataset_one_contents = input_file.read()

    # dataset_two is an Artifact handle. Use dataset_two.path to get a
    # local file path (uses GCSFuse).
    # Alternately, use dataset_two.uri to access the GCS URI directly.
    with open(dataset_two.path) as input_file:
        dataset_two_contents = input_file.read()

    with open(model.path, "w") as f:
        f.write("My Model")

    with open(imported_dataset.path) as f:
        data = f.read()
    print("Imported Dataset:", data)

    # Use model.get() to get a Model artifact, which has a .metadata dictionary
    # to store arbitrary metadata for the output artifact. This metadata is
    # recorded in Managed Metadata and can be queried later. It also shows up
    # in the Google Cloud console.
    model.metadata["accuracy"] = 0.9
    model.metadata["framework"] = "Tensorflow"
    model.metadata["time_to_train_in_seconds"] = 257

    artifact_contents = "{}\n{}".format(dataset_one_contents, dataset_two_contents)
    output_message = " ".join([message for _ in range(num_steps)])
    return (output_message, artifact_contents)

Define read_artifact_input component

Finally, you define a small component that takes as input the generic_artifact returned by the train component function, and reads and prints the Artifact's contents.

In [ ]:
@component(base_image="python:3.9")
def read_artifact_input(
    generic: Input[Artifact],
):
    with open(generic.path) as input_file:
        generic_contents = input_file.read()
        print(f"generic contents: {generic_contents}")

Define a pipeline that uses your components and the Importer

Next, define a pipeline that uses the components that were built in the previous sections, and also shows the use of the kfp.dsl.importer.

This example uses the importer to create, in this case, a Dataset artifact from an existing URI.

Note that the train_task step takes as inputs three of the outputs of the preprocess_task step, as well as the output of the importer step. In the "train" inputs we refer to the preprocess output_parameter, which gives us the output string directly.

The read_task step takes as input the train_task generic_artifact output.

In [ ]:
@dsl.pipeline(
    # Default pipeline root. You can override it when submitting the pipeline.
    pipeline_root=PIPELINE_ROOT,
    # A name for the pipeline. Use to determine the pipeline Context.
    name="metadata-pipeline-v2",
)
def pipeline(message: str):
    importer = kfp.dsl.importer(
        artifact_uri="gs://ml-pipeline-playground/shakespeare1.txt",
        artifact_class=Dataset,
        reimport=False,
    )
    preprocess_task = preprocess(message=message)
    train_task = train(
        dataset_one_path=preprocess_task.outputs["output_dataset_one"],
        dataset_two=preprocess_task.outputs["output_dataset_two_path"],
        imported_dataset=importer.output,
        message=preprocess_task.outputs["output_parameter_path"],
        num_steps=5,
    )
    read_task = read_artifact_input(  # noqa: F841
        generic=train_task.outputs["generic_artifact"]
    )

Compile the pipeline

Next, compile the pipeline.

In [ ]:
compiler.Compiler().compile(
    pipeline_func=pipeline, package_path="lightweight_pipeline.yaml"
)

Run the pipeline

Next, run the pipeline.

In [ ]:
DISPLAY_NAME = "shakespeare"

job = aiplatform.PipelineJob(
    display_name=DISPLAY_NAME,
    template_path="lightweight_pipeline.yaml",
    pipeline_root=PIPELINE_ROOT,
    parameter_values={"message": "Hello, World"},
    enable_caching=False,
)

job.run()

Click on the generated link to see your run in the Cloud Console.

In the Google Cloud console, many of the pipeline DAG nodes expand or collapse when you click on them. Here is a partially-expanded view of the DAG (click image to see larger version).

Delete the pipeline job

You can delete the pipeline job with the method delete().job.delete()

In [ ]:
job.delete()

Cleaning up

To clean up all Google Cloud resources used in this project, you can delete the Google Cloud project you used for the tutorial.

Otherwise, you can delete the individual resources you created in this tutorial.

In [ ]:
delete_bucket = False

if delete_bucket:
    ! gcloud storage rm --recursive $BUCKET_URI

! rm lightweight_pipeline.yaml