documentation/pages/pipeline.md

23 KiB
Raw Blame History

ExperimentPipeline

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

ExperimentPipeline подходит для вычислительных задач, для которых выполняется хотя бы один из критериев:

  • Требуют много вычислительных ресурсов (от нескольких секунд времени и больше, большие объёмы данных)
  • Не могут быть оформлены как Python-функция
  • Используют MLOps-модули

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

  • Добавить в модуль манифест YAML компонента ExperimentPipeline
  • Добавить в модуль манифест YAML компонента APIComponent для обращения к ExperimentPipeline
  • Добавить в модуль манифест YAML компонента APIComponent для работы с пайплайнами в целом (один на все пайплайны)

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

Общий принцип работы пайплайнов следующий.

  1. Пользователь отправляет запрос с путями к входным и выходным данным в файловом хранилище.
  2. Пользователь получает ответ, содержащий идентификатор запуска, по которому можно отслеживать статус выполнения пайплайна.
  3. Создаётся и запускается контейнер Docker.
  4. Файловое хранилище монтируется к файловой системе контейнера согласно полям манифеста ExperimentPipeline
  5. Указанные в запросе пути преобразуются в пути в локальной файловой системе и передаются в контейнер как переменные окружения.
  6. Запускается расчёт, который считывает входные данные из локальных путей, переданных в переменных окружения.
  7. Результат расчёта записывается в локальный путь, считанный из переменной окружения.
  8. Пользователь периодически проверяет статус расчёта. Если расчёт завершён, его результаты можно получить через запросы к файловому хранилищу.

Переменные

graph TB

request["Запрос"]
S3[("Хранилище S3")]

var1[/"var1"/]
var2[/"var2"/]

request --> var1
request --> var2
S3 --> var1
S3 --> var2

subgraph stage1
    UNIP_PIPELINE_VAR1[/"UNIP_PIPELINE_VAR1"/]
    UNIP_PIPELINE_VAR2[/"UNIP_PIPELINE_VAR2"/]
    UNIP_PIPELINE_VAR3_first[/"UNIP_PIPELINE_VAR3"/]
    docker1[/"docker container 1"/]
end

var3[/"var3"/]
subgraph stage2
    UNIP_PIPELINE_VAR3[/"UNIP_PIPELINE_VAR3"/]
    UNIP_PIPELINE_VAR4[/"UNIP_PIPELINE_VAR4"/]
    docker2[/"docker container 2"/]
end

docker1 --> UNIP_PIPELINE_VAR3_first
UNIP_PIPELINE_VAR1 --> docker1
UNIP_PIPELINE_VAR2 --> docker1
UNIP_PIPELINE_VAR3_first --> var3

var4[/"var4"/]

UNIP_PIPELINE_VAR3 --> docker2
docker2 --> UNIP_PIPELINE_VAR4

UNIP_PIPELINE_VAR4 --> var4

var1 --> UNIP_PIPELINE_VAR1
var2 --> UNIP_PIPELINE_VAR2

var3 --> UNIP_PIPELINE_VAR3

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

  • Внутри контейнера эти переменные доступны как переменные окружения.
  • При вызове пайплайна через API эти переменные отвечают за входные и выходные данные.
  • При создании манифеста пайплайна переменные указываются в манифесте самого пайплайна и его API-компонента.

Есть три типа переменных:

  • входные - могут быть переданы в поле inputs при вызове пайплайна через API.
  • выходные - могут быть переданы в поле output_vars при вызове пайплайна через API. Значением может быть только путь к папке, не к файлу.
  • внутренние - не передаются через API, но могут быть указаны как входные и выходные переменные отдельных этапов пайплайна, и в таком случае данные будут автоматически переданы между этапами.

Взаимодействие кода модуля с фреймворком

Для каждого контейнера, созданного во время выполнения пайплайна, будут определены переменные окружения в формате UNIP_PIPELINE_<VAR_NAME>. Значения таких переменных - пути к файлами и папкам в локальной файловой системе контейнера, с которыми должен работать код модуля.

Например, модуль обучает ML-модель на наборе данных "xy_train". Чтобы передать этот набор данных в этап обучения внутри пайплайна и использовать его при обучении, нужно:

  1. Добавить входной параметр xy_train в манифест пайплайна в пункт spec.stages.[].input.
  2. При создании контейнера, считывать переменную окружения UNIP_PIPELINE_XY_TRAIN - её название автоматически создаётся добавлением префикса UNIP_PIPELINE и переводом названия входного параметра в верхний регистр.
  3. При вызове пайплайна через API, в теле вызова передавать путь к данным xy_train (файлу или папке).

Если поле называется example_field, то внутри контейнера будет переменная UNIP_PIPELINE_EXAMPLE_FIELD.

Пример такой работы:

def main():
    model_path = os.getenv("UNIP_PIPELINE_MODEL")
    model = load_model(model_path)
    data_path = os.getenv("UNIP_PIPELINE_DATASET")
    data = load_data(data_path)

    metrics = compute_metrics(model, data)
    metrics_file_name = os.path.join(os.getenv("UNIP_PIPELINE_METRICS"), "metrics.txt")
    write_metrics_to_file(metrics, metrics_file_name)

Здесь используются две входные переменные, model и dataset и одна выходная - metrics.

Входные переменные могут быть файлами или папками, а выходная - только папкой, поэтому в примере добавляется путь к файлу.

Манифест ExperimentPipeline

Пример манифеста самого пайплайна.

apiVersion: "unified-platform.cs.hse.ru/v1"
kind: ExperimentPipeline
metadata:
  name: my-pipeline
  namespace: pu-username-pa-bm99
spec:
  vars:
    - name: xy_train
    - name: xy_test
    - name: extra_parameters
    - name: testing_results
    - name: model

  stages:
    - name: stage1
      image:
        existingImageName: registry-platform-dev-cs-hse.objectoriented.ru/lab-name/bm99-somename-train:4f4621b
      inputs:
        - name: xy_train
        - name: extra_parameters
      outputs:
        - name: model
      env:
        - name: MY_VARIABLE
          value: "some_value"
        - name: ANOTHER_VAR
          value: "2"

      entryPoint:
        cmd:
          - python3
          - src/training.py
      resourceLimits:
        cpu: 500m
        memory: 256M

    - name: stage2
      image:
        existingImageName: example.com/lab-name/bm99-somename-test:4f4621b
      inputs:
        - name: model
        - name: xy_test
        - name: extra_parameters
      outputs:
        - name: testing_results

      entryPoint:
        cmd:
          - python3
          - src/testing.py
      resourceLimits:
        cpu: 500m
        memory: 256M
  connectedBoxes:
    - name: data-box
      path: /path/to/mountpoint
      default: true
      mountS3Box:
        s3BoxName: mymodule-data-box

Здесь,

  • metadata.name - название компонента пайплайна. Это название используется в API-компоненте и при выполнении запросов к пайплайну.

  • spec.vars - список всех переменных, используемых в пайплайне, включая входные, выходные и внутренние.

  • spec.stages - список этапов пайплайна. Этапы вызываются последовательно при вызове пайплайна.

  • spec.stages.[0].name - название этапа

  • spec.stages.[0].image.existingImageName - название образа, который используется для выполнения этапа пайплайна.

  • spec.stages.[0].inputs - список входных переменных этапа.

  • spec.stages.[0].outputs - список выходных переменных этапа.

  • spec.stages.[0].entryPoint - точка входа в контейнер, консольная команда.

    • Отдельные элементы списка будут соединены пробелами и преобразованы в единую команду.
    • Команда из примера станет python3 src/training.py в терминале.
  • spec.stages.[0].env - список переменных окружения, которые будут установлены в контейнере при запуске пайплайна.

    • Переменные окружения могут принимать только строковые значения.
  • spec/connectedBoxes - подключенные компоненты DataBox (ящики)

    • В примере используется только один ящик, где хранятся данные, обучаемые модели, результаты и прочее
      • name - внутреннее имя ящика, может использоваться для явного указания, откуда монтировать данные переменной.
      • path - путь внутри контейнера, к которому монтируется ящик. Например, /data.
      • mountS3Box/s3BoxName - название компонента DataBox из соответствующего файла манифеста.
      • default - если указано true, то данные переменные автоматически монтируются из этого ящика.

Каждый этап работает с собственными входными и выходными данными. Переменные, определённые здесь, в пайплайне, но не определённые в API, считаются внутренними переменными. Например, в примере выше такой переменной является model.

Манифест APIComponent отдельного пайплайна

Пример манифеста API пайплайна

apiVersion: "unified-platform.cs.hse.ru/v1"
kind: APIComponent
metadata:
  name: api-pipeline-train-validate
  namespace: pu-username-pa-bm99
spec:
  published: true
  experimentPipeline:
    name: my-pipeline
  restfulApi:
    auth:
      basic:
        credentials: bm99-apis-cred
      identityPassThrough: true
  apiSpec:
    inputs:
      - name: xy_train
        description: "Данные для обучения. Таблица, включающая целевую переменную."
        type:
          datatypes: [ "FILE" ]
          contentTypes: [ "text/csv" ]
       - name: xy_test
        description: "Данные для тестирования. Таблица, включающая целевую переменную."
        type:
          datatypes: [ "FILE" ]
          contentTypes: [ "text/csv" ]
      - name: extra_parameters
        description: "Метаданные для обучения. Словарь с параметрами, передаваемыми в аргумент 'param_grid' при инициализации sklearn.model_selection.GridSearchCV."
        required: false
        type:
          datatypes: [ "FILE" ]
          contentTypes: [ "application/json" ]
    outputs:
      - name: testing_results
        description: "Таблица со значениями метрик качества модели на тестовых данных xy_test."
        type:
          contentTypes: [ "text/csv" ]

Здесь,

  • metadata.name - название компонента API.
  • spec.published - "включен" ли API.
  • spec.experimentPipeline - название компонента ExperimentPipeline, запуску которого соответствует этот компонент API.
  • spec.apiSpec - определение интерфейса вызова пайплайна. То есть, определение самого API. В частности,
  • inputs - входные переменные, со следующими полями
    • name - имя переменной, используется при вызове API, при доступе в контейнере и при определении самого пайплайна в ExperimentPipeline.
    • description - описание переменной, выводится при запросе спецификации API, необязательно.
    • required - обязательно ли передавать переменную для вызова пайплайна? По умолчанию true.
    • type.datatypes - список возможных типов передаваемых данных. Подробнее ниже.
    • type.contentTypes - список возможных типов файлов, если тип передаваемых данных - FILE.
  • outputs - выходные данные. Используют те же поля, что inputs, со следующими отличиями:
    • required не указывается и всегда считается false. Все выходные переменные необязательны.
    • type.datatypes почти всегда равно [ "FILE" ]. Выходные данные передаются только файлами. Для модуля построения отчётов используется тип website.

Подробнее о типах файлов в spec.type.datatypes.

Потенциальные возможные типы данных - ["FP32", "FP64", "INT32", "dict", 'str', "FILE"]. Если типа данных не указан, считаются разрешёнными все типы данных. Аналогично с типами файлов. Если contentTypes - это пустой список [], считаются допустимыми любые типы файлов.

Образы в любом случае должны работать только с файлами. Передача данных в другом виде, например в FP32, означает формат при вызове пайплайна через API, но не при работе с данными в контейнере модуля.

Если данные переданы не в виде файла, они сохраняются в файл, и этот файл уже передаётся в контейнер. Данные сохраняются простым преобразованием в строку и сохранением этой строки в текстовый файл.

Манифест APIComponent пайплайнов в целом

apiVersion: "unified-platform.cs.hse.ru/v1"
kind: APIComponent
metadata:
  name: api-pipelines
  namespace: pu-username-pa-bm99
spec:
  published: true
  pipelines:
    enabled: true
  restfulApi:
    auth:
      basic:
        credentials: bm99-apis-cred
      identityPassThrough: true

Главное отличие от других APIComponent - пункт pipelines.enabled: true. Изменяемые разработчиком пункты - имя приложения metadata.namespace, имя самого компонента metadata.name и имя секрета с реквизитами spec.restfulApi.auth.basic.credentials

Изменение пути монтирования отдельных переменных (необязательно).

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

Пример:

...
  name: my-pipeline
  namespace: pu-username-pa-bm99
spec:
  vars:
    - name: xy_train
      path: /data
      mountFrom:
        box:
          name: global-data-box
          boxPath: users/developer/file_groups/my_dataset
    - name: xy_test
    - name: extra_parameters
    - name: testing_results
    - name: model

  stages:
    - name: stage1
      image:
...

В этом примере переменная xy_train монтируется напрямую к пути в ящике global-data-box. Таким образом, данные из этого ящика будут всегда доступны при запуске пайплайна, как будто они были переданы во входой переменной xy_train.

В этом примере данные размещены в ящике global-data-box. Чтобы можно было к ним обратиться, нужно описать подключение ящика global-data-box в разделе connectedBoxes.

  connectedBoxes:
    - name: data-box
      path: /path/to/mountpoint
      default: true
      mountS3Box:
        s3BoxName: mymodule-data-box
    - name: global-data-box
      path: /path/to/another/mountpoint
      default: false
      mountS3Box:
        s3BoxName: my-global-data-box

Общая структура примера выше:

  • В приложении есть DataBox с названием my-global-data-box.
  • В connectedBoxes этот компонент подключается к пайплайну с именем global-data-box. Теперь во всём остальном файле для него используется имя global-data-box.
  • Также в connectedBoxes указано, куда ящик global-data-box монтируется по умолчанию - в /path/to/another/mountpoint. Это означает, что по умолчанию переменные, монтируемые к ящику global-data-box будут соответствовать путям /path/to/another/mountpoint/... в файловой системе контейнера.
  • Для более точного контроля в любом указании переменной (вход/выход или список vars) можно добавить поля:
    • path - глобальный путь внутри контейнера. Например /home/user/data или просто /data.
    • mountFrom - блок, указывающий расположение монтируемого пути в хранилище S3.
      • box.name - имя ящика из раздела connectedBoxes, к которому монтируется переменная.
      • box.boxPath - путь внутри S3, к которому монтируется переменная.

При указании boxPath путь начинается с системных папок users/<username>/file_groups/, которые недоступны пользователям при запросах через файловый API. Когда пользователь USERNAME загружает через API файл с путём myfolder/FILE.txt, то в S3 этот файл появляется в users/USERNAME/file_groups/myfolder/FILE.txt. Соответственно, можно выделить специального системного пользователя (в примерах - developer), который загружает "общие" файлы, и указать в манифестах путь к этим общим файлам. Например, это могут быть файлы с весами ИИ-модели, которую использует модуль.