MAATRIX / Блог / BentoML: упаковать модель в сервис за вечер

BentoML: упаковать модель в сервис за вечер

MAATRIX

Модель обучена, метрики устраивают, в Jupyter-блокноте она отвечает за миллисекунды — и тут начинается вторая, куда менее приятная часть работы. Нужно превратить model.predict(x) в сервис, который принимает запросы по сети, переживает параллельную нагрузку и не падает от кривого JSON на входе. BentoML берёт на себя именно эту обвязку: он не обучает модели и не заменяет ваш пайплайн, а стандартизирует путь от готовой модели до контейнера с рабочим API.

Почему «модель работает» — это не то же самое, что «сервис работает»

Разрыв между экспериментом и продакшеном обычно недооценивают, пока не попадаешь в него сам. В блокноте у вас один процесс, один запрос за раз, окружение из pip install в самом блокноте и полное доверие к входным данным — вы же сами их и подали. В реальном сервисе всё иначе:

  • Нужен веб-сервер. Кто-то должен слушать порт, принимать HTTP или gRPC-запросы, отдавать корректные коды ошибок и не падать от одного некорректного запроса.
  • Нужна валидация входа. Внешний клиент пришлёт не тот тип данных, не ту размерность массива, пустое поле — и без проверки это уронит процесс или, хуже, тихо вернёт мусорный ответ.
  • Нужна воспроизводимая упаковка окружения. Версия scikit-learn в контейнере обязана совпадать с той, на которой модель обучалась и сериализовалась — иначе pickle в лучшем случае откажется загружаться, в худшем — загрузится с искажённой моделью.
  • Нужна обработка параллельных запросов. Если сервис получает 50 запросов в секунду, а модель гоняется по одному объекту за раз, вы либо ставите запросы в очередь и получаете дикие задержки, либо гоняете инференс впустую, не используя то, что GPU и многие модели выигрывают от батчинга.

Каждый из этих пунктов решаем вручную — на Flask/FastAPI это займёт от нескольких часов до нескольких дней, в зависимости от того, насколько тщательно вы хотите сделать валидацию и батчинг. Если у вас один проект — это терпимо. Если моделей десять и с разными фреймворками — вы каждый раз пишете почти одну и ту же обвязку заново, и именно здесь инструмент вроде BentoML экономит время, а не заменяет инженерную работу целиком.

Что конкретно упрощает BentoML

BentoML — открытый фреймворк для упаковки ML-моделей в сервисы. Идея простая: вы описываете сервис декларативно (какая модель, какие входы и выходы, какая логика вокруг инференса), а фреймворк генерирует из этого рабочий HTTP-API, Docker-образ и манифест зависимостей.

Ключевые вещи, которые он берёт на себя:

  • Автогенерация API. Вы объявляете метод сервиса — фреймворк сам поднимает эндпоинт, парсит вход, сериализует выход, отдаёт схему (OpenAPI/Swagger из коробки), обрабатывает ошибки на уровне HTTP-кодов.
  • Батчинг запросов без ручной логики. Runner в BentoML умеет собирать несколько одновременных запросов в один батч перед вызовом модели и раздавать ответы обратно каждому клиенту — эту логику не нужно писать самому.
  • Единый процесс для разных фреймворков. PyTorch, TensorFlow, scikit-learn, XGBoost, LightGBM, ONNX, модели Hugging Face Transformers — упаковываются через один и тот же шаблон: bentoml.<framework>.save_model(...), дальше единая логика сервиса поверх.
  • Воспроизводимая сборка окружения. bentofile.yaml фиксирует Python-зависимости, системные пакеты, версию Python — и bentoml containerize собирает из этого детерминированный Docker-образ, который ведёт себя одинаково локально и на сервере.

Важная оговорка: BentoML — это упаковка и обвязка, а не автоматическая оптимизация модели. Он не ускорит саму модель и не подберёт batch size под вашу реальную нагрузку — эти решения всё равно принимаете вы, инструмент лишь избавляет от рутинного кода вокруг них.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Быстрый старт: от файла модели до работающего API

Возьмём типичный случай — модель уже обучена и сохранена (неважно, sklearn это или PyTorch). Порядок работы такой:

1. Сохраняем модель в локальный реестр BentoML.

import bentoml
from sklearn.ensemble import RandomForestClassifier

# model — уже обученный объект
bentoml.sklearn.save_model("fraud_detector", model)

Для PyTorch это bentoml.pytorch.save_model(...), для XGBoost — bentoml.xgboost.save_model(...). Разница только в имени модуля, дальше всё одинаково.

2. Описываем сервис в service.py.

import bentoml
import numpy as np
from bentoml.io import JSON, NumpyNdarray

runner = bentoml.sklearn.get("fraud_detector:latest").to_runner()

svc = bentoml.Service("fraud_service", runners=[runner])

@svc.api(input=NumpyNdarray(), output=JSON())
def predict(input_array: np.ndarray) -> dict:
    result = runner.predict.run(input_array)
    return {"prediction": result.tolist()}

NumpyNdarray() и JSON() — это дескрипторы ввода/вывода: именно они делают валидацию входных данных, о которой шла речь выше — неверный тип или форма массива вернёт клиенту понятную ошибку 400, а не упадёт процесс.

3. Запускаем локально и проверяем.

bentoml serve service:svc --reload
curl -X POST http://localhost:3000/predict \
  -H "Content-Type: application/json" \
  -d '{"input_array": [[1.2, 0.5, 3.1, 0.0]]}'

4. Описываем зависимости в bentofile.yaml.

service: "service:svc"
include:
  - "*.py"
python:
  packages:
    - scikit-learn==1.5.0
    - numpy

5. Собираем и упаковываем в контейнер.

bentoml build
bentoml containerize fraud_service:latest
docker run -p 3000:3000 fraud_service:latest

От сохранённого файла модели до Docker-образа с рабочим API — если модель простая и данные не требуют сложной предобработки, это действительно реально закрыть за вечер. Основное время уйдёт не на код обвязки (его почти нет), а на то, чтобы аккуратно описать вход и выход и проверить, что зависимости в bentofile.yaml действительно совпадают с окружением, где модель обучалась.

Батчинг: как настроить, не написав ни строки очереди

Ручная реализация батчинга — это обычно отдельный небольшой проект: очередь запросов, таймер ожидания, сборка тензора из нескольких объектов, разбор батч-ответа обратно по клиентам. В BentoML это настройка Runner'а, а не код:

# конфигурация раннера, например в configuration.yaml или через переменные окружения
runners:
  fraud_detector:
    batching:
      enabled: true
      max_batch_size: 32
      max_latency_ms: 50

max_batch_size ограничивает, сколько запросов попадёт в один батч, max_latency_ms — сколько максимум ждать перед тем, как отправить накопленный (пусть и неполный) батч на инференс. Здесь стоит держать в голове реальный компромисс: слишком большой max_latency_ms увеличивает задержку ответа отдельному клиенту ради эффективности GPU, слишком маленький — почти отключает пользу от батчинга. Универсальных цифр здесь нет — это зависит от вашей модели, размера входа и профиля трафика, и подбирать их нужно на своих данных, а не по умолчанию из документации.

Если хотите разобраться в самой проблеме батчинга подробнее — почему один пользователь без него может занимать GPU целиком, — это отдельная тема, которую мы разбирали в статье про батчинг запросов к LLM.

Один процесс для моделей разных фреймворков

Частая боль команд, где модели пишут разные люди в разных стеках: под sklearn — одна обвязка, под PyTorch — другая, под кастомный ONNX-инференс — третья. BentoML снимает эту проблему тем, что API сервиса одинаковый независимо от фреймворка модели внутри. Runner скрывает специфику фреймворка за одним и тем же интерфейсом .run() / .async_run(), а IO-дескрипторы (JSON, NumpyNdarray, PandasDataFrame, Image, Text) закрывают типовые форматы входа без ручного парсинга.

Практическое следствие: если завтра модель поменяется с XGBoost на дообученную нейросеть, шаблон сервиса вокруг неё меняется минимально — меняется только строка загрузки модели и, возможно, дескриптор входа. Это удобно в командах, где инфраструктура вокруг моделей одна, а сами модели постоянно меняются — типичная ситуация для ML-отдела, а не для одного изолированного проекта.

Отдельный случай — модели с Hugging Face Transformers (текстовые классификаторы, NER, суммаризация): BentoML поддерживает их так же, через bentoml.transformers.save_model(...), и получившийся сервис так же контейнеризуется и деплоится, как и классический sklearn.

От контейнера к реальному продакшену: чего инструмент не решает за вас

Здесь важно быть честным: скорость упаковки «за вечер» относится к первому рабочему прототипу сервиса, а не к готовности выдерживать боевую нагрузку. BentoML сокращает время на написание кода обвязки, но не проверяет и не гарантирует, что:

  • контейнер выдержит именно ваш профиль нагрузки (пиковый RPS, размер входных данных, длину очереди);
  • выбранные max_batch_size и max_latency_ms оптимальны для вашего железа и трафика;
  • память сервиса не растёт со временем при вашей специфической последовательности запросов;
  • один инстанс контейнера справится без горизонтального масштабирования, когда трафик вырастет в разы.

Всё это выясняется только нагрузочным тестированием на реальных или максимально приближенных к реальным данных — например, locust или k6 с профилем запросов, похожим на боевой. Дать сервису «поработать в бою» без предварительной проверки под нагрузкой — рискованная практика для любого критичного сценария (платежи, модерация, всё, где ошибка стоит денег или репутации), даже если сама упаковка заняла один вечер и полёт нормальный на тестовых десяти запросах.

Для деплоя готового контейнера в боевое окружение вам всё равно нужен сервер — под GPU-инференс подойдёт машина с видеокартой, под лёгкие CPU-модели хватит обычного VPS с достаточным объёмом RAM. Если модель — это LLM или другая тяжёлая по вычислениям нагрузка, разумно сразу закладывать сервер с GPU: подробнее о выборе такой машины — в статье про GPU-сервер для инференса LLM. Финальный Docker-образ, который собирает bentoml containerize, — обычный OCI-образ: его можно оптимизировать по размеру теми же приёмами, что и любой другой, в том числе multi-stage сборкой — это разобрано в статье про оптимизацию Docker-образа через multi-stage build. А прежде чем доверять сервису принимать решения в проде, стоит систематически проверить качество его ответов — как это делать методически, описано в статье про тестирование качества ответов модели.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

BentoML платный?

Сам фреймворк — open source, бесплатный (Apache 2.0). Есть отдельный коммерческий облачный продукт BentoCloud для управления деплоями, но собрать и запустить сервис через сам BentoML можно полностью бесплатно на своём сервере.

Нужен ли GPU для работы BentoML?

Нет, это не требование фреймворка. GPU нужен, если его требует сама модель — BentoML одинаково упаковывает и CPU-, и GPU-модели, разница только в базовом Docker-образе и переменных окружения для CUDA.

Можно ли обслуживать несколько моделей одним сервисом?

Да, bentoml.Service принимает список Runner'ов — например, можно объединить модель предобработки и модель классификации в один пайплайн внутри одного сервиса и одного API-эндпоинта.

Чем это отличается от простого FastAPI-обёртки над моделью?

FastAPI даёт вам веб-сервер и валидацию через Pydantic, но батчинг, унифицированную загрузку моделей разных фреймворков и готовую контейнеризацию всё равно придётся писать самому. BentoML закрывает именно эти три пункта из коробки, оставляя вам логику самой модели.

Подходит ли BentoML для очень маленьких моделей (пара килобайт весов)?

Технически да, но для совсем простых случаев (одна pickle-модель, низкий трафик) оверхед фреймворка может быть избыточным — иногда проще и быстрее поднять минимальный FastAPI-эндпоинт вручную. BentoML себя окупает, когда моделей несколько, фреймворки разные или нужен батчинг.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →