From bae8b153b50e61adf588f091cd70de1d61fd8690 Mon Sep 17 00:00:00 2001 From: Gregory Pereira Date: Tue, 14 May 2024 10:54:58 -0700 Subject: [PATCH] object detection recipe and MS tests (#278) * object detection recipe and MS tests Signed-off-by: greg pereira Signed-off-by: Gregory-Pereira Signed-off-by: greg pereira * adding a object_detection_client workflow file Signed-off-by: greg pereira * addressing michael's comments Signed-off-by: greg pereira * properly name object detection client job + moving pip install to tests Signed-off-by: greg pereira --------- Signed-off-by: greg pereira Signed-off-by: Gregory-Pereira --- .github/workflows/model_servers.yaml | 6 ++ .github/workflows/object_detection.yaml | 89 +++++++++++++++++++ model_servers/common/Makefile.common | 2 +- .../object_detection_python/Makefile | 26 +++--- .../base/Containerfile | 5 +- .../src/object_detection_server.py | 6 +- .../object_detection_python/src/run.sh | 9 ++ .../object_detection_python/tests/__init__.py | 0 .../object_detection_python/tests/conftest.py | 46 ++++++++++ .../tests/requirements.txt | 8 ++ .../tests/test_alive.py | 12 +++ models/Makefile | 7 ++ .../computer_vision/object_detection/Makefile | 15 ++++ .../{client => app}/Containerfile | 0 .../object_detection_client.py | 0 .../object_detection/app/requirements.txt | 40 +++++++++ .../object_detection/client/requirements.txt | 3 - recipes/computer_vision/tests/conftest.py | 8 ++ .../tests/functional/__init__.py | 0 .../tests/functional/conftest.py | 58 ++++++++++++ .../tests/functional/test_app.py | 17 ++++ .../tests/integration/__init__.py | 0 .../tests/integration/conftest.py | 7 ++ .../tests/integration/test_app.py | 3 + .../computer_vision/tests/requirements.txt | 8 ++ 25 files changed, 354 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/object_detection.yaml create mode 100644 model_servers/object_detection_python/src/run.sh create mode 100644 model_servers/object_detection_python/tests/__init__.py create mode 100644 model_servers/object_detection_python/tests/conftest.py create mode 100644 model_servers/object_detection_python/tests/requirements.txt create mode 100644 model_servers/object_detection_python/tests/test_alive.py create mode 100644 recipes/computer_vision/object_detection/Makefile rename recipes/computer_vision/object_detection/{client => app}/Containerfile (100%) rename recipes/computer_vision/object_detection/{client => app}/object_detection_client.py (100%) create mode 100644 recipes/computer_vision/object_detection/app/requirements.txt delete mode 100644 recipes/computer_vision/object_detection/client/requirements.txt create mode 100644 recipes/computer_vision/tests/conftest.py create mode 100644 recipes/computer_vision/tests/functional/__init__.py create mode 100644 recipes/computer_vision/tests/functional/conftest.py create mode 100644 recipes/computer_vision/tests/functional/test_app.py create mode 100644 recipes/computer_vision/tests/integration/__init__.py create mode 100644 recipes/computer_vision/tests/integration/conftest.py create mode 100644 recipes/computer_vision/tests/integration/test_app.py create mode 100644 recipes/computer_vision/tests/requirements.txt diff --git a/.github/workflows/model_servers.yaml b/.github/workflows/model_servers.yaml index e6958e66..213ba354 100644 --- a/.github/workflows/model_servers.yaml +++ b/.github/workflows/model_servers.yaml @@ -44,6 +44,12 @@ jobs: directory: whispercpp platforms: linux/amd64,linux/arm64 no_gpu: 1 + - image_name: object_detection_python + model: facebook-detr-resnet-101 + flavor: base + directory: object_detection_python + platforms: linux/amd64,linux/arm64 + no_gpu: 1 runs-on: ubuntu-22.04 permissions: contents: read diff --git a/.github/workflows/object_detection.yaml b/.github/workflows/object_detection.yaml new file mode 100644 index 00000000..f50c8f60 --- /dev/null +++ b/.github/workflows/object_detection.yaml @@ -0,0 +1,89 @@ +name: Object Detection + +on: + pull_request: + branches: + - main + paths: + - ./recipes/computer_vision/object_detection/** + - .github/workflows/object_detection.yaml + push: + branches: + - main + paths: + - ./recipes/computer_vision/object_detection/** + - .github/workflows/object_detection.yaml + + workflow_dispatch: + +env: + REGISTRY: ghcr.io + REGISTRY_ORG: containers + RECIPE_NAME: object_detection + RECIPE_TYPE: computer_vision + IMAGE_NAME: object_detection_client + +jobs: + object-detection-client-build-and-push: + if: "!contains(github.event.pull_request.labels.*.name, 'hold-tests')" + runs-on: ubuntu-22.04 + permissions: + contents: read + packages: write + services: + registry: + image: registry:2.8.3 + ports: + - 5000:5000 + steps: + - uses: actions/checkout@v4.1.4 + + - name: Install qemu dependency + run: | + sudo apt-get update + sudo apt-get install -y qemu-user-static + + - name: Build Image + id: build_image + uses: redhat-actions/buildah-build@v2.13 + with: + image: ${{ env.REGISTRY }}/${{ env.REGISTRY_ORG }}/${{ env.IMAGE_NAME }} + tags: latest + platforms: linux/amd64,linux/arm64 + containerfiles: ./recipes/${{ env.RECIPE_TYPE }}/${{ env.RECIPE_NAME }}/app/Containerfile + context: recipes/${{ env.RECIPE_TYPE }}/${{ env.RECIPE_NAME }}/app + + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: '3.11' + + - name: Install Dependencies + working-directory: ./recipes/${{ env.RECIPE_TYPE }}/${{ env.RECIPE_NAME }} + run: make install + + - name: Download model + working-directory: ./models + run: make download-model-facebook-detr-resnet-101 + + - name: Run Functional Tests + shell: bash + run: make functional-tests + working-directory: ./recipes/${{ env.RECIPE_TYPE }}/${{ env.RECIPE_NAME }} + + - name: Login to Registry + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: redhat-actions/podman-login@v1.7 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push Image + id: push_image + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: redhat-actions/push-to-registry@v2.8 + with: + image: ${{ steps.build_image.outputs.image }} + tags: ${{ steps.build_image.outputs.tags }} + registry: ${{ env.REGISTRY }} diff --git a/model_servers/common/Makefile.common b/model_servers/common/Makefile.common index ee249451..b0fff263 100644 --- a/model_servers/common/Makefile.common +++ b/model_servers/common/Makefile.common @@ -10,7 +10,7 @@ endif .PHONY: build build: - podman build --squash-all --build-arg $(PORT) -t $(IMAGE) . -f base/Containerfile + podman build --squash-all --build-arg PORT=$(PORT) -t $(IMAGE) . -f base/Containerfile .PHONY: install install: diff --git a/model_servers/object_detection_python/Makefile b/model_servers/object_detection_python/Makefile index ba9c727c..c4e2f876 100644 --- a/model_servers/object_detection_python/Makefile +++ b/model_servers/object_detection_python/Makefile @@ -1,33 +1,33 @@ APP := object_detection_python PORT ?= 8000 -include ../common/Makefile.common +REGISTRY ?= ghcr.io +REGISTRY_ORG ?= containers -IMAGE_NAME ?= $(REGISTRY_ORG)/$(COMPONENT)/$(APP):latest -IMAGE := $(REGISTRY)/$(IMAGE_NAME) -CUDA_IMAGE := $(REGISTRY)/$(REGISTRY_ORG)/$(COMPONENT)/$(APP)_cuda:latest -VULKAN_IMAGE := $(REGISTRY)/$(REGISTRY_ORG)/$(COMPONENT)/$(APP)_vulkan:latest +MODEL_NAME ?= facebook/detr-resnet-101 +MODELS_DIR := /app/models +include ../common/Makefile.common -MODEL_NAME ?= facebook/detr-resnet-101 -MODELS_DIR := /models +IMAGE_NAME ?= $(REGISTRY_ORG)/$(APP):latest +IMAGE := $(REGISTRY)/$(IMAGE_NAME) +# Run override required because of the multi-directory models and model_path vs models_dir .PHONY: run run: cd ../../models && \ podman run -it -d -p $(PORT):$(PORT) -v ./$(MODEL_NAME):$(MODELS_DIR)/$(MODEL_NAME):$(BIND_MOUNT_OPTIONS) -e MODEL_PATH=$(MODELS_DIR)/$(MODEL_NAME) -e HOST=0.0.0.0 -e PORT=$(PORT) $(IMAGE) - .PHONY: all all: build download-model-facebook-detr-resnet-101 run .PHONY: download-model-facebook-detr-resnet-101 download-model-facebook-detr-resnet-101: - cd ../../models/ && \ - python download_hf_models.py -m facebook/detr-resnet-101 + cd ../../models && \ + make download-model-facebook-detr-resnet-101 .PHONY: test test: - $(MAKE) download-model-facebook-detr-resnet-101 - ln -s ../../models/detr-resnet-101 ./ - PORT=$(PORT) MODEL_NAME=$(MODEL_NAME) MODELS_PATH=$(MODELS_PATH) IMAGE=$(IMAGE) PULL_ALWAYS=0 pytest -s -vvv + pip install -r ../../convert_models/requirements.txt + cp -r ../../models/facebook ./ + REGISTRY=$(REGISTRY) MODEL_NAME=$(MODEL_NAME) MODELS_DIR=$(MODELS_DIR) IMAGE_NAME=$(IMAGE_NAME) PORT=$(PORT) pytest -s -vvv diff --git a/model_servers/object_detection_python/base/Containerfile b/model_servers/object_detection_python/base/Containerfile index a6889919..3849cea3 100644 --- a/model_servers/object_detection_python/base/Containerfile +++ b/model_servers/object_detection_python/base/Containerfile @@ -1,9 +1,8 @@ FROM registry.access.redhat.com/ubi9/python-311:1-62.1714671026 ARG PORT=8000 WORKDIR /app -COPY src/requirements.txt . +COPY src . RUN pip install --upgrade pip && \ pip install --no-cache-dir --upgrade -r requirements.txt -COPY src/object_detection_server.py . EXPOSE $PORT -ENTRYPOINT [ "uvicorn", "object_detection_server:app", "--host", "0.0.0.0" ] \ No newline at end of file +ENTRYPOINT [ "sh", "./run.sh" ] diff --git a/model_servers/object_detection_python/src/object_detection_server.py b/model_servers/object_detection_python/src/object_detection_server.py index a0e339d5..0f8a1e99 100644 --- a/model_servers/object_detection_python/src/object_detection_server.py +++ b/model_servers/object_detection_python/src/object_detection_server.py @@ -11,7 +11,7 @@ app = FastAPI() -model = os.getenv("MODEL_PATH", default="facebook/detr-resnet-101") +model = os.getenv("MODEL_PATH", default="/app/models/facebook/detr-resnet-101") revision = os.getenv("MODEL_REVISION", default="no_timm") if os.path.isfile(model): @@ -30,6 +30,10 @@ class Item(BaseModel): image: bytes +@app.get("/health") +def tests_alive(): + return {"alive": True} + @app.post("/detection") def detection(item: Item): b64_image = item.image diff --git a/model_servers/object_detection_python/src/run.sh b/model_servers/object_detection_python/src/run.sh new file mode 100644 index 00000000..0267f48c --- /dev/null +++ b/model_servers/object_detection_python/src/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [ ${MODEL_PATH} ]; then + PORT=${PORT} MODEL_PATH=${MODEL_PATH} uvicorn object_detection_server:app --port ${PORT:=8000} --host ${HOST:=0.0.0.0} + exit 0 +fi + +echo "Please set either a MODEL_PATH" +exit 1 diff --git a/model_servers/object_detection_python/tests/__init__.py b/model_servers/object_detection_python/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/model_servers/object_detection_python/tests/conftest.py b/model_servers/object_detection_python/tests/conftest.py new file mode 100644 index 00000000..ad019c9d --- /dev/null +++ b/model_servers/object_detection_python/tests/conftest.py @@ -0,0 +1,46 @@ +import pytest_container +import os + +REGISTRY = os.getenv("REGISTRY", "ghcr.io") +IMAGE_NAME = os.getenv("IMAGE_NAME", "containers/object_detection_python:latest") +MODEL_NAME = os.getenv("MODEL_NAME", "facebook/detr-resnet-101") +MODELS_DIR = os.getenv("MODELS_DIR", "/app/models") + +MODEL_PATH = f"{MODELS_DIR}/{MODEL_NAME}" + +PORT = os.getenv("PORT", 8000) +if type(PORT) == str: + try: + PORT = int(PORT) + except: + PORT = 8000 + +MS = pytest_container.Container( + url=f"containers-storage:{REGISTRY}/{IMAGE_NAME}", + volume_mounts=[ + pytest_container.container.BindMount( + container_path=f"{MODEL_PATH}", + host_path=f"./{MODEL_NAME}", + flags=["ro"] + ) + ], + extra_environment_variables={ + "MODEL_PATH": f"{MODEL_PATH}", + "HOST": "0.0.0.0", + "PORT": f"{PORT}", + "IMAGE_NAME": f"{IMAGE_NAME}", + "REGISTRY": f"{REGISTRY}" + }, + forwarded_ports=[ + pytest_container.PortForwarding( + container_port=PORT, + host_port=PORT + ) + ], + ) + +def pytest_generate_tests(metafunc): + pytest_container.auto_container_parametrize(metafunc) + +def pytest_addoption(parser): + pytest_container.add_logging_level_options(parser) diff --git a/model_servers/object_detection_python/tests/requirements.txt b/model_servers/object_detection_python/tests/requirements.txt new file mode 100644 index 00000000..22fc97f2 --- /dev/null +++ b/model_servers/object_detection_python/tests/requirements.txt @@ -0,0 +1,8 @@ +pip==24.0 +pytest-container==0.4.0 +pytest-selenium==4.1.0 +pytest-testinfra==10.1.0 +pytest==8.1.1 +requests==2.31.0 +selenium==4.19.0 +tenacity==8.2.3 diff --git a/model_servers/object_detection_python/tests/test_alive.py b/model_servers/object_detection_python/tests/test_alive.py new file mode 100644 index 00000000..226aac1c --- /dev/null +++ b/model_servers/object_detection_python/tests/test_alive.py @@ -0,0 +1,12 @@ +import pytest_container +from .conftest import MS +import tenacity + +CONTAINER_IMAGES = [MS] + +def test_etc_os_release_present(auto_container: pytest_container.container.ContainerData): + assert auto_container.connection.file("/etc/os-release").exists + +@tenacity.retry(stop=tenacity.stop_after_attempt(5), wait=tenacity.wait_exponential()) +def test_alive(auto_container: pytest_container.container.ContainerData, host): + host.run_expect([0],f"curl http://localhost:{auto_container.forwarded_ports[0].host_port}",).stdout.strip() diff --git a/models/Makefile b/models/Makefile index 92ff4a0d..53d914e9 100644 --- a/models/Makefile +++ b/models/Makefile @@ -37,6 +37,13 @@ download-model-mistral: download-model-mistral-code: $(MAKE) MODEL_NAME=mistral-7b-code-16k-qlora.Q4_K_M.gguf MODEL_URL=https://huggingface.co/TheBloke/Mistral-7B-Code-16K-qlora-GGUF/resolve/main/mistral-7b-code-16k-qlora.Q4_K_M.gguf download-model +.PHONY: download-model-facebook-detr-resnet-101 +download-model-facebook-detr-resnet-101: + pip install -r ../convert_models/requirements.txt + cd ../convert_models/ && \ + python3 download_huggingface.py -m facebook/detr-resnet-101 + cp -r ../convert_models/converted_models/facebook ./ + .PHONY: clean clean: -rm -f *tmp diff --git a/recipes/computer_vision/object_detection/Makefile b/recipes/computer_vision/object_detection/Makefile new file mode 100644 index 00000000..b18a45b3 --- /dev/null +++ b/recipes/computer_vision/object_detection/Makefile @@ -0,0 +1,15 @@ +SHELL := /bin/bash +APP ?= object_detection_client +PORT ?= 8501 +MODEL_NAME ?= facebook/detr-resnet-101 + +include ../../common/Makefile.common + +.PHONY: functional-tests +functional-tests: + IMAGE_NAME=${IMAGE_NAME} REGISTRY=${REGISTRY} MODEL_NAME=${MODEL_NAME} pytest -vvv --driver=Chrome --driver-path=$(RECIPE_BINARIES_PATH)/chromedriver ${RELATIVE_TESTS_PATH}/functional + +RECIPE_BINARIES_PATH := $(shell realpath ../../common/bin) +RELATIVE_MODELS_PATH := ../../../models +RELATIVE_TESTS_PATH := ../tests + diff --git a/recipes/computer_vision/object_detection/client/Containerfile b/recipes/computer_vision/object_detection/app/Containerfile similarity index 100% rename from recipes/computer_vision/object_detection/client/Containerfile rename to recipes/computer_vision/object_detection/app/Containerfile diff --git a/recipes/computer_vision/object_detection/client/object_detection_client.py b/recipes/computer_vision/object_detection/app/object_detection_client.py similarity index 100% rename from recipes/computer_vision/object_detection/client/object_detection_client.py rename to recipes/computer_vision/object_detection/app/object_detection_client.py diff --git a/recipes/computer_vision/object_detection/app/requirements.txt b/recipes/computer_vision/object_detection/app/requirements.txt new file mode 100644 index 00000000..f49b8668 --- /dev/null +++ b/recipes/computer_vision/object_detection/app/requirements.txt @@ -0,0 +1,40 @@ +altair==5.3.0 +attrs==23.2.0 +blinker==1.7.0 +cachetools==5.3.3 +certifi==2024.2.2 +charset-normalizer==3.3.2 +click==8.1.7 +gitdb==4.0.11 +GitPython==3.1.43 +idna==3.7 +Jinja2==3.1.3 +jsonschema==4.21.1 +jsonschema-specifications==2023.12.1 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +numpy==1.26.4 +packaging==24.0 +pandas==2.2.2 +pillow==10.3.0 +protobuf==4.25.3 +pyarrow==15.0.2 +pydeck==0.8.1b0 +Pygments==2.17.2 +python-dateutil==2.9.0.post0 +pytz==2024.1 +referencing==0.34.0 +requests==2.31.0 +rich==13.7.1 +rpds-py==0.18.0 +six==1.16.0 +smmap==5.0.1 +streamlit==1.33.0 +tenacity==8.2.3 +toml==0.10.2 +toolz==0.12.1 +tornado==6.4 +typing_extensions==4.11.0 +tzdata==2024.1 +urllib3==2.2.1 diff --git a/recipes/computer_vision/object_detection/client/requirements.txt b/recipes/computer_vision/object_detection/client/requirements.txt deleted file mode 100644 index 7b2195a0..00000000 --- a/recipes/computer_vision/object_detection/client/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -streamlit -requests -pillow \ No newline at end of file diff --git a/recipes/computer_vision/tests/conftest.py b/recipes/computer_vision/tests/conftest.py new file mode 100644 index 00000000..7a2206fa --- /dev/null +++ b/recipes/computer_vision/tests/conftest.py @@ -0,0 +1,8 @@ +import pytest +import os + + +@pytest.fixture +def chrome_options(chrome_options): + chrome_options.add_argument("--headless") + return chrome_options diff --git a/recipes/computer_vision/tests/functional/__init__.py b/recipes/computer_vision/tests/functional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/recipes/computer_vision/tests/functional/conftest.py b/recipes/computer_vision/tests/functional/conftest.py new file mode 100644 index 00000000..35f5137b --- /dev/null +++ b/recipes/computer_vision/tests/functional/conftest.py @@ -0,0 +1,58 @@ +import pytest_container +import os +import logging + +REGISTRY=os.environ['REGISTRY'] +IMAGE_NAME=os.environ['IMAGE_NAME'] +MODEL_NAME=os.environ['MODEL_NAME'] + +logging.info(""" +Starting pytest with the following ENV vars: + REGISTRY: {REGISTRY} + IMAGE_NAME: {IMAGE_NAME} + MODEL_NAME: {MODEL_NAME} +For: + model_server: whispercpp +""".format(REGISTRY=REGISTRY, IMAGE_NAME=IMAGE_NAME, MODEL_NAME=MODEL_NAME)) + + +MS = pytest_container.Container( + url=f"containers-storage:{REGISTRY}/{IMAGE_NAME}", + volume_mounts=[ + pytest_container.container.BindMount( + container_path=f"/locallm/models/${MODEL_NAME}", + host_path=f"./{MODEL_NAME}", + flags=["ro"] + ) + ], + extra_environment_variables={ + "MODEL_PATH": f"/locall/models/{MODEL_NAME}", + "HOST": "0.0.0.0", + "PORT": "8001" + }, + forwarded_ports=[ + pytest_container.PortForwarding( + container_port=8001, + host_port=8001 + ) + ], + ) + +CB = pytest_container.Container( + url=f"containers-storage:{os.environ['REGISTRY']}/containers/{os.environ['IMAGE_NAME']}", + extra_environment_variables={ + "MODEL_ENDPOINT": "http://10.88.0.1:8001" + }, + forwarded_ports=[ + pytest_container.PortForwarding( + container_port=8501, + host_port=8501 + ) + ], + ) + +def pytest_generate_tests(metafunc): + pytest_container.auto_container_parametrize(metafunc) + +def pytest_addoption(parser): + pytest_container.add_logging_level_options(parser) diff --git a/recipes/computer_vision/tests/functional/test_app.py b/recipes/computer_vision/tests/functional/test_app.py new file mode 100644 index 00000000..73cf26de --- /dev/null +++ b/recipes/computer_vision/tests/functional/test_app.py @@ -0,0 +1,17 @@ +import pytest_container +from .conftest import CB +import tenacity + +CONTAINER_IMAGES = [CB] + +def test_etc_os_release_present(auto_container: pytest_container.container.ContainerData): + assert auto_container.connection.file("/etc/os-release").exists + +@tenacity.retry(stop=tenacity.stop_after_attempt(5), wait=tenacity.wait_exponential()) +def test_alive(auto_container: pytest_container.container.ContainerData, host): + host.run_expect([0],f"curl http://localhost:{auto_container.forwarded_ports[0].host_port}",).stdout.strip() + +def test_title(auto_container: pytest_container.container.ContainerData, selenium): + selenium.get(f"http://localhost:{auto_container.forwarded_ports[0].host_port}") + assert selenium.title == "Streamlit" + diff --git a/recipes/computer_vision/tests/integration/__init__.py b/recipes/computer_vision/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/recipes/computer_vision/tests/integration/conftest.py b/recipes/computer_vision/tests/integration/conftest.py new file mode 100644 index 00000000..3a67a71c --- /dev/null +++ b/recipes/computer_vision/tests/integration/conftest.py @@ -0,0 +1,7 @@ +import os +import pytest + + +@pytest.fixture() +def url(): + return os.environ["URL"] diff --git a/recipes/computer_vision/tests/integration/test_app.py b/recipes/computer_vision/tests/integration/test_app.py new file mode 100644 index 00000000..e2825e60 --- /dev/null +++ b/recipes/computer_vision/tests/integration/test_app.py @@ -0,0 +1,3 @@ +def test_title(url,selenium): + selenium.get(f"http://{url}:8501") + assert selenium.title == "Streamlit" diff --git a/recipes/computer_vision/tests/requirements.txt b/recipes/computer_vision/tests/requirements.txt new file mode 100644 index 00000000..22fc97f2 --- /dev/null +++ b/recipes/computer_vision/tests/requirements.txt @@ -0,0 +1,8 @@ +pip==24.0 +pytest-container==0.4.0 +pytest-selenium==4.1.0 +pytest-testinfra==10.1.0 +pytest==8.1.1 +requests==2.31.0 +selenium==4.19.0 +tenacity==8.2.3