Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 21 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FROM ghcr.io/lambda-feedback/evaluation-function-base/python:3.12 AS builder

RUN pip install poetry==1.8.3
# Must match the Poetry major version that generated poetry.lock
RUN pip install "poetry>=2,<3"

ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
Expand All @@ -25,19 +26,33 @@ ENV VIRTUAL_ENV=/app/.venv \

COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}

# Precompile python files for faster startup
RUN python -m compileall -q .

# Copy the evaluation function to the app directory
COPY evaluation_function ./evaluation_function

# Precompile python files for faster startup. Runs after the COPY so the
# function's own code is included: Lambda's filesystem is read-only, so
# anything not compiled here is recompiled on every cold start.
RUN python -m compileall -q .

# Command to start the evaluation function with
ENV FUNCTION_COMMAND="python"

# Args to start the evaluation function with
ENV FUNCTION_ARGS="-m,evaluation_function.main"

# The transport to use for the RPC server
ENV FUNCTION_RPC_TRANSPORT="ipc"
# Talk to the worker over stdio rather than an IPC socket. With ipc, shimmy
# blocks Lambda's 10 s init until Python has opened the socket, which the
# torch import can't do on a cold start. With stdio, init returns at once and
# the request waits on stdin while Python finishes importing.
ENV FUNCTION_INTERFACE="rpc"
ENV FUNCTION_RPC_TRANSPORT="stdio"

# The first request after a cold start waits for torch/ultralytics to import
# and the model to load, which takes longer than shimmy's 30 s default.
ENV FUNCTION_WORKER_SEND_TIMEOUT="90s"

# TEMPORARY: log per-module import times to stderr (shimmy forwards it to
# CloudWatch) to find what dominates the cold start. Remove once measured.
ENV PYTHONPROFILEIMPORTTIME=1

ENV LOG_LEVEL="debug"
41 changes: 25 additions & 16 deletions evaluation_function/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@
# Cache for loaded models by name
_model_cache = {}

DEFAULT_MODEL_NAME = "best.pt"


def _load_model(model_name):
if model_name not in _model_cache:
model_dir = os.path.dirname(os.path.abspath(__file__))
# Strip any directory components so `model_name` can't be used to
# load a model file from outside the evaluation_function directory.
safe_model_name = os.path.basename(model_name)
model_path = os.path.join(model_dir, safe_model_name)
_model_cache[model_name] = YOLO(model_path)
return _model_cache[model_name]


# Load the default model at import time so it overlaps the Lambda cold start
# instead of counting against the first request's timeout.
_load_model(DEFAULT_MODEL_NAME)


def evaluation_function(
response: Any,
Expand All @@ -36,7 +54,7 @@ def evaluation_function(
)

draw_images = params.get("draw_images", True)
model_name = params.get("model_name", "best.pt")
model_name = params.get("model_name", DEFAULT_MODEL_NAME)
conf_threshold = params.get("conf_threshold", 0.5)
# Optional array of class names to restrict detection to. Names are
# matched against the model's own class names (model.names),
Expand All @@ -45,19 +63,10 @@ def evaluation_function(

model_load_start = time.time()

# Use a dict to cache models by name
if model_name not in _model_cache:
model_dir = os.path.dirname(os.path.abspath(__file__))
# Strip any directory components so `model_name` can't be used to
# load a model file from outside the evaluation_function directory.
safe_model_name = os.path.basename(model_name)
model_path = os.path.join(model_dir, safe_model_name)
_model_cache[model_name] = YOLO(model_path)
model = _load_model(model_name)

model_load_time = time.time() - model_load_start

model = _model_cache[model_name]

# Resolve `allowed_classes` names to the class indices YOLO expects.
# Unmatched names are kept aside so they can be surfaced in debug
# feedback instead of silently doing nothing.
Expand Down Expand Up @@ -248,10 +257,10 @@ def analyze_images(images, draw_images=True):
{"annotated": annotated, "detections": detections, "load_error": None}
)

avg_load_time = np.mean(load_times) if load_times else 0.0
avg_prediction_time = np.mean(prediction_times) if prediction_times else 0.0
avg_process_time = np.mean(process_times) if process_times else 0.0
avg_draw_time = np.mean(draw_times) if draw_times else 0.0
avg_load_time = sum(load_times) / len(load_times) if load_times else 0.0
avg_prediction_time = sum(prediction_times) / len(prediction_times) if prediction_times else 0.0
avg_process_time = sum(process_times) / len(process_times) if process_times else 0.0
avg_draw_time = sum(draw_times) / len(draw_times) if draw_times else 0.0

return (
per_image,
Expand Down Expand Up @@ -309,7 +318,7 @@ def analyze_images(images, draw_images=True):
append_feedback(f"Separator [{idx}]", " \n\n---\n\n")
upload_times.append(0.0)

avg_upload_time = np.mean(upload_times) if upload_times else 0.0
avg_upload_time = sum(upload_times) / len(upload_times) if upload_times else 0.0

feedback_time = time.time() - feedback_start

Expand Down
Loading
Loading