1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

dev(ml): fixed docker-compose.dev.yml, updated locust (#3951)

* fixed dev docker compose

* updated locustfile

* deleted old script, moved comments to locustfile
This commit is contained in:
Mert 2023-09-01 21:59:17 -04:00 committed by GitHub
parent bea287c5b3
commit b7fd5dcb4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 64 additions and 48 deletions

View file

@ -34,7 +34,7 @@ services:
ports: ports:
- 3003:3003 - 3003:3003
volumes: volumes:
- ../machine-learning/app:/usr/src/app - ../machine-learning:/usr/src/app
- model-cache:/cache - model-cache:/cache
env_file: env_file:
- .env - .env

View file

@ -1,24 +0,0 @@
export MACHINE_LEARNING_CACHE_FOLDER=/tmp/model_cache
export MACHINE_LEARNING_MIN_FACE_SCORE=0.034 # returns 1 face per request; setting this to 0 blows up the number of faces to the thousands
export MACHINE_LEARNING_MIN_TAG_SCORE=0.0
export PID_FILE=/tmp/locust_pid
export LOG_FILE=/tmp/gunicorn.log
export HEADLESS=false
export HOST=127.0.0.1:3003
export CONCURRENCY=4
export NUM_ENDPOINTS=3
export PYTHONPATH=app
gunicorn app.main:app --worker-class uvicorn.workers.UvicornWorker \
--bind $HOST --daemon --error-logfile $LOG_FILE --pid $PID_FILE
while true ; do
echo "Loading models..."
sleep 5
if cat $LOG_FILE | grep -q -E "startup complete"; then break; fi
done
# "users" are assigned only one task, so multiply concurrency by the number of tasks
locust --host http://$HOST --web-host 127.0.0.1 \
--run-time 120s --users $(($CONCURRENCY * $NUM_ENDPOINTS)) $(if $HEADLESS; then echo "--headless"; fi)
if [[ -e $PID_FILE ]]; then kill $(cat $PID_FILE); fi

View file

@ -1,13 +1,32 @@
from io import BytesIO from io import BytesIO
import json
from typing import Any
from locust import HttpUser, events, task from locust import HttpUser, events, task
from locust.env import Environment
from PIL import Image from PIL import Image
from argparse import ArgumentParser
byte_image = BytesIO()
@events.init_command_line_parser.add_listener
def _(parser: ArgumentParser) -> None:
parser.add_argument("--tag-model", type=str, default="microsoft/resnet-50")
parser.add_argument("--clip-model", type=str, default="ViT-B-32::openai")
parser.add_argument("--face-model", type=str, default="buffalo_l")
parser.add_argument("--tag-min-score", type=int, default=0.0,
help="Returns all tags at or above this score. The default returns all tags.")
parser.add_argument("--face-min-score", type=int, default=0.034,
help=("Returns all faces at or above this score. The default returns 1 face per request; "
"setting this to 0 blows up the number of faces to the thousands."))
parser.add_argument("--image-size", type=int, default=1000)
@events.test_start.add_listener @events.test_start.add_listener
def on_test_start(environment, **kwargs): def on_test_start(environment: Environment, **kwargs: Any) -> None:
global byte_image global byte_image
image = Image.new("RGB", (1000, 1000)) assert environment.parsed_options is not None
image = Image.new("RGB", (environment.parsed_options.image_size, environment.parsed_options.image_size))
byte_image = BytesIO() byte_image = BytesIO()
image.save(byte_image, format="jpeg") image.save(byte_image, format="jpeg")
@ -19,34 +38,55 @@ class InferenceLoadTest(HttpUser):
headers: dict[str, str] = {"Content-Type": "image/jpg"} headers: dict[str, str] = {"Content-Type": "image/jpg"}
# re-use the image across all instances in a process # re-use the image across all instances in a process
def on_start(self): def on_start(self) -> None:
global byte_image global byte_image
self.data = byte_image.getvalue() self.data = byte_image.getvalue()
class ClassificationLoadTest(InferenceLoadTest): class ClassificationFormDataLoadTest(InferenceLoadTest):
@task @task
def classify(self): def classify(self) -> None:
self.client.post( data = [
"/image-classifier/tag-image", data=self.data, headers=self.headers ("modelName", self.environment.parsed_options.clip_model),
) ("modelType", "clip"),
("options", json.dumps({"minScore": self.environment.parsed_options.tag_min_score})),
]
files = {"image": self.data}
self.client.post("/predict", data=data, files=files)
class CLIPLoadTest(InferenceLoadTest): class CLIPTextFormDataLoadTest(InferenceLoadTest):
@task @task
def encode_image(self): def encode_text(self) -> None:
self.client.post( data = [
"/sentence-transformer/encode-image", ("modelName", self.environment.parsed_options.clip_model),
data=self.data, ("modelType", "clip"),
headers=self.headers, ("options", json.dumps({"mode": "text"})),
) ("text", "test search query")
]
self.client.post("/predict", data=data)
class RecognitionLoadTest(InferenceLoadTest): class CLIPVisionFormDataLoadTest(InferenceLoadTest):
@task @task
def recognize(self): def encode_image(self) -> None:
self.client.post( data = [
"/facial-recognition/detect-faces", ("modelName", self.environment.parsed_options.clip_model),
data=self.data, ("modelType", "clip"),
headers=self.headers, ("options", json.dumps({"mode": "vision"})),
) ]
files = {"image": self.data}
self.client.post("/predict", data=data, files=files)
class RecognitionFormDataLoadTest(InferenceLoadTest):
@task
def recognize(self) -> None:
data = [
("modelName", self.environment.parsed_options.face_model),
("modelType", "facial-recognition"),
("options", json.dumps({"minScore": self.environment.parsed_options.face_min_score})),
]
files = {"image": self.data}
self.client.post("/predict", data=data, files=files)

View file

@ -10,4 +10,4 @@ gunicorn app.main:app \
-k uvicorn.workers.UvicornWorker \ -k uvicorn.workers.UvicornWorker \
-w $MACHINE_LEARNING_WORKERS \ -w $MACHINE_LEARNING_WORKERS \
-b $MACHINE_LEARNING_HOST:$MACHINE_LEARNING_PORT \ -b $MACHINE_LEARNING_HOST:$MACHINE_LEARNING_PORT \
--log-config-json log_conf.json --log-config-json log_conf.json