I have a microservice (fastapi
) that serves predictions from a model that was pre-trained in a separate repo. Ideally, this microservice only has one job: given an input X
, a pre-trained model
, and a pickled my_transformer
: serve predictions y
. The microservice shouldn't know/care about what the model
or my_transformer
are or how they work.
my_transformer = sklearn.preprocessing.FunctionTransformer(
func = lambda y: -np.log(y / 100),
inverse_func = lambda y_transformed: np.exp(-y_transformed) * 100,
)
y_transformed = my_transformer.transform(y)
model.fit(X,y_transformed)
The API should just be able to load the transformer (via dill
-- because lambda functions aren't pickleable via pickle
or joblib
), and call inverse_transform
. For the most part, this works as expected... that is until I start using fastapi
and/or uvicorn
. The error/stack trace I'm getting is
Traceback (most recent call last):
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 428, in run_asgi
result = await app( # type: ignore[func-returns-value]
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
return await self.app(scope, receive, send)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/fastapi/applications.py", line 276, in __call__
await super().__call__(scope, receive, send)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/starlette/applications.py", line 122, in __call__
await self.middleware_stack(scope, receive, send)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/starlette/middleware/errors.py", line 184, in __call__
raise exc
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/starlette/middleware/errors.py", line 162, in __call__
await self.app(scope, receive, _send)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
raise exc
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
await self.app(scope, receive, sender)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
raise e
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
await self.app(scope, receive, send)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/starlette/routing.py", line 718, in __call__
await route.handle(scope, receive, send)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/starlette/routing.py", line 276, in handle
await self.app(scope, receive, send)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/starlette/routing.py", line 66, in app
response = await func(request)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/fastapi/routing.py", line 237, in app
raw_response = await run_endpoint_function(
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/fastapi/routing.py", line 165, in run_endpoint_function
return await run_in_threadpool(dependant.call, **values)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/starlette/concurrency.py", line 41, in run_in_threadpool
return await anyio.to_thread.run_sync(func, *args)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/anyio/to_thread.py", line 33, in run_sync
return await get_asynclib().run_sync_in_worker_thread(
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 877, in run_sync_in_worker_thread
return await future
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 807, in run
result = context.run(func, *args)
File "/Users/[me]/repos/perpetua1/kpi_predictor/basic_app.py", line 23, in root
my_transformer.transform(arr),
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/sklearn/utils/_set_output.py", line 140, in wrapped
data_to_wrap = f(self, X, *args, **kwargs)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/sklearn/preprocessing/_function_transformer.py", line 238, in transform
return self._transform(X, func=self.func, kw_args=self.kw_args)
File "/Users/[me]/.local/share/virtualenvs/[my_venv]/lib/python3.9/site-packages/sklearn/preprocessing/_function_transformer.py", line 310, in _transform
return func(X, **(kw_args if kw_args else {}))
File "/var/folders/td/_z1v0f3x703gj0wdnfms4h7c0000gp/T/ipykernel_9600/434310704.py", line 4, in <lambda>
NameError: name 'np' is not defined
I've narrowed it down to potentially be related to how uvicorn
is run, but don't understand why it breaks. Applying the following fix to my (more complicated) project also doesn't work, so I'm HOPING that understanding why it's breaking will help inform how to solve the more complicated case.
A basic version of my app looks like:
import dill
import numpy as np
import pandas as pd
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
class ReturnValue(BaseModel):
value: float
app = FastAPI()
@app.post("/", response_model=list[ReturnValue])
def root():
with open("my_transformer.dill", "rb") as io:
my_transformer = dill.load(io)
arr = np.random.uniform(0, 100, 10)
return pd.DataFrame(
my_transformer.transform(arr),
columns=["value"],
).to_dict(orient="records")
if __name__ == "__main__":
# uvicorn.run("basic_app:app", reload=True) # NameError
uvicorn.run(app) # runs as expected for this simple case, doesn't work for the more complicated app