2023-08-25 04:28:51 +00:00
|
|
|
import asyncio
|
2023-08-30 08:22:01 +00:00
|
|
|
import logging
|
2023-05-17 17:07:17 +00:00
|
|
|
import os
|
2023-08-25 04:28:51 +00:00
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
2023-06-05 14:40:48 +00:00
|
|
|
from typing import Any
|
2023-06-07 01:48:51 +00:00
|
|
|
|
2023-08-29 13:58:00 +00:00
|
|
|
import orjson
|
2023-06-25 03:18:09 +00:00
|
|
|
import uvicorn
|
2023-08-29 13:58:00 +00:00
|
|
|
from fastapi import FastAPI, Form, HTTPException, UploadFile
|
|
|
|
from fastapi.responses import ORJSONResponse
|
|
|
|
from starlette.formparsers import MultiPartParser
|
2023-06-25 03:18:09 +00:00
|
|
|
|
2023-08-25 04:28:51 +00:00
|
|
|
from app.models.base import InferenceModel
|
|
|
|
|
2023-08-30 08:22:01 +00:00
|
|
|
from .config import log, settings
|
2023-06-25 03:18:09 +00:00
|
|
|
from .models.cache import ModelCache
|
|
|
|
from .schemas import (
|
2023-06-05 14:40:48 +00:00
|
|
|
MessageResponse,
|
2023-06-25 03:18:09 +00:00
|
|
|
ModelType,
|
2023-06-05 14:40:48 +00:00
|
|
|
TextResponse,
|
|
|
|
)
|
2023-06-18 03:49:19 +00:00
|
|
|
|
2023-08-29 13:58:00 +00:00
|
|
|
MultiPartParser.max_file_size = 2**24 # spools to disk if payload is 16 MiB or larger
|
2023-05-17 17:07:17 +00:00
|
|
|
app = FastAPI()
|
|
|
|
|
2023-04-26 10:39:24 +00:00
|
|
|
|
2023-06-27 23:21:33 +00:00
|
|
|
def init_state() -> None:
|
2023-07-11 17:01:21 +00:00
|
|
|
app.state.model_cache = ModelCache(ttl=settings.model_ttl, revalidate=settings.model_ttl > 0)
|
2023-08-30 08:22:01 +00:00
|
|
|
log.info(
|
|
|
|
(
|
|
|
|
"Created in-memory cache with unloading "
|
|
|
|
f"{f'after {settings.model_ttl}s of inactivity' if settings.model_ttl > 0 else 'disabled'}."
|
|
|
|
)
|
|
|
|
)
|
2023-08-25 04:28:51 +00:00
|
|
|
# asyncio is a huge bottleneck for performance, so we use a thread pool to run blocking code
|
|
|
|
app.state.thread_pool = ThreadPoolExecutor(settings.request_threads)
|
2023-08-30 08:22:01 +00:00
|
|
|
log.info(f"Initialized request thread pool with {settings.request_threads} threads.")
|
2023-06-27 23:21:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
@app.on_event("startup")
|
|
|
|
async def startup_event() -> None:
|
|
|
|
init_state()
|
2023-06-25 03:18:09 +00:00
|
|
|
|
2023-06-18 03:49:19 +00:00
|
|
|
|
2023-06-05 14:40:48 +00:00
|
|
|
@app.get("/", response_model=MessageResponse)
|
|
|
|
async def root() -> dict[str, str]:
|
2023-04-26 10:39:24 +00:00
|
|
|
return {"message": "Immich ML"}
|
|
|
|
|
|
|
|
|
2023-06-05 14:40:48 +00:00
|
|
|
@app.get("/ping", response_model=TextResponse)
|
|
|
|
def ping() -> str:
|
2023-02-18 15:13:37 +00:00
|
|
|
return "pong"
|
|
|
|
|
2023-06-03 02:42:47 +00:00
|
|
|
|
2023-08-29 13:58:00 +00:00
|
|
|
@app.post("/predict")
|
|
|
|
async def predict(
|
|
|
|
model_name: str = Form(alias="modelName"),
|
|
|
|
model_type: ModelType = Form(alias="modelType"),
|
|
|
|
options: str = Form(default="{}"),
|
|
|
|
text: str | None = Form(default=None),
|
|
|
|
image: UploadFile | None = None,
|
|
|
|
) -> Any:
|
|
|
|
if image is not None:
|
|
|
|
inputs: str | bytes = await image.read()
|
|
|
|
elif text is not None:
|
|
|
|
inputs = text
|
|
|
|
else:
|
|
|
|
raise HTTPException(400, "Either image or text must be provided")
|
2023-02-18 15:13:37 +00:00
|
|
|
|
2023-08-29 13:58:00 +00:00
|
|
|
model: InferenceModel = await app.state.model_cache.get(model_name, model_type, **orjson.loads(options))
|
|
|
|
outputs = await run(model, inputs)
|
|
|
|
return ORJSONResponse(outputs)
|
2023-06-03 02:42:47 +00:00
|
|
|
|
|
|
|
|
2023-08-29 13:58:00 +00:00
|
|
|
async def run(model: InferenceModel, inputs: Any) -> Any:
|
2023-08-25 04:28:51 +00:00
|
|
|
return await asyncio.get_running_loop().run_in_executor(app.state.thread_pool, model.predict, inputs)
|
|
|
|
|
|
|
|
|
2023-02-18 15:13:37 +00:00
|
|
|
if __name__ == "__main__":
|
2023-05-20 03:37:01 +00:00
|
|
|
is_dev = os.getenv("NODE_ENV") == "development"
|
2023-06-18 03:49:19 +00:00
|
|
|
uvicorn.run(
|
2023-06-25 03:18:09 +00:00
|
|
|
"app.main:app",
|
2023-06-18 03:49:19 +00:00
|
|
|
host=settings.host,
|
|
|
|
port=settings.port,
|
|
|
|
reload=is_dev,
|
|
|
|
workers=settings.workers,
|
2023-08-30 08:22:01 +00:00
|
|
|
log_config=None,
|
|
|
|
access_log=log.isEnabledFor(logging.INFO),
|
2023-06-18 03:49:19 +00:00
|
|
|
)
|