Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dockerize runtime #59

Merged
merged 10 commits into from
Jun 26, 2024
51 changes: 51 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest as base
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: It would be nice to have a top-level comment in this Dockerfile. It looks like it's aimed at running the runtime and not for testing/dev, right?

Copy link
Collaborator Author

@alex-jw-brooks alex-jw-brooks Jun 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! It's just adding a Dockerfile for building a runtime that software can test with 😄 it's probably a good idea to actually build a release wheel + test out with a container built from this though!


RUN microdnf update -y && \
microdnf install -y \
python3-devel python-pip && \
pip install --upgrade --no-cache-dir pip wheel && \
microdnf clean all

FROM base as builder
WORKDIR /build

RUN pip install --no-cache tox
COPY README.md .
COPY pyproject.toml .
COPY tox.ini .
COPY caikit_computer_vision caikit_computer_vision
# .git is required for setuptools-scm get the version
RUN --mount=source=.git,target=.git,type=bind \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is just for wheel building, but do we need tests in there too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! I think yes, but I would like to not block this PR / the SDXL PR on it if it's alright with you, since I'm about to be OOO for a while, and I would prefer to give people an image built using stuff off of main if possible 😄

--mount=type=cache,target=/root/.cache/pip \
tox -e build


FROM base as deploy

RUN python -m venv --upgrade-deps /opt/caikit/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of a virtualenv is interesting here. On the one hand, it's a nice isolation mechanism, but on the other hand it will also create some duplication with the base python runtime.


ENV VIRTUAL_ENV=/opt/caikit
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

COPY --from=builder /build/dist/caikit_computer_vision*.whl /tmp/
RUN --mount=type=cache,target=/root/.cache/pip \
pip install /tmp/caikit_computer_vision*.whl && \
rm /tmp/caikit_computer_vision*.whl

COPY LICENSE /opt/caikit/
COPY README.md /opt/caikit/

RUN groupadd --system caikit --gid 1001 && \
adduser --system --uid 1001 --gid 0 --groups caikit \
--home-dir /caikit --shell /sbin/nologin \
--comment "Caikit User" caikit

USER caikit

ENV RUNTIME_LIBRARY=caikit_computer_vision
# Optional: use `CONFIG_FILES` and the /caikit/ volume to explicitly provide a configuration file and models
# ENV CONFIG_FILES=/caikit/caikit.yml
VOLUME ["/caikit/"]
WORKDIR /caikit

CMD ["python"]
1 change: 1 addition & 0 deletions caikit_computer_vision/data_model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
from .image_classification import *
from .image_segmentation import *
from .object_detection import *
from .text_to_image import *
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, nice! I have a few things for this that might be worth contributing

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do it!!

12 changes: 12 additions & 0 deletions caikit_computer_vision/data_model/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .image_classification import ImageClassificationResult
from .image_segmentation import ImageSegmentationResult
from .object_detection import ObjectDetectionResult
from .text_to_image import TextToImageResult


# TODO - add support for image DM primitives
Expand Down Expand Up @@ -61,3 +62,14 @@ class ImageSegmentationTask(TaskBase):
Note that at the moment, this task encapsulates all segmentation types,
I.e., instance, object, semantic, etc...
"""


@task(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks very similar to my personal definition of this (with the change of CaptionedImage, see below)

@task(
    unary_parameters={"text": dm.TextDocument},
    unary_output_type=dm.CaptionedImage,
)
class TextToImageTask(TaskBase):
    """Task of generating an image from text"""

required_parameters={"inputs": str},
output_type=TextToImageResult,
)
class TextToImageTask(TaskBase):
"""The text to image task is responsible for taking an input text prompt, along with
other optional image generation parameters, e.g., image height and width,
and generating an image.
"""
36 changes: 36 additions & 0 deletions caikit_computer_vision/data_model/text_to_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright The Caikit Authors
#
# 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.
"""Data structures for text to image."""


# Standard
from typing import List

# Third Party
from py_to_proto.dataclass_to_proto import Annotated, FieldNumber

# First Party
from caikit.core import DataObjectBase, dataobject
from caikit.interfaces.common.data_model import ProducerId
from caikit.interfaces.vision import data_model as caikit_dm
import alog

log = alog.use_channel("DATAM")


@dataobject(package="caikit_data_model.caikit_computer_vision")
class TextToImageResult(DataObjectBase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I found with my purpose-built text-to-image module was that it was helpful to bind the input text to the image in the output object. I ended up calling it a CaptionedImage and making it inherit from Image:

@dataobject
class CaptionedImage(Image):
    """A Captioned image has a caption as well as the image itself"""
    caption: Optional[str]

    # TODO: Use type hints here once caikit supports them
    # https://github.com/caikit/caikit/issues/608
    # def __init__(self, *args, caption: Optional[str] = None, **kwargs):
    def __init__(self, *args, caption = None, **kwargs):
        """Explicitly delegate to Image's initializer so that dataobject does
        not auto-create an __init__
        """
        super().__init__(*args, **kwargs)
        self.caption = caption

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this more than the current output type! Currently things are written the way they are because of the way software expects image to be formatted, which is basically wrapping encoded bytes of a compressed image. I greatly prefer this also though! For now, I would like to continue with this output format if you're alright with it, since this is what they have been testing with also, but I think it would be a good idea to add this to caikit and update the result to use this in the future.

I suspect this is the route that they will want to go too, since they had already started talking about returning a JSON object with stuff + image instead of just the encoded image 🤞

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm a little confused on how this structure accomplishes the goal of having the output be just encoded bytes. I would expect that you would still need to call some function on the output field to get those bytes encoded, right? If this is returned from a task in caikit.runtime, it would return as a json blob or serialized proto with output and producer_id fields unless there's something in the core Image that would provide custom serialization?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! That is in the data model for image, the image data model holds an export format, which by default is png. when you get the attribute of image data on the image backend, it makes a BytesIO object and exports it with PIL here!

# TODO: Align on the output format
output: Annotated[caikit_dm.Image, FieldNumber(1)]
producer_id: Annotated[ProducerId, FieldNumber(2)]
2 changes: 1 addition & 1 deletion caikit_computer_vision/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Local
from . import object_detection, segmentation
from . import object_detection, segmentation, text_to_image
16 changes: 16 additions & 0 deletions caikit_computer_vision/modules/text_to_image/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright The Caikit Authors
#
# 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.

# Local
from .tti_stub import TTIStub
76 changes: 76 additions & 0 deletions caikit_computer_vision/modules/text_to_image/tti_stub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright The Caikit Authors
#
# 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.
"""Stub module for text to image for testing runtime interfaces.
"""
# Standard
from typing import Union, get_args
import os

# Third Party
import numpy as np

# First Party
from caikit.core.modules import ModuleBase, ModuleConfig, ModuleSaver, module
from caikit.interfaces.vision import data_model as caikit_dm
import alog

# Local
from ...data_model import TextToImageResult
from ...data_model.tasks import TextToImageTask

log = alog.use_channel("TTI_STUB")


@module(
id="28aa938b-1a33-11a0-11a3-bb9c3b1cbb11",
name="Stub module for Text to Image",
version="0.1.0",
task=TextToImageTask,
)
class TTIStub(ModuleBase):
def __init__(
self,
model_name,
) -> "TTIStub":
log.debug("STUB - initializing text to image instance")
super().__init__()
self.model_name = model_name

@classmethod
def load(cls, model_path: Union[str, "ModuleConfig"]) -> "TTIStub":
config = ModuleConfig.load(model_path)
return cls.bootstrap(config.model_name)

@classmethod
def bootstrap(cls, model_name: str) -> "TTIStub":
return cls(model_name)

def save(self, model_path: str):
saver = ModuleSaver(
self,
model_path=model_path,
)
with saver:
saver.update_config({"model_name": self.model_name})

def run(self, inputs: str, height: int, width: int) -> TextToImageResult:
"""Generates an image matching the provided height and width."""
log.debug("STUB - running text to image inference")
r_channel = np.full((height, width), 0, dtype=np.uint8)
g_channel = np.full((height, width), 100, dtype=np.uint8)
b_channel = np.full((height, width), 200, dtype=np.uint8)
img = np.stack((r_channel, g_channel, b_channel), axis=2)
return TextToImageResult(
output=caikit_dm.Image(img),
)
41 changes: 41 additions & 0 deletions tests/modules/text_to_image/test_tti_stub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright The Caikit Authors
#
# 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.

# Standard
from tempfile import TemporaryDirectory
import os

# Local
from caikit_computer_vision.modules.text_to_image import TTIStub
import caikit_computer_vision


def test_tti_stub():
"""Ensure that the stubs for load / save / run work as expected."""
# Make sure we can bootstrap a model
model = TTIStub.bootstrap("foo")
assert isinstance(model, TTIStub)

# Make sure we can run a fake inference on it
pred = model.run("This is a prompt", height=500, width=550)
pil_img = pred.output.as_pil()
assert pil_img.width == 550
assert pil_img.height == 500

# Make sure we can save the model
model_dirname = "my_model"
with TemporaryDirectory() as tmpdirname:
model_path = os.path.join(tmpdirname, model_dirname)
model.save(model_path)
reloaded_model = model.load(model_path)
9 changes: 8 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,11 @@ passenv =
setenv =
FLIT_USERNAME = __token__
commands = flit publish
skip_install = True
skip_install = True

[testenv:build]
description = build wheel
deps =
build
commands = python -m build
skip_install = True
Loading