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

adding container to update models on a schedule #46

Merged
merged 22 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
sudo apt-get update
sudo apt-get install docker-ce
- name: Build Docker Image
run: docker build --tag groundlight-edge .
run: docker build -f Dockerfile.app --tag groundlight-edge .

- name: Start Docker Container
id: start_container
Expand Down Expand Up @@ -136,7 +136,7 @@ jobs:
sudo apt-get update
sudo apt-get install docker-ce
- name: Build Docker Image
run: docker build --tag groundlight-edge .
run: docker build -f Dockerfile.app --tag groundlight-edge .

- name: Start Docker Container
id: start_container
Expand Down
File renamed without changes.
42 changes: 42 additions & 0 deletions Dockerfile.model_updater
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Build args
ARG APP_ROOT="/model_updater"
ARG POETRY_HOME="/opt/poetry"
ARG POETRY_VERSION=1.5.1

FROM python:3.11-slim-bullseye

# Args that are needed in this stage
ARG APP_ROOT
ARG POETRY_HOME
ARG POETRY_VERSION

# Install required dependencies and tools
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
curl \
libglib2.0-0 \
libgl1-mesa-glx \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& curl -sSL https://install.python-poetry.org | python -

# Set Python and Poetry ENV vars
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
POETRY_HOME=${POETRY_HOME} \
POETRY_VERSION=${POETRY_VERSION} \
PATH=${POETRY_HOME}/bin:$PATH

COPY model_updater ${APP_ROOT}/model_updater
COPY app/core ${APP_ROOT}/app/core

COPY ./pyproject.toml ${APP_ROOT}/

WORKDIR ${APP_ROOT}

# Install production dependencies only
RUN poetry install --only model_updater --no-interaction --no-root

# Entry command
CMD poetry run python -m model_updater.update_models
28 changes: 3 additions & 25 deletions app/core/app_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@

from app.core.utils import safe_call_api

from .configs import LocalInferenceConfig, MotionDetectionConfig, RootEdgeConfig
from .configs import LocalInferenceConfig, MotionDetectionConfig
from .edge_inference import EdgeInferenceManager
from .file_paths import DEFAULT_EDGE_CONFIG_PATH, INFERENCE_DEPLOYMENT_TEMPLATE_PATH
from .file_paths import INFERENCE_DEPLOYMENT_TEMPLATE_PATH
from .iqe_cache import IQECache
from .motion_detection import MotionDetectionManager
from .utils import load_edge_config

logger = logging.getLogger(__name__)

Expand All @@ -27,29 +28,6 @@
TTL_TIME = 3600 # 1 hour


def load_edge_config() -> RootEdgeConfig:
"""
Reads the edge config from the EDGE_CONFIG environment variable if it exists.
If EDGE_CONFIG is not set, reads the default edge config file.
"""
yaml_config = os.environ.get("EDGE_CONFIG", "").strip()
if yaml_config:
config = yaml.safe_load(yaml_config)
return RootEdgeConfig(**config)

logger.warning("EDGE_CONFIG environment variable not set. Checking default locations.")

default_paths = [DEFAULT_EDGE_CONFIG_PATH, "configs/edge-config.yaml"]

for path in default_paths:
if os.path.exists(path):
logger.info(f"Loading edge config from {path}")
config = yaml.safe_load(open(path, "r"))
return RootEdgeConfig(**config)

raise FileNotFoundError(f"Could not find edge config file in default locations: {default_paths}")


@lru_cache(maxsize=MAX_SDK_INSTANCES_CACHE_SIZE)
def _get_groundlight_sdk_instance_internal(api_token: str):
return Groundlight(api_token=api_token)
Expand Down
31 changes: 31 additions & 0 deletions app/core/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
import logging
import os
from io import BytesIO
from typing import Callable

import ksuid
import yaml
from fastapi import HTTPException
from PIL import Image

from .configs import RootEdgeConfig
from .file_paths import DEFAULT_EDGE_CONFIG_PATH

logger = logging.getLogger(__name__)


def load_edge_config() -> RootEdgeConfig:
"""
Reads the edge config from the EDGE_CONFIG environment variable if it exists.
If EDGE_CONFIG is not set, reads the default edge config file.
"""
yaml_config = os.environ.get("EDGE_CONFIG", "").strip()
if yaml_config:
config = yaml.safe_load(yaml_config)
return RootEdgeConfig(**config)

logger.warning("EDGE_CONFIG environment variable not set. Checking default locations.")

default_paths = [DEFAULT_EDGE_CONFIG_PATH, "configs/edge-config.yaml"]
Copy link
Member

Choose a reason for hiding this comment

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

Do we need configs/edge-config.yaml still?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For some reason looks like DEFAULT_EDGE_CONFIG_PATH wasn't being recognized. Will look into this closely separately from this PR.


for path in default_paths:
if os.path.exists(path):
logger.info(f"Loading edge config from {path}")
config = yaml.safe_load(open(path, "r"))
return RootEdgeConfig(**config)

raise FileNotFoundError(f"Could not find edge config file in default locations: {default_paths}")


def safe_call_api(api_method: Callable, **kwargs):
"""
Expand Down
16 changes: 0 additions & 16 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,3 @@
app.include_router(router=ping_router)

app.state.app_state = AppState()


@app.on_event("startup")
async def on_startup():
"""
On startup, update edge inference models.
"""
if not os.environ.get("DEPLOY_DETECTOR_LEVEL_INFERENCE", None):
return

for detector_id, inference_config in app.state.app_state.edge_inference_manager.inference_config.items():
if inference_config.enabled:
try:
app.state.app_state.edge_inference_manager.update_model(detector_id)
except Exception:
logging.error(f"Failed to update model for {detector_id}", exc_info=True)
1 change: 1 addition & 0 deletions deploy/bin/build-push-edge-endpoint-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ docker buildx inspect tempgroundlightedgebuilder --bootstrap
docker buildx build \
--platform linux/arm64,linux/amd64 \
--tag 723181461334.dkr.ecr.us-west-2.amazonaws.com/edge-endpoint:${TAG} \
-f ../../Dockerfile.app \
../.. --push
40 changes: 40 additions & 0 deletions deploy/bin/build-push-model-updater.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash

set -ex


# Ensure that you're in the same directory as this script before running it
cd "$(dirname "$0")"

TAG=$(./git-tag-name.sh)

# Authenticate docker to ECR
aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 723181461334.dkr.ecr.us-west-2.amazonaws.com

# We use docker buildx to build the image for multiple platforms. buildx comes
# installed with Docker Engine when installed via Docker Desktop. If you're
# on a Linux machine with an old version of Docker Engine, you may need to
# install buildx manually. Follow these instructions to install docker-buildx-plugin:
# https://docs.docker.com/engine/install/ubuntu/

# Install QEMU, a generic and open-source machine emulator and virtualizer
docker run --rm --privileged linuxkit/binfmt:af88a591f9cc896a52ce596b9cf7ca26a061ef97

# Check if tempbuilder already exists
if ! docker buildx ls | grep -q tempmodelupdaterbuilder; then
# Prep for multiplatform build - the build is done INSIDE a docker container
docker buildx create --name tempmodelupdaterbuilder --use
else
# If tempbuilder exists, set it as the current builder
docker buildx use tempmodelupdaterbuilder
fi

# Ensure that the tempbuilder container is running
docker buildx inspect tempmodelupdaterbuilder --bootstrap

# Build model updater image for amd64 and arm64
docker buildx build \
--platform linux/arm64,linux/amd64 \
--tag 723181461334.dkr.ecr.us-west-2.amazonaws.com/edge-endpoint:${TAG} \
-f ../../Dockerfile.model_updater \
../.. --push
23 changes: 21 additions & 2 deletions deploy/k3s/edge_deployment/edge_deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ spec:
serviceAccountName: edge-endpoint-service-account
containers:
- name: edge-endpoint
image: 723181461334.dkr.ecr.us-west-2.amazonaws.com/edge-endpoint:13361241c-state-management-via-k3s
image: 723181461334.dkr.ecr.us-west-2.amazonaws.com/edge-endpoint:4526476fb-model-updater
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6717
Expand All @@ -56,12 +56,31 @@ spec:
key: api-token
volumeMounts:
- name: edge-config-volume
# This is a path inside the container not the host
mountPath: /etc/groundlight/edge-config
- name: inference-deployment-template-volume
mountPath: /etc/groundlight/inference-deployment
- name: model-repo
mountPath: /mnt/models

- name: inference-model-updater
image: 723181461334.dkr.ecr.us-west-2.amazonaws.com/edge-endpoint:953306448-model-updater
imagePullPolicy: IfNotPresent
env:
- name: LOG_LEVEL
value: "INFO"
- name: DEPLOY_DETECTOR_LEVEL_INFERENCE
value: "True"
- name: GROUNDLIGHT_API_TOKEN
valueFrom:
secretKeyRef:
name: groundlight-secrets
key: api-token
volumeMounts:
- name: edge-config-volume
mountPath: /etc/groundlight/edge-config
- name: model-repo
mountPath: /mnt/models

imagePullSecrets:
- name: registry-credentials
volumes:
Expand Down
Empty file added model_updater/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions model_updater/update_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import os
import logging
import time
from app.core.utils import load_edge_config
from app.core.configs import RootEdgeConfig
from app.core.edge_inference import EdgeInferenceManager

log_level = os.environ.get("LOG_LEVEL", "INFO").upper()
logging.basicConfig(level=log_level)


def update_models():
if not os.environ.get("DEPLOY_DETECTOR_LEVEL_INFERENCE", None):
return

edge_config: RootEdgeConfig = load_edge_config()

edge_inference_templates = edge_config.local_inference_templates
inference_config = {
detector.detector_id: edge_inference_templates[detector.local_inference_template]
for detector in edge_config.detectors
}

edge_inference_manager = EdgeInferenceManager(config=edge_config.local_inference_templates, verbose=True)

for detector_id, inference_config in inference_config.items():
if inference_config.enabled:
try:
edge_inference_manager.update_model(detector_id=detector_id)
except Exception as e:
logging.error(f"Failed to update model for {detector_id}. {e}", exc_info=True)


if __name__ == "__main__":
while True:
update_models()
time.sleep(3600)
Copy link
Member

Choose a reason for hiding this comment

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

We should make this number configurable (via the edge_config file for example) and set the default small, maybe like every 2 minutes (its very cheap to check for a new model).

11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ tritonclient = {extras = ["all"], version = "2.36.0"}
kubernetes = "^27.2.0"
jinja2 = "^3.1.2"

[tool.poetry.group.model_updater.dependencies]
python = "^3.9"
fastapi = "^0.88.0"
pydantic = "^1.10.2"
pillow = "^9.5.0"
pyyaml = "^6.0"
svix-ksuid = "^0.6.2"
tritonclient = {extras = ["all"], version = "2.36.0"}
jinja2 = "^3.1.2"
requests = "^2.28.1"

[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
pytest-cov = "^4.0.0"
Expand Down