Compare commits

..
Author SHA1 Message Date
Andrew FerlitschandGitHub b86e1abf05 Merge branch 'master' into ml_ops 2021-10-29 16:19:18 -07:00
Andrew Ferlitsch 85d5e7a999 friday updates 2021-10-29 23:10:24 +00:00
Andrew Ferlitsch 46cd3bf4e7 friday updates 2021-10-29 23:10:11 +00:00
Andrew Ferlitsch 3182fc2a24 friday updates 2021-10-29 22:59:05 +00:00
Andrew Ferlitsch 1a4f9112bb friday updates 2021-10-29 22:58:02 +00:00
Andrew FerlitschandGitHub 3560c04f54 Merge branch 'master' into ml_ops 2021-10-15 17:24:23 -07:00
Andrew Ferlitsch 7859246a06 fix: updates 2021-10-16 00:19:55 +00:00
Andrew Ferlitsch 1f198fdfee fix: updates 2021-10-16 00:17:46 +00:00
Andrew Ferlitsch 77a928f87e fix: updates 2021-10-16 00:15:39 +00:00
Andrew Ferlitsch c0d9d53cd5 fix: updates 2021-10-16 00:11:13 +00:00
Andrew FerlitschandGitHub b213d8ecf9 Merge branch 'master' into ml_ops 2021-10-12 14:26:13 -07:00
Andrew Ferlitsch 16decd0b4c feat: get started 2021-10-12 21:23:44 +00:00
Andrew Ferlitsch fb58eeb3f5 feat: get started 2021-10-12 21:21:54 +00:00
Andrew FerlitschandGitHub 890c170acd Merge branch 'master' into ml_ops 2021-10-08 10:47:07 -07:00
Andrew Ferlitsch 3007da7a29 feat: start on stage2 2021-10-08 17:32:37 +00:00
Andrew Ferlitsch 36947e08c0 feat: start on stage2 2021-10-08 17:29:46 +00:00
Andrew Ferlitsch 44b73bf9f9 Merge branch 'ml_ops' of https://github.com/GoogleCloudPlatform/vertex-ai-samples into ml_ops 2021-10-04 18:42:14 +00:00
Andrew Ferlitsch 81da094b33 feat: start stage 2 2021-10-04 18:41:20 +00:00
Andrew FerlitschandGitHub 32f465df20 Merge branch 'master' into ml_ops 2021-10-04 10:40:14 -07:00
Andrew Ferlitsch 93b48cc11c Merge branch 'ml_ops' of https://github.com/GoogleCloudPlatform/vertex-ai-samples into ml_ops 2021-10-04 17:04:40 +00:00
Andrew Ferlitsch e603f8f6f4 feat: finish stage1 2021-10-04 17:03:38 +00:00
Andrew Ferlitsch 8869af9e3b feat: finish stage1 2021-10-04 16:52:45 +00:00
Andrew FerlitschandGitHub 6217366a1b Merge branch 'master' into ml_ops 2021-09-27 13:25:10 -07:00
Andrew Ferlitsch a130a890c3 :w
Merge branch 'ml_ops' of https://github.com/GoogleCloudPlatform/vertex-ai-samples into ml_ops
2021-09-27 20:21:38 +00:00
Andrew Ferlitsch cb5a67aba2 feat: add dataflow notebook 2021-09-27 20:20:29 +00:00
Andrew Ferlitsch 237f77d5ba feat: add dataflow notebook 2021-09-27 18:43:41 +00:00
Andrew FerlitschandGitHub 46da44319d Merge branch 'master' into ml_ops 2021-09-23 13:22:53 -07:00
Andrew Ferlitsch fda6d052ed feat: refine notebook 2021-09-23 20:07:13 +00:00
Andrew Ferlitsch 44ba9c44fd feat: refine notebook 2021-09-23 20:06:28 +00:00
Andrew Ferlitsch 829c2f8d89 feat: get started notebook 2021-09-23 02:00:58 +00:00
Andrew Ferlitsch 12cdc8d04b feat: get started notebook 2021-09-23 02:00:14 +00:00
Andrew Ferlitsch 4eda5c892d feat: new notebook 2021-09-22 16:16:56 +00:00
Andrew Ferlitsch 29328fe86f feat: new notebook 2021-09-22 16:14:04 +00:00
435 changed files with 31238 additions and 251049 deletions
+194
View File
@@ -0,0 +1,194 @@
# Copyright 2020 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
#
# http://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.
# We want to use LTS ubuntu from our mirror because dockerhub has a
# rate limit.
# FROM mirror.gcr.io/library/ubuntu:18.04
# However, now the above image is not working, we're using our own cache
FROM gcr.io/cloud-devrel-kokoro-resources/ubuntu:20.04
ENV DEBIAN_FRONTEND noninteractive
# Ensure local Python is preferred over distribution Python.
ENV PATH /usr/local/bin:$PATH
# http://bugs.python.org/issue19846
# At the moment, setting "LANG=C" on a Linux system fundamentally breaks
# Python 3.
ENV LANG C.UTF-8
# Install dependencies.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
apt-transport-https \
build-essential \
ca-certificates \
curl \
dirmngr \
git \
gcc \
gpg-agent \
graphviz \
libbz2-dev \
libdb5.3-dev \
libexpat1-dev \
libffi-dev \
liblzma-dev \
libmagickwand-dev \
libmemcached-dev \
libpython3-dev \
libreadline-dev \
libsnappy-dev \
libssl-dev \
libsqlite3-dev \
portaudio19-dev \
pkg-config \
redis-server \
software-properties-common \
ssh \
sudo \
systemd \
tcl \
tcl-dev \
tk \
tk-dev \
uuid-dev \
wget \
zlib1g-dev \
&& apt-get clean autoclean \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /var/cache/apt/archives/*.deb
# Install docker
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
RUN add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
docker-ce \
&& apt-get clean autoclean \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /var/cache/apt/archives/*.deb
# Install Bazel for compiling Tink in Cloud SQL Client Side Encryption Samples
# TODO: Delete this section once google/tink#483 is resolved
RUN apt install -y curl gpgconf gpg \
&& curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor > bazel.gpg \
&& mv bazel.gpg /etc/apt/trusted.gpg.d/ \
&& echo "deb [arch=amd64] https://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list \
&& apt update && apt install -y bazel \
&& apt-get clean autoclean \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /var/cache/apt/archives/*.deb
# Install Microsoft ODBC 17 Driver and unixodbc for testing SQL Server samples
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \
&& curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list > /etc/apt/sources.list.d/mssql-release.list \
&& apt-get update \
&& ACCEPT_EULA=Y apt-get install -y --no-install-recommends \
msodbcsql17 \
unixodbc-dev \
&& apt-get clean autoclean \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /var/cache/apt/archives/*.deb
COPY fetch_gpg_keys.sh /tmp
# Install the desired versions of Python.
RUN set -ex \
&& export GNUPGHOME="$(mktemp -d)" \
&& echo "disable-ipv6" >> "${GNUPGHOME}/dirmngr.conf" \
&& /tmp/fetch_gpg_keys.sh \
&& for PYTHON_VERSION in 2.7.18 3.6.13 3.7.10 3.8.8 3.9.2; do \
wget --no-check-certificate -O python-${PYTHON_VERSION}.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz" \
&& wget --no-check-certificate -O python-${PYTHON_VERSION}.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc" \
&& gpg --batch --verify python-${PYTHON_VERSION}.tar.xz.asc python-${PYTHON_VERSION}.tar.xz \
&& rm -r python-${PYTHON_VERSION}.tar.xz.asc \
&& mkdir -p /usr/src/python-${PYTHON_VERSION} \
&& tar -xJC /usr/src/python-${PYTHON_VERSION} --strip-components=1 -f python-${PYTHON_VERSION}.tar.xz \
&& rm python-${PYTHON_VERSION}.tar.xz \
&& cd /usr/src/python-${PYTHON_VERSION} \
&& ./configure \
--enable-shared \
# This works only on Python 2.7 and throws a warning on every other
# version, but seems otherwise harmless.
--enable-unicode=ucs4 \
--with-system-ffi \
--without-ensurepip \
&& make -j$(nproc) \
&& make install \
&& ldconfig \
; done \
&& rm -rf "${GNUPGHOME}" \
&& rm -rf /usr/src/python* \
&& rm -rf ~/.cache/
# Install pip on Python 3.6 only.
# If the environment variable is called "PIP_VERSION", pip explodes with
# "ValueError: invalid truth value '<VERSION>'"
ENV PYTHON_PIP_VERSION 20.2.4
RUN wget --no-check-certificate -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \
&& python3.6 /tmp/get-pip.py "pip==$PYTHON_PIP_VERSION" \
# we use "--force-reinstall" for the case where the version of pip we're trying to install is the same as the version bundled with Python
# ("Requirement already up-to-date: pip==8.1.2 in /usr/local/lib/python3.6/site-packages")
# https://github.com/docker-library/python/pull/143#issuecomment-241032683
&& pip3 install --no-cache-dir --upgrade --force-reinstall "pip==$PYTHON_PIP_VERSION" \
# then we use "pip list" to ensure we don't have more than one pip version installed
# https://github.com/docker-library/python/pull/100
&& [ "$(pip list |tac|tac| awk -F '[ ()]+' '$1 == "pip" { print $2; exit }')" = "$PYTHON_PIP_VERSION" ]
# Ensure Pip for python3
RUN python3 /tmp/get-pip.py
RUN rm /tmp/get-pip.py
# Install "virtualenv", since the vast majority of users of this image
# will want it.
RUN pip install --no-cache-dir virtualenv
# Setup Cloud SDK
ENV CLOUD_SDK_VERSION 339.0.0
# Use system python for cloud sdk.
ENV CLOUDSDK_PYTHON python3.6
RUN wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz
RUN tar xzf google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz
RUN /google-cloud-sdk/install.sh
ENV PATH /google-cloud-sdk/bin:$PATH
# Enable redis-server on boot.
RUN sudo systemctl enable redis-server.service
# Create a user and allow sudo
# kbuilder uid on the default Kokoro image
ARG UID=1000
ARG USERNAME=kbuilder
# Add a new user to the container image.
# This is needed for ssh and sudo access.
# Add a new user with the caller's uid and the username.
RUN useradd -d /h -u ${UID} ${USERNAME}
# Allow nopasswd sudo
RUN echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
CMD ["python3.6"]
+312
View File
@@ -0,0 +1,312 @@
#!/usr/bin/env python
# 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
#
# http://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.
import argparse
import dataclasses
import datetime
import functools
import pathlib
import os
import subprocess
from pathlib import Path
from typing import List, Optional
import concurrent
from tabulate import tabulate
import ExecuteNotebook
def str2bool(v):
if isinstance(v, bool):
return v
if v.lower() in ("yes", "true", "t", "y", "1"):
return True
elif v.lower() in ("no", "false", "f", "n", "0"):
return False
else:
raise argparse.ArgumentTypeError("Boolean value expected.")
def format_timedelta(delta: datetime.timedelta) -> str:
"""Formats a timedelta duration to [N days] %H:%M:%S format"""
seconds = int(delta.total_seconds())
secs_in_a_day = 86400
secs_in_a_hour = 3600
secs_in_a_min = 60
days, seconds = divmod(seconds, secs_in_a_day)
hours, seconds = divmod(seconds, secs_in_a_hour)
minutes, seconds = divmod(seconds, secs_in_a_min)
time_fmt = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
if days > 0:
suffix = "s" if days > 1 else ""
return f"{days} day{suffix} {time_fmt}"
return time_fmt
@dataclasses.dataclass
class NotebookExecutionResult:
notebook: str
duration: datetime.timedelta
is_pass: bool
error_message: Optional[str]
def execute_notebook(
artifacts_path: str,
variable_project_id: str,
variable_region: str,
should_log_output: bool,
should_use_new_kernel: bool,
notebook: str,
) -> NotebookExecutionResult:
print(f"Running notebook: {notebook}")
result = NotebookExecutionResult(
notebook=notebook,
duration=datetime.timedelta(seconds=0),
is_pass=False,
error_message=None,
)
# TODO: Handle cases where multiple notebooks have the same name
time_start = datetime.datetime.now()
try:
ExecuteNotebook.execute_notebook(
notebook_file_path=notebook,
output_file_folder=artifacts_path,
replacement_map={
"PROJECT_ID": variable_project_id,
"REGION": variable_region,
},
should_log_output=should_log_output,
should_use_new_kernel=should_use_new_kernel,
)
result.duration = datetime.datetime.now() - time_start
result.is_pass = True
print(f"{notebook} PASSED in {format_timedelta(result.duration)}.")
except Exception as error:
result.duration = datetime.datetime.now() - time_start
result.is_pass = False
result.error_message = str(error)
print(
f"{notebook} FAILED in {format_timedelta(result.duration)}: {result.error_message}"
)
return result
def run_changed_notebooks(
test_paths_file: str,
base_branch: Optional[str],
output_folder: str,
variable_project_id: str,
variable_region: str,
should_parallelize: bool,
should_use_separate_kernels: bool,
):
"""
Run the notebooks that exist under the folders defined in the test_paths_file.
It only runs notebooks that have differences from the Git base_branch.
The executed notebooks are saved in the output_folder.
Variables are also injected into the notebooks such as the variable_project_id and variable_region.
Args:
test_paths_file (str):
Required. The new-line delimited file to folders and files that need checking.
Folders are checked recursively.
base_branch (str):
Optional. If provided, only the files that have changed from the base_branch will be checked.
If not provided, all files will be checked.
output_folder (str):
Required. The folder to write executed notebooks to.
variable_project_id (str):
Required. The value for PROJECT_ID to inject into notebooks.
variable_region (str):
Required. The value for REGION to inject into notebooks.
should_parallelize (bool):
Required. Should run notebooks in parallel using a thread pool as opposed to in sequence.
should_use_separate_kernels (bool):
Note: Dependencies don't install correctly when this is set to True
See https://github.com/nteract/papermill/issues/625
Required. Should run each notebook in a separate and independent virtual environment.
"""
test_paths = []
with open(test_paths_file) as file:
lines = [line.strip() for line in file.readlines()]
lines = [line for line in lines if len(line) > 0]
test_paths = [line for line in lines]
if len(test_paths) == 0:
raise RuntimeError("No test folders found.")
print(f"Checking folders: {test_paths}")
# Find notebooks
notebooks = []
if base_branch:
print(f"Looking for notebooks that changed from branch: {base_branch}")
notebooks = subprocess.check_output(
["git", "diff", "--name-only", f"origin/{base_branch}..."] + test_paths
)
else:
print(f"Looking for all notebooks.")
notebooks = subprocess.check_output(["git", "ls-files"] + test_paths)
notebooks = notebooks.decode("utf-8").split("\n")
notebooks = [notebook for notebook in notebooks if notebook.endswith(".ipynb")]
notebooks = [notebook for notebook in notebooks if len(notebook) > 0]
notebooks = [notebook for notebook in notebooks if Path(notebook).exists()]
# Create paths
artifacts_path = Path(output_folder)
artifacts_path.mkdir(parents=True, exist_ok=True)
artifacts_path.joinpath("success").mkdir(parents=True, exist_ok=True)
artifacts_path.joinpath("failure").mkdir(parents=True, exist_ok=True)
notebook_execution_results: List[NotebookExecutionResult] = []
if len(notebooks) > 0:
print(f"Found {len(notebooks)} modified notebooks: {notebooks}")
if should_parallelize and len(notebooks) > 1:
print(
"Running notebooks in parallel, so no logs will be displayed. Please wait..."
)
with concurrent.futures.ThreadPoolExecutor(max_workers=None) as executor:
notebook_execution_results = list(
executor.map(
functools.partial(
execute_notebook,
artifacts_path,
variable_project_id,
variable_region,
False,
should_use_separate_kernels,
),
notebooks,
)
)
else:
notebook_execution_results = [
execute_notebook(
artifacts_path=artifacts_path,
variable_project_id=variable_project_id,
variable_region=variable_region,
notebook=notebook,
should_log_output=True,
should_use_new_kernel=should_use_separate_kernels,
)
for notebook in notebooks
]
else:
print("No notebooks modified in this pull request.")
print("\n=== RESULTS ===\n")
notebooks_sorted = sorted(
notebook_execution_results,
key=lambda result: result.is_pass,
reverse=True,
)
# Print results
print(
tabulate(
[
[
os.path.basename(os.path.normpath(result.notebook)),
"PASSED" if result.is_pass else "FAILED",
format_timedelta(result.duration),
result.error_message or "--",
]
for result in notebooks_sorted
],
headers=["file", "status", "duration", "error"],
)
)
print("\n=== END RESULTS===\n")
parser = argparse.ArgumentParser(description="Run changed notebooks.")
parser.add_argument(
"--test_paths_file",
type=pathlib.Path,
help="The path to the file that has newline-limited folders of notebooks that should be tested.",
required=True,
)
parser.add_argument(
"--base_branch",
help="The base git branch to diff against to find changed files.",
required=False,
)
parser.add_argument(
"--output_folder",
type=pathlib.Path,
help="The path to the folder to store executed notebooks.",
required=True,
)
parser.add_argument(
"--variable_project_id",
type=str,
help="The GCP project id. This is used to inject a variable value into the notebook before running.",
required=True,
)
parser.add_argument(
"--variable_region",
type=str,
help="The GCP region. This is used to inject a variable value into the notebook before running.",
required=True,
)
# Note: Dependencies don't install correctly when this is set to True
parser.add_argument(
"--should_parallelize",
type=str2bool,
nargs="?",
const=True,
default=False,
help="Should run notebooks in parallel.",
)
# Note: This isn't guaranteed to work correctly due to existing Papermill issue
# See https://github.com/nteract/papermill/issues/625
parser.add_argument(
"--should_use_separate_kernels",
type=str2bool,
nargs="?",
const=True,
default=False,
help="(Experimental) Should run each notebook in a separate and independent virtual environment.",
)
args = parser.parse_args()
run_changed_notebooks(
test_paths_file=args.test_paths_file,
base_branch=args.base_branch,
output_folder=args.output_folder,
variable_project_id=args.variable_project_id,
variable_region=args.variable_region,
should_parallelize=args.should_parallelize,
should_use_separate_kernels=args.should_use_separate_kernels,
)
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python
# 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
#
# http://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.
import json
import sys
import nbformat
import os
import errno
from NotebookProcessors import RemoveNoExecuteCells, UpdateVariablesPreprocessor
from typing import Dict, Tuple
import papermill as pm
import shutil
import virtualenv
import uuid
from jupyter_client.kernelspecapp import KernelSpecManager
# This script is used to execute a notebook and write out the output notebook.
# The replaces calling the nbconvert via command-line, which doesn't write the output notebook correctly when there are errors during execution.
STAGING_FOLDER = "staging"
ENVIRONMENTS_PATH = "environments"
KERNELS_SPECS_PATH = "kernel_specs"
def create_and_install_kernel() -> Tuple[str, str]:
# Create environment
kernel_name = str(uuid.uuid4())
env_name = f"{ENVIRONMENTS_PATH}/{kernel_name}"
# venv.create(env_name, system_site_packages=True, with_pip=True)
virtualenv.cli_run([env_name, "--system-site-packages"])
# Create kernel spec
kernel_spec = {
"argv": [
f"{env_name}/bin/python",
"-m",
"ipykernel_launcher",
"-f",
"{connection_file}",
],
"display_name": "Python 3",
"language": "python",
}
kernel_spec_folder = os.path.join(KERNELS_SPECS_PATH, kernel_name)
kernel_spec_file = os.path.join(kernel_spec_folder, "kernel.json")
# Create kernel spec folder
if not os.path.exists(os.path.dirname(kernel_spec_file)):
try:
os.makedirs(os.path.dirname(kernel_spec_file))
except OSError as exc: # Guard against race condition
if exc.errno != errno.EEXIST:
raise
with open(kernel_spec_file, mode="w", encoding="utf-8") as f:
json.dump(kernel_spec, f)
# Install kernel
kernel_spec_manager = KernelSpecManager()
kernel_spec_manager.install_kernel_spec(
source_dir=kernel_spec_folder, kernel_name=kernel_name
)
return kernel_name, env_name
def execute_notebook(
notebook_file_path: str,
output_file_folder: str,
replacement_map: Dict[str, str],
should_log_output: bool,
should_use_new_kernel: bool,
):
# Create staging directory if it doesn't exist
staging_file_path = f"{STAGING_FOLDER}/{notebook_file_path}"
if not os.path.exists(os.path.dirname(staging_file_path)):
try:
os.makedirs(os.path.dirname(staging_file_path))
except OSError as exc: # Guard against race condition
if exc.errno != errno.EEXIST:
raise
file_name = os.path.basename(os.path.normpath(notebook_file_path))
# Create environments folder
if not os.path.exists(ENVIRONMENTS_PATH):
try:
os.makedirs(ENVIRONMENTS_PATH)
except OSError as exc: # Guard against race condition
if exc.errno != errno.EEXIST:
raise
# Create and install kernel
kernel_name = next(
iter(KernelSpecManager().find_kernel_specs().keys()), None
) # Find first existing kernel and use as default
env_name = None
if should_use_new_kernel:
kernel_name, env_name = create_and_install_kernel()
# Read notebook
with open(notebook_file_path) as f:
nb = nbformat.read(f, as_version=4)
has_error = False
# Execute notebook
try:
# Create preprocessors
remove_no_execute_cells_preprocessor = RemoveNoExecuteCells()
update_variables_preprocessor = UpdateVariablesPreprocessor(
replacement_map=replacement_map
)
# Use no-execute preprocessor
(
nb,
resources,
) = remove_no_execute_cells_preprocessor.preprocess(nb)
(nb, resources) = update_variables_preprocessor.preprocess(nb, resources)
# print(f"Staging modified notebook to: {staging_file_path}")
with open(staging_file_path, mode="w", encoding="utf-8") as f:
nbformat.write(nb, f)
# Execute notebook
pm.execute_notebook(
input_path=staging_file_path,
output_path=staging_file_path,
kernel_name=kernel_name,
progress_bar=should_log_output,
request_save_on_cell_execute=should_log_output,
log_output=should_log_output,
stdout_file=sys.stdout if should_log_output else None,
stderr_file=sys.stderr if should_log_output else None,
)
except Exception:
# print(f"Error executing the notebook: {notebook_file_path}.\n\n")
has_error = True
raise
finally:
# Clear env
if env_name is not None:
shutil.rmtree(path=env_name)
# Copy execute notebook
output_file_path = os.path.join(
output_file_folder, "failure" if has_error else "success", file_name
)
# Create directories if they don't exist
if not os.path.exists(os.path.dirname(output_file_path)):
try:
os.makedirs(os.path.dirname(output_file_path))
except OSError as exc: # Guard against race condition
if exc.errno != errno.EEXIST:
raise
# print(f"Writing output to: {output_file_path}")
shutil.move(staging_file_path, output_file_path)
@@ -13,11 +13,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict
from nbconvert.preprocessors import Preprocessor
from . import UpdateNotebookVariables as update_notebook_variables
from typing import Dict
import UpdateNotebookVariables
class RemoveNoExecuteCells(Preprocessor):
@@ -43,7 +41,7 @@ class UpdateVariablesPreprocessor(Preprocessor):
# VARIABLE_NAME = '[description]'
for variable_name, variable_value in replacement_map.items():
content = update_notebook_variables.get_updated_value(
content = UpdateNotebookVariables.get_updated_value(
content=content,
variable_name=variable_name,
variable_value=variable_value,
@@ -62,4 +60,4 @@ class UpdateVariablesPreprocessor(Preprocessor):
executable_cells.append(cell)
notebook.cells = executable_cells
return notebook, resources
return notebook, resources
@@ -35,8 +35,8 @@ Variables in conditionals can also be replaced:
def get_updated_value(content: str, variable_name: str, variable_value: str) -> str:
return re.sub(
rf"({variable_name}.*? = .*?[\",\'])\[.+?\]([\",\'].*?)",
rf"\g<1>{variable_value}\g<2>",
rf"({variable_name}.*?=.*?[\",\'])\[.+?\]([\",\'].*?)",
rf"\1{variable_value}\2",
content,
flags=re.M,
)
@@ -78,27 +78,4 @@ def test_region():
variable_name="REGION",
variable_value="us-central1",
)
assert new_content == 'REGION = "us-central1" # @param {type:"string"}'
def test_region_equal_equals_ignore():
# Tests that == is ignored
new_content = get_updated_value(
content='REGION == "[your-region]" # @param {type:"string"}',
variable_name="REGION",
variable_value="us-central1",
)
assert new_content == 'REGION == "[your-region]" # @param {type:"string"}'
def test_service_account():
# Tests that == is ignored
new_content = get_updated_value(
content='SERVICE_ACCOUNT = "[your-service-account]" # @param {type:"string"}',
variable_name="SERVICE_ACCOUNT",
variable_value="12345-compute@developer.gserviceaccount.com",
)
assert (
new_content
== 'SERVICE_ACCOUNT = "12345-compute@developer.gserviceaccount.com" # @param {type:"string"}'
)
assert new_content == 'REGION = "us-central1" # @param {type:"string"}'
@@ -1,2 +1 @@
ratemate
google-cloud-aiplatform
+11 -18
View File
@@ -1,14 +1,11 @@
from typing import List
from ratemate import RateLimit
from resource_cleanup_manager import (
DatasetResourceCleanupManager,
ModelResourceCleanupManager,
EndpointResourceCleanupManager,
ResourceCleanupManager,
DatasetResourceCleanupManager,
EndpointResourceCleanupManager,
ModelResourceCleanupManager,
)
rate_limit = RateLimit(max_count=25, per=60, greedy=False)
def run_cleanup_managers(managers: List[ResourceCleanupManager], is_dry_run: bool):
for manager in managers:
@@ -18,18 +15,14 @@ def run_cleanup_managers(managers: List[ResourceCleanupManager], is_dry_run: boo
resources = manager.list()
print(f"Found {len(resources)} {type_name}'s")
for resource in resources:
try:
if not manager.is_deletable(resource):
continue
if not manager.is_deletable(resource):
continue
if is_dry_run:
resource_name = manager.resource_name(resource)
print(f"Will delete '{type_name}': {resource_name}")
else:
rate_limit.wait() # wait before deleting
manager.delete(resource)
except Exception as exception:
print(exception)
if is_dry_run:
resource_name = manager.resource_name(resource)
print(f"Will delete '{type_name}': {resource_name}")
else:
manager.delete(resource)
print("")
@@ -43,7 +36,7 @@ if is_dry_run:
managers = [
DatasetResourceCleanupManager(),
EndpointResourceCleanupManager(),
ModelResourceCleanupManager(), # ModelResourceCleanupManager must follow EndpointResourceCleanupManager due to deployed models blocking model deletion.
ModelResourceCleanupManager(),
]
run_cleanup_managers(managers=managers, is_dry_run=is_dry_run)
@@ -1,9 +1,8 @@
import abc
from typing import Any, Type
from google.cloud import aiplatform
from google.cloud.aiplatform import base
from typing import Any
from proto.datetime_helpers import DatetimeWithNanoseconds
from google.cloud.aiplatform import base
# If a resource was updated within this number of seconds, do not delete.
RESOURCE_UPDATE_BUFFER_IN_SECONDS = 60 * 60 * 8
@@ -41,7 +40,7 @@ class ResourceCleanupManager(abc.ABC):
# Check that it wasn't created too recently, to prevent race conditions
if time_difference <= RESOURCE_UPDATE_BUFFER_IN_SECONDS:
print(
f"Skipping '{resource}' due to update_time being '{time_difference}', which is less than '{RESOURCE_UPDATE_BUFFER_IN_SECONDS}'."
f"Skipping '{resource}' due update_time being '{time_difference}', which is less than '{RESOURCE_UPDATE_BUFFER_IN_SECONDS}'."
)
return False
@@ -51,7 +50,7 @@ class ResourceCleanupManager(abc.ABC):
class VertexAIResourceCleanupManager(ResourceCleanupManager):
@property
@abc.abstractmethod
def vertex_ai_resource(self) -> Type[base.VertexAiResourceNounWithFutureManager]:
def vertex_ai_resource(self) -> base.VertexAiResourceNounWithFutureManager:
pass
@property
@@ -61,9 +60,7 @@ class VertexAIResourceCleanupManager(ResourceCleanupManager):
def list(self) -> Any:
return self.vertex_ai_resource.list()
def resource_name(
self, resource: Type[base.VertexAiResourceNounWithFutureManager]
) -> str:
def resource_name(self, resource: Any) -> str:
return resource.display_name
def delete(self, resource):
@@ -77,33 +74,12 @@ class VertexAIResourceCleanupManager(ResourceCleanupManager):
class DatasetResourceCleanupManager(VertexAIResourceCleanupManager):
vertex_ai_resource = aiplatform.datasets._Dataset
dataset_types = [
aiplatform.ImageDataset,
aiplatform.TabularDataset,
aiplatform.TextDataset,
aiplatform.TimeSeriesDataset,
aiplatform.VideoDataset,
]
def list(self) -> Any:
return [
dataset
for dataset_type in self.dataset_types
for dataset in dataset_type.list()
]
class EndpointResourceCleanupManager(VertexAIResourceCleanupManager):
vertex_ai_resource = aiplatform.Endpoint
def delete(self, resource):
# TODO: Remove this once https://github.com/googleapis/python-aiplatform/issues/1441 is fixed
resource._sync_gca_resource()
for deployed_model_id in [
models.id for models in resource._gca_resource.deployed_models
]:
resource._undeploy(deployed_model_id=deployed_model_id)
resource.delete(force=True)
@@ -1,130 +0,0 @@
#!/usr/bin/env python
# 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
#
# http://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.
"""A CLI to process changed notebooks and execute them on Google Cloud Build"""
import argparse
import pathlib
import execute_changed_notebooks_helper
def str2bool(v):
if isinstance(v, bool):
return v
if v.lower() in ("yes", "true", "t", "y", "1"):
return True
elif v.lower() in ("no", "false", "f", "n", "0"):
return False
else:
raise argparse.ArgumentTypeError("Boolean value expected.")
parser = argparse.ArgumentParser(description="Run changed notebooks.")
parser.add_argument(
"--test_paths_file",
type=pathlib.Path,
help="The path to the file that has newline-limited folders of notebooks that should be tested.",
required=True,
)
parser.add_argument(
"--base_branch",
help="The base git branch to diff against to find changed files.",
required=False,
)
parser.add_argument(
"--container_uri",
type=str,
help="The container uri to run each notebook in.",
required=True,
)
parser.add_argument(
"--variable_project_id",
type=str,
help="The GCP project id. This is used to inject a variable value into the notebook before running.",
required=True,
)
parser.add_argument(
"--variable_region",
type=str,
help="The GCP region. This is used to inject a variable value into the notebook before running.",
required=True,
)
parser.add_argument(
"--variable_service_account",
type=str,
help="A service account. This is used to inject a variable value into the notebook before running. This is not the account that will run the notebook.",
required=True,
)
parser.add_argument(
"--variable_vpc_network",
type=str,
help="The full VPC network name. See https://cloud.google.com/compute/docs/networks-and-firewalls#networks. Format is projects/{project}/global/networks/{network}, where {project} is a project number, as in '12345', and {network} is network name. See <https://cloud.google.com/compute/docs/reference/rest/v1/networks/insert> for details. This is used to inject a variable value into the notebook before running.",
required=False,
)
parser.add_argument(
"--staging_bucket",
type=str,
help="The GCP directory for staging temporary files.",
required=True,
)
parser.add_argument(
"--artifacts_bucket",
type=str,
help="The GCP directory for storing executed notebooks.",
required=True,
)
parser.add_argument(
"--timeout",
type=int,
help="Timeout in seconds",
default=86400,
required=False,
)
parser.add_argument(
"--private_pool_id",
type=str,
help="The private pool id.",
required=False,
)
parser.add_argument(
"--should_parallelize",
type=str2bool,
nargs="?",
const=True,
default=True,
help="Should run notebooks in parallel.",
)
args = parser.parse_args()
notebooks = execute_changed_notebooks_helper.get_changed_notebooks(
test_paths_file=args.test_paths_file,
base_branch=args.base_branch,
)
execute_changed_notebooks_helper.process_and_execute_notebooks(
notebooks=notebooks,
container_uri=args.container_uri,
staging_bucket=args.staging_bucket,
artifacts_bucket=args.artifacts_bucket,
should_parallelize=args.should_parallelize,
timeout=args.timeout,
variable_project_id=args.variable_project_id,
variable_region=args.variable_region,
variable_service_account=args.variable_service_account,
variable_vpc_network=args.variable_vpc_network,
private_pool_id=args.private_pool_id,
)
@@ -1,502 +0,0 @@
#!/usr/bin/env python
# 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
#
# http://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.
import concurrent
import dataclasses
import datetime
import functools
import json
import git
import operator
import os
import pathlib
import re
import subprocess
import utils
from typing import List, Optional
from utils import util
import execute_notebook_helper
import execute_notebook_remote
import nbformat
from google.cloud.devtools.cloudbuild_v1.types import BuildOperationMetadata
from ratemate import RateLimit
from tabulate import tabulate
from utils import NotebookProcessors, util
# A buffer so that workers finish before the orchestrating job
WORKER_TIMEOUT_BUFFER_IN_SECONDS: int = 60 * 60
PYTHON_VERSION = "3.9" # Set default python version
def format_timedelta(delta: datetime.timedelta) -> str:
"""Formats a timedelta duration to [N days] %H:%M:%S format"""
seconds = int(delta.total_seconds())
secs_in_a_day = 86400
secs_in_a_hour = 3600
secs_in_a_min = 60
days, seconds = divmod(seconds, secs_in_a_day)
hours, seconds = divmod(seconds, secs_in_a_hour)
minutes, seconds = divmod(seconds, secs_in_a_min)
time_fmt = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
if days > 0:
suffix = "s" if days > 1 else ""
return f"{days} day{suffix} {time_fmt}"
return time_fmt
@dataclasses.dataclass
class NotebookExecutionResult:
name: str
duration: datetime.timedelta
is_pass: bool
log_url: str
output_uri: str
build_id: str
logs_bucket: str
error_message: Optional[str]
@property
def output_uri_web(self) -> Optional[str]:
if self.output_uri.startswith("gs://"):
return f"https://storage.googleapis.com/{self.output_uri[5:]}"
else:
return None
def _process_notebook(
notebook_path: str,
variable_project_id: str,
variable_region: str,
variable_service_account: str,
variable_vpc_network: Optional[str],
):
# Read notebook
with open(notebook_path) as f:
nb = nbformat.read(f, as_version=4)
# Create preprocessors
remove_no_execute_cells_preprocessor = NotebookProcessors.RemoveNoExecuteCells()
update_variables_preprocessor = NotebookProcessors.UpdateVariablesPreprocessor(
replacement_map={
"PROJECT_ID": variable_project_id,
"REGION": variable_region,
"SERVICE_ACCOUNT": variable_service_account,
"VPC_NETWORK": variable_vpc_network,
},
)
# Use no-execute preprocessor
(
nb,
resources,
) = remove_no_execute_cells_preprocessor.preprocess(nb)
(nb, resources) = update_variables_preprocessor.preprocess(nb, resources)
with open(notebook_path, mode="w", encoding="utf-8") as new_file:
nbformat.write(nb, new_file)
def _get_notebook_python_version(notebook_path: str) -> str:
"""
Get the python version for running the notebook if it is specified in
the notebook.
"""
python_version = PYTHON_VERSION
# Load the notebook
file = open(notebook_path)
src = file.read()
nb_json = json.loads(src)
#Iterate over the cells in the ipynb
for cell in nb_json['cells']:
if cell['cell_type'] == 'markdown':
markdown = str.join('', cell['source'])
# Look for the python version specification pattern
re_match = re.search('python version = (\d\.\d)', markdown, flags=re.IGNORECASE)
if re_match:
# get the version number
python_version = re_match.group(1)
break
return python_version
def _create_tag(filepath: str) -> str:
tag = os.path.basename(os.path.normpath(filepath))
tag = re.sub("[^0-9a-zA-Z_.-]+", "-", tag)
if tag.startswith(".") or tag.startswith("-"):
tag = tag[1:]
return tag
rate_limit = RateLimit(max_count=50, per=60, greedy=True)
def process_and_execute_notebook(
container_uri: str,
staging_bucket: str,
artifacts_bucket: str,
variable_project_id: str,
variable_region: str,
variable_service_account: str,
variable_vpc_network: Optional[str],
private_pool_id: Optional[str],
deadline: datetime.datetime,
notebook: str,
should_get_tail_logs: bool = False,
) -> NotebookExecutionResult:
rate_limit.wait() # wait before creating the task
print(f"Running notebook: {notebook}")
# Handle empty strings
if not variable_vpc_network:
variable_vpc_network = None
if not private_pool_id:
private_pool_id = None
# Create paths
notebook_output_uri = "/".join([artifacts_bucket, pathlib.Path(notebook).name])
# Create tag from notebook
tag = _create_tag(filepath=notebook)
result = NotebookExecutionResult(
name=tag,
duration=datetime.timedelta(seconds=0),
is_pass=False,
output_uri=notebook_output_uri,
log_url="",
build_id="",
logs_bucket="",
error_message=None,
)
# TODO: Handle cases where multiple notebooks have the same name
time_start = datetime.datetime.now()
operation = None
try:
# Get the python version for running the notebook if specified
notebook_exec_python_version = _get_notebook_python_version(notebook_path=notebook)
print(f"Running notebook with python {notebook_exec_python_version}")
# Pre-process notebook by substituting variable names
_process_notebook(
notebook_path=notebook,
variable_project_id=variable_project_id,
variable_region=variable_region,
variable_service_account=variable_service_account,
variable_vpc_network=variable_vpc_network,
)
# Upload the pre-processed code to a GCS bucket
code_archive_uri = util.archive_code_and_upload(staging_bucket=staging_bucket)
# Calculate timeout in seconds
timeout_in_seconds = max(
int((deadline - datetime.datetime.now()).total_seconds()), 1
)
operation = execute_notebook_remote.execute_notebook_remote(
code_archive_uri=code_archive_uri,
notebook_uri=notebook,
notebook_output_uri=notebook_output_uri,
container_uri=container_uri,
tag=tag,
private_pool_id=private_pool_id,
private_pool_region=variable_region,
timeout_in_seconds=timeout_in_seconds,
python_version=notebook_exec_python_version
)
operation_metadata = BuildOperationMetadata(mapping=operation.metadata)
result.build_id = operation_metadata.build.id
result.log_url = operation_metadata.build.log_url
result.logs_bucket = operation_metadata.build.logs_bucket
# Block and wait for the result
operation_result = operation.result()
result.duration = datetime.datetime.now() - time_start
result.is_pass = True
print(f"{notebook} PASSED in {format_timedelta(result.duration)}.")
except Exception as error:
result.error_message = str(error)
if operation and should_get_tail_logs:
# Extract the logs
logs_bucket = operation_metadata.build.logs_bucket
# Download tail end of logs file
log_file_uri = f"{logs_bucket}/log-{result.build_id}.txt"
# Use gcloud to get tail
try:
result.error_message = subprocess.check_output(
["gsutil", "cat", "-r", "-1000", log_file_uri], encoding="UTF-8"
)
except Exception as error:
result.error_message = str(error)
result.duration = datetime.datetime.now() - time_start
result.is_pass = False
print(
f"{notebook} FAILED in {format_timedelta(result.duration)}: {result.error_message}"
)
return result
def get_changed_notebooks(
test_paths_file: str,
base_branch: Optional[str] = None,
) -> List[str]:
"""
Get the notebooks that exist under the folders defined in the test_paths_file.
It only returns notebooks that have differences from the Git base_branch.
"""
test_paths = []
with open(test_paths_file) as file:
lines = [line.strip() for line in file.readlines()]
lines = [line for line in lines if len(line) > 0]
test_paths = [line for line in lines]
if len(test_paths) == 0:
raise RuntimeError("No test folders found.")
print(f"Checking folders: {test_paths}")
# Find notebooks
notebooks = []
# Instantiate GitPython objects
repo = git.Repo(os.getcwd())
index = repo.index
if base_branch:
# Get the point at which this branch branches off from main
branching_commits = repo.merge_base("HEAD", f"origin/{base_branch}")
if len(branching_commits) > 0:
branching_commit = branching_commits[0]
print(f"Looking for notebooks that changed from branch: {branching_commit}")
notebooks = [
diff.b_path
for diff in index.diff(branching_commit, paths=test_paths)
if diff.b_path is not None
]
else:
notebooks = []
else:
print(f"Looking for all notebooks.")
notebooks_str = subprocess.check_output(["git", "ls-files"] + test_paths)
notebooks = notebooks_str.decode("utf-8").split("\n")
notebooks = [notebook for notebook in notebooks if notebook.endswith(".ipynb")]
notebooks = [notebook for notebook in notebooks if len(notebook) > 0]
notebooks = [notebook for notebook in notebooks if pathlib.Path(notebook).exists()]
if len(notebooks) > 0:
print(f"Found {len(notebooks)} notebooks:")
for notebook in notebooks:
print(f"\t{notebook}")
return notebooks
def process_and_execute_notebooks(
notebooks: List[str],
container_uri: str,
staging_bucket: str,
artifacts_bucket: str,
should_parallelize: bool,
timeout: int,
variable_project_id: str,
variable_region: str,
variable_service_account: str,
variable_vpc_network: Optional[str] = None,
private_pool_id: Optional[str] = None,
):
"""
Run the notebooks that exist under the folders defined in the test_paths_file.
It only runs notebooks that have differences from the Git base_branch.
The executed notebooks are saved in the artifacts_bucket.
Variables are also injected into the notebooks such as the variable_project_id and variable_region.
Args:
test_paths_file (str):
Required. The new-line delimited file to folders and files that need checking.
Folders are checked recursively.
base_branch (str):
Optional. If provided, only the files that have changed from the base_branch will be checked.
If not provided, all files will be checked.
staging_bucket (str):
Required. The GCS staging bucket to write source code to.
artifacts_bucket (str):
Required. The GCS staging bucket to write executed notebooks to.
variable_project_id (str):
Required. The value for PROJECT_ID to inject into notebooks.
variable_region (str):
Required. The value for REGION to inject into notebooks.
should_parallelize (bool):
Required. Should run notebooks in parallel using a thread pool as opposed to in sequence.
timeout (str):
Required. Timeout string according to https://cloud.google.com/build/docs/build-config-file-schema#timeout.
"""
# Calculate deadline
deadline = datetime.datetime.now() + datetime.timedelta(
seconds=max(timeout - WORKER_TIMEOUT_BUFFER_IN_SECONDS, 0)
)
if len(notebooks) >= 1:
notebook_execution_results: List[NotebookExecutionResult] = []
print(f"Found {len(notebooks)} modified notebooks: {notebooks}")
if should_parallelize and len(notebooks) > 1:
print(
"Running notebooks in parallel, so no logs will be displayed. Please wait..."
)
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
print(f"Max workers: {executor._max_workers}")
notebook_execution_results = list(
executor.map(
functools.partial(
process_and_execute_notebook,
container_uri,
staging_bucket,
artifacts_bucket,
variable_project_id,
variable_region,
variable_service_account,
variable_vpc_network,
private_pool_id,
deadline,
),
notebooks,
)
)
else:
notebook_execution_results = [
process_and_execute_notebook(
container_uri=container_uri,
staging_bucket=staging_bucket,
artifacts_bucket=artifacts_bucket,
variable_project_id=variable_project_id,
variable_region=variable_region,
variable_service_account=variable_service_account,
variable_vpc_network=variable_vpc_network,
private_pool_id=private_pool_id,
deadline=deadline,
notebook=notebook,
)
for notebook in notebooks
]
print("\n=== RESULTS ===\n")
results_sorted = sorted(
notebook_execution_results,
key=lambda result: result.is_pass,
reverse=True,
)
# Print results
print(
tabulate(
[
[
result.name,
"PASSED" if result.is_pass else "FAILED",
format_timedelta(result.duration),
result.log_url,
result.output_uri,
result.output_uri_web,
result.logs_bucket
]
for result in results_sorted
],
headers=[
"build_tag",
"status",
"duration",
"log_url",
"output_uri",
"output_uri_web",
"logs_bucket"
],
)
)
if len(notebooks) == 1:
print("="*100)
print("The notebook execution build log:\n")
print("="*100)
build_id = results_sorted[0].build_id
logs_bucket_name = (results_sorted[0].logs_bucket).removeprefix("gs://")
log_file_name = f"log-{build_id}.txt"
log_contents = util.download_blob_into_memory(
bucket_name=logs_bucket_name,
blob_name=log_file_name,
download_as_text=True
)
# Remove extra steps from the log
match = re.search("starting Step #4", log_contents, flags=re.IGNORECASE)
if match is not None:
match_index = match.span()[0]
print(log_contents[match_index:])
else:
print(log_contents)
print("\n=== END RESULTS===\n")
total_notebook_duration = functools.reduce(
operator.add,
[datetime.timedelta(seconds=0)]
+ [result.duration for result in results_sorted],
)
print(
f"Cumulative notebook duration: {format_timedelta(total_notebook_duration)}"
)
# Raise error if any notebooks failed
if not all([result.is_pass for result in results_sorted]):
raise RuntimeError("Notebook failures detected. See logs for details")
else:
print("No notebooks modified in this pull request.")
-41
View File
@@ -1,41 +0,0 @@
#!/usr/bin/env python
# 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
#
# http://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.
"""A CLI to download (optional) and run a single notebook locally"""
import argparse
import execute_notebook_helper
parser = argparse.ArgumentParser(description="Run a single notebook locally.")
parser.add_argument(
"--notebook_source",
type=str,
help="Local filepath or GCS URI to notebook.",
required=True,
)
parser.add_argument(
"--output_file_or_uri",
type=str,
help="Local file or GCS URI to save executed notebook to.",
required=True,
)
args = parser.parse_args()
execute_notebook_helper.execute_notebook(
notebook_source=args.notebook_source,
output_file_or_uri=args.output_file_or_uri,
should_log_output=True,
)
-102
View File
@@ -1,102 +0,0 @@
#!/usr/bin/env python
# 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
#
# http://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.
"""Methods to run a notebook locally"""
import errno
import os
import shutil
import sys
import papermill as pm
from google.cloud.aiplatform import utils
from utils import util
# This script is used to execute a notebook and write out the output notebook.
# This is used to force papermill to use this kernel to run the notebook instead of any defined inside the notebook itself
DEFAULT_KERNEL_NAME = "python3"
def execute_notebook(
notebook_source: str,
output_file_or_uri: str,
should_log_output: bool,
):
"""Execute a single notebook using Papermill"""
file_name = os.path.basename(os.path.normpath(notebook_source))
# Download notebook if it's a GCS URI
if notebook_source.startswith("gs://"):
# Extract uri components
bucket_name, prefix = utils.extract_bucket_and_prefix_from_gcs_path(
notebook_source
)
# Download remote notebook to local file system
notebook_source = file_name
util.download_file(
bucket_name=bucket_name, blob_name=prefix, destination_file=notebook_source
)
execution_exception = None
print("\n=== DOWNLOAD EXECUTED NOTEBOOK ===\n")
print(f"Please debug the executed notebook by downloading the executed notebook:")
print("Option 1. Using gsutil. Run the following command in your terminal.")
print(f'\tgsutil cp "{output_file_or_uri}" .')
print("Option 2. Using this link.")
print(f"\thttps://storage.googleapis.com/{output_file_or_uri[5:]}")
print("\n======\n")
# Execute notebook
try:
# Execute notebook
pm.execute_notebook(
input_path=notebook_source,
output_path=notebook_source,
progress_bar=should_log_output,
request_save_on_cell_execute=should_log_output,
kernel_name=DEFAULT_KERNEL_NAME,
log_output=should_log_output,
stdout_file=sys.stdout if should_log_output else None,
stderr_file=sys.stderr if should_log_output else None,
)
except Exception as exception:
execution_exception = exception
finally:
# Copy executed notebook
if output_file_or_uri.startswith("gs://"):
# Upload to GCS path
util.upload_file(notebook_source, remote_file_path=output_file_or_uri)
print("\n=== EXECUTION FINISHED ===\n")
else:
# Create directories if they don't exist
if not os.path.exists(os.path.dirname(output_file_or_uri)):
try:
os.makedirs(os.path.dirname(output_file_or_uri))
except OSError as exc: # Guard against race condition
if exc.errno != errno.EEXIST:
raise
print(f"Writing output to: {output_file_or_uri}")
shutil.move(notebook_source, output_file_or_uri)
if execution_exception:
raise execution_exception
-105
View File
@@ -1,105 +0,0 @@
#!/usr/bin/env python
# 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
#
# http://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.
"""Methods to run a notebook on Google Cloud Build"""
from re import sub
from typing import Optional
import google.auth
import yaml
from google.api_core import client_options, operation
from google.cloud.aiplatform import utils
from google.cloud.devtools import cloudbuild_v1
from google.cloud.devtools.cloudbuild_v1.types import Source, StorageSource
from google.protobuf import duration_pb2
from yaml.loader import FullLoader
CLOUD_BUILD_FILEPATH = ".cloud-build/notebook-execution-test-cloudbuild-single.yaml"
SERVICE_BASE_PATH = "cloudbuild.googleapis.com"
def execute_notebook_remote(
code_archive_uri: str,
notebook_uri: str,
notebook_output_uri: str,
container_uri: str,
private_pool_id: Optional[str],
private_pool_region: Optional[str],
tag: Optional[str],
timeout_in_seconds: Optional[int] = None,
python_version: Optional[str] = None
) -> operation.Operation:
"""Create and execute a single notebook on Google Cloud Build"""
# Load build steps from YAML
cloudbuild_config = yaml.load(open(CLOUD_BUILD_FILEPATH), Loader=FullLoader)
substitutions = {
"_PYTHON_IMAGE": container_uri,
"_NOTEBOOK_GCS_URI": notebook_uri,
"_NOTEBOOK_OUTPUT_GCS_URI": notebook_output_uri,
"_PYTHON_VERSION" : f"python{python_version}"
}
if python_version is not None:
substitutions["_PYTHON_VERSION"] = "python" + python_version
build = cloudbuild_v1.Build()
options: Optional[client_options.ClientOptions] = None
if private_pool_id and private_pool_region:
# substitutions["_PRIVATE_POOL_NAME"] = private_pool_id
build.options = cloudbuild_config.get("options")
build.options.pool = {"name": private_pool_id}
# Switch to the regional endpoint of the pool
options = client_options.ClientOptions(
api_endpoint=f"{private_pool_region}-{SERVICE_BASE_PATH}"
)
# Authorize the client with Google defaults
credentials, project_id = google.auth.default()
client = cloudbuild_v1.services.cloud_build.CloudBuildClient(client_options=options)
(
source_archived_file_gcs_bucket,
source_archived_file_gcs_object,
) = utils.extract_bucket_and_prefix_from_gcs_path(code_archive_uri)
build.source = Source(
storage_source=StorageSource(
bucket=source_archived_file_gcs_bucket,
object_=source_archived_file_gcs_object,
)
)
build.steps = cloudbuild_config["steps"]
build.substitutions = substitutions
build.timeout = duration_pb2.Duration(seconds=timeout_in_seconds)
build.queue_ttl = duration_pb2.Duration(seconds=timeout_in_seconds)
if tag:
build.tags = [tag]
operation = client.create_build(project_id=project_id, build=build)
# Print the in-progress operation
# print("IN PROGRESS:")
# print(operation.metadata)
# Print the completed status
# print("RESULT:", result.status)
return operation
@@ -1,38 +0,0 @@
steps:
# Show the gcloud info and check if gcloud exists
- name: ${_PYTHON_IMAGE}
entrypoint: /bin/sh
args:
- -c
- 'gcloud config list --quiet'
# Check the Python version
- name: ${_PYTHON_IMAGE}
entrypoint: /bin/sh
args:
- -c
- ${_PYTHON_VERSION} .cloud-build/CheckPythonVersion.py -q
# Create a virtual environment
- name: ${_PYTHON_IMAGE}
entrypoint: /bin/sh
args:
- -c
- ${_PYTHON_VERSION} -m venv workspace/env
# Install Python dependencies
- name: ${_PYTHON_IMAGE}
entrypoint: /bin/sh
args:
- -c
- . workspace/env/bin/activate &&
python -m pip -q install -U pip &&
python -m pip -q install -U -r .cloud-build/requirements.txt
# Install Python dependencies and run testing script
- name: ${_PYTHON_IMAGE}
entrypoint: /bin/sh
args:
- -c
- |
. workspace/env/bin/activate &&
python .cloud-build/execute_notebook_cli.py --notebook_source "${_NOTEBOOK_GCS_URI}" --output_file_or_uri "${_NOTEBOOK_OUTPUT_GCS_URI}"
env:
- 'IS_TESTING=1'
timeout: 86400s
@@ -4,42 +4,41 @@ steps:
entrypoint: /bin/sh
args:
- -c
- gcloud config list --quiet
- 'gcloud config list'
# Check the Python version
- name: ${_PYTHON_IMAGE}
entrypoint: /bin/sh
args:
- -c
- python3 .cloud-build/CheckPythonVersion.py -q
# Fetch full repo for diff purposes
- name: gcr.io/cloud-builders/git
args: [fetch, --unshallow, --quiet]
# Create a virtual environment
- 'python3 .cloud-build/CheckPythonVersion.py'
# Fetch base branch if required
- name: ${_PYTHON_IMAGE}
entrypoint: /bin/sh
args:
- -c
- python3 -m venv workspace/env
- 'if [ -n "${_BASE_BRANCH}" ]; then git fetch origin "${_BASE_BRANCH}":refs/remotes/origin/"${_BASE_BRANCH}"; else echo "Skipping fetch."; fi'
# Install Python dependencies
- name: ${_PYTHON_IMAGE}
entrypoint: /bin/sh
args:
- -c
- . workspace/env/bin/activate &&
python3 -m pip -q install -U pip &&
python3 -m pip -q install -U -r .cloud-build/requirements.txt
entrypoint: pip
args: ['install', '--upgrade', '--user', '--requirement', '.cloud-build/requirements.txt']
# Install Python dependencies and run testing script
# TODO: Only pass in private_pool_id if it is set
- name: ${_PYTHON_IMAGE}
entrypoint: /bin/sh
args:
- -c
- |
. workspace/env/bin/activate &&
python3 .cloud-build/execute_changed_notebooks_cli.py --test_paths_file "${_TEST_PATHS_FILE}" --base_branch "${_FORCED_BASE_BRANCH}" --container_uri ${_PYTHON_IMAGE} --staging_bucket ${_GCS_STAGING_BUCKET} --artifacts_bucket ${_GCS_STAGING_BUCKET}/executed_notebooks/PR_${_PR_NUMBER}/BUILD_${BUILD_ID} --variable_project_id ${PROJECT_ID} --variable_region ${_GCP_REGION} --variable_service_account ${_GCP_SERVICE_ACCOUNT} --variable_vpc_network "${_GPC_VPC_NETWORK_NAME}" `if [ ! -z "${_PRIVATE_POOL_NAME}" ]; then echo "--private_pool_id ${_PRIVATE_POOL_NAME}"; fi`
- 'python3 -m pip freeze && python3 .cloud-build/ExecuteChangedNotebooks.py --test_paths_file "${_TEST_PATHS_FILE}" --base_branch "${_FORCED_BASE_BRANCH}" --output_folder ${BUILD_ID} --variable_project_id ${PROJECT_ID} --variable_region ${_GCP_REGION}'
env:
- 'IS_TESTING=1'
# Manually copy artifacts to GCS
- name: gcr.io/cloud-builders/gsutil
entrypoint: /bin/sh
args:
- -c
- 'if [ $(ls -pR "/workspace/${BUILD_ID}" | grep -v / | grep -v ^$ | wc -l) -ne 0 ]; then gsutil -m -q rsync -r "/workspace/${BUILD_ID}" "gs://${_GCS_ARTIFACTS_BUCKET}/test-artifacts/PR_${_PR_NUMBER}/BUILD_${BUILD_ID}/"; else echo "No artifacts to copy."; fi'
# Fail if there is anything in the failure folder
- name: ${_PYTHON_IMAGE}
entrypoint: /bin/sh
args:
- -c
- 'echo "Download executed notebooks with this command: \"mkdir -p artifacts && gsutil rsync -r gs://${_GCS_ARTIFACTS_BUCKET}/test-artifacts/PR_${_PR_NUMBER}/BUILD_${BUILD_ID} artifacts/\"" && if [ "$(ls -A /workspace/${BUILD_ID}/failure | wc -l)" -ne 0 ]; then exit 1; else exit 0; fi'
timeout: 86400s
options:
pool:
name: ${_PRIVATE_POOL_NAME}
+7 -12
View File
@@ -1,13 +1,8 @@
ipython
numpy
jupyter
nbconvert
papermill
pandas
matplotlib
ipython>=7.0
jupyter>=1.0
nbconvert>=6.0
papermill>=2.3
numpy>=1.19
pandas>=1.2
matplotlib>=3.4
tabulate
google-cloud-aiplatform
google-cloud-storage
google-cloud-build
ratemate
GitPython
-6
View File
@@ -1,6 +0,0 @@
notebooks/official/vizier/gapic-vizier-multi-objective-optimization.ipynb
notebooks/official/pipelines/lightweight_functions_component_io_kfp.ipynb
notebooks/official/ml_metadata/sdk-metric-parameter-tracking-for-locally-trained-models.ipynb
notebooks/official/custom/custom-tabular-bq-managed-dataset.ipynb
notebooks/official/matching_engine/sdk_matching_engine_for_indexing.ipynb
.cloud-build/tests/python_version_test.ipynb
-1
View File
@@ -1 +0,0 @@
notebooks/official/custom/custom-tabular-bq-managed-dataset.ipynb
@@ -1,61 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "57a3d44ed8a8"
},
"source": [
"### Set up your Google Cloud project\n",
"\n",
"**_NOTE_**: This notebook has been tested in the following environment:\n",
"\n",
"* Python version = 3.7\n",
"\n",
"**The following steps are required, regardless of your notebook environment.**\n",
"\n",
"1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n",
"\n",
"1. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n",
"\n",
"1. [Enable the Vertex AI API and Compute Engine API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com,compute_component).\n",
"\n",
"1. If you are running this notebook locally, you will need to install the [Cloud SDK](https://cloud.google.com/sdk).\n",
"\n",
"1. Enter your project ID in the cell below. Then run the cell to make sure the\n",
"Cloud SDK uses the right project for all the commands in this notebook.\n",
"\n",
"**Note**: Jupyter runs lines prefixed with `!` as shell commands, and it interpolates Python variables prefixed with `$` into these commands."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "c6516f90311b"
},
"outputs": [],
"source": [
"# test if the right python version is being used\n",
"import sys\n",
"\n",
"actual_python_version = f\"{sys.version_info.major}.{sys.version_info.minor}\"\n",
"print(f\"Runtime python version: {actual_python_version}\")\n",
"\n",
"assert actual_python_version == \"3.7\", \"Wrong python version!\""
]
}
],
"metadata": {
"colab": {
"name": "python_version_test.ipynb",
"toc_visible": true
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
View File
-91
View File
@@ -1,91 +0,0 @@
import os
import subprocess
import tarfile
import uuid
from datetime import datetime
from typing import Optional, Union
from google.auth import credentials as auth_credentials
from google.cloud import storage
from google.cloud.aiplatform import utils
def download_file(bucket_name: str, blob_name: str, destination_file: str) -> str:
"""Copies a remote GCS file to a local path"""
remote_file_path = "".join(["gs://", "/".join([bucket_name, blob_name])])
subprocess.check_output(
["gsutil", "cp", remote_file_path, destination_file], encoding="UTF-8"
)
return destination_file
def upload_file(
local_file_path: str,
remote_file_path: str,
) -> str:
"""Copies a local file to a GCS path"""
subprocess.check_output(
["gsutil", "cp", local_file_path, remote_file_path], encoding="UTF-8"
)
return remote_file_path
def archive_code_and_upload(staging_bucket: str):
# Archive all source in current directory
unique_id = uuid.uuid4()
source_archived_file = f"source_archived_{unique_id}.tar.gz"
git_files = subprocess.check_output(
["git", "ls-tree", "-r", "HEAD", "--name-only"], encoding="UTF-8"
).split("\n")
with tarfile.open(source_archived_file, "w:gz") as tar:
for file in git_files:
if len(file) > 0 and os.path.exists(file):
tar.add(file)
# Upload archive to GCS bucket
source_archived_file_gcs = upload_file(
local_file_path=f"{source_archived_file}",
remote_file_path="/".join(
[staging_bucket, "code_archives", source_archived_file]
),
)
print(f"Uploaded source code archive to {source_archived_file_gcs}")
return source_archived_file_gcs
def download_blob_into_memory(
bucket_name: str,
blob_name: str,
download_as_text: Optional[bool]=False
) -> Union[bytes, str]:
"""
Downloads a blob into memory as byte or as text if
download_as_text is set to True.
"""
storage_client = storage.Client()
bucket = storage_client.bucket(bucket_name)
# Construct a client side representation of a blob.
blob = bucket.blob(blob_name)
# Download the blob content
if download_as_text:
contents = blob.download_as_text()
else:
contents = blob.download_as_bytes()
print(
f"Downloaded storage object {blob_name} from bucket {bucket_name}."
)
return contents
+8 -19
View File
@@ -1,28 +1,17 @@
**REQUIRED:** Add a summary of your PR here, typically including why the change is needed and what was changed. Include any design alternatives for discussion purposes.
<br>
--- YOUR PR SUMMARY GOES HERE ---
<br><br><br>
**REQUIRED:** Fill out the below checklists or remove if irrelevant
1. If you are opening a PR for `Official Notebooks` under the [notebooks/official](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/main/notebooks/official) folder, follow this mandatory checklist:
- [ ] Use the [notebook template](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/notebook_template.ipynb) as a starting point.
If you are opening a PR for `Official Notebooks` under the [notebooks/official](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/master/notebooks/official) folder, follow this mandatory checklist:
- [ ] Use the [notebook template](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/master/notebooks/notebook_template.ipynb) as a starting point.
- [ ] Follow the style and grammar rules outlined in the above notebook template.
- [ ] Verify the notebook runs successfully in Colab since the automated tests cannot guarantee this even when it passes.
- [ ] Passes all the required automated checks. You can locally test for formatting and linting with these [instructions](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/CONTRIBUTING.md#code-quality-checks).
- [ ] Passes all the required automated checks
- [ ] You have consulted with a tech writer to see if tech writer review is necessary. If so, the notebook has been reviewed by a tech writer, and they have approved it.
- [ ] This notebook has been added to the [CODEOWNERS](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/CODEOWNERS) file under the `Official Notebooks` section, pointing to the author or the author's team.
- [ ] This notebook has been added to the [CODEOWNERS](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/master/docs/CODEOWNERS) file under `# Official Notebooks` section, pointing to the author or the author's team.
- [ ] The Jupyter notebook cleans up any artifacts it has created (datasets, ML models, endpoints, etc) so as not to eat up unnecessary resources.
<br>
2. If you are opening a PR for `Community Notebooks` under the [notebooks/community](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/main/notebooks/community) folder:
- [ ] This notebook has been added to the [CODEOWNERS](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/community/CODEOWNERS) file under the `Community Notebooks` section, pointing to the author or the author's team.
- [ ] Passes all the required formatting and linting checks. You can locally test with these [instructions](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/CONTRIBUTING.md#code-quality-checks).
If you are opening a PR for `Community Notebooks` under the [notebooks/community](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/master/notebooks/community) folder:
- [ ] This notebook has been added to the [CODEOWNERS](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/master/docs/CODEOWNERS) file under the `# Community Notebooks` section, pointing to the author or the author's team.
<br>
3. If you are opening a PR for `Community Content` under the [community-content](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/main/community-content) folder:
If you are opening a PR for `Community Content` under the [community-content](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/master/community-content) folder:
- [ ] Make sure your main `Content Directory Name` is descriptive, informative, and includes some of the key products and attributes of your content, so that it is differentiable from other content
- [ ] The main content directory has been added to the [CODEOWNERS](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/community-content/CODEOWNERS) file under the `Community Content` section, pointing to the author or the author's team.
- [ ] Passes all the required formatting and linting checks. You can locally test with these [instructions](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/CONTRIBUTING.md#code-quality-checks).
- [ ] The main content directory has been added to the [CODEOWNERS](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/master/docs/CODEOWNERS) file under the `# Community Content` section, pointing to the author or the author's team.
+4 -6
View File
@@ -7,15 +7,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
uses: actions/setup-python@v2
- name: Fetch pull request branch
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Fetch base main branch
run: git fetch -u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" main:main
- name: Fetch base master branch
run: git fetch -u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" master:master
- name: Install requirements
run: python3 -m pip install -U -r .github/workflows/linter/requirements.txt
- name: Format and lint notebooks
-20
View File
@@ -1,20 +0,0 @@
# To use this image, run this command with the desired notebook args from the top-level vertex-ai-samples directory:
# 1. To lint all changed notebooks:
# docker run -v ${PWD}:/setup/app gcr.io/python-docs-samples-tests/notebook_linter:latest
# 2. To lint specific notebooks:
# docker run -v ${PWD}:/setup/app gcr.io/python-docs-samples-tests/notebook_linter:latest notebooks/1.ipynb notebooks/2.ipynb
FROM python:3.10
WORKDIR setup
COPY ./requirements.txt .
COPY ./run_linter.sh .
# Install dependencies.
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
WORKDIR app
ENTRYPOINT ["/setup/run_linter.sh"]
+5 -6
View File
@@ -2,9 +2,8 @@ git+https://github.com/tensorflow/docs
ipython
jupyter
nbconvert
black==22.6.0
pyupgrade==2.34.0
isort==5.10.1
flake8==4.0.1
nbqa==1.4.0
black==20.8b1
pyupgrade==2.7.3
isort==5.6.4
flake8==3.9.0
nbqa==0.6.0
+11 -21
View File
@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# This script automatically formats and lints all notebooks that have changed from the head of the main branch.
# This script automatically formats and lints all notebooks that have changed from the head of the master branch.
#
# Options:
# -t: Test-mode. Only test if format and linting are required but make no changes to files.
@@ -47,22 +47,12 @@ done
echo "Test mode: $is_test"
# Read in user-provided notebooks
notebooks=()
for arg in "$@"; do
if [[ $arg == *.ipynb ]]; then
notebooks+=("$arg")
fi
done
# Only check notebooks in test folders modified in this pull request.
# Note: Use process substitution to persist the data in the array
if [ ${#notebooks[@]} -eq 0 ]; then
echo "Checking for changed notebooked using git"
while read -r file || [ -n "$line" ]; do
notebooks+=("$file")
done < <(git diff --name-only main... | grep '\.ipynb$')
fi
notebooks=()
while read -r file || [ -n "$line" ]; do
notebooks+=("$file")
done < <(git diff --name-only master... | grep '\.ipynb$')
problematic_notebooks=()
if [ ${#notebooks[@]} -gt 0 ]; then
@@ -78,7 +68,7 @@ if [ ${#notebooks[@]} -gt 0 ]; then
if [ "$is_test" = true ]; then
echo "Running nbfmt..."
python3 -m tensorflow_docs.tools.nbfmt --test "$notebook"
python3 -m tensorflow_docs.tools.nbfmt --remove_outputs --test "$notebook"
NBFMT_RTN=$?
# echo "Running black..."
# python3 -m nbqa black "$notebook" --check
@@ -94,19 +84,19 @@ if [ ${#notebooks[@]} -gt 0 ]; then
FLAKE8_RTN=$?
else
echo "Running black..."
python3 -m nbqa black "$notebook"
python3 -m nbqa black "$notebook" --nbqa-mutate
BLACK_RTN=$?
echo "Running pyupgrade..."
python3 -m nbqa pyupgrade "$notebook"
python3 -m nbqa pyupgrade "$notebook" --nbqa-mutate
PYUPGRADE_RTN=$?
echo "Running isort..."
python3 -m nbqa isort "$notebook"
python3 -m nbqa isort "$notebook" --nbqa-mutate
ISORT_RTN=$?
echo "Running nbfmt..."
python3 -m tensorflow_docs.tools.nbfmt "$notebook"
python3 -m tensorflow_docs.tools.nbfmt --remove_outputs "$notebook"
NBFMT_RTN=$?
echo "Running flake8..."
python3 -m nbqa flake8 "$notebook" --show-source --extend-ignore=W391,E501,F821,E402,F404,W503,E203,E722,W293,W291
python3 -m nbqa flake8 "$notebook" --show-source --extend-ignore=W391,E501,F821,E402,F404,W503,E203,E722,W293,W291 --nbqa-mutate
FLAKE8_RTN=$?
fi
-6
View File
@@ -1,6 +0,0 @@
# See https://help.github.com/en/articles/about-code-owners
# for more info about CODEOWNERS file.
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence.
* @GoogleCloudPlatform/vertex-ai-samples-owners
-43
View File
@@ -1,43 +0,0 @@
# Contributor Code of Conduct
As contributors and maintainers of this project,
and in the interest of fostering an open and welcoming community,
we pledge to respect all people who contribute through reporting issues,
posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in this project
a harassment-free experience for everyone,
regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information,
such as physical or electronic
addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct.
By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently
applying these principles to every aspect of managing this project.
Project maintainers who do not follow or enforce the Code of Conduct
may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported by opening an issue
or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
-65
View File
@@ -1,65 +0,0 @@
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code Quality Checks
All notebooks in this project are checked for formatting and style, to ensure a
consistent experience. To test notebooks prior to submitting a pull request,
you can follow these steps.
From a command-line terminal (e.g. from Vertex Workbench or locally), install
the code analysis tools:
```shell
pip3 install --user -U nbqa black flake8 isort pyupgrade git+https://github.com/tensorflow/docs
```
You'll likely need to add the directory where these were installed to your PATH:
```shell
export PATH="$HOME/.local/bin:$PATH"
```
Then, set an environment variable for your notebook (or directory):
```shell
export notebook="your-notebook.ipynb"
```
Finally, run this code block to check for errors. Each step will attempt to
automatically fix any issues. If the fixes can't be performed automatically,
then you will need to manually address them before submitting your PR.
```shell
nbqa black "$notebook"
nbqa pyupgrade "$notebook"
nbqa isort "$notebook"
nbqa flake8 "$notebook" --extend-ignore=W391,E501,F821,E402,F404,W503,E203,E722,W293,W291
python3 -m tensorflow_docs.tools.nbfmt --remove_outputs "$notebook"
```
## Code Reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Community Guidelines
This project follows [Google's Open Source Community
Guidelines](https://opensource.google/conduct/).
+2 -19
View File
@@ -6,32 +6,15 @@ Welcome to the Google Cloud [Vertex AI](https://cloud.google.com/vertex-ai/docs/
## Overview
The repository contains [notebooks](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/master/notebooks) and [community content](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/master/community-content) that demonstrate how to develop and manage ML workflows using Google Cloud Vertex AI.
## Repository structure
```bash
├── community-content - Sample code and tutorials contributed by the community
├── notebooks
│ ├── community - Notebooks contributed by the community
│ ├── official - Notebooks demonstrating use of each Vertex AI service
│ │ ├── automl
│ │ ├── custom
│ │ ├── ...
```
The repository contains [Notebooks](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/main/notebooks) and [Community Content](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/master/community-content) that demonstrate how to develop and manage ML workflows using Google Cloud Vertex AI.
## Contributing
Contributions welcome! See the [Contributing Guide](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/master/CONTRIBUTING.md).
Contributions welcome! See the [Contributing Guide](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/docs/contributing.md).
## Getting help
Please use the [issues page](https://github.com/GoogleCloudPlatform/vertex-ai-samples/issues) to provide feedback or submit a bug report.
## Disclaimer
This is not an officially supported Google product. The code in this repository is for demonstrative purposes only.
## Feedback
Please feel free to fill out our [survey](https://bit.ly/vertex-ai-samples-survey) to give us feedback on the repo and its content.
-7
View File
@@ -1,7 +0,0 @@
# Security Policy
To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.
-5
View File
@@ -1,8 +1,3 @@
* @vertex-ai-samples-contributors @GoogleCloudPlatform/cloudml-samples-owners
/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk @yinghsienwu
/pytorch_pre_built_images_deployment @googleapis/vertex-prediction-team
/pytorch_text_classification_using_vertex_sdk_and_gcloud @RajeshThallam
/pytorch_text_classification_using_vertex_sdk_and_gcloud @RajeshThallam @ultrons
/sklearn_text_classification_from_script_using_vertex_sdk @maxhardt
/pluto_on_workbench @wkharold
/cpr-examples @samthrasher
@@ -1,824 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "pc5-mbsX9PZC"
},
"source": [
"# AlphaFold On Vertex AI Workbench\n",
"\n",
"[Vertex AI Workbench](https://cloud.google.com/vertex-ai/docs/workbench) offers an end-to-end notebook-based production environment that can be preconfigured with the runtime dependencies necessary to run AlphaFold on Vertex AI. With [User-Managed Notebooks](https://cloud.google.com/vertex-ai/docs/workbench/user-managed/introduction), you can configure a GPU accelerator to run AlphaFold using Tensorflow, without having to install and manage drivers or JupyterLab instances. This notebook allows you to easily predict the structure of a protein using a slightly simplified version of [AlphaFold v2.1.0](https://doi.org/10.1038/s41586-021-03819-2). \n",
"\n",
"## ![](https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/main/community-content/alphafold_on_workbench/vertexai_40.png) [Launch this Notebook in Vertex AI Workbench](https://console.cloud.google.com/vertex-ai/workbench/deploy-notebook?download_url=https://github.com/GoogleCloudPlatform/vertex-ai-samples/raw/main/community-content/alphafold_on_workbench/AlphaFold.ipynb)\n",
"\n",
"**Differences to AlphaFold v2.1.0**\n",
"\n",
"In comparison to AlphaFold v2.1.0, this notebook notebook uses **no templates (homologous structures)** and a selected portion of the [BFD database](https://bfd.mmseqs.com/). We have validated these changes on several thousand recent PDB structures. While accuracy will be near-identical to the full AlphaFold system on many targets, a small fraction have a large drop in accuracy due to the smaller MSA and lack of templates. For best reliability, we recommend instead using the [full open source AlphaFold](https://github.com/deepmind/alphafold/), or the [AlphaFold Protein Structure Database](https://alphafold.ebi.ac.uk/).\n",
"\n",
"**This notebook has an small drop in average accuracy for multimers compared to local AlphaFold installation, for full multimer accuracy it is highly recommended to run [AlphaFold locally](https://github.com/deepmind/alphafold#running-alphafold).** Moreover, the AlphaFold-Multimer requires searching for MSA for every unique sequence in the complex, hence it is substantially slower. If your notebook times-out due to slow multimer MSA search, we recommend running AlphaFold locally.\n",
"\n",
"Please note that this notebook is provided as an early-access prototype and is not a finished product. It is provided for theoretical modelling only and caution should be exercised in its use. \n",
"\n",
"**Citing this work**\n",
"\n",
"Any publication that discloses findings arising from using this notebook should [cite](https://github.com/deepmind/alphafold/#citing-this-work) the [AlphaFold paper](https://doi.org/10.1038/s41586-021-03819-2).\n",
"\n",
"**Licenses**\n",
"\n",
"This Colab uses the [AlphaFold model parameters](https://github.com/deepmind/alphafold/#model-parameters-license) which are subject to the Creative Commons Attribution 4.0 International ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/legalcode)) license. The Colab itself is provided under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0). See the full license statement below.\n",
"\n",
"\n",
"**More information**\n",
"\n",
"You can find more information about how AlphaFold works in the following papers:\n",
"\n",
"* [AlphaFold methods paper](https://www.nature.com/articles/s41586-021-03819-2)\n",
"* [AlphaFold predictions of the human proteome paper](https://www.nature.com/articles/s41586-021-03828-1)\n",
"* [AlphaFold-Multimer paper](https://www.biorxiv.org/content/10.1101/2021.10.04.463034v1)\n",
"\n",
"FAQ on how to interpret AlphaFold predictions are [here](https://alphafold.ebi.ac.uk/faq)."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "b7a02613eb1a"
},
"source": [
"## Download AlphaFold Data"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true,
"jupyter": {
"source_hidden": true
},
"cellView": "form",
"id": "woIxeCPygt7K"
},
"outputs": [],
"source": [
"import os\n",
"import subprocess\n",
"import sys\n",
"\n",
"import alphafold.common\n",
"import tqdm.notebook\n",
"from IPython.utils import io\n",
"\n",
"TQDM_BAR_FORMAT = (\n",
" \"{l_bar}{bar}| {n_fmt}/{total_fmt} [elapsed: {elapsed} remaining: {remaining}]\"\n",
")\n",
"\n",
"SOURCE_URL = (\n",
" \"https://storage.googleapis.com/alphafold/alphafold_params_colab_2022-01-19.tar\"\n",
")\n",
"PARAMS_DIR = \"alphafold/data/params\"\n",
"PARAMS_PATH = os.path.join(PARAMS_DIR, os.path.basename(SOURCE_URL))\n",
"ALPHAFOLD_COMMON_DIR = os.path.dirname(alphafold.common.__file__)\n",
"\n",
"try:\n",
" with tqdm.notebook.tqdm(total=100, bar_format=TQDM_BAR_FORMAT) as pbar:\n",
" with io.capture_output() as captured:\n",
"\n",
" # Download and store stereo_chemical_props.txt\n",
" !mkdir -p ~/content/alphafold/alphafold/common\n",
" !mkdir -p /opt/conda/lib/python3.7/site-packages/alphafold/common/\n",
" !wget -q -P ~/content/alphafold/alphafold/common https://git.scicore.unibas.ch/schwede/openstructure/-/raw/7102c63615b64735c4941278d92b554ec94415f8/modules/mol/alg/src/stereo_chemical_props.txt\n",
" pbar.update(18)\n",
" !cp -f ~/content/alphafold/alphafold/common/stereo_chemical_props.txt \"{ALPHAFOLD_COMMON_DIR}\"\n",
"\n",
" # Download alphafold_params_colab_2021-10-27.tar\n",
" !mkdir --parents \"{PARAMS_DIR}\"\n",
" !wget -O \"{PARAMS_PATH}\" \"{SOURCE_URL}\"\n",
" pbar.update(27)\n",
"\n",
" # Un-tar alphafold_params_colab_2021-10-27.tar\n",
" !tar --extract --verbose --file=\"{PARAMS_PATH}\" --directory=\"{PARAMS_DIR}\" --preserve-permissions\n",
" # !rm \"{PARAMS_PATH}\"\n",
" pbar.update(55)\n",
"\n",
"except subprocess.CalledProcessError:\n",
" print(captured)\n",
" raise"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "d8926b7d5529"
},
"source": [
"## Configure GPU Acceleration"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true,
"jupyter": {
"source_hidden": true
},
"cellView": "form",
"id": "VzJ5iMjTtoZw"
},
"outputs": [],
"source": [
"# Confirm accelerator configuration\n",
"import jax\n",
"\n",
"if jax.local_devices()[0].platform == \"tpu\":\n",
" raise RuntimeError(\n",
" \"TPU runtime not supported. Please configure GPU acceleration on the VM.\"\n",
" )\n",
"elif jax.local_devices()[0].platform == \"cpu\":\n",
" print(\n",
" \"CPU-only runtime is not recommended, because prediction execution will be slow. For better performance, consider GPU acceleration on the VM.\"\n",
" )\n",
"else:\n",
" print(f\"Running with {jax.local_devices()[0].device_kind} GPU\")\n",
"\n",
"# Make sure all necessary environment variables are set.\n",
"import os\n",
"\n",
"os.environ[\"TF_FORCE_UNIFIED_MEMORY\"] = \"1\"\n",
"os.environ[\"XLA_PYTHON_CLIENT_MEM_FRACTION\"] = \"2.0\""
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "W4JpOs6oA-QS"
},
"source": [
"## Making a prediction\n",
"\n",
"Please paste the sequence of your protein in the text box below, then run the remaining cells via _Run_ > _Run Selected Cell and All Below_. You can also run the cells individually by pressing the _Play_ button on the left.\n",
"\n",
"Note that the search against databases and the actual prediction can take some time, from minutes to hours, depending on the length of the protein and what type of GPU you allocate (see FAQ below).\n",
"\n",
"To start, enter the amino acid sequence(s) to fold ⬇️\n",
"\n",
"If you enter only a single sequence, the monomer model will be used. If you enter multiple sequences, the multimer model will be used."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "b310d44229d0"
},
"outputs": [],
"source": [
"# Input sequences (type: str)\n",
"sequence_1 = \"MAAHKGAEHHHKAAEHHEQAAKHHHAAAEHHEKGEHEQAAHHADTAYAHHKHAEEHAAQAAKHDAEHHAPKPH\"\n",
"sequence_2 = \"\"\n",
"sequence_3 = \"\"\n",
"sequence_4 = \"\"\n",
"sequence_5 = \"\"\n",
"sequence_6 = \"\"\n",
"sequence_7 = \"\"\n",
"sequence_8 = \"\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true,
"jupyter": {
"source_hidden": true
},
"cellView": "form",
"id": "rowN0bVYLe9n"
},
"outputs": [],
"source": [
"from alphafold.notebooks import notebook_utils\n",
"\n",
"input_sequences = (\n",
" sequence_1,\n",
" sequence_2,\n",
" sequence_3,\n",
" sequence_4,\n",
" sequence_5,\n",
" sequence_6,\n",
" sequence_7,\n",
" sequence_8,\n",
")\n",
"\n",
"# If folding a complex target and all the input sequences are\n",
"# prokaryotic then set `is_prokaryotic` to `True`. Set to `False`\n",
"# otherwise or if the origin is unknown.\n",
"\n",
"is_prokaryote = False # @param {type:\"boolean\"}\n",
"\n",
"MIN_SINGLE_SEQUENCE_LENGTH = 16\n",
"MAX_SINGLE_SEQUENCE_LENGTH = 2500\n",
"MAX_MULTIMER_LENGTH = 2500\n",
"\n",
"# Validate the input.\n",
"sequences, model_type_to_use = notebook_utils.validate_input(\n",
" input_sequences=input_sequences,\n",
" min_length=MIN_SINGLE_SEQUENCE_LENGTH,\n",
" max_length=MAX_SINGLE_SEQUENCE_LENGTH,\n",
" max_multimer_length=MAX_MULTIMER_LENGTH,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "db551d4877ea"
},
"source": [
"## Search against genetic databases\n",
"\n",
"Once this cell has been executed, you will see statistics about the multiple sequence alignment (MSA) that will be used by AlphaFold. In particular, you’ll see how well each residue is covered by similar sequences in the MSA."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true,
"jupyter": {
"source_hidden": true
},
"cellView": "form",
"id": "2tTeTTsLKPjB"
},
"outputs": [],
"source": [
"import collections\n",
"import copy\n",
"import random\n",
"from concurrent import futures\n",
"from urllib import request\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import py3Dmol\n",
"from alphafold.common import protein\n",
"from alphafold.data import (feature_processing, msa_pairing, pipeline,\n",
" pipeline_multimer)\n",
"from alphafold.data.tools import jackhmmer\n",
"from alphafold.model import config, data, model\n",
"from alphafold.relax import relax, utils\n",
"from IPython import display\n",
"from ipywidgets import GridspecLayout, Output\n",
"\n",
"# Color bands for visualizing plddt\n",
"PLDDT_BANDS = [\n",
" (0, 50, \"#FF7D45\"),\n",
" (50, 70, \"#FFDB13\"),\n",
" (70, 90, \"#65CBF3\"),\n",
" (90, 100, \"#0053D6\"),\n",
"]\n",
"\n",
"# --- Find the closest source ---\n",
"test_url_pattern = (\n",
" \"https://storage.googleapis.com/alphafold-colab{:s}/latest/uniref90_2021_03.fasta.1\"\n",
")\n",
"ex = futures.ThreadPoolExecutor(3)\n",
"\n",
"\n",
"def fetch(source):\n",
" request.urlretrieve(test_url_pattern.format(source))\n",
" return source\n",
"\n",
"\n",
"fs = [ex.submit(fetch, source) for source in [\"\", \"-europe\", \"-asia\"]]\n",
"source = None\n",
"for f in futures.as_completed(fs):\n",
" source = f.result()\n",
" ex.shutdown()\n",
" break\n",
"\n",
"JACKHMMER_BINARY_PATH = \"/usr/bin/jackhmmer\"\n",
"DB_ROOT_PATH = f\"https://storage.googleapis.com/alphafold-colab{source}/latest/\"\n",
"# The z_value is the number of sequences in a database.\n",
"MSA_DATABASES = [\n",
" {\n",
" \"db_name\": \"uniref90\",\n",
" \"db_path\": f\"{DB_ROOT_PATH}uniref90_2021_03.fasta\",\n",
" \"num_streamed_chunks\": 59,\n",
" \"z_value\": 135_301_051,\n",
" },\n",
" {\n",
" \"db_name\": \"smallbfd\",\n",
" \"db_path\": f\"{DB_ROOT_PATH}bfd-first_non_consensus_sequences.fasta\",\n",
" \"num_streamed_chunks\": 17,\n",
" \"z_value\": 65_984_053,\n",
" },\n",
" {\n",
" \"db_name\": \"mgnify\",\n",
" \"db_path\": f\"{DB_ROOT_PATH}mgy_clusters_2019_05.fasta\",\n",
" \"num_streamed_chunks\": 71,\n",
" \"z_value\": 304_820_129,\n",
" },\n",
"]\n",
"\n",
"# Search UniProt and construct the all_seq features only for heteromers, not homomers.\n",
"if model_type_to_use == notebook_utils.ModelType.MULTIMER and len(set(sequences)) > 1:\n",
" MSA_DATABASES.extend(\n",
" [\n",
" # Swiss-Prot and TrEMBL are concatenated together as UniProt.\n",
" {\n",
" \"db_name\": \"uniprot\",\n",
" \"db_path\": f\"{DB_ROOT_PATH}uniprot_2021_03.fasta\",\n",
" \"num_streamed_chunks\": 98,\n",
" \"z_value\": 219_174_961 + 565_254,\n",
" },\n",
" ]\n",
" )\n",
"\n",
"TOTAL_JACKHMMER_CHUNKS = sum(cfg[\"num_streamed_chunks\"] for cfg in MSA_DATABASES)\n",
"\n",
"MAX_HITS = {\n",
" \"uniref90\": 10_000,\n",
" \"smallbfd\": 5_000,\n",
" \"mgnify\": 501,\n",
" \"uniprot\": 50_000,\n",
"}\n",
"\n",
"\n",
"def get_msa(fasta_path):\n",
" \"\"\"Searches for MSA for the given sequence using chunked Jackhmmer search.\"\"\"\n",
"\n",
" # Run the search against chunks of genetic databases.\n",
" raw_msa_results = collections.defaultdict(list)\n",
" with tqdm.notebook.tqdm(\n",
" total=TOTAL_JACKHMMER_CHUNKS, bar_format=TQDM_BAR_FORMAT\n",
" ) as pbar:\n",
"\n",
" def jackhmmer_chunk_callback(i):\n",
" pbar.update(n=1)\n",
"\n",
" for db_config in MSA_DATABASES:\n",
" db_name = db_config[\"db_name\"]\n",
" pbar.set_description(f\"Searching {db_name}\")\n",
" jackhmmer_runner = jackhmmer.Jackhmmer(\n",
" binary_path=JACKHMMER_BINARY_PATH,\n",
" database_path=db_config[\"db_path\"],\n",
" get_tblout=True,\n",
" num_streamed_chunks=db_config[\"num_streamed_chunks\"],\n",
" streaming_callback=jackhmmer_chunk_callback,\n",
" z_value=db_config[\"z_value\"],\n",
" )\n",
" # Group the results by database name.\n",
" raw_msa_results[db_name].extend(jackhmmer_runner.query(fasta_path))\n",
"\n",
" return raw_msa_results\n",
"\n",
"\n",
"features_for_chain = {}\n",
"raw_msa_results_for_sequence = {}\n",
"for sequence_index, sequence in enumerate(sequences, start=1):\n",
" print(f\"\\nGetting MSA for sequence {sequence_index}\")\n",
"\n",
" fasta_path = f\"target_{sequence_index}.fasta\"\n",
" with open(fasta_path, \"wt\") as f:\n",
" f.write(f\">query\\n{sequence}\")\n",
"\n",
" # Don't do redundant work for multiple copies of the same chain in the multimer.\n",
" if sequence not in raw_msa_results_for_sequence:\n",
" raw_msa_results = get_msa(fasta_path=fasta_path)\n",
" raw_msa_results_for_sequence[sequence] = raw_msa_results\n",
" else:\n",
" raw_msa_results = copy.deepcopy(raw_msa_results_for_sequence[sequence])\n",
"\n",
" # Extract the MSAs from the Stockholm files.\n",
" # NB: deduplication happens later in pipeline.make_msa_features.\n",
" single_chain_msas = []\n",
" uniprot_msa = None\n",
" for db_name, db_results in raw_msa_results.items():\n",
" merged_msa = notebook_utils.merge_chunked_msa(\n",
" results=db_results, max_hits=MAX_HITS.get(db_name)\n",
" )\n",
" if merged_msa.sequences and db_name != \"uniprot\":\n",
" single_chain_msas.append(merged_msa)\n",
" msa_size = len(set(merged_msa.sequences))\n",
" print(\n",
" f\"{msa_size} unique sequences found in {db_name} for sequence {sequence_index}\"\n",
" )\n",
" elif merged_msa.sequences and db_name == \"uniprot\":\n",
" uniprot_msa = merged_msa\n",
"\n",
" notebook_utils.show_msa_info(\n",
" single_chain_msas=single_chain_msas, sequence_index=sequence_index\n",
" )\n",
"\n",
" # Turn the raw data into model features.\n",
" feature_dict = {}\n",
" feature_dict.update(\n",
" pipeline.make_sequence_features(\n",
" sequence=sequence, description=\"query\", num_res=len(sequence)\n",
" )\n",
" )\n",
" feature_dict.update(pipeline.make_msa_features(msas=single_chain_msas))\n",
" # We don't use templates in AlphaFold notebook, add only empty placeholder features.\n",
" feature_dict.update(\n",
" notebook_utils.empty_placeholder_template_features(\n",
" num_templates=0, num_res=len(sequence)\n",
" )\n",
" )\n",
"\n",
" # Construct the all_seq features only for heteromers, not homomers.\n",
" if (\n",
" model_type_to_use == notebook_utils.ModelType.MULTIMER\n",
" and len(set(sequences)) > 1\n",
" ):\n",
" valid_feats = msa_pairing.MSA_FEATURES + (\n",
" \"msa_uniprot_accession_identifiers\",\n",
" \"msa_species_identifiers\",\n",
" )\n",
" all_seq_features = {\n",
" f\"{k}_all_seq\": v\n",
" for k, v in pipeline.make_msa_features([uniprot_msa]).items()\n",
" if k in valid_feats\n",
" }\n",
" feature_dict.update(all_seq_features)\n",
"\n",
" features_for_chain[protein.PDB_CHAIN_IDS[sequence_index - 1]] = feature_dict\n",
"\n",
"\n",
"# Do further feature post-processing depending on the model type.\n",
"if model_type_to_use == notebook_utils.ModelType.MONOMER:\n",
" np_example = features_for_chain[protein.PDB_CHAIN_IDS[0]]\n",
"\n",
"elif model_type_to_use == notebook_utils.ModelType.MULTIMER:\n",
" all_chain_features = {}\n",
" for chain_id, chain_features in features_for_chain.items():\n",
" all_chain_features[chain_id] = pipeline_multimer.convert_monomer_features(\n",
" chain_features, chain_id\n",
" )\n",
"\n",
" all_chain_features = pipeline_multimer.add_assembly_features(all_chain_features)\n",
"\n",
" np_example = feature_processing.pair_and_merge(\n",
" all_chain_features=all_chain_features, is_prokaryote=is_prokaryote\n",
" )\n",
"\n",
" # Pad MSA to avoid zero-sized extra_msa.\n",
" np_example = pipeline_multimer.pad_msa(np_example, min_num_seq=512)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "9640643486bd"
},
"source": [
"## Run AlphaFold\n",
"\n",
"Once this cell has been executed, a zip-archive \"prediction.zip\" with the obtained prediction will be saved on the VM, and available for download to your computer in the sidebar. In case you are having issues with the relaxation stage, you can disable it below. Warning: This means that the prediction might have distracting small stereochemical violations."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true,
"jupyter": {
"source_hidden": true
},
"cellView": "form",
"id": "XUo6foMQxwS2"
},
"outputs": [],
"source": [
"run_relax = True\n",
"\n",
"# --- Run the model ---\n",
"if model_type_to_use == notebook_utils.ModelType.MONOMER:\n",
" model_names = config.MODEL_PRESETS[\"monomer\"] + (\"model_2_ptm\",)\n",
"elif model_type_to_use == notebook_utils.ModelType.MULTIMER:\n",
" model_names = config.MODEL_PRESETS[\"multimer\"]\n",
"\n",
"output_dir = \"prediction\"\n",
"os.makedirs(output_dir, exist_ok=True)\n",
"\n",
"plddts = {}\n",
"ranking_confidences = {}\n",
"pae_outputs = {}\n",
"unrelaxed_proteins = {}\n",
"\n",
"with tqdm.notebook.tqdm(total=len(model_names) + 1, bar_format=TQDM_BAR_FORMAT) as pbar:\n",
" for model_name in model_names:\n",
" pbar.set_description(f\"Running {model_name}\")\n",
"\n",
" cfg = config.model_config(model_name)\n",
" if model_type_to_use == notebook_utils.ModelType.MONOMER:\n",
" cfg.data.eval.num_ensemble = 1\n",
" elif model_type_to_use == notebook_utils.ModelType.MULTIMER:\n",
" cfg.model.num_ensemble_eval = 1\n",
" params = data.get_model_haiku_params(model_name, \"./alphafold/data\")\n",
" model_runner = model.RunModel(cfg, params)\n",
" processed_feature_dict = model_runner.process_features(\n",
" np_example, random_seed=0\n",
" )\n",
" prediction = model_runner.predict(\n",
" processed_feature_dict, random_seed=random.randrange(sys.maxsize)\n",
" )\n",
"\n",
" mean_plddt = prediction[\"plddt\"].mean()\n",
"\n",
" if model_type_to_use == notebook_utils.ModelType.MONOMER:\n",
" if \"predicted_aligned_error\" in prediction:\n",
" pae_outputs[model_name] = (\n",
" prediction[\"predicted_aligned_error\"],\n",
" prediction[\"max_predicted_aligned_error\"],\n",
" )\n",
" else:\n",
" # Monomer models are sorted by mean pLDDT. Do not put monomer pTM models here as they\n",
" # should never get selected.\n",
" ranking_confidences[model_name] = prediction[\"ranking_confidence\"]\n",
" plddts[model_name] = prediction[\"plddt\"]\n",
" elif model_type_to_use == notebook_utils.ModelType.MULTIMER:\n",
" # Multimer models are sorted by pTM+ipTM.\n",
" ranking_confidences[model_name] = prediction[\"ranking_confidence\"]\n",
" plddts[model_name] = prediction[\"plddt\"]\n",
" pae_outputs[model_name] = (\n",
" prediction[\"predicted_aligned_error\"],\n",
" prediction[\"max_predicted_aligned_error\"],\n",
" )\n",
"\n",
" # Set the b-factors to the per-residue plddt.\n",
" final_atom_mask = prediction[\"structure_module\"][\"final_atom_mask\"]\n",
" b_factors = prediction[\"plddt\"][:, None] * final_atom_mask\n",
" unrelaxed_protein = protein.from_prediction(\n",
" processed_feature_dict,\n",
" prediction,\n",
" b_factors=b_factors,\n",
" remove_leading_feature_dimension=(\n",
" model_type_to_use == notebook_utils.ModelType.MONOMER\n",
" ),\n",
" )\n",
" unrelaxed_proteins[model_name] = unrelaxed_protein\n",
"\n",
" # Delete unused outputs to save memory.\n",
" del model_runner\n",
" del params\n",
" del prediction\n",
" pbar.update(n=1)\n",
"\n",
" # --- AMBER relax the best model ---\n",
"\n",
" # Find the best model according to the mean pLDDT.\n",
" best_model_name = max(\n",
" ranking_confidences.keys(), key=lambda x: ranking_confidences[x]\n",
" )\n",
"\n",
" if run_relax:\n",
" pbar.set_description(\"AMBER relaxation\")\n",
" amber_relaxer = relax.AmberRelaxation(\n",
" max_iterations=0,\n",
" tolerance=2.39,\n",
" stiffness=10.0,\n",
" exclude_residues=[],\n",
" max_outer_iterations=3,\n",
" )\n",
" relaxed_pdb, _, _ = amber_relaxer.process(\n",
" prot=unrelaxed_proteins[best_model_name]\n",
" )\n",
" else:\n",
" print(\"Warning: Running without the relaxation stage.\")\n",
" relaxed_pdb = protein.to_pdb(unrelaxed_proteins[best_model_name])\n",
" pbar.update(n=1) # Finished AMBER relax.\n",
"\n",
"# Construct multiclass b-factors to indicate confidence bands\n",
"# 0=very low, 1=low, 2=confident, 3=very high\n",
"banded_b_factors = []\n",
"for plddt in plddts[best_model_name]:\n",
" for idx, (min_val, max_val, _) in enumerate(PLDDT_BANDS):\n",
" if plddt >= min_val and plddt <= max_val:\n",
" banded_b_factors.append(idx)\n",
" break\n",
"banded_b_factors = np.array(banded_b_factors)[:, None] * final_atom_mask\n",
"to_visualize_pdb = utils.overwrite_b_factors(relaxed_pdb, banded_b_factors)\n",
"\n",
"\n",
"# Write out the prediction\n",
"pred_output_path = os.path.join(output_dir, \"selected_prediction.pdb\")\n",
"with open(pred_output_path, \"w\") as f:\n",
" f.write(relaxed_pdb)\n",
"\n",
"\n",
"# --- Visualise the prediction & confidence ---\n",
"show_sidechains = True\n",
"\n",
"\n",
"def plot_plddt_legend():\n",
" \"\"\"Plots the legend for pLDDT.\"\"\"\n",
" thresh = [\n",
" \"Very low (pLDDT < 50)\",\n",
" \"Low (70 > pLDDT > 50)\",\n",
" \"Confident (90 > pLDDT > 70)\",\n",
" \"Very high (pLDDT > 90)\",\n",
" ]\n",
"\n",
" colors = [x[2] for x in PLDDT_BANDS]\n",
"\n",
" plt.figure(figsize=(2, 2))\n",
" for c in colors:\n",
" plt.bar(0, 0, color=c)\n",
" plt.legend(thresh, frameon=False, loc=\"center\", fontsize=20)\n",
" plt.xticks([])\n",
" plt.yticks([])\n",
" ax = plt.gca()\n",
" ax.spines[\"right\"].set_visible(False)\n",
" ax.spines[\"top\"].set_visible(False)\n",
" ax.spines[\"left\"].set_visible(False)\n",
" ax.spines[\"bottom\"].set_visible(False)\n",
" plt.title(\"Model Confidence\", fontsize=20, pad=20)\n",
" return plt\n",
"\n",
"\n",
"# Show the structure coloured by chain if the multimer model has been used.\n",
"if model_type_to_use == notebook_utils.ModelType.MULTIMER:\n",
" multichain_view = py3Dmol.view(width=800, height=600)\n",
" multichain_view.addModelsAsFrames(to_visualize_pdb)\n",
" multichain_style = {\"cartoon\": {\"colorscheme\": \"chain\"}}\n",
" multichain_view.setStyle({\"model\": -1}, multichain_style)\n",
" multichain_view.zoomTo()\n",
" multichain_view.show()\n",
"\n",
"# Color the structure by per-residue pLDDT\n",
"color_map = {i: bands[2] for i, bands in enumerate(PLDDT_BANDS)}\n",
"view = py3Dmol.view(width=800, height=600)\n",
"view.addModelsAsFrames(to_visualize_pdb)\n",
"style = {\"cartoon\": {\"colorscheme\": {\"prop\": \"b\", \"map\": color_map}}}\n",
"if show_sidechains:\n",
" style[\"stick\"] = {}\n",
"view.setStyle({\"model\": -1}, style)\n",
"view.zoomTo()\n",
"\n",
"grid = GridspecLayout(1, 2)\n",
"out = Output()\n",
"with out:\n",
" view.show()\n",
"grid[0, 0] = out\n",
"\n",
"out = Output()\n",
"with out:\n",
" plot_plddt_legend().show()\n",
"grid[0, 1] = out\n",
"\n",
"display.display(grid)\n",
"\n",
"# Display pLDDT and predicted aligned error (if output by the model).\n",
"if pae_outputs:\n",
" num_plots = 2\n",
"else:\n",
" num_plots = 1\n",
"\n",
"plt.figure(figsize=[8 * num_plots, 6])\n",
"plt.subplot(1, num_plots, 1)\n",
"plt.plot(plddts[best_model_name])\n",
"plt.title(\"Predicted LDDT\")\n",
"plt.xlabel(\"Residue\")\n",
"plt.ylabel(\"pLDDT\")\n",
"\n",
"if num_plots == 2:\n",
" plt.subplot(1, 2, 2)\n",
" pae, max_pae = list(pae_outputs.values())[0]\n",
" plt.imshow(pae, vmin=0.0, vmax=max_pae, cmap=\"Greens_r\")\n",
" plt.colorbar(fraction=0.046, pad=0.04)\n",
"\n",
" # Display lines at chain boundaries.\n",
" best_unrelaxed_prot = unrelaxed_proteins[best_model_name]\n",
" total_num_res = best_unrelaxed_prot.residue_index.shape[-1]\n",
" chain_ids = best_unrelaxed_prot.chain_index\n",
" for chain_boundary in np.nonzero(chain_ids[:-1] - chain_ids[1:]):\n",
" if chain_boundary.size:\n",
" plt.plot([0, total_num_res], [chain_boundary, chain_boundary], color=\"red\")\n",
" plt.plot([chain_boundary, chain_boundary], [0, total_num_res], color=\"red\")\n",
"\n",
" plt.title(\"Predicted Aligned Error\")\n",
" plt.xlabel(\"Scored residue\")\n",
" plt.ylabel(\"Aligned residue\")\n",
"\n",
"# Save the predicted aligned error (if it exists).\n",
"pae_output_path = os.path.join(output_dir, \"predicted_aligned_error.json\")\n",
"if pae_outputs:\n",
" # Save predicted aligned error in the same format as the AF EMBL DB.\n",
" pae_data = notebook_utils.get_pae_json(pae=pae, max_pae=max_pae.item())\n",
" with open(pae_output_path, \"w\") as f:\n",
" f.write(pae_data)\n",
"\n",
"!zip -q -r {output_dir}.zip {output_dir}"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "lUQAn5LYC5n4"
},
"source": [
"### Interpreting the prediction\n",
"\n",
"In general predicted LDDT (pLDDT) is best used for intra-domain confidence, whereas Predicted Aligned Error (PAE) is best used for determining between domain or between chain confidence.\n",
"\n",
"Please see the [AlphaFold methods paper](https://www.nature.com/articles/s41586-021-03819-2), the [AlphaFold predictions of the human proteome paper](https://www.nature.com/articles/s41586-021-03828-1), and the [AlphaFold-Multimer paper](https://www.biorxiv.org/content/10.1101/2021.10.04.463034v1) as well as [our FAQ](https://alphafold.ebi.ac.uk/faq) on how to interpret AlphaFold predictions."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "jeb2z8DIA4om"
},
"source": [
"## FAQ & Troubleshooting\n",
"\n",
"\n",
"* How do I get a predicted protein structure for my protein?\n",
" * Connect the notebook to the Jupyter kernel \"Python 3 (ipykernel)\".\n",
" * Paste the amino acid sequence of your protein (without any headers) into the variable sequence_1 in \"Making a Prediction\".\n",
" * Run all cells in the notebook, either by running them individually or via \"Kernel\"/\"Restart Kernel and Run All Cells...\"\n",
" * The predicted protein structure will be downloaded once all cells have been executed. Note: This can take minutes to hours - see below.\n",
"* How long will this take?\n",
" * The search against genetic databases can take minutes to hours.\n",
" * Running AlphaFold and generating the prediction can take minutes to hours, depending on the length of your protein and on which GPU-type your VM has access to.\n",
"* My notebook no longer seems to be doing anything, what should I do?\n",
" * Some steps may take minutes to hours to complete.\n",
" * If nothing happens or if you receive an error message, try restarting your notebook runtime via \"Kernel\"/\"Restart Kernel and Run All Cells...\".\n",
" * If this doesn’t help, try resetting restarting your VM inside the GCloud Console (\"Compute Engine\"/\"VM Instances\").\n",
"* How does this compare to the open-source version of AlphaFold?\n",
" * This notebook version of AlphaFold searches a selected portion of the BFD dataset and currently doesn’t use templates, so its accuracy is reduced in comparison to the full version of AlphaFold that is described in the [AlphaFold paper](https://doi.org/10.1038/s41586-021-03819-2) and [Github repo](https://github.com/deepmind/alphafold/) (the full version is available via the inference script).\n",
"* I received a warning “Notebook requires high RAM”, what do I do?\n",
" * In the \"Compute Engine\"/\"VM Instances\" Console menu, you can reconfigure the host VM settings. See [Changing the machine type of a VM instance](https://cloud.google.com/compute/docs/instances/changing-machine-type-of-stopped-instance) for instructions.\n",
"* Does this tool install anything on my computer?\n",
" * No, everything happens in the VM instance within your Google Cloud project.\n",
"* How should I share feedback and bug reports?\n",
" * Please share any feedback and bug reports as an [issue](https://github.com/GoogleCloudPlatform/vertex-ai-samples/issues) on Github.\n",
"\n",
"\n",
"## Related work\n",
"\n",
"Take a look at these Colab notebooks provided by the community (please note that these notebooks may vary from our validated AlphaFold system and we cannot guarantee their accuracy):\n",
"\n",
"* The [ColabFold AlphaFold2 notebook](https://colab.research.google.com/github/sokrypton/ColabFold/blob/main/AlphaFold2.ipynb) by Sergey Ovchinnikov, Milot Mirdita and Martin Steinegger, which uses an API hosted at the Södinglab based on the MMseqs2 server ([Mirdita et al. 2019, Bioinformatics](https://academic.oup.com/bioinformatics/article/35/16/2856/5280135)) for the multiple sequence alignment creation.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "YfPhvYgKC81B"
},
"source": [
"# License and Disclaimer\n",
"\n",
"This is not an officially-supported Google product.\n",
"\n",
"This notebook and other information provided is for theoretical modelling only, caution should be exercised in its use. It is provided ‘as-is’ without any warranty of any kind, whether expressed or implied. Information is not intended to be a substitute for professional medical advice, diagnosis, or treatment, and does not constitute medical or other professional advice.\n",
"\n",
"Copyright 2021 DeepMind Technologies Limited.\n",
"\n",
"\n",
"## AlphaFold Code License\n",
"\n",
"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.\n",
"\n",
"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.\n",
"\n",
"## Model Parameters License\n",
"\n",
"The AlphaFold parameters are made available under the terms of the Creative Commons Attribution 4.0 International (CC BY 4.0) license. You can find details at: https://creativecommons.org/licenses/by/4.0/legalcode\n",
"\n",
"\n",
"## Third-party software\n",
"\n",
"Use of the third-party software, libraries or code referred to in the [Acknowledgements section](https://github.com/deepmind/alphafold/#acknowledgements) in the AlphaFold README may be governed by separate terms and conditions or license provisions. Your use of the third-party software, libraries or code is subject to any such terms and you should check that you can comply with any applicable restrictions or terms and conditions before use.\n",
"\n",
"\n",
"## Mirrored Databases\n",
"\n",
"The following databases have been mirrored by DeepMind, and are available with reference to the following:\n",
"* UniProt: v2021\\_03 (unmodified), by The UniProt Consortium, available under a [Creative Commons Attribution-NoDerivatives 4.0 International License](http://creativecommons.org/licenses/by-nd/4.0/).\n",
"* UniRef90: v2021\\_03 (unmodified), by The UniProt Consortium, available under a [Creative Commons Attribution-NoDerivatives 4.0 International License](http://creativecommons.org/licenses/by-nd/4.0/).\n",
"* MGnify: v2019\\_05 (unmodified), by Mitchell AL et al., available free of all copyright restrictions and made fully and freely available for both non-commercial and commercial use under [CC0 1.0 Universal (CC0 1.0) Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/).\n",
"* BFD: (modified), by Steinegger M. and Söding J., modified by DeepMind, available under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by/4.0/). See the Methods section of the [AlphaFold proteome paper](https://www.nature.com/articles/s41586-021-03828-1) for details."
]
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"collapsed_sections": [],
"name": "AlphaFold.ipynb",
"toc_visible": true
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
@@ -1,82 +0,0 @@
# Copyright 2022 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
#
# http://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.
ARG CUDA_MAJOR=11
ARG CUDA_MINOR=0
FROM gcr.io/deeplearning-platform-release/base-cu110
ARG CUDA_MAJOR
ARG CUDA_MINOR
SHELL ["/bin/bash", "-c"]
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
build-essential \
cmake \
cuda-command-line-tools-${CUDA_MAJOR}-${CUDA_MINOR} \
git \
hmmer \
kalign \
tzdata \
wget \
&& rm -rf /var/lib/apt/lists/*
# Compile HHsuite from source.
RUN git clone --branch v3.3.0 https://github.com/soedinglab/hh-suite.git /tmp/hh-suite \
&& mkdir /tmp/hh-suite/build \
&& pushd /tmp/hh-suite/build \
&& cmake -DCMAKE_INSTALL_PREFIX=/opt/hhsuite .. \
&& make -j 4 && make install \
&& ln -s /opt/hhsuite/bin/* /usr/bin \
&& popd \
&& rm -rf /tmp/hh-suite
ENV PATH="/opt/conda/bin:$PATH"
RUN conda update -qy conda \
&& conda install -y -c conda-forge \
openmm=7.5.1 \
cudatoolkit==${CUDA_VERSION} \
pdbfixer \
pip \
python=3.7
COPY . /app/alphafold
# Install pip packages.
RUN pip3 install --upgrade pip \
&& pip3 install -r /app/alphafold/requirements.txt \
&& pip3 install py3Dmol tqdm \
&& pip3 install --upgrade jax==0.2.14 jaxlib==0.1.69+cuda${CUDA_MAJOR}${CUDA_MINOR} -f \
https://storage.googleapis.com/jax-releases/jax_releases.html
# Install alphafold.
WORKDIR /app/alphafold
RUN python setup.py install
# Apply OpenMM patch.
WORKDIR /opt/conda/lib/python3.7/site-packages
RUN patch -p0 < /app/alphafold/docker/openmm.patch
# Creating a tmp location for jackhmmr; not mounting through to host though.
RUN sudo mkdir -m 777 --parents /tmp/ramdisk
# We need to run `ldconfig` first to ensure GPUs are visible, due to some quirk
# with Debian. See https://github.com/NVIDIA/nvidia-docker/issues/1399 for
# details.
# ENTRYPOINT does not support easily running multiple commands, so instead we
# write a shell script to wrap them up.
WORKDIR /home/jupyter
RUN echo '#!/bin/bash\nldconfig\n\'
@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -e
# Prod (Publicly viewable)
PROJECT=cloud-devrel-public-resources
REPOSITORY=alphafold
LOCAL_IMAGE=alphafold-on-gcp
REMOTE_IMAGE=${LOCAL_IMAGE?}
TAG=latest
REGISTRY="us-west1-docker.pkg.dev/${PROJECT?}/${REPOSITORY?}/${REMOTE_IMAGE?}:${TAG?}"
git clone https://github.com/deepmind/alphafold.git
cp Dockerfile alphafold/docker/Dockerfile
cp AlphaFold.ipynb alphafold/notebooks/AlphaFold.ipynb
cd alphafold && sudo docker build --tag ${LOCAL_IMAGE?}:${TAG?} -f docker/Dockerfile .
sudo docker tag ${LOCAL_IMAGE?}:${TAG?} ${REGISTRY?}
sudo docker push ${REGISTRY?}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

@@ -1,5 +0,0 @@
testdata/*
build.py
test.py
state_dict.pth
config.json
@@ -1,6 +0,0 @@
cpr_model_server.py
entrypoint.py
state_dict.pth
config.json
**/__pycache__
!testdata/**
@@ -1,110 +0,0 @@
# CPR Example: PyTorch Image Models (timm)
## About CPR
CPR ([custom prediction routines](https://github.com/googleapis/python-aiplatform/blob/main/google/cloud/aiplatform/prediction/README.md)) is a framework designed by Google Cloud developers to make it easier to combine machine learning models with custom preprocessing and postprocessing logic in a real-time serving application.
## Using this example
This code is a self-contained example of a custom model server project built using CPR.
As is, you can use it to serve the ViT-Small image classification model from Ross Wightman's [`timm`](https://github.com/rwightman/pytorch-image-models) library of image model implementations in PyTorch. Both CPU and GPU are supported.
You can also consider using the code here as a template for your own CPR project if you want to use a different model from `timm`, a different PyTorch model, or an entirely different framework.
### Requirements
In order to use this example, you'll need Docker and Python 3 installed on your system.
To get started, first create a virtual environment in an empty directory:
```sh
mkdir cpr-example
python3 -m venv cpr-example
cd cpr-example && source bin/activate
```
Then, clone the [vertex-ai-samples repo](https://github.com/GoogleCloudPlatform/vertex-ai-samples) in that directory:
```sh
git clone https://github.com/GoogleCloudPlatform/vertex-ai-samples.git
cd vertex-ai-samples/community-content/cpr-examples/timm_serving
```
Finally, install the Python modules required to build and run the model server:
```sh
pip install -r requirements.txt
```
### Auth
This example uses Google Cloud Storage for hosting model artifacts and Artifact Registry to store the container image.
You'll need to authorize yourself before you can interact with these.
First, log in to GCP with application default credentials:
```sh
gcloud auth application-default login
```
Next, if you haven't done so already, set up the [gcloud credential helper](https://cloud.google.com/artifact-registry/docs/docker/authentication)
for the Artifact Registry region where you intend to host the image.
```
gcloud auth configure-docker <region>-docker.pkg.dev
```
### Predictor
The `TimmPredictor` class in `timm_serving/predictor.py` implements most of the important logic for the server.
- `load(artifacts_dir)`: The predictor's `load` method is called when the server starts up in order to set up the predictor, usually by loading model weights and any artifacts needed for preprocessing and postprocessing. In this example, we initialize the saved model from the `state_dict.pth` file located inside the `artifacts_dir` folder and create the preprocessing transform from the model config.
- `preprocess`, `predict`, `postprocess`: These methods are applied in sequence to the deserialized JSON data from each request.
- `preprocess` decodes images from base64 and apply cropping, scaling and normalizing transforms.
- `predict` runs the ViT-Small model on the preprocessed images and returns class scores.
- `postprocess` finds the top five classes and packs the class names, probabilities, and indices in a serializable result.
### Building the container
To build the model server locally, run the build command:
```sh
python build.py build
```
You can edit configuration values such as the model server's base image, the name and tag assigned to the image, and the path where model weights are stored locally.
When you run the build command, model weights are downloaded and the model server container is built.
### Running local tests
`test.py` contains a suite of unit tests for the predictor as well as end-to-end tests for the model server.
To run the tests:
```sh
python test.py
```
All of the test images are public domain.
- [Cat](https://commons.wikimedia.org/wiki/File:Stray_cat_on_wall.jpg)
- [Airplane](https://commons.wikimedia.org/wiki/File:Airplanes_jets.jpg)
- The infamous [mandrill](https://commons.wikimedia.org/wiki/File:Wikipedia-sipi-image-db-mandrill-4.2.03.png)
### Deploying to Vertex AI
Before uploading or deploying the container, you'll need to modify `config.py` to set appropriate values for:
- `project_id`: Your GCP project id.
- `region`: Region where the model will be uploaded and deployed.
- `repository`: [Artifact Registry repository](https://cloud.google.com/artifact-registry/docs/repositories/create-repos) in your project where the container image will be uploaded.
- `artifacts_gcs_dir`: Folder in a [Google Cloud Storage bucket](https://cloud.google.com/storage/docs/creating-buckets) where the model weights will be uploaded.
Once this is done, first upload the model:
```sh
python build.py upload
```
Then deploy it:
```sh
python build.py deploy
```
If you run the deploy command again, it will create a new endpoint. If you want to undeploy the model, you can do so using the Vertex AI dashboard on the Google Cloud console, or use `gcloud ai endpoints undeploy` from the command line.
After deploying successfully, you can run `python build.py probe` to send a sample request to the deployed model.
@@ -1,117 +0,0 @@
# Copyright 2022 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.
"""Build the model server container."""
import json
import logging
import os
import pathlib
from typing import Sequence
from absl import app
from absl import logging
from config import CPRConfig
from google.cloud import aiplatform
from google.cloud.aiplatform import prediction as cpr
import smart_open
import timm
from timm_serving import predictor
import torch
def build_container(config: CPRConfig, tag: str) -> cpr.LocalModel:
"""Build the model server container.
Args:
tag: Output image tag.
Returns:
LocalModel exposing the built model server.
"""
return cpr.LocalModel.build_cpr_model(
src_dir=os.path.join(os.getcwd()),
output_image_uri=tag,
base_image=config.base_image,
predictor=predictor.TimmPredictor,
requirements_path=os.path.join(os.getcwd(), "requirements.txt"),
)
def save_model_artifact(destination: str) -> None:
"""Save a copy of the model state dict."""
model = timm.create_model(predictor.TimmPredictor.TIMM_MODEL_NAME, pretrained=True)
dest_file = os.path.join(destination, predictor.TimmPredictor.WEIGHTS_FILE)
with smart_open.open(dest_file, "wb") as f:
torch.save(model, f)
logging.info("Saved model to %s", dest_file)
logging.info("%s parameters", sum(p.numel() for p in model.parameters()))
def upload_model(config: CPRConfig) -> aiplatform.Model:
"""Tag and upload the model server."""
ar_tag = (
f"{config.region}-docker.pkg.dev/{config.project_id}"
f"/{config.repository}/{config.image}"
)
local_model = build_container(config, tag=ar_tag)
aiplatform.init(project=config.project_id, location=config.region)
local_model.push_image()
aip_model = aiplatform.Model.upload(
local_model=local_model,
display_name=predictor.TimmPredictor.TIMM_MODEL_NAME,
artifact_uri=config.artifact_gcs_dir,
)
config.model_name = aip_model.resource_name
config.save()
return aip_model
def deploy_model(config: CPRConfig) -> aiplatform.Endpoint:
"""Deploy the model server to a Vertex Prediction endpoint."""
aiplatform.init(project=config.project_id, location=config.region)
aip_model = aiplatform.Model(model_name=config.model_name)
endpoint = aip_model.deploy(machine_type=config.machine_type)
config.endpoint_name = endpoint.resource_name
config.save()
return endpoint
def probe_prediction(config: CPRConfig, request_path: str) -> None:
"""Send a sample prediction request to the Vertex Prediction endpoint."""
aiplatform.init(project=config.project_id, location=config.region)
aip_endpoint = aiplatform.Endpoint(endpoint_name=config.endpoint_name)
with open(request_path) as f:
logging.info(aip_endpoint.predict(**json.load(f)))
def main(argv: Sequence[str]):
config = CPRConfig()
if pathlib.Path(config.config_file).exists():
config.load()
actions = set(argv[1:])
if "build" in actions:
build_container(config, config.image)
save_model_artifact(config.artifact_local_dir)
if "upload" in actions:
save_model_artifact(config.artifact_gcs_dir)
upload_model(config)
if "deploy" in actions:
deploy_model(config)
if "probe" in actions:
probe_prediction(config, request_path="sample_request.json")
if __name__ == "__main__":
app.run(main)
@@ -1,76 +0,0 @@
# Copyright 2022 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.
import dataclasses
import json
@dataclasses.dataclass
class CPRConfig(object):
"""Configure the build process by editing the default values here.
config_file: File path used to save values in this config. (Some
values, such as the model name, are generated at build time and
depended on by future steps, so saving it allows this script to
deploy the model without re-uploading it, for example.)
base_image: Base Docker image on top of which the model server will
be built. By default, a Debian-based Python 3 image without GPU
support will be used.
image: Name and tag assigned to the built model server image.
artifact_local_dir: Local directory where a copy of the pretrained model weights
will be saved.
region: Google Cloud Region where the model will be uploaded during the
build process.
project_id: Google Cloud project ID.
repository: Name of the Artifact Registry repository where the container
will be uploaded.
artifact_gcs_dir: Location on GCS where a copy of the pretrained model
weights will be uploaded.
model_name: Full resource path of the uploaded model. This is a write-only
field, the value is generated by Vertex AI when the model is uploaded.
endpoint_name: Full resource path of the created endpoint. This is a
write-only field, the value is generated by Vertex AI when the model is
deployed to an endpoint.
machine_type: Machine type to use when deploying the model.
"""
config_file: str = "config.json"
base_image: str = "python:3.10-bullseye"
image: str = "timm_predictor:latest"
artifact_local_dir: str = ""
region: str = "us-central1"
project_id: str = "<your project ID here>"
repository: str = "cpr-images"
artifact_gcs_dir: str = "gs://<your bucket ID here>/timm-vit224/"
model_name: str = ""
endpoint_name: str = ""
machine_type: str = "n1-standard-2"
def save(self):
with open(self.config_file, "w") as f:
json.dump(dataclasses.asdict(self), f, indent=2)
def load(self):
with open(self.config_file) as f:
self.__init__(**json.load(f))
@@ -1,8 +0,0 @@
absl-py==1.1.0
fastapi==0.75.2
uvicorn==0.18.2
timm==0.5.4
smart_open==6.0.0
google-cloud-storage>=1.26.0,<2.0.0dev
google-cloud-aiplatform[prediction]>=1.16.0
File diff suppressed because one or more lines are too long
@@ -1,255 +0,0 @@
"""Test the timm_serving predictor."""
import base64
import json
import logging
import os
import pickle
from typing import List, Dict
from absl import flags
from absl import logging
from absl.testing import absltest
from config import CPRConfig
import fastapi
from google.cloud import aiplatform
from google.cloud.aiplatform import prediction as cpr
import PIL
from timm_serving import predictor
import torch
VIT_SMALL_PARAMS = 22878952
def b64_encode_file(path: str) -> str:
"""Encode a file's contents as base64.
Args:
path: Path to the file.
Returns:
Base64-encoded contents of the file.
"""
with open(path, "rb") as f:
return str(base64.b64encode(f.read()), encoding="utf-8")
def make_instance_dict(
image_paths: List[str], base64_encodings: List[str]
) -> Dict[str, List[str]]:
"""Generate a dictionary similar to a parsed prediction server request.
Args:
image_paths: Paths to image files to include.
base64_encodings: Pre-encoded base64 strings.
Returns:
Dictionary of instances in the format accepted by the preprocessor.
"""
instances = [s for s in base64_encodings]
for path in image_paths:
instances.append(b64_encode_file(path))
return {"instances": instances}
def count_parameters(model: torch.nn.Module):
"""Count the parameters in a Pytorch model.
Args:
model: Pytorch model (nn.Module).
Returns:
Number of parameters in the model.
"""
return sum(p.numel() for p in model.parameters())
class PredictorUnitTests(absltest.TestCase):
"""Unit tests for timm_serving.predictor."""
def setUp(self):
super().setUp()
self.config = CPRConfig()
try:
self.config.load()
except FileNotFoundError:
logging.info("No saved config file found, using default values.")
self.predictor = predictor.TimmPredictor()
def test_load_from_saved_state_dict_ok(self):
self.predictor.load(self.config.artifact_local_dir)
self.assertEqual(count_parameters(self.predictor._model), VIT_SMALL_PARAMS)
def test_load_bad_path(self):
with self.assertRaises(FileNotFoundError):
self.predictor.load("testdata/")
with self.assertRaisesRegex(ValueError, "not a directory"):
self.predictor.load("blah")
def test_load_bad_data(self):
with self.assertRaises(pickle.UnpicklingError):
self.predictor.load("testdata/bad_model_1")
with self.assertRaisesRegex(RuntimeError, "Invalid magic number"):
self.predictor.load("testdata/bad_model_2")
def test_preprocess_ok(self):
self.predictor.load(self.config.artifact_local_dir)
instance_dict = make_instance_dict(
base64_encodings=[],
image_paths=[
"testdata/airplane.jpg",
"testdata/mandrill.tiff",
"testdata/mandrill.tiff",
"testdata/cat_alpha.png",
],
)
result = self.predictor.preprocess(instance_dict)
self.assertEqual(result.size(), torch.Size([4, 3, 224, 224]))
self.assertEqual(result.dtype, torch.float32)
def test_preprocess_no_instances(self):
self.predictor.load(self.config.artifact_local_dir)
with self.assertRaises(fastapi.HTTPException) as ctx:
self.predictor.preprocess({})
self.assertEqual(ctx.exception.status_code, 400)
self.assertRegex(ctx.exception.detail, 'must contain "instances"')
def test_preprocess_wrong_shape_instances(self):
self.predictor.load(self.config.artifact_local_dir)
instance_dict = {"instances": [[b64_encode_file("testdata/mandrill.tiff")]]}
with self.assertRaises(fastapi.HTTPException) as ctx:
self.predictor.preprocess(instance_dict)
self.assertEqual(ctx.exception.status_code, 400)
self.assertRegex(ctx.exception.detail, "not 'list'")
def test_preprocess_bad_base64(self):
self.predictor.load(self.config.artifact_local_dir)
instance_dict = make_instance_dict(base64_encodings=["!@#$"], image_paths=[])
with self.assertRaises(fastapi.HTTPException) as ctx:
self.predictor.preprocess(instance_dict)
self.assertEqual(ctx.exception.status_code, 400)
self.assertRegex(ctx.exception.detail, "[Bb]ase64")
def test_preprocess_not_image_data(self):
self.predictor.load(self.config.artifact_local_dir)
instance_dict = make_instance_dict(
base64_encodings=[], image_paths=["testdata/bad.jpg"]
)
with self.assertRaises(fastapi.HTTPException) as ctx:
self.predictor.preprocess(instance_dict)
self.assertEqual(ctx.exception.status_code, 400)
self.assertRegex(ctx.exception.detail, "image file")
def test_predict_ok(self):
self.predictor.load(self.config.artifact_local_dir)
inputs = torch.zeros(size=[2, 3, 224, 224], dtype=torch.float32)
if torch.cuda.device_count() > 0:
inputs = inputs.cuda()
result = self.predictor.predict(inputs)
self.assertEqual(result.size(), torch.Size([2, 1000]))
self.assertEqual(result.dtype, torch.float32)
def test_postprocess_ok(self):
class_probs = torch.zeros(size=[2, 1000])
class_probs[0, 0] = 1
class_probs[1, 123] = 1
result = self.predictor.postprocess(class_probs)
predictions = result["predictions"]
self.assertLen(predictions[0]["class_names"], 5)
self.assertLen(predictions[0]["indices"], 5)
self.assertLen(predictions[0]["probabilities"], 5)
self.assertLen(predictions[1]["class_names"], 5)
self.assertLen(predictions[1]["indices"], 5)
self.assertLen(predictions[1]["probabilities"], 5)
self.assertContainsSubsequence(predictions[0]["class_names"][0], "tench")
self.assertContainsSubsequence(
predictions[1]["class_names"][0], "spiny lobster"
)
class ServerEndToEndTests(absltest.TestCase):
"""End-to-end tests for the model server, using LocalEndpoint."""
def setUp(self):
super().setUp()
self.config = CPRConfig()
try:
self.config.load()
except FileNotFoundError:
logging.info("No saved config file found, using default values.")
self.local_model = cpr.LocalModel(
serving_container_spec=aiplatform.gapic.ModelContainerSpec(
image_uri=self.config.image
)
)
self.local_endpoint = self.local_model.deploy_to_local_endpoint(
artifact_uri=self.config.artifact_local_dir or os.getcwd()
)
self.local_endpoint.serve()
def tearDown(self):
self.local_endpoint.stop()
super().tearDown()
def test_e2e_healthcheck_ok(self):
health_check_response = self.local_endpoint.run_health_check()
self.assertEqual(health_check_response.status_code, 200)
self.assertEqual(health_check_response.content, b"{}")
def test_e2e_predict_ok(self):
predict_request = json.dumps(
make_instance_dict(
base64_encodings=[],
image_paths=[
"testdata/mandrill.tiff",
],
)
)
response = self.local_endpoint.predict(
request=predict_request, headers={"Content-Type": "application/json"}
)
logging.info(response.content)
self.assertEqual(response.status_code, 200)
predictions = response.json()["predictions"]
self.assertContainsSubsequence(predictions[0]["class_names"][0], "baboon")
def test_e2e_predict_bad_json_returns_400(self):
predict_request = "blah"
response = self.local_endpoint.predict(
request=predict_request, headers={"Content-Type": "application/json"}
)
logging.info(response.content)
self.assertEqual(response.status_code, 400)
def test_e2e_predict_no_instances_returns_400(self):
predict_request = json.dumps({})
response = self.local_endpoint.predict(
request=predict_request, headers={"Content-Type": "application/json"}
)
logging.info(response.content)
self.assertEqual(response.status_code, 400)
def test_e2e_predict_bad_base64_returns_400(self):
predict_request = json.dumps(
make_instance_dict(base64_encodings=["blah"], image_paths=[])
)
response = self.local_endpoint.predict(
request=predict_request, headers={"Content-Type": "application/json"}
)
logging.info(response.content)
self.assertEqual(response.status_code, 400)
def test_e2e_predict_bad_image_returns_400(self):
predict_request = json.dumps(
make_instance_dict(base64_encodings=[], image_paths=["testdata/bad.jpg"])
)
response = self.local_endpoint.predict(
request=predict_request, headers={"Content-Type": "application/json"}
)
logging.info(response.content)
self.assertEqual(response.status_code, 400)
if __name__ == "__main__":
absltest.main()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

@@ -1 +0,0 @@
some non-image data
@@ -1 +0,0 @@
some non-image data
@@ -1 +0,0 @@
blah
Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

File diff suppressed because it is too large Load Diff
@@ -1,178 +0,0 @@
# Copyright 2022 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.
"""Adapts a pretrained TIMM image classification model to the CPR framework.
Documentation for the TIMM (Torch IMage Models) library is here:
https://rwightman.github.io/pytorch-image-models/
Its source can also be found here:
https://github.com/rwightman/pytorch-image-models
"""
import base64
import binascii
import io
import os
from typing import Dict, List, Union
from fastapi import HTTPException
from google.cloud.aiplatform import prediction as cpr
from pathlib import Path
import PIL
import smart_open
import timm
import torch
import torch.nn.functional as F
with open(Path(__file__).parent.absolute().joinpath("imagenet.txt")) as f:
IMAGENET_CLASSES = f.read().splitlines()
class TimmPredictor(cpr.predictor.Predictor):
"""Predictor class for image models based on TIMM."""
TIMM_MODEL_NAME = os.getenv("TIMM_MODEL_NAME", default="vit_small_patch32_224")
WEIGHTS_FILE = "state_dict.pth"
NUM_TOP_CLASSES_TO_RETURN = 5
def __init__(self):
self._cuda = torch.cuda.device_count() > 0
def load(self, artifacts_uri: str = ""):
"""Initializes the model and preprocessing transforms.
Args:
artifacts_uri: Directory where state dict is stored. Can be a
GCS URI or local path.
"""
if artifacts_uri:
artifact_path = os.path.join(artifacts_uri)
if not (os.path.isdir(artifact_path) or artifact_path.startswith("gs://")):
raise ValueError("Provided artifact_uri is not a directory.")
else:
artifact_path = os.getcwd()
artifact_path = os.path.join(artifact_path, self.WEIGHTS_FILE)
with smart_open.open(artifact_path, "rb") as f:
self._model = torch.load(f)
if self._cuda:
self._model.cuda()
config = timm.data.resolve_data_config(model=self.TIMM_MODEL_NAME, args=[])
self._transform = timm.data.create_transform(
is_training=False, use_prefetcher=False, **config
)
def preprocess(self, request_dict: Dict[str, List[str]]) -> torch.Tensor:
"""Performs preprocessing.
By default, the server expects a request body consisting of a valid JSON
object. This will be parsed by the handler before it's evaluated by the
preprocess method.
Args:
request_dict: Parsed request body. We expect that the input consists of
a list of base64-encoded image files under the "instances" key. (Any
image format that PIL.image.open can handle is okay.)
Returns:
torch.Tensor containing the preprocessed images as a batch. If GPU is
available, the result tensor will be stored on GPU.
"""
if "instances" not in request_dict:
raise HTTPException(
status_code=400,
detail='Request must contain "instances" as a top-level key.',
)
tensors = []
for (i, image) in enumerate(request_dict["instances"]):
# We use Base64 encoding to handle image data.
# This is probably the best we can do while still using JSON input.
# Overriding the input format requires building a custom Handler.
try:
image_bytes = base64.b64decode(image, validate=True)
except (binascii.Error, TypeError) as e:
raise HTTPException(
status_code=400,
detail=f"Base64 decoding of the input image at index {i} failed:"
f" {str(e)}",
)
try:
pil_image = PIL.Image.open(io.BytesIO(image_bytes)).convert("RGB")
except PIL.UnidentifiedImageError:
raise HTTPException(
status_code=400,
detail=f"The input image at index {i} could not be identified as an"
" image file.",
)
tensors.append(self._transform(pil_image))
with torch.inference_mode():
result = torch.stack(tensors)
if self._cuda:
result = result.cuda()
return result
def predict(self, instances: torch.Tensor) -> torch.Tensor:
"""Performs prediction.
Args:
instances: torch.Tensor with type torch.float32 and shape
[?, 3, 224, 224], containing the pre-processed input images.
Returns:
Vector of scores with type torch.float32 and shape [?, 1000],
representing the model's estimate of the likelihood that the
input belongs to the Imagenet class with that index.
"""
with torch.inference_mode():
class_scores = self._model(instances)
return class_scores
def postprocess(
self, class_scores: torch.Tensor
) -> Dict[str, List[Dict[str, Union[str, int, float]]]]:
"""Translate the model output into a classification result.
Args:
class_scores: torch.Tensor with type torch.float32 and shape
[?, 1000], containing the scores assigned to each class by
the model.
Returns:
Dictionary containing the list of classification results. Each
classification result contains the probabilities, class names, and
class indices of the classes with the top class scores as reported by
the model.
"""
class_probs = F.softmax(class_scores, dim=1)
top_k = class_probs.topk(self.NUM_TOP_CLASSES_TO_RETURN)
top_k_values = top_k.values.numpy().tolist()
top_k_indices = top_k.indices.numpy().tolist()
predictions = [
dict(
probabilities=values,
indices=indices,
class_names=[IMAGENET_CLASSES[int(class_num)] for class_num in indices],
)
for (values, indices) in zip(top_k_values, top_k_indices)
]
return {"predictions": predictions}
@@ -1,52 +0,0 @@
# Overview
*Pluto* is a programming environment for Julia, designed to be interactive and helpful. It provides a familiar notebook interface but it is not a Jupyter notebook. The biggest difference is that Pluto notebooks are reactive, changing a variable or function in one cell causes the cells that depend on that variable or function to be reevaluated. Pluto also provides useful interaction mechanisms that allow users to dynamically interact with the notebooks computation state.
The JuliaCon 2020 presentation: [Interactive notebooks ~ Pluto.jl]() provides a good introduction to Pluto. The source is at [fonsp/Pluto.jl]()
# Install Pluto
## Create a Vertex AI JupyterLab Instance
1. From the [GCP console](https://console.cloud.google.com) "hamburger menu"
select Vertex AI > Workbench
2. Click NEW NOTEBOOK
* Choose Python 3 if you won't be using a GPU
* Choose Python 3 (CUDA Toolkit xx.y) if you do want use a GPU
3. Give the notebook an appropriate name
4. Edit Notebook properties if you have special requirements otherwise accept the defaults and click CREATE
5. When the notebook instance is ready click OPEN JUPYTERLAB
## Configure JupyterLab
1. Open a terminal by clicking the Terminal icon.
1. Install the plutoserver
pip3 install git+https://github.com/fonsp/pluto-on-jupyterlab.git
1. In a browser go to [julialang.org/downloads](https://julialang.org/downloads/)
1. In the Current stable release right click on the `Generic Linux on x86 / 64-bit (glibc)` link
Select copy link address
1. Back in the terminal switch to root via
sudo -i
1. Download the release to /opt and install julia in /usr/local/bin
```bash
cd /opt
wget <paste the release link address>
tar xf <name of the downloaded tar file>
ln -s /opt/<julia-x.y.z>/bin/julia /usr/local/bin
^d
```
1. Add the Pluto package to Julia
```bash
julia
julia> ]add Pluto
julia> bksp
julia> using Pluto
julia> ^d
```
1. From the JupyterLab menu bar select File > Shut Down
# Start Pluto
1. Click OPEN JUPYTERLAB in the Workbench
1. In the Notebook section of the Launcher click Pluto.jl
1. The welcome to Pluto.jl screen should appear
@@ -0,0 +1,474 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "a6b56b1c7b76"
},
"outputs": [],
"source": [
"# Copyright 2021 Google LLC\n",
"#\n",
"# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
"# you may not use this file except in compliance with the License.\n",
"# You may obtain a copy of the License at\n",
"#\n",
"# https://www.apache.org/licenses/LICENSE-2.0\n",
"#\n",
"# Unless required by applicable law or agreed to in writing, software\n",
"# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
"# See the License for the specific language governing permissions and\n",
"# limitations under the License."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "c414a395a19b"
},
"source": [
"# PyTorch Image Classification Multi-Node Distributed Data Parallel Training on CPU using Vertex Training with Custom Container"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "b98238e32cf7"
},
"source": [
"<table align=\"left\">\n",
" <td>\n",
" <a href=\"https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/master/community-content/pytorch_image_classification_distributed_data_parallel_training_with_vertex_sdk/multi_node_ddp_gloo_vertex_training_with_custom_container.ipynb\">\n",
" <img src=\"https://cloud.google.com/ml-engine/images/github-logo-32px.png\" alt=\"GitHub logo\">\n",
" View on GitHub\n",
" </a>\n",
" </td>\n",
"</table>"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "03d216c7f7b1"
},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "c5ac73516218"
},
"outputs": [],
"source": [
"PROJECT_ID = \"YOUR PROJECT ID\"\n",
"BUCKET_NAME = \"gs://YOUR BUCKET NAME\"\n",
"REGION = \"YOUR REGION\"\n",
"SERVICE_ACCOUNT = \"YOUR SERVICE ACCOUNT\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "0b5ae674177e"
},
"outputs": [],
"source": [
"! gsutil ls -al $BUCKET_NAME"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "19a9b3bdd553"
},
"outputs": [],
"source": [
"content_name = \"pt-img-cls-multi-node-ddp-cust-cont\""
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "57bf6f8b4361"
},
"source": [
"## Local Training"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "e5d8a3443da0"
},
"outputs": [],
"source": [
"! ls trainer"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "07f79309472d"
},
"outputs": [],
"source": [
"! cat trainer/requirements.txt"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "e16cd8bb7483"
},
"outputs": [],
"source": [
"! pip install -r trainer/requirements.txt"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "0b8a210718c4"
},
"outputs": [],
"source": [
"! cat trainer/task.py"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "c0c6e7dfb3c6"
},
"outputs": [],
"source": [
"%run trainer/task.py --epochs 5 --no-cuda --local-mode"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "31dfdeede587"
},
"outputs": [],
"source": [
"! ls ./tmp"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "48d56ec621cc"
},
"outputs": [],
"source": [
"! rm -rf ./tmp"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "8f3ea1210749"
},
"source": [
"## Vertex Training using Vertex SDK and Custom Container"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "93002a20a2a6"
},
"source": [
"### Build Custom Container"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "4130ce43fd08"
},
"outputs": [],
"source": [
"hostname = \"gcr.io\"\n",
"image_name = content_name\n",
"tag = \"latest\"\n",
"\n",
"custom_container_image_uri = f\"{hostname}/{PROJECT_ID}/{image_name}:{tag}\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "2f1fc5b05240"
},
"outputs": [],
"source": [
"! cd trainer && docker build -t $custom_container_image_uri -f Dockerfile ."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "b4f274f499ac"
},
"outputs": [],
"source": [
"! docker run --rm $custom_container_image_uri --epochs 5 --no-cuda --local-mode"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "ee1a0a06d0b4"
},
"outputs": [],
"source": [
"! docker push $custom_container_image_uri"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "cb763be12fc9"
},
"outputs": [],
"source": [
"! gcloud container images list --repository $hostname/$PROJECT_ID"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "10c8cc6b3334"
},
"source": [
"### Initialize Vertex SDK"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "1a12348169fa"
},
"outputs": [],
"source": [
"! pip install -r requirements.txt"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "42e981cefe41"
},
"outputs": [],
"source": [
"from google.cloud import aiplatform\n",
"\n",
"aiplatform.init(\n",
" project=PROJECT_ID,\n",
" staging_bucket=BUCKET_NAME,\n",
" location=REGION,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "73c92c9298e9"
},
"source": [
"### Create a Vertex Tensorboard Instance"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "bde509558cd5"
},
"outputs": [],
"source": [
"content_name = content_name + \"-cpu\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "6d7908c0083c"
},
"outputs": [],
"source": [
"tensorboard = aiplatform.Tensorboard.create(\n",
" display_name=content_name,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "a1f0a4f54037"
},
"source": [
"#### Option: Use a Previously Created Vertex Tensorboard Instance\n",
"\n",
"```\n",
"tensorboard_name = \"Your Tensorboard Resource Name or Tensorboard ID\"\n",
"tensorboard = aiplatform.Tensorboard(tensorboard_name=tensorboard_name)\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "a4cac84e04ac"
},
"source": [
"### Run a Vertex SDK CustomContainerTrainingJob"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "f92e8fdd44ee"
},
"outputs": [],
"source": [
"display_name = content_name\n",
"gcs_output_uri_prefix = f\"{BUCKET_NAME}/{display_name}\"\n",
"\n",
"replica_count = 4\n",
"machine_type = \"n1-standard-4\"\n",
"\n",
"args = [\n",
" \"--backend\",\n",
" \"gloo\",\n",
" \"--no-cuda\",\n",
" \"--batch-size\",\n",
" \"128\",\n",
" \"--epochs\",\n",
" \"25\",\n",
"]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "ae4c57df7e07"
},
"outputs": [],
"source": [
"custom_container_training_job = aiplatform.CustomContainerTrainingJob(\n",
" display_name=display_name,\n",
" container_uri=custom_container_image_uri,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "35cf3ecdf0df"
},
"outputs": [],
"source": [
"custom_container_training_job.run(\n",
" args=args,\n",
" base_output_dir=gcs_output_uri_prefix,\n",
" replica_count=replica_count,\n",
" machine_type=machine_type,\n",
" tensorboard=tensorboard.resource_name,\n",
" service_account=SERVICE_ACCOUNT,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "49d10dded73b"
},
"outputs": [],
"source": [
"print(f\"Custom Training Job Name: {custom_container_training_job.resource_name}\")\n",
"print(f\"GCS Output URI Prefix: {gcs_output_uri_prefix}\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "78398f52807b"
},
"source": [
"### Training Output Artifact"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "fc74422de1d1"
},
"outputs": [],
"source": [
"! gsutil ls $gcs_output_uri_prefix"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "5e99a6a05b10"
},
"source": [
"## Clean Up Artifact"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "b0c1b3f7466b"
},
"outputs": [],
"source": [
"! gsutil rm -rf $gcs_output_uri_prefix"
]
}
],
"metadata": {
"colab": {
"name": "multi_node_ddp_gloo_vertex_training_with_custom_container.ipynb",
"toc_visible": true
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
@@ -0,0 +1,347 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "a6b56b1c7b76"
},
"outputs": [],
"source": [
"# Copyright 2021 Google LLC\n",
"#\n",
"# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
"# you may not use this file except in compliance with the License.\n",
"# You may obtain a copy of the License at\n",
"#\n",
"# https://www.apache.org/licenses/LICENSE-2.0\n",
"#\n",
"# Unless required by applicable law or agreed to in writing, software\n",
"# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
"# See the License for the specific language governing permissions and\n",
"# limitations under the License."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "20a5ea0081d0"
},
"source": [
"# PyTorch Image Classification Multi-Node Distributed Data Parallel Training on GPU using Vertex Training with Custom Container"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "8752d4a255fb"
},
"source": [
"<table align=\"left\">\n",
" <td>\n",
" <a href=\"https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/master/community-content/pytorch_image_classification_distributed_data_parallel_training_with_vertex_sdk/multi_node_ddp_nccl_vertex_training_with_custom_container.ipynb\">\n",
" <img src=\"https://cloud.google.com/ml-engine/images/github-logo-32px.png\" alt=\"GitHub logo\">\n",
" View on GitHub\n",
" </a>\n",
" </td>\n",
"</table>"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "03d216c7f7b1"
},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "c5ac73516218"
},
"outputs": [],
"source": [
"PROJECT_ID = \"YOUR PROJECT ID\"\n",
"BUCKET_NAME = \"gs://YOUR BUCKET NAME\"\n",
"REGION = \"YOUR REGION\"\n",
"SERVICE_ACCOUNT = \"YOUR SERVICE ACCOUNT\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "0b5ae674177e"
},
"outputs": [],
"source": [
"! gsutil ls -al $BUCKET_NAME"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "19a9b3bdd553"
},
"outputs": [],
"source": [
"content_name = \"pt-img-cls-multi-node-ddp-cust-cont\""
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "5307fe28b633"
},
"source": [
"## Vertex Training using Vertex SDK and Custom Container"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "46cb58c7fbf9"
},
"source": [
"### Built Custom Container"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "97e66e9f9bab"
},
"outputs": [],
"source": [
"hostname = \"gcr.io\"\n",
"image_name = content_name\n",
"tag = \"latest\"\n",
"\n",
"custom_container_image_uri = f\"{hostname}/{PROJECT_ID}/{image_name}:{tag}\""
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "ae9b29c4773f"
},
"source": [
"### Initialize Vertex SDK"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "dc1e84d5dec2"
},
"outputs": [],
"source": [
"! pip install -r requirements.txt"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "6964be27b98e"
},
"outputs": [],
"source": [
"from google.cloud import aiplatform\n",
"\n",
"aiplatform.init(\n",
" project=PROJECT_ID,\n",
" staging_bucket=BUCKET_NAME,\n",
" location=REGION,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "594a91f438f2"
},
"source": [
"### Create a Vertex Tensorboard Instance"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "93134273261e"
},
"outputs": [],
"source": [
"content_name = content_name + \"-gpu\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "c2bd82dbcd9b"
},
"outputs": [],
"source": [
"tensorboard = aiplatform.Tensorboard.create(\n",
" display_name=content_name,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "ebc593c6472e"
},
"source": [
"#### Option: Use a Previously Created Vertex Tensorboard Instance\n",
"\n",
"```\n",
"tensorboard_name = \"Your Tensorboard Resource Name or Tensorboard ID\"\n",
"tensorboard = aiplatform.Tensorboard(tensorboard_name=tensorboard_name)\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "0769e8e34c2f"
},
"source": [
"### Run a Vertex SDK CustomContainerTrainingJob"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "023f33ece826"
},
"outputs": [],
"source": [
"display_name = content_name\n",
"gcs_output_uri_prefix = f\"{BUCKET_NAME}/{display_name}\"\n",
"\n",
"replica_count = 1\n",
"machine_type = \"n1-standard-4\"\n",
"accelerator_count = 4\n",
"accelerator_type = \"NVIDIA_TESLA_K80\"\n",
"\n",
"args = [\n",
" \"--backend\",\n",
" \"nccl\",\n",
" \"--batch-size\",\n",
" \"128\",\n",
" \"--epochs\",\n",
" \"25\",\n",
"]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "d4b599e726ef"
},
"outputs": [],
"source": [
"custom_container_training_job = aiplatform.CustomContainerTrainingJob(\n",
" display_name=display_name,\n",
" container_uri=custom_container_image_uri,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "81321e3bdf7f"
},
"outputs": [],
"source": [
"custom_container_training_job.run(\n",
" args=args,\n",
" base_output_dir=gcs_output_uri_prefix,\n",
" replica_count=replica_count,\n",
" machine_type=machine_type,\n",
" accelerator_count=accelerator_count,\n",
" accelerator_type=accelerator_type,\n",
" tensorboard=tensorboard.resource_name,\n",
" service_account=SERVICE_ACCOUNT,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "5100712c2c4c"
},
"outputs": [],
"source": [
"print(f\"Custom Training Job Name: {custom_container_training_job.resource_name}\")\n",
"print(f\"GCS Output URI Prefix: {gcs_output_uri_prefix}\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "f9b77676e5a6"
},
"source": [
"### Training Output Artifact"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "0e171ce95ace"
},
"outputs": [],
"source": [
"! gsutil ls $gcs_output_uri_prefix"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "cf1b74a12b87"
},
"source": [
"## Clean Up Artifact"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "a0b15089c341"
},
"outputs": [],
"source": [
"! gsutil rm -rf $gcs_output_uri_prefix"
]
}
],
"metadata": {
"colab": {
"name": "multi_node_ddp_nccl_vertex_training_with_custom_container.ipynb",
"toc_visible": true
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
@@ -1,30 +0,0 @@
# PyTorch Deployment on Google Cloud: Text Classification
**This is an Experimental release**, covered by the Pre-GA Offerings Terms of your Google Cloud Platform [Terms of Service](https://cloud.google.com/terms).
Experiments are focused on validating a prototype and are not guaranteed to be released. They are not intended for production use or covered by any SLA, support obligation, or deprecation policy and might be subject to backward-incompatible changes.
**Kindly drop us a note before you run any scale tests.**
**Do not hesitate to contact vertexai-prediction-preview-feedback@google.com if you have any questions or run into any issues.**
The projects need to be added to the allowlist in order to deploy PyTorch models using Vertex AI Prediction pre-built PyTorch images. If you are interested in the feature, please send an email to vertexai-prediction-preview-feedback@google.com to provide your project numbers OR project ids.
## Overview
In the PyTorch on Google Cloud series of blog posts, we aim to share how to deploy PyTorch models at scale on [Vertex AI](https://cloud.google.com/vertex-ai).
This tutorial on text classification shows how to deploy a PyTorch based text classification model on [Vertex AI](https://cloud.google.com/vertex-ai/docs/start/client-libraries#python) using Vertex SDK and [`gcloud ai`](https://cloud.google.com/sdk/gcloud/reference/beta/ai).
## Notebooks
| <h4>Notebook</h4> | <h4>Description</h4> |
| :-------- | :------- |
| [pytorch-text-classification-vertex-ai-deploy.ipynb](./pytorch-text-classification-vertex-ai-deploy.ipynb) | Notebook to show deploying a PyTorch model on Vertex AI |
## Folders
| <h4>Folder Name</h4> | <h4>Description</h4> |
| :-------- | :------- |
| [`predictor`](./predictor) | Folder with custom prediction handler to deploy a PyTorch model to Vertex Prediction. In the [notebook](./pytorch-text-classification-vertex-ai-deploy.ipynb), this folder is used for deploying a PyTorch model on Vertex AI using Vertex Prediction pre-built PyTorch images |
@@ -1,91 +0,0 @@
import os
import json
import logging
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
from ts.torch_handler.base_handler import BaseHandler
logger = logging.getLogger(__name__)
class TransformersClassifierHandler(BaseHandler):
"""
The handler takes an input string and returns the classification text
based on the serialized transformers checkpoint.
"""
def __init__(self):
super(TransformersClassifierHandler, self).__init__()
self.initialized = False
def initialize(self, ctx):
""" Loads the model.pt file and initialized the model object.
Instantiates Tokenizer for preprocessor to use
Loads labels to name mapping file for post-processing inference response
"""
self.manifest = ctx.manifest
properties = ctx.system_properties
model_dir = properties.get("model_dir")
self.device = torch.device("cuda:" + str(properties.get("gpu_id")) if torch.cuda.is_available() else "cpu")
# Read model serialize/pt file
serialized_file = self.manifest["model"]["serializedFile"]
model_pt_path = os.path.join(model_dir, serialized_file)
if not os.path.isfile(model_pt_path):
raise RuntimeError("Missing the model.pt or pytorch_model.bin file")
# Load model
self.model = AutoModelForSequenceClassification.from_pretrained(model_dir)
self.model.to(self.device)
self.model.eval()
logger.debug('Transformer model from path {0} loaded successfully'.format(model_dir))
# Ensure to use the same tokenizer used during training
self.tokenizer = AutoTokenizer.from_pretrained('bert-base-cased')
# Read the mapping file, index to object name
mapping_file_path = os.path.join(model_dir, "index_to_name.json")
if os.path.isfile(mapping_file_path):
with open(mapping_file_path) as f:
self.mapping = json.load(f)
else:
logger.warning('Missing the index_to_name.json file. Inference output will default.')
self.mapping = {"0": "Negative", "1": "Positive"}
self.initialized = True
def preprocess(self, data):
""" Preprocessing input request by tokenizing
Extend with your own preprocessing steps as needed
"""
text = data[0].get("data")
if text is None:
text = data[0].get("body")
sentences = text.decode('utf-8')
logger.info("Received text: '%s'", sentences)
# Tokenize the texts
tokenizer_args = ((sentences,))
inputs = self.tokenizer(*tokenizer_args,
padding='max_length',
max_length=128,
truncation=True,
return_tensors = "pt")
return inputs
def inference(self, inputs):
""" Predict the class of a text using a trained transformer model.
"""
prediction = self.model(inputs['input_ids'].to(self.device))[0].argmax().item()
if self.mapping:
prediction = self.mapping[str(prediction)]
logger.info("Model predicted: '%s'", prediction)
return [prediction]
def postprocess(self, inference_output):
return inference_output
@@ -1,5 +0,0 @@
{
"0": "Negative",
"1": "Positive"
}
@@ -1,6 +1,6 @@
# PyTorch on Google Cloud: Text Classification
In the PyTorch on Google Cloud series of blog posts, we aim to share how to build, train, deploy and orchestrate PyTorch models at scale and how to create reproducible machine learning pipelines on Google Cloud with [Vertex AI](https://cloud.google.com/vertex-ai).
In the PyTorch on Google Cloud series of blog posts, we aim to share how to build, train and deploy PyTorch models at scale and how to create reproducible machine learning pipelines on Google Cloud with [Vertex AI](https://cloud.google.com/vertex-ai).
This tutorial on text classification shows how to train a PyTorch based text classification model by fine tuning a pre-trained Huggingface Transformers model and deploy the model on [Vertex AI](https://cloud.google.com/vertex-ai/docs/start/client-libraries#python) using Vertex SDK and [`gcloud ai`](https://cloud.google.com/sdk/gcloud/reference/beta/ai).
@@ -9,7 +9,6 @@ This tutorial on text classification shows how to train a PyTorch based text cla
| <h4>Notebook</h4> | <h4>Description</h4> |
| :-------- | :------- |
| [pytorch-text-classification-vertex-ai-train-tune-deploy.ipynb](./pytorch-text-classification-vertex-ai-train-tune-deploy.ipynb) | Notebook to show training, hyper-parameter tuning and deploying a PyTorch model on Vertex AI |
| [pytorch-text-classification-vertex-ai-pipelines.ipynb](./pytorch-text-classification-vertex-ai-pipelines.ipynb) | Notebook to show orchestration of PyTorch ML workflows on Vertex AI Pipelines using Kubeflow Pipelines SDK |
## Folders
@@ -1,7 +1,6 @@
# Use pytorch GPU base image
# FROM gcr.io/cloud-aiplatform/training/pytorch-gpu.1-7
FROM us-docker.pkg.dev/vertex-ai/training/pytorch-gpu.1-10:latest
FROM gcr.io/cloud-aiplatform/training/pytorch-gpu.1-7
# set working directory
WORKDIR /app
@@ -22,18 +22,15 @@ PROJECT_ID=$(gcloud config list --format 'value(core.project)')
# BUCKET_NAME: Change to your bucket name.
BUCKET_NAME="[your-bucket-name]" # <-- CHANGE TO YOUR BUCKET NAME
# validate bucket name
if [ "${BUCKET_NAME}" = "[your-bucket-name]" ]
then
echo "[ERROR] INVALID VALUE: Please update the variable BUCKET_NAME with valid Cloud Storage bucket name. Exiting the script..."
exit 1
fi
BUCKET_NAME=cloud-ai-platform-2f444b6a-a742-444b-b91a-c7519f51bd77
# JOB_NAME: the name of your job running on AI Platform.
JOB_PREFIX="finetuned-bert-classifier-pytorch-cstm-cntr"
JOB_PREFIX="finetuned-bert-classifier-pytorch-cstm-cntr-"
JOB_NAME=${JOB_PREFIX}-$(date +%Y%m%d%H%M%S)-custom-job
# This can be a GCS location to a zipped and uploaded package
PACKAGE_PATH=./trainer
# REGION: select a region from https://cloud.google.com/vertex-ai/docs/general/locations#available_regions
# or use the default '`us-central1`'. The region is where the job will be run.
REGION="us-central1"
@@ -44,8 +41,11 @@ JOB_DIR=gs://${BUCKET_NAME}/${JOB_PREFIX}/models/${JOB_NAME}
# IMAGE_REPO_NAME: set a local repo name to distinquish our image
IMAGE_REPO_NAME=pytorch_gpu_train_finetuned-bert-classifier
# IMAGE_TAG: an easily identifiable tag for your docker image
IMAGE_TAG=latest
# IMAGE_URI: the complete URI location for Cloud Container Registry
CUSTOM_TRAIN_IMAGE_URI=gcr.io/${PROJECT_ID}/${IMAGE_REPO_NAME}
CUSTOM_TRAIN_IMAGE_URI=gcr.io/${PROJECT_ID}/${IMAGE_REPO_NAME}:${IMAGE_TAG}
# Build the docker image
docker build --no-cache -f Dockerfile -t $CUSTOM_TRAIN_IMAGE_URI ../python_package
@@ -53,19 +53,11 @@ docker build --no-cache -f Dockerfile -t $CUSTOM_TRAIN_IMAGE_URI ../python_packa
# Deploy the docker image to Cloud Container Registry
docker push ${CUSTOM_TRAIN_IMAGE_URI}
# worker pool spec
worker_pool_spec="\
replica-count=1,\
machine-type=n1-standard-8,\
accelerator-type=NVIDIA_TESLA_V100,\
accelerator-count=1,\
container-image-uri=${CUSTOM_TRAIN_IMAGE_URI}"
# Submit Custom Job to Vertex AI
gcloud beta ai custom-jobs create \
--display-name=${JOB_NAME} \
--region ${REGION} \
--worker-pool-spec="${worker_pool_spec}" \
--worker-pool-spec=replica-count=1,machine-type='n1-standard-8',accelerator-type='NVIDIA_TESLA_V100',accelerator-count=1,container-image-uri=${CUSTOM_TRAIN_IMAGE_URI} \
--args="--model-name","finetuned-bert-classifier","--job-dir",$JOB_DIR
echo "After the job is completed successfully, model files will be saved at $JOB_DIR/"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

@@ -2,13 +2,10 @@
FROM pytorch/torchserve:latest-cpu
# install dependencies
RUN python3 -m pip install --upgrade pip
RUN pip3 install transformers
USER model-server
# copy model artifacts, custom handler and other dependencies
COPY ./custom_handler.py /home/model-server/
COPY ./custom_text_handler.py /home/model-server/
COPY ./index_to_name.json /home/model-server/
COPY ./model/finetuned-bert-classifier/ /home/model-server/
@@ -24,7 +21,7 @@ EXPOSE 7080
EXPOSE 7081
# create model archive file packaging model artifacts and dependencies
RUN torch-model-archiver -f --model-name=finetuned-bert-classifier --version=1.0 --serialized-file=/home/model-server/pytorch_model.bin --handler=/home/model-server/custom_handler.py --extra-files "/home/model-server/config.json,/home/model-server/tokenizer.json,/home/model-server/training_args.bin,/home/model-server/tokenizer_config.json,/home/model-server/special_tokens_map.json,/home/model-server/vocab.txt,/home/model-server/index_to_name.json" --export-path=/home/model-server/model-store
RUN torch-model-archiver -f --model-name=finetuned-bert-classifier --version=1.0 --serialized-file=/home/model-server/pytorch_model.bin --handler=/home/model-server/custom_text_handler.py --extra-files "/home/model-server/config.json,/home/model-server/tokenizer.json,/home/model-server/training_args.bin,/home/model-server/tokenizer_config.json,/home/model-server/special_tokens_map.json,/home/model-server/vocab.txt,/home/model-server/index_to_name.json" --export-path=/home/model-server/model-store
# run Torchserve HTTP serve to respond to prediction requests
CMD ["torchserve", "--start", "--ts-config=/home/model-server/config.properties", "--models", "finetuned-bert-classifier=finetuned-bert-classifier.mar", "--model-store", "/home/model-server/model-store"]
@@ -1,37 +0,0 @@
FROM pytorch/torchserve:latest-cpu
USER root
# run and update some basic packages software packages, including security libs
RUN apt-get update && apt-get install -y software-properties-common && add-apt-repository -y ppa:ubuntu-toolchain-r/test && apt-get update && apt-get install -y gcc-9 g++-9 apt-transport-https ca-certificates gnupg curl
# Install gcloud tools for gsutil as well as debugging
RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - && apt-get update -y && apt-get install google-cloud-sdk -y
USER model-server
# install dependencies
RUN python3 -m pip install --upgrade pip
RUN pip3 install transformers
ARG MODEL_NAME=finetuned-bert-classifier
ENV MODEL_NAME="${MODEL_NAME}"
# health and prediction listener ports
ARG AIP_HTTP_PORT=7080
ENV AIP_HTTP_PORT="${AIP_HTTP_PORT}"
ARG MODEL_MGMT_PORT=7081
# expose health and prediction listener ports from the image
EXPOSE "${AIP_HTTP_PORT}"
EXPOSE "${MODEL_MGMT_PORT}"
EXPOSE 8080 8081 8082 7070 7071
# create torchserve configuration file
USER root
RUN echo "service_envelope=json\n" "inference_address=http://0.0.0.0:${AIP_HTTP_PORT}\n" "management_address=http://0.0.0.0:${MODEL_MGMT_PORT}" >> /home/model-server/config.properties
USER model-server
# run Torchserve HTTP serve to respond to prediction requests
CMD ["echo", "AIP_STORAGE_URI=${AIP_STORAGE_URI}", ";", "gsutil", "cp", "-r", "${AIP_STORAGE_URI}/${MODEL_NAME}.mar", "/home/model-server/model-store/", ";", "ls", "-ltr", "/home/model-server/model-store/", ";", "torchserve", "--start", "--ts-config=/home/model-server/config.properties", "--models", "${MODEL_NAME}=${MODEL_NAME}.mar", "--model-store", "/home/model-server/model-store"]
@@ -52,8 +52,7 @@ class TransformersClassifierHandler(BaseHandler):
with open(mapping_file_path) as f:
self.mapping = json.load(f)
else:
logger.warning('Missing the index_to_name.json file. Inference output will default.')
self.mapping = {"0": "Negative", "1": "Positive"}
logger.warning('Missing the index_to_name.json file. Inference output will not include class name.')
self.initialized = True
@@ -89,3 +88,4 @@ class TransformersClassifierHandler(BaseHandler):
def postprocess(self, inference_output):
return inference_output
@@ -19,19 +19,13 @@ echo "Submitting Custom Job to Vertex AI to train PyTorch model"
# BUCKET_NAME: Change to your bucket name
BUCKET_NAME="[your-bucket-name]" # <-- CHANGE TO YOUR BUCKET NAME
# validate bucket name
if [ "${BUCKET_NAME}" = "[your-bucket-name]" ]
then
echo "[ERROR] INVALID VALUE: Please update the variable BUCKET_NAME with valid Cloud Storage bucket name. Exiting the script..."
exit 1
fi
BUCKET_NAME="cloud-ai-platform-2f444b6a-a742-444b-b91a-c7519f51bd77"
# The PyTorch image provided by Vertex AI Training.
IMAGE_URI="us-docker.pkg.dev/vertex-ai/training/pytorch-gpu.1-7:latest"
# JOB_NAME: the name of your job running on Vertex AI.
JOB_PREFIX="finetuned-bert-classifier-pytorch-pkg-ar"
JOB_PREFIX="finetuned-bert-classifier-pytorch-pkg-ar-"
JOB_NAME=${JOB_PREFIX}-$(date +%Y%m%d%H%M%S)-custom-job
# REGION: select a region from https://cloud.google.com/vertex-ai/docs/general/locations#available_regions
@@ -41,21 +35,19 @@ REGION="us-central1"
# JOB_DIR: Where to store prepared package and upload output model.
JOB_DIR=gs://${BUCKET_NAME}/${JOB_PREFIX}/model/${JOB_NAME}
# worker pool spec
worker_pool_spec="\
replica-count=1,\
machine-type=n1-standard-8,\
accelerator-type=NVIDIA_TESLA_V100,\
accelerator-count=1,\
executor-image-uri=${IMAGE_URI},\
python-module=trainer.task,\
local-package-path=../python_package/"
# validate bucket name
if [ "${BUCKET_NAME}" = "[your-bucket-name]" ]
then
echo "[ERROR] INVALID VALUE: Please update the variable BUCKET_NAME with valid Cloud Storage bucket name. Exiting the script..."
exit 1
fi
# Submit Custom Job to Vertex AI
gcloud beta ai custom-jobs create \
--display-name=${JOB_NAME} \
--region ${REGION} \
--worker-pool-spec="${worker_pool_spec}" \
--python-package-uris=${PACKAGE_PATH} \
--worker-pool-spec=replica-count=1,machine-type='n1-standard-8',accelerator-type='NVIDIA_TESLA_V100',accelerator-count=1,executor-image-uri=${IMAGE_URI},python-module='trainer.task',local-package-path="../python_package/" \
--args="--model-name","finetuned-bert-classifier","--job-dir",$JOB_DIR
echo "After the job is completed successfully, model files will be saved at $JOB_DIR/"
@@ -122,9 +122,6 @@ def run(args):
# Train / Test the model
trainer = train(args, text_classifier, train_dataset, test_dataset)
metrics = trainer.evaluate(eval_dataset=test_dataset)
trainer.save_metrics("all", metrics)
# Export the trained model
trainer.save_model(os.path.join("/tmp", args.model_name))
@@ -63,20 +63,20 @@
"- [Training](#Training)\n",
" - [Run Training Locally in the Notebook](#Training-locally-in-the-notebook)\n",
" - [Run Training Job on Vertex AI](#Training-on-Vertex-AI)\n",
" - [Training with pre-built container](#Run-Custom-Job-on-Vertex-AI-Training-with-a-pre-built-container)\n",
" - [Training with custom container](#Run-Custom-Job-on-Vertex-AI-Training-with-custom-container)\n",
" - [Training with pre-built container](#Run-Custom-Job-on-Vertex-Training-with-a-pre-built-container)\n",
" - [Training with custom container](#Run-Custom-Job-on-Vertex-Training-with-custom-container)\n",
"- [Tuning](#Hyperparameter-Tuning) \n",
" - [Run Hyperparameter Tuning job on Vertex AI](#Run-Hyperparameter-Tuning-Job-on-Vertex-AI)\n",
"- [Deploying](#Deploying)\n",
" - [Deploying model on Vertex AI Predictions with custom container](#Deploying-model-on-Vertex AI-Predictions-with-custom-container)\n",
" - [Deploying model on Vertex Predictions with custom container](#Deploying-model-on-Vertex-Predictions-with-custom-container)\n",
"\n",
"### Costs \n",
"\n",
"This tutorial uses billable components of Google Cloud Platform (GCP):\n",
"\n",
"* [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench)\n",
"* [Vertex AI Training](https://cloud.google.com/vertex-ai/docs/training/custom-training)\n",
"* [Vertex AI Predictions](https://cloud.google.com/vertex-ai/docs/predictions/getting-predictions)\n",
"* [Notebooks](https://cloud.google.com/notebooks)\n",
"* [Vertex Training](https://cloud.google.com/vertex-ai/docs/training/custom-training)\n",
"* [Vertex Predictions](https://cloud.google.com/vertex-ai/docs/predictions/getting-predictions)\n",
"* [Cloud Storage](https://cloud.google.com/storage)\n",
"* [Container Registry](https://cloud.google.com/container-registry)\n",
"* [Cloud Build](https://cloud.google.com/build) *[Optional]*\n",
@@ -202,9 +202,9 @@
"id": "e0c1dcadc2c8"
},
"source": [
"We will be using [Vertex AI SDK for Python](https://cloud.google.com/vertex-ai/docs/start/client-libraries#python) to interact with Vertex AI services. The high-level `aiplatform` library is designed to simplify common data science workflows by using wrapper classes and opinionated defaults. \n",
"We will be using [Vertex SDK for Python](https://cloud.google.com/vertex-ai/docs/start/client-libraries#python) to interact with Vertex AI services. The high-level `aiplatform` library is designed to simplify common data science workflows by using wrapper classes and opinionated defaults. \n",
"\n",
"#### Install Vertex AI SDK for Python"
"#### Install Vertex SDK for Python"
]
},
{
@@ -658,8 +658,8 @@
},
"outputs": [],
"source": [
"dataset = load_dataset(\"imdb\")\n",
"dataset"
"datasets = load_dataset(\"imdb\")\n",
"datasets"
]
},
{
@@ -668,7 +668,7 @@
"id": "RzfPtOMoIrIu"
},
"source": [
"The `dataset` object itself is [`DatasetDict`](https://huggingface.co/docs/datasets/package_reference/main_classes.html#datasetdict), which contains one key for the training, validation and test set."
"The `datasets` object itself is [`DatasetDict`](https://huggingface.co/docs/datasets/package_reference/main_classes.html#datasetdict), which contains one key for the training, validation and test set."
]
},
{
@@ -681,12 +681,12 @@
"source": [
"print(\n",
" \"Total # of rows in training dataset {} and size {:5.2f} MB\".format(\n",
" dataset[\"train\"].shape[0], dataset[\"train\"].size_in_bytes / (1024 * 1024)\n",
" datasets[\"train\"].shape[0], datasets[\"train\"].size_in_bytes / (1024 * 1024)\n",
" )\n",
")\n",
"print(\n",
" \"Total # of rows in test dataset {} and size {:5.2f} MB\".format(\n",
" dataset[\"test\"].shape[0], dataset[\"test\"].size_in_bytes / (1024 * 1024)\n",
" datasets[\"test\"].shape[0], datasets[\"test\"].size_in_bytes / (1024 * 1024)\n",
" )\n",
")"
]
@@ -708,7 +708,7 @@
},
"outputs": [],
"source": [
"dataset[\"train\"][0]"
"datasets[\"train\"][0]"
]
},
{
@@ -728,7 +728,7 @@
},
"outputs": [],
"source": [
"label_list = dataset[\"train\"].unique(\"label\")\n",
"label_list = datasets[\"train\"].unique(\"label\")\n",
"label_list"
]
},
@@ -779,7 +779,7 @@
},
"outputs": [],
"source": [
"show_random_elements(dataset[\"train\"])"
"show_random_elements(datasets[\"train\"])"
]
},
{
@@ -883,7 +883,7 @@
},
"outputs": [],
"source": [
"example = dataset[\"train\"][4]\n",
"example = datasets[\"train\"][4]\n",
"print(example)"
]
},
@@ -920,7 +920,7 @@
"source": [
"# Dataset loading repeated here to make this cell idempotent\n",
"# Since we are over-writing datasets variable\n",
"dataset = load_dataset(\"imdb\")\n",
"datasets = load_dataset(\"imdb\")\n",
"\n",
"# Mapping labels to ids\n",
"# NOTE: We can extract this automatically but the `Unique` method of the datasets\n",
@@ -948,7 +948,7 @@
"\n",
"\n",
"# apply preprocessing function to input examples\n",
"dataset = dataset.map(preprocess_function, batched=True, load_from_cache_file=True)"
"datasets = datasets.map(preprocess_function, batched=True, load_from_cache_file=True)"
]
},
{
@@ -1091,8 +1091,8 @@
"trainer = Trainer(\n",
" model,\n",
" args,\n",
" train_dataset=dataset[\"train\"],\n",
" eval_dataset=dataset[\"test\"],\n",
" train_dataset=datasets[\"train\"],\n",
" eval_dataset=datasets[\"test\"],\n",
" data_collator=default_data_collator,\n",
" tokenizer=tokenizer,\n",
" compute_metrics=compute_metrics,\n",
@@ -1199,7 +1199,7 @@
"source": [
"### Run predictions locally with sample examples\n",
"\n",
"Using the trained model, we can predict the sentiment label for an input text after applying the preprocessing function that was used during the training. We will run the predictions locally in the notebook and later show how you can deploy the model to an endpoint using [TorchServe](https://pytorch.org/serve/) on Vertex AI Predictions."
"Using the trained model, we can predict the sentiment label for an input text after applying the preprocessing function that was used during the training. We will run the predictions locally in the notebook and later show how you can deploy the model to an endpoint using [TorchServe](https://pytorch.org/serve/) on Vertex Predictions."
]
},
{
@@ -1382,7 +1382,7 @@
"id": "f7466d414a0e"
},
"source": [
"### Run Custom Job on Vertex AI Training with a pre-built container"
"### Run Custom Job on Vertex Training with a pre-built container"
]
},
{
@@ -1395,7 +1395,7 @@
"\n",
"In this notebook, we are using Hugging Face Datasets and fine tuning a transformer model from Hugging Face Transformers Library for sentiment analysis task using PyTorch. We will use [pre-built container for PyTorch](https://cloud.google.com/vertex-ai/docs/training/pre-built-containers#pytorch) and package the training application code by adding standard Python dependencies - `transformers`, `datasets` and `tqdm` - in the `setup.py` file. \n",
"\n",
"![Training with Prebuilt Containers on Vertex AI Training](./images/training-with-prebuilt-containers-on-vertex-training.png)"
"![Training with Prebuilt Containers on Vertex Training](./images/training-with-prebuilt-containers-on-vertex-training.png)"
]
},
{
@@ -1569,7 +1569,7 @@
"source": [
"#### **Run custom training job on Vertex AI**\n",
"\n",
"We use [Vertex AI SDK for Python](https://cloud.google.com/vertex-ai/docs/start/client-libraries#client_libraries) to create and submit training job to the Vertex AI training service."
"We use [Vertex SDK for Python](https://cloud.google.com/vertex-ai/docs/start/client-libraries#client_libraries) to create and submit training job to the Vertex training service."
]
},
{
@@ -1578,7 +1578,7 @@
"id": "5d2957ef04fd"
},
"source": [
"##### **Initialize the Vertex AI SDK for Python**"
"##### **Initialize the Vertex SDK for Python**"
]
},
{
@@ -1598,7 +1598,7 @@
"id": "6b0fed34b728"
},
"source": [
"##### **Configure and submit Custom Job to Vertex AI Training service**"
"##### **Configure and submit Custom Job to Vertex Training service**"
]
},
{
@@ -1609,7 +1609,7 @@
"source": [
"Configure a [Custom Job](https://cloud.google.com/vertex-ai/docs/training/create-custom-job) with the [pre-built container](https://cloud.google.com/vertex-ai/docs/training/pre-built-containers) image for PyTorch and training code packaged as Python source distribution. \n",
"\n",
"**NOTE:** When using Vertex AI SDK for Python for submitting a training job, it creates a [Training Pipeline](https://cloud.google.com/vertex-ai/docs/training/create-training-pipeline) which launches the Custom Job on Vertex AI Training service."
"**NOTE:** When using Vertex SDK for Python for submitting a training job, it creates a [Training Pipeline](https://cloud.google.com/vertex-ai/docs/training/create-training-pipeline) which launches the Custom Job on Vertex Training service."
]
},
{
@@ -1686,7 +1686,7 @@
"\n",
"You can monitor the custom job launched from Cloud Console following the link [here](https://console.cloud.google.com/vertex-ai/training/training-pipelines/) or use gcloud CLI command [`gcloud beta ai custom-jobs stream-logs`](https://cloud.google.com/sdk/gcloud/reference/beta/ai/custom-jobs/stream-logs)\n",
"\n",
"![Monitor custom job progress in Vertex AI Training](./images/vertex-training-monitor-custom-job.png)"
"![Monitor custom job progress in Vertex Training](./images/vertex-training-monitor-custom-job.png)"
]
},
{
@@ -1798,7 +1798,7 @@
"id": "c170d386492b"
},
"source": [
"### Run Custom Job on Vertex AI Training with custom container"
"### Run Custom Job on Vertex Training with custom container"
]
},
{
@@ -1807,7 +1807,7 @@
"id": "035227b6e581"
},
"source": [
"To create a [training job with custom container](https://cloud.google.com/vertex-ai/docs/training/create-custom-container?hl=hr), you define a `Dockerfile` to install or add the dependencies required for the training job. Then, you build and test your Docker image locally to verify, push the image to Container Registry and submit a Custom Job to Vertex AI Training service.\n",
"To create a [training job with custom container](https://cloud.google.com/vertex-ai/docs/training/create-custom-container?hl=hr), you define a `Dockerfile` to install or add the dependencies required for the training job. Then, you build and test your Docker image locally to verify, push the image to Container Registry and submit a Custom Job to Vertex Training service.\n",
"\n",
"![Training with custom containers on Vertex AI](./images/training-with-custom-containers-on-vertex-training.png)"
]
@@ -1834,7 +1834,7 @@
"%%writefile ./custom_container/Dockerfile\n",
"\n",
"# Use pytorch GPU base image\n",
"FROM us-docker.pkg.dev/vertex-ai/training/pytorch-gpu.1-10:latest\n",
"FROM gcr.io/cloud-aiplatform/training/pytorch-gpu.1-7\n",
"\n",
"# set working directory\n",
"WORKDIR /app\n",
@@ -1968,7 +1968,7 @@
"id": "a23e5e34bea9"
},
"source": [
"##### **Initialize the Vertex AI SDK for Python**"
"##### **Initialize the Vertex SDK for Python**"
]
},
{
@@ -1988,11 +1988,11 @@
"id": "abf1fa4085cb"
},
"source": [
"##### **Configure and submit Custom Job to Vertex AI Training service**\n",
"##### **Configure and submit Custom Job to Vertex Training service**\n",
"\n",
"Configure a [Custom Job](https://cloud.google.com/vertex-ai/docs/training/create-custom-job) with the [custom container](https://cloud.google.com/vertex-ai/docs/training/create-custom-container) image with training code and other dependencies\n",
"\n",
"**NOTE:** When using Vertex AI SDK for Python for submitting a training job, it creates a [Training Pipeline](https://cloud.google.com/vertex-ai/docs/training/create-training-pipeline) which launches the Custom Job to train on Vertex AI Training."
"**NOTE:** When using Vertex SDK for Python for submitting a training job, it creates a [Training Pipeline](https://cloud.google.com/vertex-ai/docs/training/create-training-pipeline) which launches the Custom Job to train on Vertex Training."
]
},
{
@@ -2044,7 +2044,7 @@
},
"outputs": [],
"source": [
"# submit the custom job to Vertex AI training service\n",
"# submit the custom job to Vertex training service\n",
"model = job.run(\n",
" replica_count=1,\n",
" machine_type=\"n1-standard-8\",\n",
@@ -2065,7 +2065,7 @@
"\n",
"You can monitor the custom job launched from Cloud Console following the link [here](https://console.cloud.google.com/vertex-ai/training/training-pipelines/) or use gcloud CLI command [`gcloud beta ai custom-jobs stream-logs`](https://cloud.google.com/sdk/gcloud/reference/beta/ai/custom-jobs/stream-logs)\n",
"\n",
"![Monitor custom job progress in Vertex AI Training](./images/vertex-training-monitor-custom-job-container.png)"
"![Monitor custom job progress in Vertex Training](./images/vertex-training-monitor-custom-job-container.png)"
]
},
{
@@ -2148,11 +2148,11 @@
"id": "ba6122f929e3"
},
"source": [
"The training application code for fine-tuning a transformer model for sentiment analysis task uses hyperparameters such as learning rate and weight decay. These hyperparameters control the behavior of the training algorithm and can have a significant effect on the performance of the resulting model. This part of the notebook show how you can automate tuning these hyperparameters with Vertex AI Training service.\n",
"The training application code for fine-tuning a transformer model for sentiment analysis task uses hyperparameters such as learning rate and weight decay. These hyperparameters control the behavior of the training algorithm and can have a significant effect on the performance of the resulting model. This part of the notebook show how you can automate tuning these hyperparameters with Vertex Training service.\n",
"\n",
"We submit a [Hyperparameter Tuning job](https://cloud.google.com/vertex-ai/docs/training/hyperparameter-tuning-overview) to Vertex AI Training service by packaging the training application code and dependencies in a Docker container and push the container to Google Container Registry, similar to running a Custom Job on Vertex AI with Custom Container.\n",
"We submit a [Hyperparameter Tuning job](https://cloud.google.com/vertex-ai/docs/training/hyperparameter-tuning-overview) to Vertex Training service by packaging the training application code and dependencies in a Docker container and push the container to Google Container Registry, similar to running a Custom Job on Vertex AI with Custom Container.\n",
"\n",
"![Hyperparameter Tuning with Custom Containers on Vertex AI Training](./images/hp-tuning-with-custom-containers-on-vertex-training.png)"
"![Hyperparameter Tuning with Custom Containers on Vertex Training](./images/hp-tuning-with-custom-containers-on-vertex-training.png)"
]
},
{
@@ -2163,7 +2163,7 @@
"source": [
"### How hyperparameter tuning works in Vertex AI?\n",
"\n",
"Following are the high level steps involved in running a Hyperparameter Tuning job on Vertex AI Training service:\n",
"Following are the high level steps involved in running a Hyperparameter Tuning job on Vertex Training service:\n",
"\n",
"- You define the hyperparameters to tune the model along with the metric (or goal) to optimize\n",
"- Vertex AI runs multiple trials of your training application with the hyperparameters and limits you specified - maximum number of trials to run and number of parallel trials. \n",
@@ -2297,7 +2297,7 @@
"source": [
"### Run Hyperparameter Tuning Job on Vertex AI\n",
"\n",
"Before submitting the hyperparameter tuning job to Vertex AI, push the custom container image with training application to Google Cloud Container Registry and then submit the job to Vertex AI. We will be using the same image used for running Custom Job on Vertex AI Training service."
"Before submitting the hyperparameter tuning job to Vertex AI, push the custom container image with training application to Google Cloud Container Registry and then submit the job to Vertex AI. We will be using the same image used for running Custom Job on Vertex Training service."
]
},
{
@@ -2326,7 +2326,7 @@
"id": "f60fab07d67c"
},
"source": [
"##### **Initialize the Vertex AI SDK for Python**"
"##### **Initialize the Vertex SDK for Python**"
]
},
{
@@ -2346,7 +2346,7 @@
"id": "6652aa63ddff"
},
"source": [
"##### **Configure and submit Hyperparameter Tuning Job to Vertex AI Training service**\n",
"##### **Configure and submit Hyperparameter Tuning Job to Vertex Training service**\n",
"\n",
"Configure a [Hyperparameter Tuning Job](https://cloud.google.com/vertex-ai/docs/training/using-hyperparameter-tuning) with the [custom container](https://cloud.google.com/vertex-ai/docs/training/create-custom-container) image with training code and other dependencies.\n",
"\n",
@@ -2374,7 +2374,7 @@
"id": "9d46db3a8b23"
},
"source": [
"Define the training arguments with `hp-tune` argument set to `y` so that training application code can report metrics to Vertex AI"
"Define the training arguments with `hp-tune` argument set to `y` so that training application code can report metrics to Vertex"
]
},
{
@@ -2548,7 +2548,7 @@
"\n",
"You can monitor the hyperparameter tuning job launched from Cloud Console following the link [here](https://console.cloud.google.com/vertex-ai/training/hyperparameter-tuning-jobs/) or use gcloud CLI command [`gcloud beta ai custom-jobs stream-logs`](https://cloud.google.com/sdk/gcloud/reference/beta/ai/custom-jobs/stream-logs)\n",
"\n",
"![Monitor hyperparameter tuning job progress in Vertex AI Training](./images/vertex-training-monitor-hptuning-job-container.png)"
"![Monitor hyperparameter tuning job progress in Vertex Training](./images/vertex-training-monitor-hptuning-job-container.png)"
]
},
{
@@ -2557,7 +2557,7 @@
"id": "ba934b434f03"
},
"source": [
"After the job is finished, you can view and format the results of the hyperparameter tuning Trials (run by Vertex AI Training service) as a Pandas dataframe"
"After the job is finished, you can view and format the results of the hyperparameter tuning Trials (run by Vertex Training service) as a Pandas dataframe"
]
},
{
@@ -2612,7 +2612,7 @@
"id": "5dbccb2b7d32"
},
"source": [
"Now from the results of Trials, you can pick the best performing Trial to deploy to Vertex AI Predictions"
"Now from the results of Trials, you can pick the best performing Trial to deploy to Vertex Predictions"
]
},
{
@@ -2701,8 +2701,8 @@
"JOB_NAME=${JOB_PREFIX}-pytorch-hptune-$(date +%Y%m%d%H%M%S)\n",
"echo \"Launching hyperparameter tuning job with display name as \"$JOB_NAME\n",
"\n",
"# BUCKET_NAME is a required parameter to run the cell.\n",
"BUCKET_NAME=$1\n",
"# BUCKET_NAME: Change to your bucket name\n",
"BUCKET_NAME=$1 # <-- CHANGE TO YOUR BUCKET NAME\n",
"\n",
"# APP_NAME: get application name\n",
"APP_NAME=$2\n",
@@ -2711,7 +2711,7 @@
"JOB_DIR=${BUCKET_NAME}/${JOB_PREFIX}/model/${JOB_NAME}\n",
"\n",
"# custom container image URI\n",
"CUSTOM_TRAIN_IMAGE_URI='gcr.io/'${PROJECT_ID}'/pytorch_gpu_train_'${APP_NAME}\n",
"CUSTOM_TRAIN_IMAGE_URI=f'gcr.io/'${PROJECT_ID}'/pytorch_gpu_train_'${APP_NAME}\n",
"\n",
"# ========================================================\n",
"# create hyperparameter tuning configuration file\n",
@@ -2772,20 +2772,20 @@
"source": [
"## Deploying\n",
"\n",
"Deploying a PyTorch model on [Vertex AI Predictions](https://cloud.google.com/vertex-ai/docs/predictions/getting-predictions) requires to use a custom container that serves online predictions. You will deploy a container running [PyTorch's TorchServe](https://pytorch.org/serve/) tool in order to serve predictions from a fine-tuned transformer model from Hugging Face Transformers for sentiment analysis task. You can then use Vertex AI Predictions to classify sentiment of input texts. \n",
"Deploying a PyTorch model on [Vertex Predictions](https://cloud.google.com/vertex-ai/docs/predictions/getting-predictions) requires to use a custom container that serves online predictions. You will deploy a container running [PyTorch's TorchServe](https://pytorch.org/serve/) tool in order to serve predictions from a fine-tuned transformer model from Hugging Face Transformers for sentiment analysis task. You can then use Vertex Predictions to classify sentiment of input texts. \n",
"\n",
"### Deploying model on Vertex AI Predictions with custom container\n",
"### Deploying model on Vertex Predictions with custom container\n",
"\n",
"To use a custom container to serve predictions from a PyTorch model, you must provide Vertex AI with a Docker container image that runs an HTTP server, such as TorchServe in this case. Please refer to [documentation](https://cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements) that describes the container image requirements to be compatible with Vertex AI Predictions.\n",
"To use a custom container to serve predictions from a PyTorch model, you must provide Vertex AI with a Docker container image that runs an HTTP server, such as TorchServe in this case. Please refer to [documentation](https://cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements) that describes the container image requirements to be compatible with Vertex Predictions.\n",
"\n",
"![Serving with Custom Containers on Vertex AI Predictions](./images/serve-pytorch-model-on-vertex-predictions-with-custom-containers.png)\n",
"![Serving with Custom Containers on Vertex Predictions](./images/serve-pytorch-model-on-vertex-predictions-with-custom-containers.png)\n",
"\n",
"Essentially, to deploy a PyTorch model on Vertex AI Predictions following are the steps:\n",
"Essentially, to deploy a PyTorch model on Vertex Predictions following are the steps:\n",
"\n",
"1. Package the trained model artifacts including [default](https://pytorch.org/serve/#default-handlers) or [custom](https://pytorch.org/serve/custom_service.html) handlers by creating an archive file using [Torch model archiver](https://github.com/pytorch/serve/tree/master/model-archiver)\n",
"2. Build a [custom container](https://cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements) compatible with Vertex AI Predictions to serve the model using Torchserve\n",
"3. Upload the model with custom container image to serve predictions as a Vertex AI Model resource\n",
"4. Create a Vertex AI Endpoint and [deploy the model](https://cloud.google.com/vertex-ai/docs/predictions/deploy-model-api) resource"
"2. Build a [custom container](https://cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements) compatible with Vertex Predictions to serve the model using Torchserve\n",
"3. Upload the model with custom container image to serve predictions as a Vertex Model resource\n",
"4. Create a Vertex Endpoint and [deploy the model](https://cloud.google.com/vertex-ai/docs/predictions/deploy-model-api) resource"
]
},
{
@@ -2815,7 +2815,7 @@
},
"outputs": [],
"source": [
"%%writefile predictor/custom_handler.py\n",
"%%writefile predictor/custom_text_handler.py\n",
"\n",
"import os\n",
"import json\n",
@@ -2870,8 +2870,7 @@
" with open(mapping_file_path) as f:\n",
" self.mapping = json.load(f)\n",
" else:\n",
" logger.warning('Missing the index_to_name.json file. Inference output will default.')\n",
" self.mapping = {\"0\": \"Negative\", \"1\": \"Positive\"}\n",
" logger.warning('Missing the index_to_name.json file. Inference output will not include class name.')\n",
"\n",
" self.initialized = True\n",
"\n",
@@ -3048,13 +3047,10 @@
"FROM pytorch/torchserve:latest-cpu\n",
"\n",
"# install dependencies\n",
"RUN python3 -m pip install --upgrade pip\n",
"RUN pip3 install transformers\n",
"\n",
"USER model-server\n",
"\n",
"# copy model artifacts, custom handler and other dependencies\n",
"COPY ./custom_handler.py /home/model-server/\n",
"COPY ./custom_text_handler.py /home/model-server/\n",
"COPY ./index_to_name.json /home/model-server/\n",
"COPY ./model/$APP_NAME/ /home/model-server/\n",
"\n",
@@ -3074,7 +3070,7 @@
" --model-name=$APP_NAME \\\n",
" --version=1.0 \\\n",
" --serialized-file=/home/model-server/pytorch_model.bin \\\n",
" --handler=/home/model-server/custom_handler.py \\\n",
" --handler=/home/model-server/custom_text_handler.py \\\n",
" --extra-files \"/home/model-server/config.json,/home/model-server/tokenizer.json,/home/model-server/training_args.bin,/home/model-server/tokenizer_config.json,/home/model-server/special_tokens_map.json,/home/model-server/vocab.txt,/home/model-server/index_to_name.json\" \\\n",
" --export-path=/home/model-server/model-store\n",
"\n",
@@ -3133,7 +3129,7 @@
"source": [
"#### **Run the container locally** ***[Optional]***\n",
"\n",
"Before push the container image to Container Registry to use it with Vertex AI Predictions, you can run it as a container in your local environment to verify that the server works as expected"
"Before push the container image to Container Registry to use it with Vertex Predictions, you can run it as a container in your local environment to verify that the server works as expected"
]
},
{
@@ -3271,9 +3267,9 @@
"id": "69477b3a00c0"
},
"source": [
"#### **Deploying the serving container to Vertex AI Predictions**\n",
"#### **Deploying the serving container to Vertex Predictions**\n",
"\n",
"We create a model resource on Vertex AI and deploy the model to a Vertex AI Endpoints. You must deploy a model to an endpoint before using the model. The deployed model runs the custom container image to serve predictions. "
"We create a model resource on Vertex AI and deploy the model to a Vertex Endpoints. You must deploy a model to an endpoint before using the model. The deployed model runs the custom container image to serve predictions. "
]
},
{
@@ -3304,7 +3300,7 @@
"id": "a3da91e19af4"
},
"source": [
"##### **Initialize the Vertex AI SDK for Python**"
"##### **Initialize the Vertex SDK for Python**"
]
},
{
@@ -3441,7 +3437,7 @@
"id": "bc4673478269"
},
"source": [
"#### **Invoking the Endpoint with deployed Model using Vertex AI SDK to make predictions**"
"#### **Invoking the Endpoint with deployed Model using Vertex SDK to make predictions**"
]
},
{
@@ -3491,7 +3487,7 @@
"source": [
"##### **Formatting input for online prediction**\n",
"\n",
"This notebook uses [Torchserve's KServe based inference API](https://pytorch.org/serve/inference_api.html#kserve-inference-api) which is also [Vertex AI Predictions compatible format](https://cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements#prediction). For online prediction requests, format the prediction input instances as JSON with base64 encoding as shown here:\n",
"For online prediction requests, the prediction input instances must be formatted as JSON with base64 encoding as shown here:\n",
"\n",
"```\n",
"[\n",
@@ -3564,9 +3560,9 @@
},
"source": [
"##### ***[Optional]*** **Make prediction requests using gcloud CLI**\n",
"You can also call the Vertex AI Endpoint to make predictions using [`gcloud beta ai endpoints predict`](https://cloud.google.com/sdk/gcloud/reference/beta/ai/endpoints/predict). \n",
"You can also call the Vertex Endpoint to make predictions using [`gcloud beta ai endpoints predict`](https://cloud.google.com/sdk/gcloud/reference/beta/ai/endpoints/predict). \n",
"\n",
"The following cell shows how to make a prediction request to Vertex AI Endpoints using `gcloud` CLI: "
"The following cell shows how to make a prediction request to Vertex Endpoints using `gcloud` CLI: "
]
},
{
@@ -3657,12 +3653,12 @@
},
"outputs": [],
"source": [
"delete_custom_job = False\n",
"delete_hp_tuning_job = False\n",
"delete_custom_job = True\n",
"delete_hp_tuning_job = True\n",
"delete_endpoint = True\n",
"delete_model = False\n",
"delete_bucket = False\n",
"delete_image = False"
"delete_model = True\n",
"delete_bucket = True\n",
"delete_image = True"
]
},
{
@@ -3690,7 +3686,7 @@
"\n",
"client_options = {\"api_endpoint\": API_ENDPOINT}\n",
"\n",
"# Initialize Vertex AI SDK\n",
"# Initialize Vertex SDK\n",
"aiplatform.init(project=PROJECT_ID, staging_bucket=BUCKET_NAME)"
]
},
@@ -3928,7 +3924,7 @@
"if delete_bucket and \"BUCKET_NAME\" in globals():\n",
" print(f\"Deleting all contents from the bucket {BUCKET_NAME}\")\n",
"\n",
" shell_output = ! gsutil du -as $BUCKET_NAME\n",
" shell_output=! gsutil du -as $BUCKET_NAME\n",
" print(\n",
" f\"Size of the bucket {BUCKET_NAME} before deleting = {shell_output[0].split()[0]} bytes\"\n",
" )\n",
@@ -3936,7 +3932,7 @@
" # uncomment below line to delete contents of the bucket\n",
" # ! gsutil rm -r $BUCKET_NAME\n",
"\n",
" shell_output = ! gsutil du -as $BUCKET_NAME\n",
" shell_output=! gsutil du -as $BUCKET_NAME\n",
" if float(shell_output[0].split()[0]) > 0:\n",
" print(\n",
" \"PLEASE UNCOMMENT LINE TO DELETE BUCKET. CONTENT FROM THE BUCKET NOT DELETED\"\n",
@@ -1,28 +0,0 @@
# Train and deploy a scikit-learn model with Vertex AI
This repository shows how to train and deploy a text classifier using scikit-learn and Vertex AI.
The main used Vertex AI features are:
- Vertex AI Custom Training
- Vertex AI Model
- Vertex AI Endpoint
Further used GCP services are:
- Google Cloud Logging
- Google Cloud Storage
## Repository
├── README.md
├── create_job.ipynb # <-- creates the training job and deploys the model
├── requirements.txt # <-- requirements for deploying the job
└── task.py # <-- contains the training application
## Training job overview
The training job performs the following steps:
1. Downloads the `NewsAggregator` dataset from the UCI Machine Learning Repository
2. Trains and evaluates a classifier using scikit-learn
3. Exports model and evaluation artifacts to GCS
4. Deploys the model as a `Vertex AI Endpoint`
@@ -1,290 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "72b875d67303"
},
"source": [
"# Create and run a custom Vertex AI Training Job from a local script"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "398b976f501e"
},
"source": [
"## Install Vertex AI Python Client"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "2162adc1d8fe"
},
"outputs": [],
"source": [
"!pip install -r requirements.txt --upgrade"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "d4bf4ab70e65"
},
"source": [
"## GCP authentication"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "41084de2e96a"
},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\n",
" \"GOOGLE_APPLICATION_CREDENTIALS\"\n",
"] = \"\" # TODO: path to credentials .json file"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "1ce7efb99a95"
},
"source": [
"## Create the custom Vertex AI Training Job\n",
"\n",
"1. Define the custom job parameters\n",
"2. Submit the job to create a `Vertex AI Model`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "c04b9efb5eb3"
},
"outputs": [],
"source": [
"# Import the Vertex AI SDK (Python Client)\n",
"from google.cloud import aiplatform"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "b56f5fdcefd6"
},
"outputs": [],
"source": [
"# Project meta data\n",
"PROJECT_ID = \"\" # TODO\n",
"REGION = \"\" # TODO e.g. europe\n",
"ZONE = \"\" # TODO e.g. west4\n",
"LOCATION = f\"{REGION}-{ZONE}\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "4af8cfb1e1a1"
},
"outputs": [],
"source": [
"aiplatform.init(project=PROJECT_ID, location=LOCATION)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "e23d0161b489"
},
"outputs": [],
"source": [
"# Variables for specifying the job\n",
"DISPLAY_NAME = (\n",
" \"news-classifier-training\" # TODO: How the job is displayed on Vertex AI GUI\n",
")\n",
"SCRIPT_PATH = \"./task.py\" # Path to local training script\n",
"STAGING_BUCKET = (\n",
" \"\" # TODO GCS URI where meta data and artifacts are stored for this job\n",
")\n",
"MODEL_TRAINING_IMAGE = f\"{REGION}-docker.pkg.dev/vertex-ai/training/scikit-learn-cpu.0-23:latest\" # Pre-built training image\n",
"REQUIREMENTS = [\"wget\"] # Additional requirements not already part of the base image\n",
"# !Required if the Training Pipeline produces a managed Vertex AI Model!\n",
"MODEL_SERVING_IMAGE = f\"{REGION}-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.0-23:latest\" # Pre-built serving image"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "61565ec3e6de"
},
"outputs": [],
"source": [
"# Job definition\n",
"custom_training_job = aiplatform.CustomTrainingJob(\n",
" project=PROJECT_ID,\n",
" location=LOCATION,\n",
" display_name=DISPLAY_NAME,\n",
" script_path=SCRIPT_PATH,\n",
" staging_bucket=STAGING_BUCKET,\n",
" container_uri=MODEL_TRAINING_IMAGE,\n",
" requirements=REQUIREMENTS,\n",
" model_serving_container_image_uri=MODEL_SERVING_IMAGE,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "8c8d4bd78688"
},
"outputs": [],
"source": [
"# Variables for running the job\n",
"MACHINE_TYPE = \"n1-standard-4\" # Standard VM with 4 CPUs\n",
"# !Required if the Training Pipeline produces a managed Vertex AI Model!\n",
"MODEL_DISPLAY_NAME = (\n",
" \"news-classifier-model\" # TODO: Name for the resulting managed Vertex AI Model.\n",
")\n",
"# Note that a single job may produce multiple models (e.g. one per run).\n",
"# The url to download the training data from.\n",
"DATASET_URL = \"https://archive.ics.uci.edu/ml/machine-learning-databases/00359/NewsAggregatorDataset.zip\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "13d31a7d0bb5"
},
"outputs": [],
"source": [
"# Run the job\n",
"model = custom_training_job.run(\n",
" machine_type=MACHINE_TYPE,\n",
" model_display_name=MODEL_DISPLAY_NAME,\n",
" args=[f\"--dataset_url={DATASET_URL}\", f\"--project_id={PROJECT_ID}\"],\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "8026ac119722"
},
"outputs": [],
"source": [
"MODEL_RESOURCE_NAME = model.resource_name"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "2d985fca37f3"
},
"source": [
"## Deploy model to Vertex AI Endpoint\n",
"\n",
"1. Retrieve the registered `Vertex AI Model`\n",
"2. Deploy the model to a new `Vertex AI Endpoint`\n",
"3. Get some test predictions from the endpoint"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "58c08631f94c"
},
"outputs": [],
"source": [
"ENDPOINT_DISPLAY_NAME = \"news-classifier-endpoint\" # TODO\n",
"MACHINE_TYPE_SERVING = \"n1-standard-2\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "b54867240880"
},
"outputs": [],
"source": [
"endpoint = aiplatform.Endpoint.create(\n",
" display_name=ENDPOINT_DISPLAY_NAME,\n",
" location=LOCATION,\n",
" project=PROJECT_ID,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "a76e7c721b88"
},
"outputs": [],
"source": [
"model = aiplatform.Model(model_name=MODEL_RESOURCE_NAME)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "d06714302f81"
},
"outputs": [],
"source": [
"model.deploy(\n",
" endpoint=endpoint,\n",
" deployed_model_display_name=MODEL_DISPLAY_NAME,\n",
" machine_type=MACHINE_TYPE,\n",
" traffic_percentage=100,\n",
" min_replica_count=1,\n",
" max_replica_count=1,\n",
" accelerator_type=None,\n",
" accelerator_count=None,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "5b7eca31d80e"
},
"outputs": [],
"source": [
"endpoint.predict(instances={\"instances\": [\"A news headline to be classified\"]})"
]
}
],
"metadata": {
"colab": {
"name": "create_job.ipynb",
"toc_visible": true
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
@@ -1,2 +0,0 @@
google-cloud-aiplatform
ipykernel
@@ -1,167 +0,0 @@
import argparse
import logging
import os
import pickle
import zipfile
from typing import List, Tuple
import pandas as pd
import wget
from google.cloud import storage
from google.cloud.logging import Client as LogClient
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
def download_dataset_from_url(url: str) -> pd.DataFrame:
"""Downloads and unzips the dataset from `url` and reads it with pandas.
Args:
url (str, optional): URL to the dataset.
"""
zip_filepath = wget.download(url, out=".")
with zipfile.ZipFile(zip_filepath, "r") as zf:
zf.extract(path=".", member="newsCorpora.csv")
COLUMN_NAMES = ["id", "title", "url", "publisher",
"category", "story", "hostname", "timestamp"]
return pd.read_csv(
"newsCorpora.csv", delimiter="\t", names=COLUMN_NAMES, index_col=0
)
def get_train_test_data(dataframe: pd.DataFrame, test_size: float = 0.2
) -> Tuple[List, List, List, List]:
"""Splits the news dataset into train and test features and labels.
Args:
news (pd.DataFrame): The dataset as pandas DataFrame.
test_size (float): The size in percent of the test data.
Returns:
Tuple[List, List, List, List]: Tuple with train and test data
"""
train, test = train_test_split(dataframe, test_size=test_size)
x_train, y_train = train["title"].values, train["category"].values
x_test, y_test = test["title"].values, test["category"].values
return x_train, y_train, x_test, y_test
def export_model_to_gcs(fitted_pipeline: Pipeline, gcs_uri: str) -> str:
"""Exports trained pipeline to GCS
Parameters:
fitted_pipeline (sklearn.pipelines.Pipeline): the Pipeline object
with data already fitted (trained pipeline object).
gcs_uri (str): GCS path to store the trained pipeline
i.e gs://example_bucket/training-job.
Returns:
export_path (str): Model GCS location
"""
artifact_filename = 'model.pkl'
# Save model artifact to local filesystem (doesn't persist)
local_path = artifact_filename
with open(local_path, 'wb') as model_file:
pickle.dump(fitted_pipeline, model_file)
# Upload model artifact to Cloud Storage
storage_path = os.path.join(gcs_uri, artifact_filename)
blob = storage.blob.Blob.from_string(storage_path, client=storage.Client())
blob.upload_from_filename(local_path)
def export_evaluation_report_to_gcs(report: str, gcs_uri: str) -> None:
"""
Exports training job report to GCS
Parameters:
report (str): Full report in text to sent to GCS
gcs_uri (str): GCS path to store the report
i.e gs://example_bucket/training-job
"""
artifact_filename = 'report.txt'
# Upload model artifact to Cloud Storage
storage_path = os.path.join(gcs_uri, artifact_filename)
blob = storage.blob.Blob.from_string(storage_path, client=storage.Client())
blob.upload_from_string(report)
def train_and_score(X_train: List, y_train: List, X_test: List, y_test: List
) -> Tuple[Pipeline, float]:
"""Trains and cross-validates a text classifier pipeline.
Args:
X_train (List): Train features as list of strings.
y_train (List): Train labels as list of strings.
X_test (List): Test labels as list of strings.
y_test (List): Test labels as list of strings.
Returns:
Tuple[Pipeline, float]: Fitted pipeline and mean accuracy.
"""
pipeline = Pipeline([
("vectorizer", CountVectorizer()),
("tfidf", TfidfTransformer()),
("naivebayes", MultinomialNB()),
])
pipeline.fit(X_train, y_train)
score = pipeline.score(X_test, y_test)
return pipeline, score
# Define all the command line arguments your model can accept for training
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--dataset_url",
help="Download url for the training data.",
type=str
)
parser.add_argument(
"--project_id",
help="GCP project id for cloud logging.",
type=str
)
args = parser.parse_args()
arguments = args.__dict__
# set up the GCP logger
client = LogClient(project=arguments["project_id"])
client.setup_logging(log_level=logging.INFO)
logging.info("Starting custom training job.")
# download the data from url
logging.info("Downloading training data from: {}".format(arguments["dataset_url"]))
dataframe = download_dataset_from_url(arguments["dataset_url"])
train_test_data = get_train_test_data(dataframe)
# train and cross validate
logging.info("Training started ...")
model, score = train_and_score(*train_test_data)
logging.info(f"Training completed with model score: {score}")
# export model to gcs
_gcs_uri = os.environ["AIP_MODEL_DIR"]
logging.info("Exporting model artifacts ...")
export_model_to_gcs(model, _gcs_uri)
export_evaluation_report_to_gcs(str(score), _gcs_uri)
logging.info(f"Exported model artifacts to GCS bucket: {_gcs_uri}")
@@ -188,14 +188,11 @@
},
"outputs": [],
"source": [
"! pip3 install {USER_FLAG} google-cloud-aiplatform\n",
"! pip3 install {USER_FLAG} google-cloud-pipeline-components\n",
"! pip3 install {USER_FLAG} google-cloud-aiplatform==1.0.1\n",
"! pip3 install {USER_FLAG} google-cloud-pipeline-components==0.1.3\n",
"! pip3 install {USER_FLAG} --upgrade kfp\n",
"! pip3 install {USER_FLAG} numpy\n",
"! pip3 install {USER_FLAG} --upgrade tensorflow\n",
"! pip3 install {USER_FLAG} --upgrade pillow\n",
"! pip3 install {USER_FLAG} --upgrade tf-agents\n",
"! pip3 install {USER_FLAG} --upgrade fastapi"
"! pip3 install {USER_FLAG} numpy==1.20.3\n",
"! pip3 install {USER_FLAG} --upgrade tensorflow"
]
},
{
@@ -290,7 +287,7 @@
"\n",
"# Get your Google Cloud project ID from gcloud\n",
"if not os.getenv(\"IS_TESTING\"):\n",
" shell_output = !gcloud config list --format 'value(core.project)' 2>/dev/null\n",
" shell_output=!gcloud config list --format 'value(core.project)' 2>/dev/null\n",
" PROJECT_ID = shell_output[0]\n",
" print(\"Project ID: \", PROJECT_ID)"
]
@@ -521,7 +518,6 @@
"import os\n",
"import sys\n",
"\n",
"from google.cloud import aiplatform\n",
"from google_cloud_pipeline_components import aiplatform as gcc_aip\n",
"from kfp.v2 import compiler, dsl\n",
"from kfp.v2.google.client import AIPlatformClient"
@@ -561,30 +557,6 @@
"You may use the default values below as is."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "895ac243c125"
},
"outputs": [],
"source": [
"# Dataset parameters\n",
"RAW_DATA_PATH = \"gs://[your-bucket-name]/raw_data/u.data\" # @param {type:\"string\"}"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "62bfb9a820f6"
},
"outputs": [],
"source": [
"# Download the sample data into your RAW_DATA_PATH\n",
"! gsutil cp \"gs://cloud-samples-data/vertex-ai/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/u.data\" $RAW_DATA_PATH"
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -593,6 +565,9 @@
},
"outputs": [],
"source": [
"# Dataset parameters\n",
"RAW_DATA_PATH = \"gs://cloud-samples-data/vertex-ai/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/u.data\" # Location of the MovieLens 100K dataset's \"u.data\" file.\n",
"\n",
"# Pipeline parameters\n",
"PIPELINE_NAME = \"movielens-pipeline\" # Pipeline display name.\n",
"ENABLE_CACHING = False # Whether to enable execution caching for the pipeline.\n",
@@ -660,7 +635,7 @@
"source": [
"#### Run unit tests on the Generator component\n",
"\n",
"Before running the command, you should update the `RAW_DATA_PATH` in [`src/generator/test_generator_component.py`](src/generator/test_generator_component.py)."
"Before running the command, fill in `RAW_DATA_PATH` in [`src/generator/test_generator_component.py`](src/generator/test_generator_component.py)."
]
},
{
@@ -738,12 +713,12 @@
"TRAINING_ARTIFACTS_DIR = (\n",
" f\"{BUCKET_NAME}/artifacts\" # Root directory for training artifacts.\n",
")\n",
"TRAINING_REPLICA_COUNT = 1 # Number of replica to run the custom training job.\n",
"TRAINING_REPLICA_COUNT = \"1\" # Number of replica to run the custom training job.\n",
"TRAINING_MACHINE_TYPE = (\n",
" \"n1-standard-4\" # Type of machine to run the custom training job.\n",
")\n",
"TRAINING_ACCELERATOR_TYPE = \"ACCELERATOR_TYPE_UNSPECIFIED\" # Type of accelerators to run the custom training job.\n",
"TRAINING_ACCELERATOR_COUNT = 0 # Number of accelerators for the custom training job."
"TRAINING_ACCELERATOR_COUNT = \"0\" # Number of accelerators for the custom training job."
]
},
{
@@ -794,12 +769,8 @@
"TRAINED_POLICY_DISPLAY_NAME = (\n",
" \"movielens-trained-policy\" # Display name of the uploaded and deployed policy.\n",
")\n",
"TRAFFIC_SPLIT = {\"0\": 100}\n",
"ENDPOINT_DISPLAY_NAME = \"movielens-endpoint\" # Display name of the prediction endpoint.\n",
"ENDPOINT_MACHINE_TYPE = \"n1-standard-4\" # Type of machine of the prediction endpoint.\n",
"ENDPOINT_REPLICA_COUNT = 1 # Number of replicas of the prediction endpoint.\n",
"ENDPOINT_ACCELERATOR_TYPE = \"ACCELERATOR_TYPE_UNSPECIFIED\" # Type of accelerators to run the custom training job.\n",
"ENDPOINT_ACCELERATOR_COUNT = 0 # Number of accelerators for the custom training job."
"ENDPOINT_MACHINE_TYPE = \"n1-standard-4\" # Type of machine of the prediction endpoint."
]
},
{
@@ -929,17 +900,16 @@
},
"outputs": [],
"source": [
"from google_cloud_pipeline_components.experimental.custom_job import utils\n",
"from kfp.components import load_component_from_url\n",
"\n",
"generate_op = load_component_from_url(\n",
" \"https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/62a2a7611499490b4b04d731d48a7ba87c2d636f/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/mlops_pipeline_tf_agents_bandits_movie_recommendation/src/generator/component.yaml\"\n",
" \"https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/68d6cf46ee22a9b9295d62ea71996150baf8db94/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/mlops_pipeline_tf_agents_bandits_movie_recommendation/src/generator/component.yaml\"\n",
")\n",
"ingest_op = load_component_from_url(\n",
" \"https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/62a2a7611499490b4b04d731d48a7ba87c2d636f/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/mlops_pipeline_tf_agents_bandits_movie_recommendation/src/ingester/component.yaml\"\n",
" \"https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/68d6cf46ee22a9b9295d62ea71996150baf8db94/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/mlops_pipeline_tf_agents_bandits_movie_recommendation/src/ingester/component.yaml\"\n",
")\n",
"train_op = load_component_from_url(\n",
" \"https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/62a2a7611499490b4b04d731d48a7ba87c2d636f/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/mlops_pipeline_tf_agents_bandits_movie_recommendation/src/trainer/component.yaml\"\n",
" \"https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/68d6cf46ee22a9b9295d62ea71996150baf8db94/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/mlops_pipeline_tf_agents_bandits_movie_recommendation/src/trainer/component.yaml\"\n",
")\n",
"\n",
"\n",
@@ -1008,7 +978,7 @@
" bigquery_location=bigquery_location,\n",
" bigquery_table_id=bigquery_table_id,\n",
" )\n",
" \n",
"\n",
" # Run the Ingester component.\n",
" ingest_task = ingest_op(\n",
" project_id=project_id,\n",
@@ -1018,16 +988,7 @@
" )\n",
"\n",
" # Run the Trainer component and submit custom job to Vertex AI.\n",
" # Convert the train_op component into a Vertex AI Custom Job pre-built component\n",
" custom_job_training_op = utils.create_custom_training_job_op_from_component(\n",
" component_spec=train_op,\n",
" replica_count=TRAINING_REPLICA_COUNT,\n",
" machine_type=TRAINING_MACHINE_TYPE,\n",
" accelerator_type=TRAINING_ACCELERATOR_TYPE,\n",
" accelerator_count=TRAINING_ACCELERATOR_COUNT,\n",
" )\n",
"\n",
" train_task = custom_job_training_op(\n",
" train_task = train_op(\n",
" training_artifacts_dir=training_artifacts_dir,\n",
" tfrecord_file=ingest_task.outputs[\"tfrecord_file\"],\n",
" num_epochs=num_epochs,\n",
@@ -1035,10 +996,28 @@
" num_actions=num_actions,\n",
" tikhonov_weight=tikhonov_weight,\n",
" agent_alpha=agent_alpha,\n",
" project=PROJECT_ID,\n",
" location=REGION,\n",
" )\n",
"\n",
" worker_pool_specs = [\n",
" {\n",
" \"containerSpec\": {\n",
" \"imageUri\": train_task.container.image,\n",
" },\n",
" \"replicaCount\": TRAINING_REPLICA_COUNT,\n",
" \"machineSpec\": {\n",
" \"machineType\": TRAINING_MACHINE_TYPE,\n",
" \"acceleratorType\": TRAINING_ACCELERATOR_TYPE,\n",
" \"acceleratorCount\": TRAINING_ACCELERATOR_COUNT,\n",
" },\n",
" },\n",
" ]\n",
" train_task.custom_job_spec = {\n",
" \"displayName\": train_task.name,\n",
" \"jobSpec\": {\n",
" \"workerPoolSpecs\": worker_pool_specs,\n",
" },\n",
" }\n",
"\n",
" # Run the Deployer components.\n",
" # Upload the trained policy as a model.\n",
" model_upload_op = gcc_aip.ModelUploadOp(\n",
@@ -1055,14 +1034,11 @@
" # Deploy the uploaded, trained policy to the created endpoint. (This operation\n",
" # has to occur after both model uploading and endpoint creation complete.)\n",
" gcc_aip.ModelDeployOp(\n",
" project=project_id,\n",
" endpoint=endpoint_create_op.outputs[\"endpoint\"],\n",
" model=model_upload_op.outputs[\"model\"],\n",
" deployed_model_display_name=TRAINED_POLICY_DISPLAY_NAME,\n",
" traffic_split=TRAFFIC_SPLIT,\n",
" dedicated_resources_machine_type=ENDPOINT_MACHINE_TYPE,\n",
" dedicated_resources_accelerator_type=ENDPOINT_ACCELERATOR_TYPE,\n",
" dedicated_resources_accelerator_count=ENDPOINT_ACCELERATOR_COUNT,\n",
" dedicated_resources_min_replica_count=ENDPOINT_REPLICA_COUNT,\n",
" machine_type=ENDPOINT_MACHINE_TYPE,\n",
" )"
]
},
@@ -1077,11 +1053,12 @@
"# Compile the authored pipeline.\n",
"compiler.Compiler().compile(pipeline_func=pipeline, package_path=PIPELINE_SPEC_PATH)\n",
"\n",
"# Createa Vertex AI client.\n",
"api_client = AIPlatformClient(project_id=PROJECT_ID, region=REGION)\n",
"\n",
"# Create a pipeline run job.\n",
"job = aiplatform.PipelineJob(\n",
" display_name=f\"{PIPELINE_NAME}-startup\",\n",
" template_path=PIPELINE_SPEC_PATH,\n",
" pipeline_root=PIPELINE_ROOT,\n",
"response = api_client.create_run_from_job_spec(\n",
" job_spec_path=PIPELINE_SPEC_PATH,\n",
" parameter_values={\n",
" # Pipeline configs\n",
" \"project_id\": PROJECT_ID,\n",
@@ -1093,9 +1070,7 @@
" \"bigquery_table_id\": BIGQUERY_TABLE_ID,\n",
" },\n",
" enable_caching=ENABLE_CACHING,\n",
")\n",
"\n",
"job.run()"
")"
]
},
{
@@ -1136,11 +1111,7 @@
"SIMULATOR_SCHEDULE = \"*/5 * * * *\" # Cloud Scheduler cron job schedule for the Simulator. Eg. \"*/5 * * * *\" means every 5 mins.\n",
"SIMULATOR_SCHEDULER_MESSAGE = (\n",
" \"simulator-message\" # Cloud Scheduler message for the Simulator.\n",
")\n",
"# TF-Agents RL configs\n",
"BATCH_SIZE = 8\n",
"RANK_K = 20\n",
"NUM_ACTIONS = 20"
")"
]
},
{
@@ -1250,7 +1221,7 @@
},
"outputs": [],
"source": [
"endpoints = ! gcloud ai endpoints list \\\n",
"endpoints = ! gcloud beta ai endpoints list \\\n",
" --region=$REGION \\\n",
" --filter=display_name=$ENDPOINT_DISPLAY_NAME\n",
"print(\"\\n\".join(endpoints), \"\\n\")\n",
@@ -1453,11 +1424,13 @@
},
"outputs": [],
"source": [
"from kfp.components import load_component_from_url\n",
"\n",
"ingest_op = load_component_from_url(\n",
" \"https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/62a2a7611499490b4b04d731d48a7ba87c2d636f/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/mlops_pipeline_tf_agents_bandits_movie_recommendation/src/ingester/component.yaml\"\n",
" \"https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/68d6cf46ee22a9b9295d62ea71996150baf8db94/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/mlops_pipeline_tf_agents_bandits_movie_recommendation/src/ingester/component.yaml\"\n",
")\n",
"train_op = load_component_from_url(\n",
" \"https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/62a2a7611499490b4b04d731d48a7ba87c2d636f/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/mlops_pipeline_tf_agents_bandits_movie_recommendation/src/trainer/component.yaml\"\n",
" \"https://raw.githubusercontent.com/GoogleCloudPlatform/vertex-ai-samples/68d6cf46ee22a9b9295d62ea71996150baf8db94/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk/mlops_pipeline_tf_agents_bandits_movie_recommendation/src/trainer/component.yaml\"\n",
")\n",
"\n",
"\n",
@@ -1508,16 +1481,7 @@
" )\n",
"\n",
" # Run the Trainer component and submit custom job to Vertex AI.\n",
" # Convert the train_op component into a Vertex AI Custom Job pre-built component\n",
" custom_job_training_op = utils.create_custom_training_job_op_from_component(\n",
" component_spec=train_op,\n",
" replica_count=TRAINING_REPLICA_COUNT,\n",
" machine_type=TRAINING_MACHINE_TYPE,\n",
" accelerator_type=TRAINING_ACCELERATOR_TYPE,\n",
" accelerator_count=TRAINING_ACCELERATOR_COUNT,\n",
" )\n",
"\n",
" train_task = custom_job_training_op(\n",
" train_task = train_op(\n",
" training_artifacts_dir=training_artifacts_dir,\n",
" tfrecord_file=ingest_task.outputs[\"tfrecord_file\"],\n",
" num_epochs=num_epochs,\n",
@@ -1525,10 +1489,28 @@
" num_actions=num_actions,\n",
" tikhonov_weight=tikhonov_weight,\n",
" agent_alpha=agent_alpha,\n",
" project=PROJECT_ID,\n",
" location=REGION,\n",
" )\n",
"\n",
" worker_pool_specs = [\n",
" {\n",
" \"containerSpec\": {\n",
" \"imageUri\": train_task.container.image,\n",
" },\n",
" \"replicaCount\": TRAINING_REPLICA_COUNT,\n",
" \"machineSpec\": {\n",
" \"machineType\": TRAINING_MACHINE_TYPE,\n",
" \"acceleratorType\": TRAINING_ACCELERATOR_TYPE,\n",
" \"acceleratorCount\": TRAINING_ACCELERATOR_COUNT,\n",
" },\n",
" },\n",
" ]\n",
" train_task.custom_job_spec = {\n",
" \"displayName\": train_task.name,\n",
" \"jobSpec\": {\n",
" \"workerPoolSpecs\": worker_pool_specs,\n",
" },\n",
" }\n",
"\n",
" # Run the Deployer components.\n",
" # Upload the trained policy as a model.\n",
" model_upload_op = gcc_aip.ModelUploadOp(\n",
@@ -1545,13 +1527,11 @@
" # Deploy the uploaded, trained policy to the created endpoint. (This operation\n",
" # has to occur after both model uploading and endpoint creation complete.)\n",
" gcc_aip.ModelDeployOp(\n",
" project=project_id,\n",
" endpoint=endpoint_create_op.outputs[\"endpoint\"],\n",
" model=model_upload_op.outputs[\"model\"],\n",
" deployed_model_display_name=TRAINED_POLICY_DISPLAY_NAME,\n",
" dedicated_resources_machine_type=ENDPOINT_MACHINE_TYPE,\n",
" dedicated_resources_accelerator_type=ENDPOINT_ACCELERATOR_TYPE,\n",
" dedicated_resources_accelerator_count=ENDPOINT_ACCELERATOR_COUNT,\n",
" dedicated_resources_min_replica_count=ENDPOINT_REPLICA_COUNT,\n",
" machine_type=ENDPOINT_MACHINE_TYPE,\n",
" )"
]
},
@@ -39,15 +39,14 @@ outputs:
- {name: bigquery_table_id, type: String}
implementation:
container:
image: python:3.7
image: tensorflow/tensorflow:2.5.0
command:
- sh
- -c
- (PIP_DISABLE_PIP_VERSION_CHECK=1 python3 -m pip install --quiet --no-warn-script-location
'google-cloud-bigquery==2.20.0' 'pillow' 'tensorflow==2.5.0' 'tf-agents==0.8.0'
|| PIP_DISABLE_PIP_VERSION_CHECK=1 python3 -m pip install --quiet --no-warn-script-location
'google-cloud-bigquery==2.20.0' 'pillow' 'tensorflow==2.5.0' 'tf-agents==0.8.0'
--user) && "$0" "$@"
'google-cloud-bigquery==2.20.0' 'tensorflow==2.5.0' 'tf-agents==0.8.0' || PIP_DISABLE_PIP_VERSION_CHECK=1
python3 -m pip install --quiet --no-warn-script-location 'google-cloud-bigquery==2.20.0'
'tensorflow==2.5.0' 'tf-agents==0.8.0' --user) && "$0" "$@"
- sh
- -ec
- |
@@ -297,8 +296,7 @@ implementation:
def _serialize_str(str_value: str) -> str:
if not isinstance(str_value, str):
raise TypeError('Value "{}" has type "{}" instead of str.'.format(
str(str_value), str(type(str_value))))
raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value))))
return str_value
import argparse
@@ -20,7 +20,7 @@ outputs:
- {name: tfrecord_file, type: String}
implementation:
container:
image: python:3.7
image: tensorflow/tensorflow:2.5.0
command:
- sh
- -c
@@ -187,8 +187,7 @@ implementation:
def _serialize_str(str_value: str) -> str:
if not isinstance(str_value, str):
raise TypeError('Value "{}" has type "{}" instead of str.'.format(
str(str_value), str(type(str_value))))
raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value))))
return str_value
import argparse
@@ -1,4 +1,3 @@
google-cloud-bigquery==2.20.0
tensorflow==2.7.2
pillow==9.0.1
tensorflow==2.5.0
tf-agents==0.8.0
@@ -1,4 +1,2 @@
google-cloud-pubsub==2.5.0
pillow==9.0.1
tf-agents==0.8.0
tensorflow==2.7.2
tensorflow==2.5.0
@@ -1,5 +0,0 @@
dataclasses==0.6
google-cloud-aiplatform==1.8.1
tensorflow==2.7.2
pillow==9.0.1
tf-agents==0.8.0
@@ -27,14 +27,14 @@ outputs:
- {name: training_artifacts_dir, type: String}
implementation:
container:
image: python:3.7
image: tensorflow/tensorflow:2.5.0
command:
- sh
- -c
- (PIP_DISABLE_PIP_VERSION_CHECK=1 python3 -m pip install --quiet --no-warn-script-location
'tensorflow==2.5.0' 'tf-agents==0.8.0' 'Pillow' || PIP_DISABLE_PIP_VERSION_CHECK=1
python3 -m pip install --quiet --no-warn-script-location 'tensorflow==2.5.0'
'tf-agents==0.8.0' 'Pillow' --user) && "$0" "$@"
'tensorflow==2.5.0' 'tf-agents==0.8.0' || PIP_DISABLE_PIP_VERSION_CHECK=1 python3
-m pip install --quiet --no-warn-script-location 'tensorflow==2.5.0' 'tf-agents==0.8.0'
--user) && "$0" "$@"
- sh
- -ec
- |
@@ -270,8 +270,7 @@ implementation:
def _serialize_str(str_value: str) -> str:
if not isinstance(str_value, str):
raise TypeError('Value "{}" has type "{}" instead of str.'.format(
str(str_value), str(type(str_value))))
raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value))))
return str_value
import argparse
@@ -22,13 +22,13 @@ from src.training import task
# Paths and configurations
DATA_PATH = "gs://[your-bucket-name]/artifacts/u.data" # FILL IN
DATA_PATH = "gs://[your-bucket-name]/[your-dataset-dir]/u.data" # FILL IN
ROOT_DIR = "gs://[your-bucket-name]/artifacts" # FILL IN
ARTIFACTS_DIR = "gs://[your-bucket-name]/artifacts" # FILL IN
PROFILER_DIR = "gs://[your-bucket-name]/profiler" # FILL IN
HPTUNING_RESULT_DIR = "[your-hptuning-result-dir]/" # FILL IN
HPTUNING_RESULT_PATH = os.path.join(HPTUNING_RESULT_DIR,
"result.json") # FILL IN
"[your-file-name].json") # FILL IN
RAW_BUCKET_NAME = "[your-hptuning-result-bucket-name]" # FILL IN
# Hyperparameters
@@ -1 +1 @@
tensorflow==2.7.2
tensorflow==2.4.1
@@ -8,7 +8,7 @@
},
"outputs": [],
"source": [
"# Copyright 2022 Google LLC\n",
"# Copyright 2021 Google LLC\n",
"#\n",
"# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
"# you may not use this file except in compliance with the License.\n",
@@ -113,8 +113,8 @@
},
"outputs": [],
"source": [
"! gcloud ai custom-jobs local-run \\\n",
" --executor-image-uri=$BASE_IMAGE_URI \\\n",
"! gcloud beta ai custom-jobs local-run \\\n",
" --base-image=$BASE_IMAGE_URI \\\n",
" --script=$SCRIPT_PATH \\\n",
" --output-image-uri=$OUTPUT_IMAGE_NAME \\\n",
" -- \\\n",
+93
View File
@@ -0,0 +1,93 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of
experience, education, socio-economic status, nationality, personal appearance,
race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening,
offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
This Code of Conduct also applies outside the project spaces when the Project
Steward has a reasonable belief that an individual's behavior may have a
negative impact on the project or its community.
## Conflict Resolution
We do not believe that all conflict is bad; healthy debate and disagreement
often yield positive results. However, it is never okay to be disrespectful or
to engage in behavior that violates the project’s code of conduct.
If you see someone violating the code of conduct, you are encouraged to address
the behavior directly with those involved. Many issues can be resolved quickly
and easily, and this gives people more control over the outcome of their
dispute. If you are unable to resolve the matter for any reason, or if the
behavior is threatening or harassing, report it. We are dedicated to providing
an environment where participants feel welcome and safe.
Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the
Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to
receive and address reported violations of the code of conduct. They will then
work with a committee consisting of representatives from the Open Source
Programs Office and the Google Open Source Strategy team. If for any reason you
are uncomfortable reaching out to the Project Steward, please email
opensource@google.com.
We will investigate every complaint, but you may not receive a direct response.
We will use our discretion in determining when and how to follow up on reported
incidents, which may range from not taking action to permanent expulsion from
the project and project-sponsored spaces. We will notify the accused of the
report and provide them an opportunity to discuss it before any action is taken.
The identity of the reporter will be omitted from the details of the report
supplied to the accused. In potentially harmful situations, such as ongoing
harassment or threats to anyone's safety, we may take action without notice.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
available at
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+28
View File
@@ -0,0 +1,28 @@
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code Reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Community Guidelines
This project follows [Google's Open Source Community
Guidelines](https://opensource.google/conduct/).
-5
View File
@@ -1,5 +0,0 @@
The [official](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/main/notebooks/official) folder contains notebooks organized by Google Cloud product. These are tested weekly and maintained by Google.
The [community](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/main/notebooks/community) folder contains notebooks that may be created by Google or external contributors. They are not necessary maintained.
Contributions to the repo should use the [notebook template](https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/notebook_template.ipynb) as a starting point.
+6 -30
View File
@@ -3,34 +3,10 @@
# @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request.
/sdk/sdk_* @andrewferlitsch
/gapic @andrewferlitsch
/gapic/custom/showcase_custom_image_classification_online_explain_example_based_api.ipynb @inardini
/ml_ops @andrewferlitsch
/model_monitoring/* @andrewferlitsch
/sdk/sdk_* @aferlitsch
/gapic @aferlitsch
/ml_ops @aferlitsch
/model_monitoring/* @mco
/structured_data/rapid_prototyping_* @rafael-carvalho
/managed_notebooks/
/bigquery_ml/ @polong
/sdk/SDK_FBProphet_Forecasting_Online.ipynb @brianchunkang
/pipelines/google_cloud_pipeline_components_TPU_model_train_upload_deploy.ipynb @brianchunkang
/explainable_ai/SDK_Custom_Container_XAI.ipynb @brianchunkang
/matching_engine/sdk_matching_engine_for_indexing.ipynb @ivanmkc
/matching_engine/matching_engine_for_indexing.ipynb @yinghsienwu
/matching_engine/stream_update_for_matching_engine.ipynb @peterping666
/sdk/pytorch_lightning_custom_container_training.ipynb @brianchunkang
/tensorboard @yfang1
/feature_store @nayaknishant @morgandu
/prediction @googleapis/vertex-prediction-team
/vertex_endpoints/tf_hub_obj_detection/deploy_tfhub_object_detection_on_vertex_endpoints.ipynb @entrpn
/vertex_endpoints/nvidia-triton/nvidia-triton-custom-container-prediction.ipynb @RajeshThallam
/vertex_endpoints/optimized_tensorflow_runtime @vlasenkoalexey
/notebooks/community/ml_ops/stage2/get_started_with_visionapi_and_automl.ipynb @mansari
/notebooks/community/neo4j/graph_paysim.ipynb @benofben @laeg
/notebooks/community/ml_ops/stage1/get_started_with_visionapi_and_vertex_datasets.ipynb @mansari
/notebooks/community/pipelines/google_cloud_pipeline_components_bqml_pipeline_demand_forecasting.ipynb @inardini
/notebooks/community/ml_ops/stage2/get_started_vertex_hpt_r_kernel.ipynb @fhirschmann
/notebooks/community/ml_ops/stage2/get_started_vertex_training_r_using_r_kernel.ipynb @fhirschmann
/notebooks/community/vertex-ai-samples/notebooks/community/model_registry/vertex_ai_model_registry_bqml_custom_model_versioning.ipynb @inardini
/notebooks/community/vertex-ai-samples/notebooks/community/model_registry/vertex_ai_model_registry_automl_model_versioning.ipynb @inardini
/notebooks/community/vizier/conversions_vertex_vizier_and_open_source_vizier.ipynb @halio-g
/managed_notebooks/ @notebooks-team
/sdk/SDK_FBProphet_Forecasting_Online.ipynb @brianchunkang
Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Some files were not shown because too many files have changed in this diff Show More