Вокруг так много фреймворков для инференса нейронок, что глаза разбегаются. Продолжаем цикл о реализации сервинга одной задачи, но разными инструментами. В прошлый раз реализация была на Nvidia Triton Inference Serve (за анонсами прошу в мой телеграм канал. Код к статье находится в репозитории.
Задача
В качестве задачи взято распознавание российских автомобильных номеров. Модели были взяты из этого репозитория.
Пайплайн распознавания следующий:
1. Детекция номеров с помощью Yolov5;
2. Вырезанные номера прогоняются через Spatial transformer (STN) для выравнивания;
3. Текст номера распознается с LPR-net.
Фреймворк
Для инференса используется [TorchServe]. Данный фреймворк является частью экосистемы Pytorch. Он активно развивается.
В документации о нем говорится следующее:
TorchServe is a performant, flexible and easy to use tool for serving PyTorch eager mode and torschripted models.
Возможности:
Поддержка нескольких форматов моделей (torchscript, onnx, ipex, tensorrt);
Объединение нескольких моделей в один граф/workflow;
Инференс API (REST и GRPC);
API для управления моделями;
Метрики из коробки.
Конвертируем модели
Как и Triton, TorchServe требует от пользователя перевести модели в свой формат. Для этого есть утилиты torch-model-archiver и torch-workflow-archiver для моделей и графов соответственно.
Для конвертации нам нужно:
Модель в формате TorchServe/Onnx/др.;
Скрипт, описывающий пайплайн работы модели.
Такой скрипт называется handler. В нем определяются основные этапы жизненного цикла модели (инициализация, предобработка, предсказание, постобработка и др.). Для типовых задач они уже предопределены.
Модели STN и LPR легко конверуются в TorchServe, поэтому в их хэндлерах не используются дополнительные библиотеки. Импорты выглядят так:
import json
import logging
from abc import ABC
import numpy as np
import torch
from ts.torch_handler.base_handler import BaseHandler
Yolo нельзя было просто перевести в TorchScript, так как часть логики для обработки запросов оставалась снаружи модели. Так как копаться с этим желания не было, а также ради более приближенного к жизни сценария, в хэндлере модели Yolo инициализируется из TorchHub. В импортах мы уже видим и сторонние модули:
from inference_torchserve.data_models import PlatePrediction
from nn.inference.predictor import prepare_detection_input, prepare_recognition_input
from nn.models.yolo import load_yolo
from nn.settings import settings
Чтобы это работало, необходимо в докерфайле установить в глобальный интерпретатор необходимые вам пакеты.
В TorchServe не нужно жестко задавать тип и размерность входов и выходов модели, поэтому никакие конфиги для моделей определять не нужно. С одной стороны это удобно, а с другой - порождает хаос, если не следовать какому-то одному формату.
Конвертированная модель представляет собой zip архив с расширением .mar, в котором лежат все артефакты (служебная информация, веса, скрипты и дополнительные файлы).
.
├── MAR-INF
│ └── MANIFEST.json
├── stn.pt
└── stn.py
На мой взгляд, решение с архивом неудобно для разработки. После любого изменения необходимо заново конвертировать модель. Также я испытывал проблемы при запуске в нем удаленного дебагера.
Чтобы TorchServe загрузил модели, их нужно положить в одну папку - model storage
и указать путь до нее в параметрах. Чтобы при запуске поднимались все модели, необходимо указать --models all
.
Делаем пайплайн распознавания
Выбранный пайплайн распознавания номера автомобиля состоит из последовательного предсказания несколькими моделями. Для этого в TorchServe есть Workflow. Он позволяет задать как последовательный, так и параллельный граф обработки:
# последовательный
dag:
pre_processing : [m1]
m1 : [m2]
m2 : [postprocessing]
input -> function1 -> model1 -> model2 -> function2 -> output
# параллельный граф
dag:
pre_processing: [model1, model2]
model1: [aggregate_func]
model2: [aggregate_func]
model1
/ \
input -> preprocessing -> -> aggregate_func
\ /
model2
Для рассматриваемой задачи получился следующий последовательно-параллельный граф. Узел aggregate объединяет координаты номеров с распознанными текстами.
┌──────┐
│ YOLO ├─────┐
└──┬───┘ │
│ v
│ ┌─────┐
plate │ │ STN │
coords │ └──┬──┘
│ │
│ v
│ ┌──────┐
│ │LPRNET│
│ └──┬───┘
v │
┌─────────┐ │ plate
│aggregate│<──┘ texts
└─────────┘
Для удобства и простоты данные между моделями передаются в виде словарей. Сериализация таких данных в TorchServe весьма неэффективна (переводят в строку и добавляют переносов строк), поэтому старайтесь передавать их как тензоры или байты.
Учтите, что workflow нельзя стартовать автоматически при запуске сервера - необходимо явно послать запрос на это. Если очень хочется делать при поднятии сервера, то можно так.
curl -X POST http://localhost:8081/workflows?url=plate_recognition
Использование моделей
Модели определены. Сервер запущен.
Чтобы выполнить определенную ранее модель или workflow нужно послать в TorchServe запрос на использование plate_recognition (я пользовался REST, но есть еще и GRPC). Для моделей используется эндпоинт predictions
, а для workflow wfpredict
.
response = requests.post(
"http://localhost:8080/predictions/yolo", data=image.open("rb").read()
)
response = requests.post(
"http://localhost:8080/wfpredict/plate_recognition",
data=image.open("rb").read()
)
Заключение
Ну вот инференс и написан. Данный пример не слишком простой, чтобы быть в целом бесполезным, но и не слишком сложный, чтобы покрыть все фишки этого фреймоврка для инференса.
В этом туториале были раскрыты не все возможности TorchServe, поэтому советую посмотреть в документации про:
Получение explanations;
Снятие метрик работающего сервера;
Подписывайтесь на мой канал - там я рассказываю про нейронки с упором в сервинг.