Data Scientist: от данных до деплоя моделейПодготовка модели к деплоюРазработка минимального FastAPI-сервиса

Разработка минимального FastAPI-сервиса

Уроки курсаРазработка минимального FastAPI-сервиса

5. Типичная ошибка: загрузка модели внутри функции-обработчика

Это одна из тех ошибок, которую делают почти все, кто впервые пишет ML-сервис. Выглядит логично: клиент прислал запрос — открыл файл, загрузил модель, получил предсказание. На практике это катастрофа по производительности. Антипример:


@app.post("/predict", response_model=PredictResponse)
def predict(data: InputData):
    # ❌ Так делать нельзя
    with open('model.pkl', 'rb') as f:
        clf = pickle.load(f)
    X = np.array([data.Store, data.DayOfWeek, data.Promo, data.SchoolHoliday]).reshape(1, -1)
    return PredictResponse(predicted_sales=float(clf.predict(X)[0]))

Что здесь происходит при каждом запросе: открывается файл на диске, Python читает и десериализует весь объект модели заново, только потом вызывается predict(). Если модель занимает 50 МБ (градиентный бустинг на данных Rossmann вполне может весить столько), то при 100 одновременных запросах в секунду сервис будет заниматься исключительно чтением диска, а не предсказаниями. Правильный вариант — загрузить модель один раз в lifespan и держать её в памяти:


ml_model = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
    with open('model.pkl', 'rb') as f:
        ml_model['clf'] = pickle.load(f)
    yield
    ml_model.clear()

app = FastAPI(lifespan=lifespan)

@app.post("/predict", response_model=PredictResponse)
def predict(data: InputData):
    # ✅ Модель уже в памяти — только вызов predict()
    X = np.array([data.Store, data.DayOfWeek, data.Promo, data.SchoolHoliday]).reshape(1, -1)
    return PredictResponse(predicted_sales=float(ml_model['clf'].predict(X)[0]))

Теперь pickle.load() выполняется ровно один раз при старте uvicorn. Все последующие запросы обращаются к уже готовому объекту ml_model['clf'] в оперативной памяти — это на несколько порядков быстрее дискового I/O. Почему именно словарь, а не просто global clf? В Python переназначение глобальной переменной внутри async-функции требует явного объявления global clf в каждой функции, которая её меняет, иначе Python создаст локальную переменную с тем же именем. Словарь ml_model — это изменяемый объект, ссылка на который остаётся глобальной; мутировать его ключи можно без global-декларации, и это работает предсказуемо.