Skip to content

Commit

Permalink
Ersilia Pack Dockerization Clean up (#1274)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
DhanshreeA committed Sep 29, 2024
1 parent 24336e5 commit f96af14
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 71 deletions.
11 changes: 6 additions & 5 deletions dockerfiles/dockerize-ersiliapack/base/Dockerfile.conda
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand All @@ -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 . && \
mv /ersilia-pack/docker-entrypoint.sh /root/docker-entrypoint.sh && \
chmod +x /root/docker-entrypoint.sh

EXPOSE 80
ENTRYPOINT [ "sh", "/root/docker-entrypoint.sh"]
11 changes: 6 additions & 5 deletions dockerfiles/dockerize-ersiliapack/base/Dockerfile.pip
Original file line number Diff line number Diff line change
@@ -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"]
49 changes: 29 additions & 20 deletions dockerfiles/dockerize-ersiliapack/base/generate_dockerfiles.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sys

CONDA_VERSIONS = [
"py38_23.11.0-2",
Expand All @@ -16,15 +17,18 @@
"3.12-slim-bullseye"
]

DOCKER_ENTRYPOINT = """
#!/bin/bash
# By default, we generate all versions
version_to_build = sys.argv[1] if len(sys.argv) > 1 else "all"

# 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..."
"""

Expand All @@ -40,26 +44,31 @@ def write_entrypoint():
with open("docker-entrypoint.sh", "w") as f:
f.write(DOCKER_ENTRYPOINT)

def generate_conda_dockerfile():
conda_versions = CONDA_VERSIONS
def generate_conda_dockerfile(version):
dockerfile = read_conda_base_dockerfile()
for version in conda_versions:
version = version.strip()
with open(os.path.join("Dockerfile.conda" + version), "w") as f:
for line in dockerfile:
f.write(line.replace("version", version))
version = version.strip()
with open(os.path.join("Dockerfile.conda" + version), "w") as f:
for line in dockerfile:
f.write(line.replace("version", version))

def generate_pip_dockerfile():
pip_versions = PIP_VERSIONS
def generate_pip_dockerfile(version):
dockerfile = read_python_base_dockerfile()

for version in pip_versions:
version = version.strip()
with open(os.path.join("Dockerfile.pip" + version), "w") as f:
for line in dockerfile:
f.write(line.replace("version", version))
version = version.strip()
with open(os.path.join("Dockerfile.pip" + version), "w") as f:
for line in dockerfile:
f.write(line.replace("version", version))

if __name__ == "__main__":
generate_conda_dockerfile()
generate_pip_dockerfile()
if version_to_build == "all":
for version in CONDA_VERSIONS:
generate_conda_dockerfile(version)
for version in PIP_VERSIONS:
generate_pip_dockerfile(version)
else:
if version_to_build in CONDA_VERSIONS:
generate_conda_dockerfile(version_to_build)
elif version_to_build in PIP_VERSIONS:
generate_pip_dockerfile(version_to_build)
else:
print("Invalid version specified. Please specify a valid version or 'all' to build all versions")
write_entrypoint()
4 changes: 3 additions & 1 deletion dockerfiles/dockerize-ersiliapack/local.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down
9 changes: 6 additions & 3 deletions dockerfiles/dockerize-ersiliapack/model/Dockerfile.conda
Original file line number Diff line number Diff line change
Expand Up @@ -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



Expand All @@ -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"]
7 changes: 5 additions & 2 deletions dockerfiles/dockerize-ersiliapack/model/Dockerfile.pip
Original file line number Diff line number Diff line change
@@ -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
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
1 change: 1 addition & 0 deletions ersilia/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
]
DEFAULT_ERSILIA_ERROR_EXIT_CODE = 1
METADATA_JSON_FILE = "metadata.json"
METADATA_YAML_FILE = "metadata.yml"
SERVICE_CLASS_FILE = "service_class.txt"
MODEL_SOURCE_FILE = "model_source.txt"
APIS_LIST_FILE = "apis_list.txt"
Expand Down
49 changes: 34 additions & 15 deletions ersilia/hub/content/card.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import json
import requests
import yaml
from ... import ErsiliaBase
from ...utils.terminal import run_command
from ...auth.auth import Auth
Expand Down Expand Up @@ -49,6 +50,7 @@
CARD_FILE,
METADATA_JSON_FILE,
INFORMATION_FILE,
METADATA_YAML_FILE,
)


Expand Down Expand Up @@ -425,7 +427,7 @@ def __init__(self, model_id=None, config_json=None):
self.model_id = model_id
ErsiliaBase.__init__(self, config_json=config_json, credentials_json=None)

def _github_url(self, org=None, branch=None):
def _github_json_url(self, org=None, branch=None):
if org is None:
org = "ersilia-os"
if branch is None:
Expand All @@ -434,29 +436,46 @@ def _github_url(self, org=None, branch=None):
org, self.model_id, branch, METADATA_JSON_FILE
)

def get_json_file(self, org=None, branch=None):
url = self._github_url(org=org, branch=branch)
self.logger.debug("Reading from {0}".format(url))
r = requests.get(url)
if r.status_code == 200:
text = r.content
data = json.loads(text)
return data
def _github_yaml_url(self, org=None, branch=None):
if org is None:
org = "ersilia-os"
if branch is None:
branch = "main"
return "https://raw.githubusercontent.com/{0}/{1}/{2}/{3}".format(
org, self.model_id, branch, METADATA_YAML_FILE
)

def _get_file_content_from_github(self, org, branch):
json_url = self._github_json_url(org, branch)
r = requests.get(json_url)
if r.status_code == 404:
yaml_url = self._github_yaml_url(org, branch)
r = requests.get(yaml_url)
if r.status_code == 404:
return None
else:
return yaml.safe_load(r.content)
else:
return None
return json.loads(r.content)

def get_json_or_yaml_file(self, org=None, branch=None):
return self._get_file_content_from_github(org, branch)

def exists(self, org=None, branch=None):
if self.get_json_file(org=org, branch=branch) is None:
return False
else:
return True

def read_information(self, org=None, branch=None, json_path=None):
if json_path is None:
data = self.get_json_file(org=org, branch=branch)
def read_information(self, org=None, branch=None, json_or_yaml_path=None):
if json_or_yaml_path is None:
data = self.get_json_or_yaml_file(org=org, branch=branch)
else:
with open(json_path, "r") as f:
data = json.load(f)
with open(json_or_yaml_path, "r") as f:
if json_or_yaml_path.endswith(".json"):
data = json.load(f)
else:
data = yaml.safe_load(f)
bi = BaseInformation(config_json=self.config_json)
bi.from_dict(data)
return bi
Expand Down
43 changes: 26 additions & 17 deletions ersilia/hub/fetch/lazy_fetchers/dockerhub.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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")
Expand Down
16 changes: 15 additions & 1 deletion ersilia/utils/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
13 changes: 11 additions & 2 deletions ersilia/utils/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

0 comments on commit f96af14

Please sign in to comment.