Skip to content

Commit

Permalink
feat(bento): build process
Browse files Browse the repository at this point in the history
include gRPC options and dependencies

Signed-off-by: Aaron Pham <29749331+aarnphm@users.noreply.github.com>
  • Loading branch information
aarnphm committed Sep 13, 2022
1 parent 3c07d9b commit 810516c
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 81 deletions.
71 changes: 54 additions & 17 deletions bentoml/_internal/bento/build_config.py
Expand Up @@ -64,7 +64,7 @@ def _convert_python_version(py_version: str | None) -> str | None:
target_python_version = f"{major}.{minor}"
if target_python_version != py_version:
logger.warning(
"BentoML will install the latest python%s instead of the specified version %s. To use the exact python version, use a custom docker base image. See https://docs.bentoml.org/en/latest/concepts/bento.html#custom-base-image-advanced",
"BentoML will install the latest 'python%s' instead of the specified 'python%s'. To use the exact python version, use a custom docker base image. See https://docs.bentoml.org/en/latest/concepts/bento.html#custom-base-image-advanced",
target_python_version,
py_version,
)
Expand Down Expand Up @@ -165,20 +165,27 @@ def __attrs_post_init__(self):
if self.base_image is not None:
if self.distro is not None:
logger.warning(
f"docker base_image {self.base_image} is used, 'distro={self.distro}' option is ignored.",
"docker base_image %s is used, 'distro=%s' option is ignored.",
self.base_image,
self.distro,
)
if self.python_version is not None:
logger.warning(
f"docker base_image {self.base_image} is used, 'python={self.python_version}' option is ignored.",
"docker base_image %s is used, 'python=%s' option is ignored.",
self.base_image,
self.python_version,
)
if self.cuda_version is not None:
logger.warning(
f"docker base_image {self.base_image} is used, 'cuda_version={self.cuda_version}' option is ignored.",
"docker base_image %s is used, 'cuda_version=%s' option is ignored.",
self.base_image,
self.cuda_version,
)
if self.system_packages:
logger.warning(
f"docker base_image {self.base_image} is used, "
f"'system_packages={self.system_packages}' option is ignored.",
"docker base_image %s is used, 'system_packages=%s' option is ignored.",
self.base_image,
self.system_packages,
)

if self.distro is not None and self.cuda_version is not None:
Expand Down Expand Up @@ -225,14 +232,14 @@ def write_to_bento(
try:
setup_script = resolve_user_filepath(self.setup_script, build_ctx)
except FileNotFoundError as e:
raise InvalidArgument(f"Invalid setup_script file: {e}")
raise InvalidArgument(f"Invalid setup_script file: {e}") from None
if not os.access(setup_script, os.X_OK):
message = f"{setup_script} is not executable."
if not psutil.WINDOWS:
raise InvalidArgument(
f"{message} Ensure the script has a shebang line, then run 'chmod +x {setup_script}'."
)
raise InvalidArgument(message)
) from None
raise InvalidArgument(message) from None
copy_file_to_fs_folder(
setup_script, bento_fs, docker_folder, "setup_script"
)
Expand Down Expand Up @@ -376,6 +383,9 @@ def is_empty(self) -> bool:
)


ADDITIONAL_COMPONENTS = ["tracing", "grpc", "zipkin", "jaeger", "otlp"]


@attr.frozen
class PythonOptions:

Expand Down Expand Up @@ -424,15 +434,27 @@ class PythonOptions:
default=None,
validator=attr.validators.optional(attr.validators.instance_of(list)),
)
components: t.Optional[t.Dict[str, bool]] = attr.field(
default=None,
validator=attr.validators.optional(
attr.validators.deep_mapping(
key_validator=attr.validators.in_(ADDITIONAL_COMPONENTS),
value_validator=attr.validators.instance_of(bool),
)
),
)

def __attrs_post_init__(self):
if self.requirements_txt and self.packages:
logger.warning(
f'Build option python: `requirements_txt="{self.requirements_txt}"` found, will ignore the option: `packages="{self.packages}"`.'
"Build option python: 'requirements_txt={self.requirements_txt}' found, will ignore the option: 'packages=%s'.",
self.requirements_txt,
self.packages,
)
if self.no_index and (self.index_url or self.extra_index_url):
logger.warning(
f'Build option python: `no_index="{self.no_index}"` found, will ignore `index_url` and `extra_index_url` option when installing PyPI packages.'
"Build option python: 'no_index=%s' found, will ignore 'index_url' and 'extra_index_url' option when installing PyPI packages.",
self.no_index,
)

def is_empty(self) -> bool:
Expand Down Expand Up @@ -479,17 +501,19 @@ def write_to_bento(self, bento_fs: FS, build_ctx: str) -> None:
pip_args.extend(self.pip_args.split())

with bento_fs.open(fs.path.combine(py_folder, "install.sh"), "w") as f:
args = " ".join(map(quote, pip_args)) if pip_args else ""
install_script_content = (
args = ["--no-warn-script-location"]
if pip_args:
args.extend(pip_args)
install_sh = (
"""\
#!/usr/bin/env bash
set -exuo pipefail

# Parent directory https://stackoverflow.com/a/246128/8643197
BASEDIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"

PIP_ARGS=(--no-warn-script-location """
+ args
PIP_ARGS=("""
+ " ".join(map(quote, args))
+ """)

# BentoML by default generates two requirement files:
Expand Down Expand Up @@ -525,11 +549,22 @@ def write_to_bento(self, bento_fs: FS, build_ctx: str) -> None:
echo "WARNING: using BentoML version ${existing_bentoml_version}"
fi
else
pip install bentoml=="$BENTOML_VERSION"
pip install bentoml=="$BENTOML_VERSION"
fi
"""
)
f.write(install_script_content)
if self.components:
components = [
component
for component, enabled in self.components.items()
if enabled
]
install_sh += f"""\

# Install additional bentoml components
pip install "bentoml[{','.join(components)}]"
"""
f.write(install_sh)

if self.requirements_txt is not None:
requirements_txt_file = resolve_user_filepath(
Expand Down Expand Up @@ -591,6 +626,8 @@ def with_defaults(self) -> PythonOptions:
if self.requirements_txt is None:
if self.lock_packages is None:
defaults["lock_packages"] = True
if self.components is None:
defaults["components"] = {k: False for k in ADDITIONAL_COMPONENTS}

return attr.evolve(self, **defaults)

Expand Down
38 changes: 24 additions & 14 deletions bentoml/_internal/bento/build_dev_bentoml_whl.py
@@ -1,18 +1,17 @@
from __future__ import annotations

import os
import shutil
import logging
import tempfile

from bentoml.exceptions import BentoMLException

from ..utils.pkg import source_locations
from ...exceptions import BentoMLException
from ...exceptions import MissingDependencyException
from ..configuration import is_pypi_installed_bentoml

logger = logging.getLogger(__name__)

BENTOML_DEV_BUILD = "BENTOML_BUNDLE_LOCAL_BUILD"
_exc_message = f"'{BENTOML_DEV_BUILD}=True', which requires the 'pypa/build' package. Install development dependencies with 'pip install -r requirements/dev-requirements.txt' and try again."


def build_bentoml_editable_wheel(target_path: str) -> None:
Expand All @@ -28,11 +27,27 @@ def build_bentoml_editable_wheel(target_path: str) -> None:
# skip this entirely if BentoML is installed from PyPI
return

try:
from build.env import IsolatedEnvBuilder

from build import ProjectBuilder
except ModuleNotFoundError as e:
raise MissingDependencyException(_exc_message) from e

# Find bentoml module path
module_location = source_locations("bentoml")
if not module_location:
raise BentoMLException("Could not find bentoml module location.")

try:
from importlib import import_module

_ = import_module("bentoml.grpc.v1alpha1.service_pb2")
except ModuleNotFoundError:
raise ModuleNotFoundError(
f"Generated stubs are not found. Make sure to run '{module_location}/scripts/generate_grpc_stubs.sh' beforehand to generate gRPC stubs."
) from None

pyproject = os.path.abspath(os.path.join(module_location, "..", "pyproject.toml"))

# this is for BentoML developer to create Service containing custom development
Expand All @@ -42,17 +57,12 @@ def build_bentoml_editable_wheel(target_path: str) -> None:
logger.info(
"BentoML is installed in `editable` mode; building BentoML distribution with the local BentoML code base. The built wheel file will be included in the target bento."
)
try:
from build import ProjectBuilder
except ModuleNotFoundError:
raise BentoMLException(
f"Environment variable {BENTOML_DEV_BUILD}=True detected, which requires the `pypa/build` package. Make sure to install all dev dependencies via `pip install -r requirements/dev-requirements.txt` and try again."
)

with tempfile.TemporaryDirectory() as dist_dir:
with IsolatedEnvBuilder() as env:
builder = ProjectBuilder(os.path.dirname(pyproject))
builder.build("wheel", dist_dir)
shutil.copytree(dist_dir, target_path)
builder.python_executable = env.executable
builder.scripts_dir = env.scripts_dir
env.install(builder.build_system_requires)
builder.build("wheel", target_path)
else:
logger.info(
"Custom BentoML build is detected. For a Bento to use the same build at serving time, add your custom BentoML build to the pip packages list, e.g. `packages=['git+https://github.com/bentoml/bentoml.git@13dfb36']`"
Expand Down
Expand Up @@ -6,8 +6,8 @@

import attr

from ...exceptions import InvalidArgument
from ...exceptions import BentoMLException
from bentoml.exceptions import InvalidArgument
from bentoml.exceptions import BentoMLException

if TYPE_CHECKING:
P = t.ParamSpec("P")
Expand Down Expand Up @@ -109,7 +109,7 @@ class DistroSpec:
),
)

supported_cuda_versions: t.List[str] = attr.field(
supported_cuda_versions: t.Optional[t.List[str]] = attr.field(
default=None,
validator=attr.validators.optional(
attr.validators.deep_iterable(
Expand Down
23 changes: 17 additions & 6 deletions bentoml/_internal/bento/docker/entrypoint.sh
Expand Up @@ -11,32 +11,43 @@ _is_sourced() {

_main() {
# for backwards compatibility with the yatai<1.0.0, adapting the old "yatai" command to the new "start" command
if [ "${#}" -gt 0 ] && [ "${1}" = 'python' ] && [ "${2}" = '-m' ] && ([ "${3}" = 'bentoml._internal.server.cli.runner' ] || [ "${3}" = "bentoml._internal.server.cli.api_server" ]); then
if [ "${#}" -gt 0 ] && [ "${1}" = 'python' ] && [ "${2}" = '-m' ] && { [ "${3}" = 'bentoml._internal.server.cli.runner' ] || [ "${3}" = "bentoml._internal.server.cli.api_server" ]; }; then # SC2235, use { } to avoid subshell overhead
if [ "${3}" = 'bentoml._internal.server.cli.runner' ]; then
set -- bentoml start-runner-server "${@:4}"
elif [ "${3}" = 'bentoml._internal.server.cli.api_server' ]; then
set -- bentoml start-http-server "${@:4}"
fi
# if no arg or first arg looks like a flag
elif [ -z "$@" ] || [ "${1:0:1}" = '-' ]; then
# if no arg or first arg looks like a flag
elif [[ -z "$*" ]] || [[ "${1:0:1}" =~ '-' ]]; then # SC2198, use "$*" since array won't work with [ ] operand
if [[ -v BENTOML_SERVE_COMPONENT ]]; then
echo "\$BENTOML_SERVE_COMPONENT is set! Calling 'bentoml start-*' instead"
if [ "${BENTOML_SERVE_COMPONENT}" = 'http_server' ]; then
set -- bentoml start-rest-server "$@" "$BENTO_PATH"
set -- bentoml start-http-server "$@" "$BENTO_PATH"
elif [ "${BENTOML_SERVE_COMPONENT}" = 'grpc_server' ]; then
set -- bentoml start-grpc-server "$@" "$BENTO_PATH"
elif [ "${BENTOML_SERVE_COMPONENT}" = 'runner' ]; then
set -- bentoml start-runner-server "$@" "$BENTO_PATH"
fi
elif [[ -v BENTOML_USE_GRPC ]] && [ "${BENTOML_USE_GRPC}" = "true" ]; then
set -- bentoml serve-grpc --production "$@" "$BENTO_PATH"
else
set -- bentoml serve --production "$@" "$BENTO_PATH"
fi
fi

# Overide the BENTOML_PORT if PORT env var is present. Used for Heroku **and Yatai**
if [[ -v PORT ]]; then
echo "\$PORT is set! Overiding \$BENTOML_PORT with \$PORT ($PORT)"
export BENTOML_PORT=$PORT
fi
exec "$@"
# handle serve and start commands that is passed to the container.
# assuming that serve and start commands are the first arguments
if [ "${#}" -gt 0 ] && { [ "${1}" = 'serve' ] || [ "${1}" = 'serve-grpc' ] || [ "${1}" = 'start-http-server' ] || [ "${1}" = 'start-grpc-server' ] || [ "${1}" = 'start-runner-server' ]; }; then
exec bentoml "$@" "$BENTO_PATH"
else
# otherwise default to run whatever the command is
# This should allow running bash, sh, python, etc
exec "$@"
fi
}

if ! _is_sourced; then
Expand Down
14 changes: 5 additions & 9 deletions bentoml/_internal/bento/docker/templates/base.j2
Expand Up @@ -2,7 +2,7 @@
{# users can use these values #}
{% import '_macros.j2' as common %}
{% set bento__entrypoint = bento__entrypoint | default(expands_bento_path("env", "docker", "entrypoint.sh", bento_path=bento__path)) %}
# syntax = docker/dockerfile:1.4-labs
# syntax = docker/dockerfile:1.4.3
#
# ===========================================
#
Expand All @@ -12,7 +12,7 @@

# Block SETUP_BENTO_BASE_IMAGE
{% block SETUP_BENTO_BASE_IMAGE %}
FROM {{ __base_image__ }}
FROM {{ __base_image__ }} as base-{{ __options__distro }}

ENV LANG=C.UTF-8

Expand All @@ -21,7 +21,6 @@ ENV LC_ALL=C.UTF-8
ENV PYTHONIOENCODING=UTF-8

ENV PYTHONUNBUFFERED=1

{% endblock %}

# Block SETUP_BENTO_USER
Expand All @@ -34,7 +33,6 @@ RUN groupadd -g $BENTO_USER_GID -o $BENTO_USER && useradd -m -u $BENTO_USER_UID
{% block SETUP_BENTO_ENVARS %}
{% if __options__env is not none %}
{% for key, value in __options__env.items() -%}

ENV {{ key }}={{ value }}
{% endfor -%}
{% endif -%}
Expand All @@ -46,32 +44,30 @@ ENV BENTOML_HOME={{ bento__home }}
RUN mkdir $BENTO_PATH && chown {{ bento__user }}:{{ bento__user }} $BENTO_PATH -R
WORKDIR $BENTO_PATH

# init related components
COPY --chown={{ bento__user }}:{{ bento__user }} . ./

{% endblock %}

# Block SETUP_BENTO_COMPONENTS
{% block SETUP_BENTO_COMPONENTS %}

{% set __install_python_scripts__ = expands_bento_path("env", "python", "install.sh", bento_path=bento__path) %}
{% set __pip_cache__ = common.mount_cache("/root/.cache/pip") %}
# install python packages with install.sh
RUN {{ __pip_cache__ }} bash -euxo pipefail {{ __install_python_scripts__ }}

{% if __options__setup_script is not none %}
{% set __setup_script__ = expands_bento_path("env", "docker", "setup_script", bento_path=bento__path) %}
RUN chmod +x {{ __setup_script__ }}
RUN {{ __setup_script__ }}
{% endif %}

{% endblock %}

# Block SETUP_BENTO_ENTRYPOINT
{% block SETUP_BENTO_ENTRYPOINT %}
# Default port for BentoServer
EXPOSE 3000

# Expose Prometheus port
EXPOSE {{ __prometheus_port__ }}

RUN chmod +x {{ bento__entrypoint }}

USER bentoml
Expand Down

0 comments on commit 810516c

Please sign in to comment.