From 1c383ec93235c613f8fb40a3f74e49c1aae5bda0 Mon Sep 17 00:00:00 2001 From: Dhanshree Arora Date: Sun, 29 Sep 2024 17:05:18 +0530 Subject: [PATCH] Ersilia Pack Dockerization Clean up (#1274) * WIP * Rework dockerfiles to build ersilia pack from source and not through a git url; make it easy to copy the necessary files from bundle when ersilia runs a dockerized model --- .../base/Dockerfile.conda | 11 ++--- .../dockerize-ersiliapack/base/Dockerfile.pip | 11 ++--- .../base/generate_dockerfiles.py | 6 +-- dockerfiles/dockerize-ersiliapack/local.md | 4 +- .../model/Dockerfile.conda | 9 ++-- .../model/Dockerfile.pip | 7 ++- ersilia/hub/fetch/lazy_fetchers/dockerhub.py | 43 +++++++++++-------- ersilia/utils/docker.py | 16 ++++++- ersilia/utils/paths.py | 13 +++++- 9 files changed, 81 insertions(+), 39 deletions(-) diff --git a/dockerfiles/dockerize-ersiliapack/base/Dockerfile.conda b/dockerfiles/dockerize-ersiliapack/base/Dockerfile.conda index 3adf446bf..f0087a1e8 100644 --- a/dockerfiles/dockerize-ersiliapack/base/Dockerfile.conda +++ b/dockerfiles/dockerize-ersiliapack/base/Dockerfile.conda @@ -3,6 +3,7 @@ ARG VERSION=version ENV VERSION=$VERSION ENV PATH=$PATH:/usr/bin/conda/bin WORKDIR /root +COPY . /ersilia-pack RUN set -x && \ apt-get update && \ apt-get install -y wget && \ @@ -27,16 +28,16 @@ RUN set -x && \ echo "Unsupported architecture: $ARCH"; \ echo "$ARCH" > arch.sh; \ fi && \ - apt-get install -y libxrender1 build-essential git && \ mkdir -p /usr/bin/conda && \ bash /root/miniconda.sh -b -u -p /usr/bin/conda && \ rm /root/miniconda.sh && \ . /usr/bin/conda/etc/profile.d/conda.sh && \ conda init && \ /usr/bin/conda/bin/conda clean -afy && \ - /usr/bin/conda/bin/python -m pip install git+https://github.com/ersilia-os/ersilia-pack.git - -COPY docker-entrypoint.sh /root/docker-entrypoint.sh -RUN chmod + /root/docker-entrypoint.sh + cd /ersilia-pack && \ + /usr/bin/conda/bin/python -m pip install -e . && \ + mv /ersilia-pack/docker-entrypoint.sh /root/docker-entrypoint.sh && \ + chmod +x /root/docker-entrypoint.sh + EXPOSE 80 ENTRYPOINT [ "sh", "/root/docker-entrypoint.sh"] \ No newline at end of file diff --git a/dockerfiles/dockerize-ersiliapack/base/Dockerfile.pip b/dockerfiles/dockerize-ersiliapack/base/Dockerfile.pip index 894dac468..8430e0b6c 100644 --- a/dockerfiles/dockerize-ersiliapack/base/Dockerfile.pip +++ b/dockerfiles/dockerize-ersiliapack/base/Dockerfile.pip @@ -1,8 +1,9 @@ FROM python:version -RUN apt-get update && \ - apt-get install git -y && \ - python -m pip install git+https://github.com/ersilia-os/ersilia-pack.git -COPY docker-entrypoint.sh /root/docker-entrypoint.sh -RUN chmod + /root/docker-entrypoint.sh +WORKDIR /root +COPY . /ersilia-pack +RUN apt-get clean && apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* && cd /ersilia-pack && pip install -e . && \ + mv /ersilia-pack/docker-entrypoint.sh /root/docker-entrypoint.sh && \ + chmod + /root/docker-entrypoint.sh EXPOSE 80 ENTRYPOINT [ "sh", "/root/docker-entrypoint.sh"] \ No newline at end of file diff --git a/dockerfiles/dockerize-ersiliapack/base/generate_dockerfiles.py b/dockerfiles/dockerize-ersiliapack/base/generate_dockerfiles.py index 534a36ae9..fc2b133ea 100644 --- a/dockerfiles/dockerize-ersiliapack/base/generate_dockerfiles.py +++ b/dockerfiles/dockerize-ersiliapack/base/generate_dockerfiles.py @@ -16,15 +16,15 @@ "3.12-slim-bullseye" ] -DOCKER_ENTRYPOINT = """ -#!/bin/bash +# We serve the model at port 80 bec Ersilia port maps all containers to port 80 +DOCKER_ENTRYPOINT = """#!/bin/bash set -ex if [ -z "${MODEL}" ]; then echo "Model name has not been specified" exit 1 fi -ersilia_model_serve --bundle_path /root/bundles/$MODEL --port 3000 +ersilia_model_serve --bundle_path /root/bundles/$MODEL --port 80 echo "Serving model $MODEL..." """ diff --git a/dockerfiles/dockerize-ersiliapack/local.md b/dockerfiles/dockerize-ersiliapack/local.md index 1f2158978..2dca49257 100644 --- a/dockerfiles/dockerize-ersiliapack/local.md +++ b/dockerfiles/dockerize-ersiliapack/local.md @@ -22,7 +22,9 @@ Ersilia Pack dockerization is divided into two steps - building the base image t And an entrypoint script called `docker-entrypoint.sh`. -4. Build the base docker image, eg for Python 3.12 conda image, as follows: +4. Copy the desired Dockerfile and `docker-entrypoint.sh` file into ersilia-pack directory. + +5. This step assumes you are inside the ersilia-pack directory and you have completed Step 4. Build the base docker image, eg for Python 3.12 conda image, as follows: ``` docker build -f Dockerfile3.12-slim-bullseye -t ersiliaos/ersiliapack-py312:latest . diff --git a/dockerfiles/dockerize-ersiliapack/model/Dockerfile.conda b/dockerfiles/dockerize-ersiliapack/model/Dockerfile.conda index 8bb01c6d4..d9962b814 100644 --- a/dockerfiles/dockerize-ersiliapack/model/Dockerfile.conda +++ b/dockerfiles/dockerize-ersiliapack/model/Dockerfile.conda @@ -9,7 +9,7 @@ RUN conda install -c conda-forge conda-pack RUN conda-pack -n baseclone -o /tmp/env.tar && \ mkdir /venv && cd /venv && tar -xf /tmp/env.tar && \ rm /tmp/env.tar - RUN /venv/bin/conda-unpack +RUN /venv/bin/conda-unpack @@ -26,9 +26,12 @@ ENV PATH="/usr/bin/conda/bin/:$PATH" COPY --from=build /root/bundles /root/bundles -COPY --from=build /root/docker-entrypoint.sh docker-entrypoint.sh +COPY --from=build /root/docker-entrypoint.sh /root/docker-entrypoint.sh COPY --from=build /venv /usr/bin/conda -RUN chmod + docker-entrypoint.sh +RUN cp /root/bundles/$MODEL/*/information.json /root/information.json && \ + cp /root/bundles/$MODEL/*/api_schema.json /root/api_schema.json && \ + cp /root/bundles/$MODEL/*/status.json /root/status.json && \ + chmod + docker-entrypoint.sh EXPOSE 80 ENTRYPOINT [ "sh", "docker-entrypoint.sh"] \ No newline at end of file diff --git a/dockerfiles/dockerize-ersiliapack/model/Dockerfile.pip b/dockerfiles/dockerize-ersiliapack/model/Dockerfile.pip index 18803d6bb..f3650ad5b 100644 --- a/dockerfiles/dockerize-ersiliapack/model/Dockerfile.pip +++ b/dockerfiles/dockerize-ersiliapack/model/Dockerfile.pip @@ -1,7 +1,10 @@ -FROM ersiliapack-VERSION:latest +FROM ersiliaos/ersiliapack-VERSION:latest ARG MODEL=eos_identifier ENV MODEL=$MODEL WORKDIR /root COPY ./$MODEL /root/$MODEL RUN mkdir /root/bundles && ersilia_model_pack --repo_path $MODEL --bundles_repo_path /root/bundles && \ - rm -rf /root/$MODEL && rm -rf /root/.cache \ No newline at end of file + rm -rf /root/$MODEL && rm -rf /root/.cache && \ + cp /root/bundles/$MODEL/*/information.json /root/information.json && \ + cp /root/bundles/$MODEL/*/api_schema.json /root/api_schema.json && \ + cp /root/bundles/$MODEL/*/status.json /root/status.json \ No newline at end of file diff --git a/ersilia/hub/fetch/lazy_fetchers/dockerhub.py b/ersilia/hub/fetch/lazy_fetchers/dockerhub.py index b3f98fa9a..0ac877261 100644 --- a/ersilia/hub/fetch/lazy_fetchers/dockerhub.py +++ b/ersilia/hub/fetch/lazy_fetchers/dockerhub.py @@ -15,7 +15,7 @@ from ...pull.pull import ModelPuller from ....serve.services import PulledDockerImageService from ....setup.requirements.docker import DockerRequirement -from ....utils.docker import SimpleDocker +from ....utils.docker import SimpleDocker, resolve_pack_method_docker, PACK_METHOD_BENTOML from ....utils.exceptions_utils.fetch_exceptions import DockerNotActiveError from .. import STATUS_FILE @@ -50,9 +50,9 @@ def write_apis(self, model_id): di.serve() di.close() - def copy_information(self, model_id): - fr_file = "/root/eos/dest/{0}/{1}".format(model_id, INFORMATION_FILE) - to_file = "{0}/dest/{1}/{2}".format(EOS, model_id, INFORMATION_FILE) + def _copy_from_bentoml_image(self, model_id, file): + fr_file = "/root/eos/dest/{0}/{1}".format(model_id, file) + to_file = "{0}/dest/{1}/{2}".format(EOS, model_id, file) self.simple_docker.cp_from_image( img_path=fr_file, local_path=to_file, @@ -61,9 +61,9 @@ def copy_information(self, model_id): tag=DOCKERHUB_LATEST_TAG, ) - def copy_metadata(self, model_id): - fr_file = "/root/eos/dest/{0}/{1}".format(model_id, API_SCHEMA_FILE) - to_file = "{0}/dest/{1}/{2}".format(EOS, model_id, API_SCHEMA_FILE) + def _copy_from_ersiliapack_image(self, model_id, file): + fr_file = "/root/{0}".format(file) + to_file = "{0}/dest/{1}/{2}".format(EOS, model_id, file) self.simple_docker.cp_from_image( img_path=fr_file, local_path=to_file, @@ -72,18 +72,27 @@ def copy_metadata(self, model_id): tag=DOCKERHUB_LATEST_TAG, ) - def copy_status(self, model_id): - fr_file = "/root/eos/dest/{0}/{1}".format(model_id, STATUS_FILE) - to_file = "{0}/dest/{1}/{2}".format(EOS, model_id, STATUS_FILE) - self.simple_docker.cp_from_image( - img_path=fr_file, - local_path=to_file, - org=DOCKERHUB_ORG, - img=model_id, - tag=DOCKERHUB_LATEST_TAG, - ) + def _copy_from_image_to_local(self, model_id, file): + pack_method = resolve_pack_method_docker(model_id) + if pack_method == PACK_METHOD_BENTOML: + self._copy_from_bentoml_image(model_id, file) + else: + self._copy_from_ersiliapack_image(model_id, file) + + def copy_information(self, model_id): + self.logger.debug("Copying information file from model container") + self._copy_from_image_to_local(model_id, INFORMATION_FILE) + def copy_metadata(self, model_id): + self.logger.debug("Copying api_schema_file file from model container") + self._copy_from_image_to_local(model_id, API_SCHEMA_FILE) + + def copy_status(self, model_id): + self.logger.debug("Copying status file from model container") + self._copy_from_image_to_local(model_id, STATUS_FILE) + def copy_example_if_available(self, model_id): + # TODO This also needs to change to accomodate ersilia pack for pf in PREDEFINED_EXAMPLE_FILES: fr_file = "/root/eos/dest/{0}/{1}".format(model_id, pf) to_file = "{0}/dest/{1}/{2}".format(EOS, model_id, "input.csv") diff --git a/ersilia/utils/docker.py b/ersilia/utils/docker.py index db60cb74c..2ec5561a5 100644 --- a/ersilia/utils/docker.py +++ b/ersilia/utils/docker.py @@ -10,10 +10,24 @@ from .terminal import run_command, run_command_check_output from .. import logger -from ..default import DEFAULT_DOCKER_PLATFORM, DEFAULT_UDOCKER_USERNAME +from ..default import (DEFAULT_DOCKER_PLATFORM, DEFAULT_UDOCKER_USERNAME, + DOCKERHUB_ORG, DOCKERHUB_LATEST_TAG, + PACK_METHOD_BENTOML, PACK_METHOD_FASTAPI) from ..utils.system import SystemChecker from ..utils.logging import make_temp_dir +def resolve_pack_method_docker(model_id): + client = docker.from_env() + model_image = client.images.get( + f"{DOCKERHUB_ORG}/{model_id}:{DOCKERHUB_LATEST_TAG}" + ) + image_history = model_image.history() + for hist in image_history: + # Very hacky, but works bec we don't have nginx in ersilia-pack images + if "nginx" in hist["CreatedBy"]: + return PACK_METHOD_BENTOML + return PACK_METHOD_FASTAPI + def resolve_platform(): if SystemChecker().is_arm64(): diff --git a/ersilia/utils/paths.py b/ersilia/utils/paths.py index 75ed364be..6112a6ee0 100644 --- a/ersilia/utils/paths.py +++ b/ersilia/utils/paths.py @@ -3,6 +3,7 @@ import collections from pathlib import Path from ersilia import logger +from .docker import resolve_pack_method_docker from ..default import PACK_METHOD_BENTOML, PACK_METHOD_FASTAPI MODELS_DEVEL_DIRNAME = "models" @@ -58,11 +59,19 @@ def exists(path): else: return False - -def resolve_pack_method(model_path): +def resolve_pack_method_source(model_path): if os.path.exists(os.path.join(model_path, "installs", "install.sh")): return PACK_METHOD_FASTAPI elif os.path.exists(os.path.join(model_path, "bentoml.yml")): return PACK_METHOD_BENTOML logger.warning("Could not resolve pack method") return None + +def resolve_pack_method(model_path): + with open(os.path.join(model_path, "service_class.txt"), "r") as f: + service_class = f.read().strip() + if service_class == "pulled_docker": + model_id = Paths().model_id_from_path(model_path) + return resolve_pack_method_docker(model_id) + else: + return resolve_pack_method_source(model_path) \ No newline at end of file