Назад
1139

Сервинг модели Grounding DINO с BentoML

1139

Введение

Доставка ML-модели до конечного пользователя так же важна, как и её разработка: крутые модели бесполезны, если ими не пользуются. Запуск ML-моделей в production-среде сопряжён с рядом сложностей, например:

  • необходимо хранить и версионировать модели;
  • нужен мониторинг ресурсопотребления, производительности и дрейфа данных в реальном времени;
  • нужна поддержка разнородных сценариев: пакетная обработка или стриминг.

Эти и другие нюансы превращают деплой и мониторинг модели в ресурсоёмкую задачу.

Для облегчения жизни разработчиков в индустрии появляются инструменты для сервинга ML-моделей, например: Triton, Torchserve, BentoML и т.д. В одной из предыдущих статей блога рассказывали про внедрение ML-модели с помощью Trition и Torchserve в качестве инструмента для сервинга ML-моделей. В этой статье приведём простой пример сервинга модели при помощи другого популярного фреймворка — BentoML, без сравнения с другими инструментами model-serving.

Кратко о BentoML

Это open-source инструмент, который помогает:

  1. Оборачивать модель в сервис и обращаться к нему по API;
  2. Масштабировать сервис, а также объединять несколько сервисов в пайплайны;
  3. Мониторить сервис, используя Prometheus и Grafana;
  4. Хранить и версионировать модели.

BentoML написан на Python, в этом его сила и слабость. Например, он будет уступать Triton по скорости работы, но им проще пользоваться. У BentoML есть интеграции с разными библиотеками: Hugging Face, sklearn, torch, gradio, MLflow и многими другими.

В платном инструменте BentoCloud есть возможность деплоить сервис одной командой и отслеживать показатели модели в UI. Также у них существует бесплатная версия для деплоя в кубере Yatai, где уже всё чуть сложнее, и он пока сыроват.

У BentoML множество примеров деплоя, в том числе и для LLM. Также стоит отметить подробную документацию, наличие сообщества в Slack и блог, который помогает пользователям лучше разобраться в нюансах BentoML.

В статье разберём простейший вариант сервинга ML-модели Grounding DINO c помощью BentoML, включая запуск и использование созданного сервиса. Устройство модели Grounding DINO подробно рассматривали в одной из предыдущих статьей, а её сравнение с другими моделями, в т.ч. и Open Vocabulary, приведено в статье VLM для детекции объектов на изображении.

Весь код статьи и инструкция по его запуску доступны в репозитории.

Сервинг модели

Пример инференса модели Grounding DINO в dev-окружении на одной картинке выглядит следующим образом:

import requests

import torch
from PIL import Image
from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection
from utils.utils import draw_detections # метод для отрисовки ббоксов на изображении

model_id = "IDEA-Research/grounding-dino-tiny"
device = "cuda:1" if torch.cuda.is_available() else "cpu" # если 1 гпу, замените на cuda:0

processor = AutoProcessor.from_pretrained(model_id)
model = AutoModelForZeroShotObjectDetection.from_pretrained(model_id).to(device)

image_url = "<http://images.cocodataset.org/val2017/000000039769.jpg>"
image = Image.open(requests.get(image_url, stream=True).raw)
# Check for cats and remote controls
text_labels = [["a cat", "a remote control"]]

inputs = processor(images=image, text=text_labels, return_tensors="pt").to(device)
with torch.no_grad():
    outputs = model(**inputs)

results = processor.post_process_grounded_object_detection(
    outputs,
    threshold=0.4,
    text_threshold=0.3,
    target_sizes=[(image.height, image.width)]
)
# Retrieve the first image result
print(results)
result = results[0]
for box, score, text_label in zip(result["boxes"], result["scores"], result["text_labels"]):
    box = [round(x, 2) for x in box.tolist()]
    print(f"Detected {text_label} with confidence {round(score.item(), 3)} at location {box}")

# Результат запуска кода
# Detected a cat with confidence 0.479 at location [344.7, 23.11, 637.18, 374.27]
# Detected a cat with confidence 0.438 at location [12.27, 51.91, 316.86, 472.43]
# Detected a remote control with confidence 0.476 at location [38.59, 70.01, 176.78, 118.17]

Для запуска кода выше необходимо создать виртуальное окружение и установить зависимости согласно инструкции в README.md.

Чтобы обернуть этот код в сервис с помощью BentoML, нужно выполнить следующие шаги:

  1. Определить конфигурацию для сборки сервиса;
  2. Создать класс GroundingDinoService. В нём будет вся логика сервиса: инференс модели и отрисовка результатов на изображении;
  3. Добавить валидацию входных параметров.

В итоге мы получим Bento — артефакт, предназначенный для развертывания ML-сервиса и включающий исходный код сервиса, все необходимые зависимости и сохранённые модели. С его помощью создаётся docker-образ с автоматически сгенерированным API-сервером.

Запущенный сервис будет работать через HTTP API. Он будет принимать изображение, промпт и пороги для инференса. В ответе он будет выдавать класс объекта на изображении, координаты bounding boxes и конфиденс.

Вся логика сервиса будет реализована в файле service.py.

Рисунок 1. Визуализация Bento-объекта, который содержит всю основную логику сервиса в service.py

Определение конфигурации сборки сервиса

BentoML создаёт докерезированные сервисы. По умолчанию Dockerfile самому писать не надо, он будет генерироваться автоматически. Можно воспользоваться версией Dockerfile, генерируемой по умолчанию, или создать свой кастомный. Чтобы получить докер контейнер, нужно создать Bento-объект.

Определим кастомную конфигурацию сборки нашего сервиса:

# образ по умолчанию при использовании gpu debian: "image": "nvidia/cuda:{spec_version}-cudnn8-runtime-ubuntu20.04"
runtime_image = bentoml.images.Image(
    python_version="3.11"
).pyproject_toml("pyproject.toml")

В этой части кода указывается базовый докер-образ, версия питона, дистрибутив, зависимости и всё, что обычно указывается в Dockerfile.

Больше про настройку runtime_image — в секции Define the runtime environment документации.

Определение класса сервиса

Сервисы BentoML определяются на основе классов. Каждый класс представляет собой отдельный сервис, который может выполнять определённые задачи: предварительная обработка данных или инференс. Декоратором @bentoml.service оборачивается класс, указывающий, что это сервис BentoML. Сервис может предоставлять доступ к одному или нескольким API-интерфейсам через HTTP.

Основные параметры сервиса определяются при помощи декоратора @bentoml.service для соответствующего класса:

@bentoml.service(
   name='grounding-dino-service', # имя сервиса
   traffic={'timeout': 300}, # допустимое время ответа сервиса
   resources={'gpu': 1}, # количество GPU или CPU, если GPU нет
   workers=1, # количество воркеров (экземпляров сервиса)
   image=runtime_image # кастомная конфигурация сборки сервиса
)
class GroundingDinoService:

    hf_model = bentoml.models.HuggingFaceModel(MODEL_ID)

    def __init__(self) -> None:
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model = AutoModelForZeroShotObjectDetection.from_pretrained(self.hf_model).to(self.device)
        self.processor = AutoProcessor.from_pretrained(self.hf_model)
        print("Model grounding-dino loaded", "device:", self.device)

Поля, доступные для конфигурации сервиса, можно найти в секции Сonfigurations документации. Если параметров стало слишком много, то определение сервиса можно вынести в yaml-файл bentoml_configuration.yaml и перезаписать нужные параметры из декоратора.

Используемая в сервисе модель задаётся как переменная класса — hf_model, которая скачивается средствами BentoML. Это обеспечивает однократную загрузку весов модели при сборке Docker-образа с оптимизированным параллельным скачиванием весов. Если перенести скачивание весов модели в конструктор, это приведёт к ошибке «model NotFound», так как Bento не сможет создать ссылку на модель, привязанную к конкретному сервису.

В конструктор класса переносим инициализацию процессора и модели.

Реализация API

Реализуем ключевые методы в классе GroundingDinoService:

  1. _detect — приватный метод, содержащий основную логику инференса модели. Здесь находится вся логика из начального кода детекции объектов;
  2. detect_image — публичный метод, просто обёртка над _detect , которая возвращает результаты детекции;
  3. render — публичный метод, возвращает изображение с отрисованными результатами метода _detect.

Все методы принимают одинаковые параметры — изображение и параметры для инференса.

  @bentoml.api
  def detect_image(
      self,
      image: PILImage.Image,
      params: DetectionParams
      ) -> tp.List[tp.Dict[str, tp.Any]]:
      '''
      Detect objects in the image.
      '''
      return self._detect(image, params)

  def _detect(
      self,
      image: PILImage.Image,
      params: DetectionParams
  ) -> tp.List[tp.Dict[str, tp.Any]]:
      text_labels = params.detection_prompt
      inputs = self.processor(images=[image], text=text_labels, return_tensors="pt").to(self.device)
      with torch.no_grad():
          outputs = self.model(**inputs)
      results = self.processor.post_process_grounded_object_detection(
          outputs,
          threshold=params.box_threshold,
          text_threshold=params.text_threshold,
          target_sizes=[(image.height, image.width)],
      )
      return serialize_detections(results) 
 
  @bentoml.api
  def render(
      self,
      image: PILImage.Image,
      params: DetectionParams,
  ) -> PILImage.Image:
      '''
      Render detections on the image.
      '''
      result = self._detect(image, params)[0]
      image = draw_detections(image, result) 
      Path("images").mkdir(exist_ok=True)
      image.save("images/out_render.jpg")
      return image

Реализация вспомогательных функций serialize_detections и draw_detections доступна в исходниках репозитория на GitHub.

Методы detect_image и render помечаются декоратором @bentoml.api, что автоматически превращает их в HTTP endpoints нашего сервиса. Эти методы станут доступными для внешних вызовов через REST API. Подробнее про создание API с BentoML в секции Service APIs документации.

Валидация входных параметров сервиса

BentoML поддерживает использование Pydantic. С помощью него можно описывать классы-модели для валидации входных параметров сервиса. Модель для валидации входных параметров нашего сервиса выглядит следующим образом:

class DetectionParams(BaseModel):
    detection_prompt: tp.List[tp.List[str]] = Field(..., description="List of lists of labels, e.g. [['a cat', 'a remote control']]")
    box_threshold: float = Field(default=0.25, description="Box threshold between 0 and 1")
    text_threshold: float = Field(default=0.25, description="Text threshold between 0 and 1")
    
    class Config:
        arbitrary_types_allowed = True.

Запуск сервиса

Запуск сервисов на BentoML можно делать либо локально, либо в docker. Локальный запуск удобен, когда нужно протестировать сервис, внести изменение и не ждать каждый раз сборки. Для использования в production-среде предпочтителен запуск через docker.

Запуск сервиса локально для тестирования приложения

Для запуска сервиса локально необходимо выполнить в терминале команду:

 **bentoml serve** --port 3025 **--reload True**

После выполнения команды по ссылке http://localhost:3025/ в браузере откроется Swagger UI.

По умолчанию сервис запускается на 3000 порту, но в примере выше мы поменяли порт на 3025 с помощью флага --port. Стоит обратить внимание на флаг --reload : в значении True. При изменении кода сервис автоматически будет подхватывать изменения без перезапуска.

Остальные параметры можно посмотреть в секции bentoml serve документации.

Запуск сервиса c Docker

Чтобы запустить сервис в docker, сначала нужно собрать Bento. Для этого необходимо выполнить в терминале команду:

bentoml build 

По умолчанию она упаковывает все файлы в каталог, из которого выполняется. Чтобы исключить определённые файлы или каталоги, укажите их в файле .bentoignore. Команда нужна, чтобы подготовить артефакт нашего сервиса, который будет содержать исходный код, все зависимости и сохранённые модели.

Рисунок 2. Вывод в консоль при создании Bento с определенным тегом, информацией о зависимостях и дальнейших возможных действиях с Bento-объектом

На Рисунке 2 показан вывод в консоль в случае успешного создания Bento. Далее предлагается несколько вариантов того, что можно сделать с ним. Наш вариант — создать образ сервиса. Dockerfile генерируется автоматически, его можно будет посмотреть внутри собранного контейнера.

Для сборки образа сервиса необходимо выполнить следующую команду в терминале:

bentoml containerize grounding-dino-service:latest
Рисунок 3. Вывод в консоль процесса сборки docker-образа

Для запуска сервиса на GPU необходимо выполнить команду:

docker run --rm --gpus '"device=1"' -p 3025:3000 grounding-dino-service:k76plrt6x6jkulue
Рисунок 4. Вывод в консоль процесса запуска docker-контейнера

Готово! Образы и контейнеры можно посмотреть стандартным способом через docker.

Рисунок 5. Результат вывода команды docker ps | grep 3025. По выводу команды видно, что сервис запущен

Использование сервиса

Запущенный сервис доступен по адресу http://localhost:3025. Отправлять запросы можно, используя код или любой http client. В BentoML при запуске сервиса генерируется Swagger UI, через который так же можно отправлять запросы к сервису. Пример Swagger UI для запущенного сервиса представлен на Рисунке 6.

Рисунок 6. Swagger UI по адресу http://localhost:3025

Согласно Swagger, в сервисе доступно два endpoint:

  • /detect_image — возвращает json с классами, ббоксами и конфиденсами;
  • /render — отрисовывает боксы на картинке, возвращает и сохраняет её в папке images.

Пример использования endpoint /render

Рис. 7. Ручка render в Swagger UI

Нажимаем кнопку Try it out, загружаем фото и вводим параметры:

Изображение с котиками в репозитории

Рисунок 8. Ручка render в Swagger UI с введёнными параметрами и изображением

После нажатия Execute можем увидеть следующий результат:

Рисунок 9. Результат работы ручки render в Swagger UI

Аналогичный запрос можно выполнить через консольную утилиту cURL, получив изображение с отрисованными ббоксами в папке images:

curl -X 'POST' \\
  '<http://localhost:3025/render>' \\
  -H 'accept: image/*' \\
  -H 'Content-Type: multipart/form-data' \\
  -F 'image=@images/original_cat.jpg;type=image/jpeg' \\
  -F 'params={
  "detection_prompt": [
    [
      "a cat", "a remote control"
    ]
  ],
  "box_threshold": 0.25,
  "text_threshold": 0.25
};type=application/json' \\
  --output images/result_render.jpg

Пример использования endpoint /detect_image

Отправим запрос на endpoint /detect_image через SDK. Код доступен в файле client.py репозитория:

import bentoml
import requests
from io import BytesIO
from PIL import Image as PILImage

img_url = "<http://images.cocodataset.org/val2017/000000039769.jpg>"
img = PILImage.open(BytesIO(requests.get(img_url, stream=True).content))

with bentoml.SyncHTTPClient("<http://localhost:3025>") as client:
    result = client.detect_image(
        image=img,
        params={
            "detection_prompt": [["a cat", "a remote control"]],
            "box_threshold": 0.25,
            "text_threshold": 0.25,
        },
    )
    print(result)
    
    
# Результат в консоли
#[{'boxes': [[344.69305419921875, 23.10898208618164, 637.1846923828125, 374.27471923828125], [12.265024185180664, 51.91496276855469, 316.8591003417969, 472.438720703125], [38.58332061767578, 70.0059585571289, 176.77804565429688, 118.17623901367188], [332.17315673828125, 74.5613784790039, 370.69976806640625, 186.94830322265625]],
# 'scores': [0.4784787893295288, 0.4381333887577057, 0.475873202085495, 0.3312825560569763],
# 'text_labels': ['a cat', 'a cat', 'a remote control', 'a remote control']}]    

Итого

В этой статье мы рассмотрели, как создать простой сервис на BentoML. Для этого нужно выполнить несколько шагов:

  1. Обернуть код инференса в класс;
  2. Добавить валидацию входных параметров через pydantic;
  3. Обернуть методы класса декоратором @bentoml.api .
  4. Обернуть реализованный класс декоратором @bentoml.service c нужными параметрами.

И всё это в одном python-файле менее, чем на 100 строк!

В статье приведён пример реализации простейшего варианта сервиса на BentoML. Однако у BentoML есть и другие полезные функции для более сложных сценариев:

  • версионирование и хранение моделей с помощью Bento локально и на S3;
  • сборка и деплой сервиса с gitlab CI/CD;
  • синхронность и асинхронность запросов;
  • асинхронный инференс;
  • онлайн и офлайн-батчинг и многое другое.

Обработку более сложных сценариев при помощи BentoML продемонстрируем в следующих статьях 🙂

Деплой DL-сервисов

Научитесь создавать и деплоить DL-сервисы за 4 месяца. Наведите порядок в репозиториях, внедрите лучшие практики и повысьте свою ценность на рынке

0/0

Телеграм-канал

DeepSchool

Короткие посты по теории ML/DL, полезные
библиотеки и фреймворки, вопросы с собеседований
и советы, которые помогут в работе

Открыть Телеграм

Увидели ошибку?

Напишите нам в Telegram!