首先,您不应该在每次请求到达时都加载模型,而应该在启动时加载一次(您可以使用启动事件 https://fastapi.tiangolo.com/advanced/events/#startup-event为此)和将其存储在应用程序实例上 https://www.starlette.io/applications/#accessing-the-app-instance——使用通用的app.state
属性(参见实现State https://github.com/encode/starlette/blob/212fa46b23be0701a5963cdeff14f05ed352e22a/starlette/datastructures.py#L674也)——您可以稍后检索,如所述here https://stackoverflow.com/a/71537393/17865804 and here https://stackoverflow.com/a/71298949/17865804。例如:
from fastapi import Request
@app.on_event("startup")
async def startup_event():
app.state.model = torch.load('<model_path>')
其次,如果你没有任何async
您必须在端点内执行的函数await
,你可以定义你的端点def
代替async def
。这样,FastAPI 将并发处理请求,因为每个请求将在单独的线程中运行;然而,async def
端点在主线程上运行,即服务器顺序处理请求,只要没有await
调用此类路由内的某些 CPU/IO 绑定(阻塞)操作。如果是这样,则关键字await
会将函数控制传递回事件循环,从而允许事件循环中的其他任务/请求运行。请看一下答案here https://stackoverflow.com/a/71188190/17865804 and here https://stackoverflow.com/a/71517830/17865804以及其中包含的所有参考文献,以理解async
/await
,以及使用之间的区别def
and async def
。示例为def
端点:
@app.post('/')
def your_endpoint(request: Request):
model = request.app.state.model
# run your synchronous ask_query() function here
或者,如上所述here https://stackoverflow.com/a/71517830/17865804,您最好可以在单独的进程中运行 CPU 密集型任务,使用ProcessPoolExecutor https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor,并与asyncio
, 为了await
它完成工作并返回结果 - 在这种情况下,您需要使用以下命令定义端点async def
,作为await
关键字仅在async
功能。请注意,重要的是保护代码的主循环以避免子进程的递归生成 https://stackoverflow.com/a/45302590, ETC。;也就是说,您的代码必须位于if __name__ == '__main__' https://stackoverflow.com/questions/419163/what-does-if-name-main-do。例子:
from fastapi import FastAPI, Request
import concurrent.futures
import asyncio
import uvicorn
class MyAIClass():
def __init__(self) -> None:
super().__init__()
def ask_query(self, model, query, topN):
# ...
ai = MyAIClass()
app = FastAPI()
@app.on_event("startup")
async def startup_event():
app.state.model = torch.load('<model_path>')
@app.post('/')
async def your_endpoint(request: Request):
model = request.app.state.model
loop = asyncio.get_running_loop()
with concurrent.futures.ProcessPoolExecutor() as pool:
res = await loop.run_in_executor(pool, ai.ask_query, model, item.text, item.topN)
if __name__ == '__main__':
uvicorn.run(app)
Note如果你打算拥有若干工人 https://fastapi.tiangolo.com/deployment/server-workers/同时活跃,每个工人都有自己的记忆 https://fastapi.tiangolo.com/deployment/concepts/#memory-per-process换句话说,工作人员不共享相同的内存,因此每个工作人员都会将自己的 ML 模型实例加载到内存 (RAM) 中。例如,如果您的应用程序使用四个工作线程,则模型将被加载四次到 RAM 中。因此,如果模型以及代码中的其他变量消耗大量内存,每个进程/工人将消耗等量的内存。如果您想避免这种情况,您可以看看如何在多个工作人员之间共享对象 https://stackoverflow.com/questions/65686318/sharing-python-objects-across-multiple-workers,以及 - 如果您正在使用Gunicorn 作为 Uvicorn 工人的流程经理 https://fastapi.tiangolo.com/deployment/server-workers/#gunicorn-with-uvicorn-workers——你可以使用 Gunicorn 的--preload https://docs.gunicorn.org/en/stable/settings.html#preload-app旗帜。根据文档:
命令行: --preload
Default: False
在分叉工作进程之前加载应用程序代码。
通过预加载应用程序,您还可以节省一些 RAM 资源
加快服务器启动时间。不过,如果您推迟申请
加载到每个工作进程,您可以重新加载您的应用程序代码
通过重新启动工人即可轻松完成。
Example:
gunicorn --workers 4 --preload --worker-class=uvicorn.workers.UvicornWorker app:app
Note你不能结合 Gunicorn 的--preload https://docs.gunicorn.org/en/stable/settings.html#preload-app with --reload https://docs.gunicorn.org/en/stable/settings.html#reload标志,因为当代码预加载到主进程中时,如果您的应用程序代码发生更改,新的工作进程(将自动创建)仍将在内存中保留旧代码,因为fork() https://en.wikipedia.org/wiki/Fork_(system_call) works.