# MLComponent

- [Функция inference](#функция-inference)
- [Docstring](#docstring)
- [Логика функции inference](#логика-функции-inference)
    - [Передача модели](#передача-модели)
    - [Пример функции](#пример-функции)
- [Манифесты](#манифесты)


Компонент MLComponent позволяет создать сервис для синхронной обработки запросов пользователей.

MLComponent подходит для быстрых вычислений, в частности, запуска предобученной модели машинного обучения на небольшой выборке данных. Например, распознавание объекта на одном изображении, классификация таблицы текстовых данных. Если алгоритм работает за доли секунды, то он подходит для создания MLComponent.

На основе образа Docker, указанного в манифесте MLComponent, создаётся Docker-контейнер. В него устанавливается системная библиотека платформы, которая создаёт веб-сервис. При получении запроса от пользователя веб-сервис вызывает функцию inference, указанную в манифесте MLComponent, и передаёт вывод функции пользователю.

Для добавления MLComponent к модулю разработчику нужно:

- Реализовать функцию, на основе которой будет создан веб-сервис.
- Добавить в модуль манифест YAML компонента MLComponent
- Добавить в модуль манифест YAML компонента APIComponent для обращения к MLComponent

Также в модуле уже должен присутствовать хотя бы один компонент DataBox и соответствующий ему APIComponent.


## Функция inference

Для работы MLComponent нужно реализовать функцию со следующим интерфейсом:

```
def inference(parameters: List[Dict[str, Any]],
              inputs: List[Dict[str, Any]],
              output_fields: List[Dict[str, Any]],
              model_key: str,
              model: Any = None) -> Tuple[List[Dict[str, Any]], Any]:
```

Входные параметры:

* `parameters` - список пар `[name, value]`, произвольных параметров примитивных типов.
* `inputs` - список входных данных, где каждый элемент - это словарь.  Ключи словаря:  
    * `name` - название, произвольная строка, используется в коде модуля. Например, `"image"`.
    * `datatype` - тип входных данных, одна из строк `"FP32", "FP64", "INT32", "FILE", "str", "dict"`.
    * `content_type` - тип содержимого файла, если тип входной переменной `datatype` - это `"FILE"`.
    * `data` - строка с путём к входному файлу или одномерный список входных данных соответствующего типа.
    * `shape` - размерность входной переменной как массива, применимо даже к файлам (не размер файла в файловой системе).
* `output_fields` - список выходных данных, где каждый элемент - это словарь. Ключи словаря:
    * `name` - название, произвольная строка, используется в коде модуля. Например, `"prediction"`.
    * `datatype` - тип выходных данных, одна из строк `"FP32", "FP64", "INT32", "FILE", "str", "dict"`.
    * `content_type` - тип содержимого файла, если тип выходной переменной `datatype` - это `"FILE"`.
    * `data` - строка с путём к выходному файлу или одномерный список выходных данных соответствующего типа.
    * `shape` - размерность выходной переменной как массива, применимо даже к файлам (не размер файла в файловой системе).
* `model_key` - строка, по которой можно загрузить модель (обычно путь).
* `model` - объект модели, с помощью которого проводится инференс.

Функция выводит tuple из двух объектов:

* список выходных данных, где каждый элемент - это словарь. Ключи словаря:
    * `name` - название, произвольная строка, используется в коде модуля. Например, `"prediction"`.
    * `datatype` - тип выходных данных, одна из строк `"FP32", "FP64", "INT32", "FILE", "str", "dict"`.
    * `content_type` - тип содержимого файла, если тип выходной переменной `datatype` - это `"FILE"`.
    * `data` - строка с путём к выходному файлу или одномерный список выходных данных соответствующего типа.
    * `shape` - размерность выходной переменной как массива, применимо даже к файлам (не размер файла в файловой системе).
* объект модели, который можно передать в следующий вызов функции как аргумент `model`.

При этом функции inference передаются уже обработанные данные - в частности, скачанные файлы. В полях output_fields данные data заменяются на None.


### Docstring

Формат самих входных данных, которые передаются в аргументе `inputs` при вызове функции, и выходных данных, возвращаемых функцией, должен быть указан в docstring-спецификации.

Для каждой переменной нужно указать следующее:

1. Информация о переменной в формате `:param inputs.NAME: DESCRIPTION`
2. Информация о типе данных в формате `:type inputs.NAME: TYPE1 or TYPE2 or TYPE3[, optional]`

Здесь,

- `inputs` - тип переменной. Для входных это `inputs`, для выходных - `outputs`.
- `NAME` - имя переменной. Должно в точности соответствовать имени, которое передаётся через API.
- `DESCRIPTION` - описание переменной. Может содержать несколько строк текста. Используется в OpenAPI спецификации и каталоге.
- `TYPE1`, `TYPE2`, `TYPE3` - типы данных и типы содержимых, которые могут быть присвоены переменной.
  - Возможные типы данных: `FP32, FP64, INT32, FILE, str, dict`.
  - Типы содержимого (content_type) указываются так же, как типы данных - `FP32 or text/csv`.
  - Если указано хотя бы один `content_type`, то тип `FILE` добавляется автоматически, даже если не указан - `:type inputs.mydata: text/csv`.
  - Если не указан `content_type`, предполагается, что поддерживается любой.
  - Если про тип переменной не указано ничего, предполагается, что поддерживается любой тип и любое содержимое.
- `[, optional]` в конце типа переменной может быть указано `, optional` (без квадратных скобок), что означает, что переменная опциональная, вызов функции должен работать и без неё.

Пример простой спецификации:

```python
def inference(parameters: List[Dict[str, Any]],
              inputs: List[Dict[str, Any]],
              output_fields: List[Dict[str, Any]],
              model_key: str,
              model: Any = None) -> Tuple[List[Dict[str, Any]], Any]:
"""
    :param inputs.image: Изображение, на котором производится обнаружение объекта;
    :type inputs.image: FILE or image/png or image/bmp or image/jpeg

    :param outputs.detection: Словарь с результатом обнаружения.
        Ключ "labels" содержит список меток
        Ключ "scores" содержит список оценок 'уверенности' модели
        Ключ "bboxes" содержит список кортежей [x1 y1 x2 y2] с координатами обнаруженного объекта
    :type outputs.detection: dict
"""
```

Пример, иллюстрирующий синтаксис спецификации.

```python
def example_inference_func(parameters: list,
                           inputs: list,
                           output_fields: set,
                           model_key: str,
                           model: Any = None) -> (list, Any):
    """
    :param inputs.p: Параметр прогнозирования;
        Если вход опциональный, то строка типа завершается ', optional',
        это соответствует синтаксису sphinx rst
    :type inputs.p: FP32, optional

    :param inputs.X: Матрица экземпляров для классификации;
        Если типов несколько, они перечисляются через 'or',
        это соответствует синтаксису sphinx rst
    :type inputs.X: FP64 or FP32 or FILE or text/csv

    :param inputs.abc: Входной параметр;
        Тип может быть не указан, тогда считается,
        что поддерживаются все допустимые datatype и любые content-type
    :type inputs.abc:

    :param outputs.predict: Результат прогнозирования
    :type outputs.predict: INT32 or 'FILE' or text/csv or application/json or BED

    :param outputs.scores: Скоры прогнозирования;
        Для этого выхода datatype 'FILE' будет добавлен, потому что указан content-type 'text/csv'
    :type outputs.scores: FP32 or FP64 or text/csv

    :param outputs.obj: Только объектный выход;
        Для этого выхода тип только объектный
    :type outputs.obj: INT32 or str, optional

    :param model_parameters.mode: Режим модели;
        Допустимые типы параметров модели - примитивные типы OpenAPI Spec
    :type model_parameters.mode: number or integer or float

    :param model_parameters.param1: Параметр работы модели
    :type model_parameters.param1: string, optional
    """
    pass
```

Пример совмещения обычного docstring и спецификации выше для API.

```python
def example_inference_func(parameters: list,
                           inputs: list,
                           output_fields: set,
                           model_key: str,
                           model: Any = None) -> (list, Any):
    """
    Обычное описание функции, которое не будет частью спецификации.

    Более подробное описание.
    Подробное описание может продолжаться на несколько строк.
    Подробное описание не может содержать docstring в форматах ReST, Google, Numpydoc-style и Epydoc,
    так как это может конфликтовать со спецификацией для API.

    :param inputs.X: Матрица экземпляров для классификации;
    :type inputs.X: FP64 or FP32 or FILE or text/csv

    :param outputs.predict: Результат прогнозирования
    :type outputs.predict: INT32 or 'FILE' or text/csv or application/json
    """
    pass
```

### Логика функции inference


В этом разделе описаны особенностей реализации функции inference и рекомендации. Технически функция может быть реализована любым способом, который соответствует интерфейсу, но при возможности желательно следовать рекомендациям ниже.

#### Передача модели

За передачу модели в функцию отвечают два параметра: `model_key` и `model`.

- `model_key` - строка-ключ, позволяющая загрузить модель с диска или хранилища файлов. Обычно, это путь. Например, `/home/project/data/model_weights.pth` для загрузки файла или `/home/project/google-bert/bert-base-uncased` для загрузки модели из папки.
- `model` - уже загруженный объект модели. Этот же объект возвращается функцией в кортеже `outputs, model`.

Так как ML-компонент предназначен для быстрых вычислений, предполагается, что время выполнения одного запроса гораздо меньше времени загрузки модели или запуска контейнера. Поэтому один и тот же контейнер и один и тот же объект модели используется для множества запросов.

Если модель `model` не передана в функцию inference (например, это первый запрос к новому модулю), веса ИИ-модели загружаются с использованием ключа `model_key`. Эти загруженные веса возвращаются в конце работы функции и передаются в аргумент `model` при каждом следующем вызове.

Тип объекта `model` может быть любым, то есть это может быть и набор моделей, если это соответствует логике вычислений.

#### Пример функции

```python

def inference(
    parameters: list,
    inputs: list,
    output_fields: list,
    model_key: str,
    model: Any = None,
) -> (list, Any):

    """
    :param inputs.X: Матрица экземпляров для классификации;
    :type inputs.X: FP64 or FP32 or FILE or text/csv

    :param outputs.predict: Результат прогнозирования;
    :type outputs.predict: INT32 or 'FILE' or text/csv
    """


    outputs = []
    if not model:
        model = joblib.load(model_key)


    for entry in inputs:
        if entry["datatype"] == "FILE":
            if entry["content_type"] == "text/csv":
                input_table = load_csv(entry["data"])
            else:
                raise AttributeError(f'Not supported file type {entry["content_type"]}')
        elif entry["datatype"] == "FP32":
            input_table = load_array_fp32(entry["data"], entry["shape"])
        else:
            raise AttributeError(f'Not supported data type {entry["datatype"]}')

    for output in output_fields:
        if output["name"] == "predict":
            y_pred = model.predict(input_table)
            output_shape = y_pred.shape
            if output["datatype"] == "FILE":
                path = output["data"]
                y_pred.tofile(path, sep=",")
                output_data = path
            elif output["datatype"] == "INT32":
                output_data = y_pred.astype(int).flatten().tolist()
            else:
                raise AttributeError(f'Not supported datatype {output["datatype"]}')

            output_data, output_shape = predict(output, model, model_inputs)
            outputs.append(
                dict(
                    name=output["name"],
                    datatype=output["datatype"],
                    shape=output_shape,
                    data=output_data,
                )
            )

    return outputs, model
```


## Манифесты


Пример манифеста ML-компонента.


```
apiVersion: "unified-platform.cs.hse.ru/v1"
kind: MLComponent
metadata:
  name: somename-mlcmp
  namespace: pu-username-pa-bm99
spec:
  image:
    existingImageName: example.com/lab-name/bm99-module-container-name:12ab345
  resourceLimits:
    cpu: 500m
    memory: 256M
  env:
    - name: CUSTOM_PARAMETER_1
      value: "3.14"
    - name: ANOTHER_PARAMETER
      value: "some_value"
  mlService:
    packageRegistryName: python-package-registry
    inference:
      fileExchange:
        fileBox: user-box
        inferenceFilesPath: /home/appname/users/tmp
      model:
        modelBox: model-box
        modelPath: /home/path/to/model.joblib
      entryPoint:
        pythonPath: .
        pythonFunction: modulename.predict.inference
  connectedBoxes:
    - name: model-box
      path: /home/path/to
      mountS3Box:
        subPath: users/developer/file_groups/models
        s3BoxName: mymodule-data-box
    - name: user-box
      copyS3Box:
        s3BoxName: users
```

Здесь,

- `metadata/name` - название ML-компонента. Оно используется в API-компоненте и может использоваться в других обращениях к компоненту через kubernetes.
- `spec/image/existingImageName` - название образа, из которого собирается ML-компонент. Этот образ был подготовлен в пункте **5**.
- `spec/image/resourceLimits` - ограничения по ресурсам для ML-компонента.
  - Ресурсы процессора `cpu` измеряются в ядрах. `500m` - 500 миллиядер, то есть половина ядра процессора занята развёрнутым ML-компонентом.
  - Ресурсы оперативной памяти `memory` могут измеряться в мегабайтах, гигабайтах и других метриках памяти
  - Подробнее - https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
- `spec/env` - переменные окружения, передаваемые в контейнер. Вы можете вынести настройки контейнера в переменные окружения и задавать их в ML-компоненте, чтобы не нужно было изменять код и пересоздавать образ Docker с новыми значениями.
- `spec/mlService`
  - `packageRegistryName` - название репозитория с пакетами Python, должно соответствовать "внешнему" названию из компонента приложения в пункте **7.6**. Название может быть другим, главное - соответствие названий друг другу.
  - `inference/fileExchange` - соединение файловой системы контейнера (сервиса) и ящика S3 для обмена файлами.
    - `fileBox` - название ящика S3 для файлов пользователей.  
      *Должно соответствовать названию из пункта `connectedBoxes` ниже в этом файле, а не названию `metadata/name` из компонента `DataBox`*.
    - `inferenceFilesPath` - путь, куда будут записываться создаваемые файлы внутри контейнера. К этому пути должны быть права доступа у пользователя контейнера. С этой папкой фреймворк взаимодействует автоматически, то есть лучше сделать уникальный путь, который нигде больше не используется.
  - `inference/model` - соединение файловой системы контейнера (сервиса) и ящика S3 для подключения модели расчётов.
    - `modelBox` - название ящика S3, где размещена модель.  
      *Должно соответствовать названию из пункта `connectedBoxes` ниже в этом файле, а не названию `metadata/name` из компонента `DataBox`*.
    - `modelPath` - путь, по которому модель будет доступна внутри контейнера.  
      *Этот путь передаётся в функцию `inference`. Может быть папкой или файлом.*
  - `inference/entryPoint`
    - `pythonPath` - относительный путь от корня репозитория к папке, которая будет добавлена в `PYTHONPATH`.
    - `pythonFunction` - имя [функции-адаптера](#251-функция-inference), включая имена промежуточных модулей.
- `spec/connectedBoxes` - подключенные компоненты `DataBox` (ящики)  
  - Первый ящик в примере используется для доступа к модели.
    - `name` - имя, по которому к ящику можно обращаться выше в пункте `spec/mlService/inference/model/modelBox`.
    - `path` - путь внутри контейнера, к которому монтируется ящик.
    - `mountS3Box/s3BoxName` - название компонента `DataBox`.
    - `mountS3Box/subPath` - путь внутри ящика, который монтируется к пути `path`.  
      *Здесь используется путь `users/developer/file_groups/models` как решение проблемы загрузки весов моделей и прочих данных в хранилище S3. При загрузке через файловый API от лица пользователя USER файлы загружаются по пути `users/USER/file_groups/...`. То есть в текущем примере веса можно загрузить от лица пользователя `developer` в папку `models/`, и через ML-компонент дать к ним доступ всем остальным пользователям.*
  - Второй ящик в примере используеся для взаимодействия с пользователями
    - `name` - имя, по которому к ящику можно обращаться выше в пункте `spec/mlService/inference/fileExchange/fileBox`.
    - `copyS3Box/s3BoxName` - назание компонента `DataBox`.
