我有以下内容:
- 一个使用PyTorch的机器学习模型,能够将数据向量化并在约3.5毫秒内做出预测(中位数 ≈ 平均数)
- 一个使用FastAPI + uvicorn的HTTP API,能够在约2毫秒内处理简单请求
但当我将它们结合使用时,中位数响应时间几乎变成了200毫秒。
导致这种性能下降的原因可能是什么?
请注意以下几点:
- 我还尝试了单独使用aiohttp,以及aiohttp + gunicorn和Flask开发服务器进行服务,结果相同
- 我尝试每秒发送2次、20次和100次请求,结果相同
- 我意识到并行请求可能会导致延迟降低,但不会降低30倍!
- CPU负载仅为约7%
这是我测量模型性能的方式(我单独测量了中位数时间,它几乎与平均时间相同):
def predict_all(predictor, data): for i in range(len(data)): predictor(data[i])data = load_random_data()predictor = load_predictor()%timeit predict_all(predictor, data)# 手动将总时间除以数据中的记录数
这是FastAPI版本:
from fastapi import FastAPIfrom starlette.requests import Requestfrom my_code import load_predictorapp = FastAPI()app.predictor = load_predictor()@app.post("/")async def root(request: Request): predictor = request.app.predictor data = await request.json() return predictor(data)
HTTP性能测试:
wrk2 -t2 -c50 -d30s -R100 --latency -s post.lua http://localhost:8000/
编辑。
这是我尝试过的稍微修改的版本,带有和不带async
:
@app.post("/")# async def root(request: Request, user_dict: dict):def root(request: Request, user_dict: dict): predictor = request.app.predictor start_time = time.time() y = predictor(user_dict) finish_time = time.time() logging.info(f"user {user_dict['user_id']}: " "prediction made in {:.2f}ms".format((finish_time - start_time) * 1000)) return y
所以我只是添加了预测时间的日志记录。
异步版本的日志:
2021-02-03 11:14:31,822: user 12345678-1234-1234-1234-123456789123: prediction made in 2.87msINFO: 127.0.0.1:49284 - "POST / HTTP/1.1" 200 OK2021-02-03 11:14:56,329: user 12345678-1234-1234-1234-123456789123: prediction made in 3.93msINFO: 127.0.0.1:49286 - "POST / HTTP/1.1" 200 OK2021-02-03 11:14:56,345: user 12345678-1234-1234-1234-123456789123: prediction made in 15.06msINFO: 127.0.0.1:49287 - "POST / HTTP/1.1" 200 OK2021-02-03 11:14:56,351: user 12345678-1234-1234-1234-123456789123: prediction made in 4.78msINFO: 127.0.0.1:49288 - "POST / HTTP/1.1" 200 OK2021-02-03 11:14:56,358: user 12345678-1234-1234-1234-123456789123: prediction made in 6.85msINFO: 127.0.0.1:49289 - "POST / HTTP/1.1" 200 OK2021-02-03 11:14:56,363: user 12345678-1234-1234-1234-123456789123: prediction made in 3.71msINFO: 127.0.0.1:49290 - "POST / HTTP/1.1" 200 OK2021-02-03 11:14:56,369: user 12345678-1234-1234-1234-123456789123: prediction made in 5.49msINFO: 127.0.0.1:49291 - "POST / HTTP/1.1" 200 OK2021-02-03 11:14:56,374: user 12345678-1234-1234-1234-123456789123: prediction made in 5.00ms
所以预测速度很快,平均不到10毫秒,但整个请求需要200毫秒。
同步版本的日志:
2021-02-03 11:17:58,332: user 12345678-1234-1234-1234-123456789123: prediction made in 65.49ms2021-02-03 11:17:58,334: user 12345678-1234-1234-1234-123456789123: prediction made in 23.05msINFO: 127.0.0.1:49481 - "POST / HTTP/1.1" 200 OKINFO: 127.0.0.1:49482 - "POST / HTTP/1.1" 200 OK2021-02-03 11:17:58,338: user 12345678-1234-1234-1234-123456789123: prediction made in 72.39ms2021-02-03 11:17:58,341: user 12345678-1234-1234-1234-123456789123: prediction made in 78.66ms2021-02-03 11:17:58,341: user 12345678-1234-1234-1234-123456789123: prediction made in 85.74ms
现在预测需要很长时间!出于某种原因,相同的调用,但在同步上下文中执行时,开始需要大约30倍的时间。但整个请求所需的时间大致相同 – 160-200毫秒。
回答:
对于进行高强度计算且与其他端点相比可能需要更长时间的端点,请使用非协程处理程序。
当您使用def
而不是async def
时,FastAPI默认会使用Starlette的run_in_threadpool
,其底层也使用loop.run_in_executor
。
run_in_executor
将在默认循环执行器中执行函数,它会在一个单独的线程中执行函数,如果您在进行高CPU密集型工作,您可能还想检查像ProcessPoolExecutor
和ThreadPoolExecutor
这样的选项。
这个简单的数学计算在处理协程时非常有帮助。
function if function_takes ≥ 500ms use `def` else use `async def`
使您的函数成为非协程应该会很有帮助。
@app.post("/")def root(request: Request): predictor = request.app.predictor data = await request.json() return predictor(data)